Diet for a fat client

Gone are the days when a client can snack on fast food records served up directly from the McServer. Reading records one at a time over a network connection is like consuming empty calories. It’s not a good situation where the client is so fat that it can’t move or function quickly.

The way things were

Many OpenEdge legacy applications exist today that were written when all clients were located on the same server where the database resides. Typically, this is a character user interface (CHUI) running on a Linux or Unix host or a graphical user interface (GUI) running on a Windows host.

Ah, I remember these carefree days well, when all a developer had to worry about was the correct index to use while connecting to the database during the entire session.  It was very convenient to access database tables directly, reading as many records as desired and next simply discarding unused ones when finished with the procedure.

Things change

However, like most things in life, application architecture has become more complex. Clients resided on their own PCs and connected across a Local Area Network (LAN) to a database residing on a separate PC or workstation called a server.

Moving these legacy applications over to the network worked ok at first, since the LANs moved data reasonably quickly in an office setting with a limited number of users.

But, with the advent of larger numbers of users, and with databases growing in size and complexity, end users started to notice a degradation in performance. This trend continued to worsen, when organizations attempted to operate these applications on a Wide Area Network (WAN) or directly over the Internet. This caused the realization that the legacy application needed to be re-engineered or to state it another way, it must go on a diet!

Like consuming food, consuming network packets need to be minimized for a well-running, healthy and thin application. Fortunately, OpenEdge provides many tools and products that allow a developer to easily convert a legacy application into a lean, mean-functioning client.

 

A fat example

Let’s look at the following example.  Suppose we have a program that prompts the end user for a sales rep and returns all the customers and orders for that sales rep, representing it in two browse widgets, one for the customer and one for sales orders:


The Sales Rep combo box at the top allows the user to select the desired rep, populating the corresponding customer and order browses. Further, each time a different customer is selected in the top browse, its related orders then appear in the bottom order browse.

Here is the code for custorder1.p using the sports2000 database:

/*** custorder1.p ***/
DEFINE VARIABLE csalesrep AS CHARACTER NO-UNDO
VIEW-AS COMBO-BOX INNER-LINES 9.

define button bdone label "Done" auto-endkey.

define query q1 for customer.

define browse b1 query q1
display
custnum name format "x(20)"
contact format "x(20)" creditlimit
with 4 down.

define query q2 for order.

define browse b2 query q2
display ordernum orderdate promisedate shipdate with 8 down.

FORM skip(1) csalesrep LABEL "Sales Rep" skip(1)
b1 skip(1)
b2 skip(1)
bdone
WITH frame a three-d centered title " Fat Client " side-labels.

on value-changed of csalesrep
do:
assign csalesrep.
run openq1.
run openq2.
end.

on value-changed of b1
run openq2.

run openq1.

run openq2.

FOR EACH Salesrep WITH FRAME a:
csalesrep:ADD-LAST (Salesrep.SalesRep).
END.

do with frame a:
ASSIGN csalesrep:SCREEN-VALUE = csalesrep:ENTRY(1).
apply "value-changed" to csalesrep.
display csalesrep.
enable all.
b1:select-focused-row().
end.
wait-for choose of bdone.

procedure openq1:
open query q1 for each customer no-lock
where customer.salesrep = csalesrep.
end.

procedure openq2:
open query q2 for each order no-lock of customer.
end.
/*** end custorder1.p ***/


Notice in the above program that both queries, q1, and q2, connect to database tables customer and order, respectively. Every time these queries are opened (see internal procedures openq1 and openq2), database records are fetched from the database, and network traffic is incurred. This happens whenever the selected sales rep changes or when a new customer is selected.

Like any diet, you always have other options!

OpenEdge provides 3 tools that can easily convert this program into a thin client architecture:

  1. Temp Tables
  2. ProDataSets
  3. Progress Application Server for OpenEdge (PASOE)

The following program custorder2.p takes advantage of the first two tools, Temp Tables and ProDataSets:


/*** custorder2.p ***/
DEFINE VARIABLE csalesrep AS CHARACTER NO-UNDO
VIEW-AS COMBO-BOX INNER-LINES 9.

define button bdone label "Done" auto-endkey.

define temp-table tcustomer no-undo like customer.

define query q1 for tcustomer.

define browse b1 query q1
display custnum name format "x(20)" contact format "x(20)" creditlimit
with 4 down.

define temp-table torder no-undo like order.

define query q2 for torder.

define browse b2 query q2
display ordernum orderdate promisedate shipdate
with 8 down.

define dataset dscustorder for tcustomer, torder
data-relation custorder for tcustomer, torder
relation-fields (custnum,custnum).

define query qcustomer for customer.

define data-source srccustomer
for query qcustomer.

define data-source srcorder for order.

FORM skip(1) csalesrep LABEL "Sales Rep" skip(1)
b1 skip(1)
b2 skip(1)
bdone
WITH frame a three-d centered title " Fat Client " side-labels.

on value-changed of csalesrep
do:
assign csalesrep.
run fillds(input csalesrep,
output dataset dscustorder).
run openq1.
run openq2.
end.

on value-changed of b1
run openq2.

run openq1.

run openq2.

FOR EACH Salesrep WITH FRAME a:
csalesrep:ADD-LAST (Salesrep.SalesRep).
END.

do with frame a:
ASSIGN csalesrep:SCREEN-VALUE = csalesrep:ENTRY(1).
apply "value-changed" to csalesrep.
display csalesrep.
enable all.

if num-results("q1") > 0 then
b1:select-focused-row().
end.
wait-for choose of bdone.

procedure openq1:
open query q1 for each tcustomer no-lock
where tcustomer.salesrep = csalesrep.
end.

