Peter van Dam Dynamic WebClient Programming
Peter van Dam Progress fanatic since 1985 Version CHUI-GUI-Batch-WebSpeed-WebClient Founder of Author in Progressions & user group magazines Founder of
Dynamic WebClient Programming Dynamic Windows, Frames & Widgets Dynamic Browses Dynamic Buffers & Queries Dynamic Temp-Tables Objects, Handles, Methods & Attributes AppServers WebClient
What is WebClient? WebClient = runtime – db connect
Need AppServer to talk to a database Separate User Interface from Business Logic AppServer DB
Run Progress over inter/intranet Full GUI capabilities on end user PC WebClient does NOT run in browser AppServer DB Internet or Intranet
Prepare your application No database connects from the client Port your application to AppServer User Interface runs on client Business Logic runs on server Client issues AppServer calls ADM II offers these capabilities
Consider the following window
Window architecture Built in ADM II SDO, SmartDataBrowser, SmartDataViewer, SmartPanel Run with AppServer RowsToBatch=100 Nothing fancy (we’ll get to that later)
Startup time 6 s (!) over 28k8
Startup statistics 198 KB of client-side r-code to start 6 AppServer calls over internet 117 KB of server-side r-code to start 19 KB of data received 100 rows cached on client
After 100 rows 4 AppServer calls 18 KB of data received 200 rows cached on client Response time 4 seconds on 28k8
Deployment This screen compiles to 198 KB of client-side r-code Remember, nothing fancy Not a single bit of custom code Imagine deploying an entire application…
Deploying an application (I) Consider an average application with 100 screens Some screens are simpler Many screens are more complex Some screens are much more complex Some objects are being reused
Deploying an application (II) A conservative guess: average screen size 250 KB of client side code 100 x 250 KB = 25 MB Not counting images, adm tree, support code etc. Download times: 28k8 modem: ~ 150 mins Dual ISDN: ~ 34 mins
Deploying an application (III) Compressing.r code saves just 20 % Initial deployment on CD possible (with WebClient image) However, even small application updates turn into megabytes to deploy Did we choose internet to limit deployment possibilities?
Surely there is a better way?!?
Three Golden Rules: 1. Reduce AppServer calls 2. Reduce network traffic 3. Reduce client.r code
Consider this window again: What do we really really need?
Apply the Golden Rules 1. A single AppServer call should do 2. Do not send all viewer properties for all records 3. Create windows dynamically
A single AppServer call should do All data for the entire window should be prepared on the server and returned in a single call The server needs to know about the screen configuration Describe screen definitions in a database (‘Repository’)
Do not send unnecessary data The browser shows just 3 fields Fetch the viewer data only when needed Activate a time delay when the user scrolls The ideal call returns just a single packet (up to about 1,500 bytes)
Do not send unnecessary data (2) Ask yourself: Does the client REALLY need this information from the server? Do I send information to the server that the server already knows?
Do not send unnecessary data (3) The browser shows just 3 fields Fetch the viewer data only when needed Activate a time delay when the user scrolls A call should ideally return just a single packet (1500 bytes)
3: Create windows dynamically Send repository data to the client Client creates all screens dynamically NO r-code deployment at all! In V9.1 this is NOT fiction
New startup statistics Startup time 2 seconds on 28k8 Client code always resides in memory A single AppServer call 41 KB of server-side r-code to start 6 KB of data received 100 rows cached on client
Why do we show the first 100? The example window shows the first 100 customers What are the chances that this is useful? The first thing the user probably will do is find the customer she needs Why start with a useless data transfer?
A new approach: start empty Let the user enter her selections first
A new approach: start empty Transfer only what the user asks for
Results for the new approach 1 second response time on 28k8 for each user interaction Just 2-3 KB received on each call The application is now usable with a 28K8 modem and simply fast on anything better
9600 baud Demonstration
How does it all work? Write a generic ‘rendering’ program for the client (ui.p) Introduce a repository Write a generic program on the server that can merge repository information with data
The Big Picture Appserver boundary Repository ui.p Applicationdata bl.p WebClient AppServer Applicationdata uihooks.pblhooks.p
The implementation Appserver boundary Repository ui.p Applicationdata save.p Applicationdata Delete.p getscreen.p
Creates: Dynamic windows and dialogs Dynamic field level widgets Dynamic panels Dynamic browsers And fills them with dynamic data from the server ui.p
Fetches screen description from repository Creates dynamic database queries for each data set Puts the results in dynamic temp-tables Returns dynamic data and static screen description to client getscreen.p
Dynamic Temp-Tables New in Progress v9.1 Created specifically for dynamically passing data from an AppServer to a WebClient Create temp-table structures on the fly Requires only run-time Progress
Dynamic Temp-Tables (2) Move data from getscreen.p to ui.p without knowing about db tables at compile time Create in getscreen.p at runtime Pass definitions & data to ui.p using new OUTPUT TABLE-HANDLE parameter
Creating a Dynamic Temp-Table DEFINE VARIABLE hTemp as HANDLE NO-UNDO CREATE TEMP-TABLE hTemp ASSIGN hTemp:UNDO = FALSE. Just like any dynamic widget Now we have created an empty structure Use methods to define the structure
Dynamic Temp-Table Methods CREATE-LIKE ADD-FIELDS-FROM ADD-NEW-FIELD ADD-LIKE-FIELD ADD-LIKE-INDEX ADD-NEW-INDEX ADD-INDEX-FIELD
Use ADD-NEW-FIELD Most versatile We may want to overrule some dictionary properties in the repository We want to add some extra fields such as the database ROWID of a record Check the return value for errors
ADD-NEW-FIELD ADD-NEW-FIELD( field-name-exp, datatype-exp [, extent-exp [, format-exp [, initial-exp [, label-exp [, column-label-exp ] ] ] ] ] )
ADD-NEW-FIELD examples hTemp:ADD-NEW-FIELD ("rowid","rowid”). hTemp:ADD-NEW-FIELD (hField:NAME, /* from dd */ hField:DATA-TYPE, /* from dd */ hField:EXTENT, /* from dd */ repository.cFormat, repository.cInitial, repository.cSideLabel repository.cColumnLabel).
Indexes We do not need any indexes on these temp-tables The client shows the records in the order in which they were created Any server-side sorting included Client may sort the records locally as well
Prepare the Temp-Table hTemp:TEMP-TABLE-PREPARE( ). ‘Compiles’ the temp-table at run time Check the return value for errors Provide the name argument for debugging purposes Now you can fill the new structure with data… dynamically
Populate the Dynamic Temp-Table We need a buffer handle for this: DEF VAR hDefault AS HANDLE NO-UNDO. hDefault = hTemp:DEFAULT-BUFFER-HANDLE. But how do we get the data into the dynamic temp-table?
Populate the dynamic temp-table First we need to create a dynamic buffer for each database table involved in the query: DEF VAR cTable AS CHAR INITIAL “customer”. DEF VAR hData AS HANDLE NO-UNDO. CREATE BUFFER hData for table cTable.
Populate the dynamic temp-table Then we create a dynamic query: DEF VAR hQuery AS HANDLE NO-UNDO. CREATE QUERY hQuery. hQuery:ADD-BUFFER(hData). hQuery:QUERY-PREPARE(SUBSTITUTE( “FOR EACH &1 NO-LOCK”, cTable)).
Populate the dynamic temp-table Then we open the query and buffer-copy the data: hQuery:QUERY-OPEN(). hQuery:GET-FIRST(). DO WHILE hQuery:QUERY-OFF-END = FALSE: hDefault:BUFFER-CREATE(). hDefault:BUFFER-COPY(hData). hQuery:GET-NEXT(). END.
Returning the dynamic temp-table Don’t forget to fill the ROWID field as well Maximize the number of records to be returned (e.g. 100 or even 50) The new dynamic temp-table (meta schema + data) is returned to the client using OUTPUT PARAMETER TABLE-HANDLE DELETE OBJECT hTemp. (never forget!) Delete magically postponed by Progress
OUTPUT TABLE-HANDLE Repository ui.p Applicationdata Applicationdata getscreen.p 1 2 Appserver boundary 3 OUTPUT TABLE-HANDLE
Receiving the dynamic temp-table The dynamic temp-table is received by the client using OUTPUT TABLE-HANDLE Client must analyze meta schema + data How the heck do we go about this?
Receiving the dynamic temp-table First analyze the meta schema: DEF VAR hData AS HANDLE NO-UNDO. DEF VAR iField AS INT NO-UNDO. DEF VAR hField AS HANDLE NO-UNDO. hData = hTemp:DEFAULT-BUFFER-HANDLE. DO iField = 1 TO hData:NUM-FIELDS: hField = hData:BUFFER-FIELD(iField). /* Access field properties here */ END.
Analyzing the meta schema By inspecting the buffer fields we can find out meta information Note that the client receives a virtual table Meta information includes data-type, format and labels Other information such as row and column needs to be sent separately
Analyzing the meta schema The client can use the meta information creatively to render the user interface Could even be tailored individually The server does not care about the user interface However, the server DOES care about the data and the relationships within it
Analyzing the data Looks familiar already: DEF VAR hQuery AS HANDLE NO-UNDO. CREATE QUERY hQuery. hQuery:SET-BUFFERS(hData). /* Just 1 */ Now open the query and fetch the records A viewer should return a single record, a browser may return many
Record scoping Dynamic BUFFERS have no scope – they are created in the session Use a WIDGET-POOL to scope to your procedure You are responsible for releasing the record to write it to the database Then you must delete the buffer object
Record scoping DEF VAR hBuffer AS HANDLE NO-UNDO. CREATE BUFFER hBuffer for table ‘customer’. DO TRANSACTION: hBuffer:BUFFER-CREATE(). /* fill here */ hBuffer:BUFFER-RELEASE(). END. /* Transaction */
Memory leaks Show up after a long time Can be extremely difficult to track down Bites you at deployment!
Memory leaks Dynamic objects are scoped to the session You are responsible for deleting objects when you are done with them: DELETE BUFFER hBuffer. hBuffer = ?.
Memory leaks Use WIDGET-POOLS to scope objects to your procedure TABLE-HANDLES are created in the SESSION WIDGET-POOL (whether you like it or not) on both sides You need to delete those explicitly
TABLE-HANDLES DEFINE OUTPUT PARAMETER TABLE-HANDLE hTemp. /* Creates the table */ /* Fill hTemp with data */ DELETE OBJECT hTemp. /* Will be deferred by Progress until procedure goes out of scope */
Dynamic Temp-Table Viewer Helps you troubleshoot dynamic temp-table memory management problems One ADM II issue shown here Download tool from
Advantages of dynamic programming Eliminate CRC errors Create extremely powerful code No more.r code to deploy Even hardly any.r code on the server Faster applications Easier multi-database programming Run Progress applications across the internet
Drawbacks of dynamic programming New paradigm to learn More abstract, work with handles Difficult to debug Repository replaces code Different deployment issues More runtime errors Possible query performance problems
More information Progress documentation: – V9 Product Update Bulletin – Progress on the Web Progressions magazine ( Walvis Database Viewer Progress Group (
Question time
WebClient & WebSpeed consultants Progress Solutions for Internet