procedure openq2:
open query q2 for each torder no-lock of tcustomer.
end.

procedure fillds:
define input parameter ipsalesrep as character.
define output parameter dataset for dscustorder.

buffer tcustomer:attach-data-source(data-source srccustomer:handle).
buffer torder:attach-data-source(data-source srcorder:handle).

query qcustomer:query-prepare(substitute("for each customer where
customer.salesrep = '&1'",ipsalesrep)).

dataset dscustorder:fill().

buffer torder:detach-data-source().
buffer tcustomer:detach-data-source().
end.
/*** end custorder2.p ***/


Defining temp tables using the naming convention of a “t” in front of the table name makes this program easy to modify. The temp table tcustomer is defined like the customer table, and the torder temp table is defined like the order table.

After defining the temp tables, modify the query definitions by specifying the temp-table buffer instead of the database buffer. Replace customer with tcustomer and order with torder on the define query statements.

The like option on the temp table definition eliminates the need to modify the browse definitions. The only other statements that need to change by adding the “t” for temp table are the open query statements in procedures openq1 and openq2.

The ProDataSet is the most efficient way to pass data from one procedure to another because it groups multiple temp tables into one parameter.

In addition to the dataset definition representing both the tcustomer and torder temp tables, the query qcustomer and the srccustomer and srcorder data sources must be defined to fill the ProDataSet.

Finally, the internal procedure fillds is created to attach the corresponding data sources, prepare the qcustomer query and fill the dataset based upon the selected sales rep before detaching the data sources.

The program custorder2.p has now batched our request for customers and orders based upon a selected sales rep. Once the tcustomer and torder temp tables are filled, selecting a new customer to show its corresponding orders will no longer require access to the database, and processing will be performed locally on the client.

Take thinning to the next level

So, our client is now becoming thinner, but we still have more work to do. This involves using PASOE to process the fill of the dataset locally on the server and then pass the dataset back to the client, thereby minimizing the amount of network traffic.

This is accomplished by removing the internal procedure fillds and creating an external procedure fillds.p that will be called from the client and executed on the PASOE.

Below are the fillds.p and custorder3.p programs.

/*** fillds.p ***/
define temp-table tcustomer no-undo like customer.

define temp-table torder no-undo like order.

define dataset dscustorder for tcustomer, torder
data-relation custorder for tcustomer, torder
relation-fields (custnum,custnum).

define query qcustomer for customer.

define data-source srccustomer for query qcustomer.

define data-source srcorder for order.

define input parameter ipsalesrep as character.

define output parameter dataset for dscustorder.

buffer tcustomer:attach-data-source(data-source srccustomer:handle).

buffer torder:attach-data-source(data-source srcorder:handle).

query qcustomer:query-prepare(substitute("for each customer
where customer.salesrep = '&1'",ipsalesrep)).

dataset dscustorder:fill().

buffer torder:detach-data-source().

buffer tcustomer:detach-data-source().
/*** end fillds.p ***/


/*** custorder3.p ***/
DEFINE VARIABLE csalesrep AS CHARACTER NO-UNDO
VIEW-AS COMBO-BOX INNER-LINES 9.

DEFINE VARIABLE happsrv AS HANDLE NO-UNDO.

DEFINE BUTTON bdone LABEL "Done" AUTO-ENDKEY.

DEFINE TEMP-TABLE tcustomer NO-UNDO LIKE customer.

define query q1 for tcustomer.

define browse b1 query q1
display custnum name format "x(20)" contact format "x(20)" creditlimit
with 4 down.

define temp-table torder no-undo like order.

define query q2 for torder.

define browse b2 query q2
display ordernum orderdate promisedate shipdate
with 8 down.

define dataset dscustorder for tcustomer, torder
data-relation custorder for tcustomer, torder
relation-fields (custnum,custnum).

FORM skip(1) csalesrep LABEL "Sales Rep" skip(1)
b1 skip(1)
b2 skip(1)
bdone
WITH frame a three-d centered title " Thin Client " side-labels.

on value-changed of csalesrep
do:
assign csalesrep.
run fillds.p on server happsrv (input csalesrep,
output dataset dscustorder).
run openq1.
run openq2.
end.

on value-changed of b1
run openq2.

run openq1.

run openq2.

create server happsrv.

happsrv:connect("-URL http://localhost:12111/apsv") no-error.

if not happsrv:connected() then do:
message "Cannot connect to AppServer" view-as alert-box.
return.
end.

FOR EACH Salesrep WITH FRAME a:
csalesrep:ADD-LAST (Salesrep.SalesRep).
END.

do on endkey undo,leave with frame a:
ASSIGN csalesrep:SCREEN-VALUE = csalesrep:ENTRY(1).
apply "value-changed" to csalesrep.
display csalesrep.
enable all.
if num-results("q1") > 0 then
b1:select-focused-row().
wait-for choose of bdone.
end.

happsrv:disconnect() no-error.
delete object happsrv no-error.

procedure openq1:
open query q1 for each tcustomer no-lock
where tcustomer.salesrep = csalesrep.
end.

procedure openq2:
open query q2 for each torder no-lock of tcustomer.
end.
/*** end custorder3.p ***/

The difference between custorder2.p and custorder3.p is that custorder3.p connects to the PASOE at the start of the program, and the call to the fillds.p program is executed on the PASOE which replaces the call to the internal procedure fillds inside of the on value-changed of csalesrep trigger.

Setting up the PASOE is beyond the scope of this article, Check out the following links for more information on PASOE:

What is the Progress Application Server?

Create and configure PAS for OpenEdge Instances

Conclusion

In summary, Progress provides the necessary tools, including Temp Tables, ProDataSets, and PASOE, to convert fat clients to thin clients easily.

 

Leave a Comment