Internet Technologies:waa

The showroom Professional

The "Showroom" of the Klondike Computer Shop displays records of one database which represent the items that can be ordered via the Internet. It is implemented in the SHOWROOM.PRG file and uses a variety of programming techniques you may find useful for your own Web applications. The techniques are in particular:

The database is listed page-wise so that each page displays a subset of records.
A variable number of submit buttons is displayed at the end of each HTML page. For example, a "Next page" button is only displayed if the user has not reached the last record.
Images are stored in a memo field and transferred via temporary files.
Records can be marked for selection in multiple HTML pages whose state is maintained.

The following discussion describes the implementation of these techniques as you find them in SHOWROOM.PRG. However, only those lines of code are discussed that apply to the respective technique.

Displaying subsets of records

When a Web application distributes data from multiple records of a database, it is always a good idea to estimate the volume of data to be transferred to a remote station. As a rule of thumb: if the amount of data exceeds approximately 50Kb, it is advantageous to transfer subsets of records rather than the entire database in order to keep the response time, or download time, low. Data for the showroom of the Klondike Computer Shop, for example, sum up to an amount of 200Kb. Therefore, the showroom is split into four HTML pages displaying consecutive records of the COMPUTER.DBF file. This requires the Web application to keep track of the current HTML page sent to the client station so that the next or previous set of records can be transferred when the user decides to view the corresponding page.

Keeping track of data sent to a Web browser means to maintain the state on the client side of the Web application. This can be done via session management, as described in the previous chapter, or by binding a state variable to each HTML page sent to the client station. The latter approach is used for the showroom of the Klondike Computer Shop to demonstrate another technique for maintaining a state. This approach can be used as long as a state requires only one or few variables. Let us see now how this is done in SHOWROOM.PRG:

01: #define  RECORDS_PER_PAGE   8 
02: 
03: FUNCTION PageCount( nRecordsPerPage ) 
04: RETURN Int( ( LastRec() / nRecordsPerPage )  + ; 
05:          IIf( LastRec() % nRecordsPerPage <> 0, 1, 0 ) ) 
06: 
07: 
08: FUNCTION ViewShowRoom( oHTML, oContext, cPage ) 
09:    LOCAL nPageCount 
10: 
11:    OpenComputerDbf() 
12:    nPageCount := PageCount( RECORDS_PER_PAGE ) 
13: 
14:    IF cPage == NIL 
15:       cPage := oHtml:getVar( "START_PAGE" ) 
16:    ENDIF 
17: 
18:    IF Val( cPage ) < 1 
19:       cPage := "1" 
20:    ELSEIF Val( cPage ) > nPageCount 
21:       cPage := LTrim( Str(nPageCount) ) 
22:    ENDIF 
23: 
24:    Comp->( DbSkip( RECORDS_PER_PAGE * ( Val(cPage)-1 ) ) ) 
25: 
26:    KlondikeHeader( oHtml ) 
27:    oHtml:formStart() 
28: 
29:    BuildShopTable( oHtml, oContext ) 
30: 
31:    oHtml:setVar( "WAA_PACKAGE" , "Klondike"   ) 
32:    oHtml:setVar( "WAA_FORM"    , "Dispatcher" ) 
33:    oHtml:setVar( "START_PAGE"  , cPage        ) 
34: 
35:    IF cPage <> "1" .AND. nPageCount > 1 
36:       oHtml:submitButton( " Previous Page " , 'NAME="WEB_ACTION"' ) 
37:    ENDIF 
38: 
39:    IF Val(cPage) <> nPageCount 
40:       oHtml:submitButton( "   Next Page   " , 'NAME="WEB_ACTION"' ) 
41:    ENDIF 
42: 
43:    oHtml:formEnd() 
44:    KlondikeFooter( oHtml ) 
45: 
46:    CloseComputerDbf() 
47: RETURN .T. 
48: 
49: 
50: FUNCTION Dispatcher( oHTML, oContext ) 
51:    LOCAL cPage   := oHtml:getVar( "START_PAGE" ) 
52:    LOCAL cAction := ALLtrim( oHtml:getVar( "WEB_ACTION" ) ) 
53: 
54:    DO CASE 
55:    CASE cAction == "Previous Page" 
56:       cPage := LTrim( Str( Val(cPage)-1 ) ) 
57:    CASE cAction == "Next Page" 
58:       cPage := LTrim( Str( Val(cPage)+1 ) ) 
59:    ENDCASE 
60: RETURN ViewShowRoom( oHtml, oContext, cPage ) 

Function ViewShowRoom() is invoked by the WAA server. It starts with opening the database COMPUTER.DBF in line #11 and determining how many HTML pages are required to display all records of the database using the RECORDS_PER_PAGE constant. The next step is to retrieve the value of the state variable START_PAGE from the HTML3 object in line #15 which is a null string when ViewShowRoom() is executed for the first time. As a result, the function begins with the first page, or first set of records, and moves the record pointer accordingly (lines #18-24).

The definition of the HTML page begins when the record pointer is positioned on the first record of the required record set. Data from the records is added to the HTML page in function BuildShopTable() which is discussed in a next chapter. For now it is important to know that checkboxes are created as input controls in this function which allow for selecting one or more records when the HTML page is transferred to the client station. This means that the checkboxes belong to the same form-definition (:formStart() and :formEnd()) as the two submit buttons created in lines #36 and #40. Also, the state variable START_PAGE, which is set in line #33 for indicating the current page, belongs to this form-definition.

This leads to the situation that two different submit buttons return the same values to the Web application if the user clicks either button in the Web browser. However, both submit buttons must trigger different actions in the Web application. The ambiguity of having two submit buttons returning the same data is resolved by using a dispatcher function that detects which submit button is clicked by a user. The function's name is bound to the WAA_FORM varible in line #33 and the variable name WEB_ACTION is attached to both submit buttons.

Now we assume that ViewShowRoom() has completed the HTML page and the user has clicked the "Next page" button. What happens? The WAA server calls the Dispatcher() function in line #50 where the value of the state variable START_PAGE is retrieved from the HTML3 object, plus the value of the variable named WEB_ACTION. Its value is the caption of the clicked submit button, so that we know that the next page is requested. Consequently, the current page is incremented in line #58 and ViewShowRoom() is called again to build the HTML page containing the next set of records.

The program logic for displaying a database page-wise is based on a state variable, a dispatcher function and two submit buttons having the same variable name but two different captions. This programming technique can be used in all situations where a HTML page allows for user input and returned data may be processed in different functions. If more than one submit button is included in an HTML page, the clicked button can be detected by its caption so that the Web application can branch to the appropriate function.

Displaying multiple records

Displaying data from multiple records of a database is an easy task when using a HTML table definition. The contents of a record is added to the table as a row where each cell displays data of one database field. The value of field variables must be transformed to the character data type since this is the only one supported by HTML. The PRG code for building the list of computers in the Klondike Computer Shop consists basically of the following lines of code:

01: STATIC PROCEDURE BuildShopTable( oHtml, oContext ) 
02:    LOCAL i 
03: 
04:    oHtml:put( '<TABLE BORDER=0 BGCOLOR="#EEEEEE">' ) 
05: 
06:    oHtml:put( '<TR BGCOLOR="#CCCCCC">' ) 
07:    oHtml:put( '<TH>Part #</TH>' ) 
08:    oHtml:put( '<TH>Producer</TH>' ) 
09:    oHtml:put( '<TH>Computer</TH>' ) 
10:    oHtml:put( '<TH># and Type<BR>of Processors</TH>' ) 
11:    oHtml:put( '<TH>Price</TH>' ) 
12:    oHtml:put( '<TH>Check here<BR>to order</TH>' ) 
13:    oHtml:put( "<TH>That's how<BR>they look</TH>" ) 
14:    oHtml:put( '</TR>') 
15: 
16:    FOR i:=1 TO RECORDS_PER_PAGE 
17:       oHtml:put( '<TR>') 
18:       oHtml:put( '<TD ALIGN=RIGHT>' + Comp->PARTNO + '</TD>' ) 
19:       oHtml:put( '<TD>' +  Trim( Comp->PRODUCER  ) + '</TD>' ) 
20:       oHtml:put( '<TD>' +  Trim( Comp->COMPUTER  ) + '</TD>' ) 
21:       oHtml:put( '<TD>' + LTrim( Str(Comp->CPUCOUNT) ) + '<BR>' + ; 
22:                                Trim( Comp->CPUTYPE ) + '</TD>' ) 
23:       oHtml:put( '<TD ALIGN=RIGHT>' + LTrim( Str(Comp->PRICE) ) + '</TD>' ) 
24: 
25:       <... include checkbox ...> 
26:       <... include image ...> 
27:       SKIP 
28: 
29:       IF Eof() 
30:         EXIT 
31:       ENDIF 
32:    NEXT 
33: 
34:    oHtml:put( '</TABLE><BR>' ) 
35: RETURN 

The entire table is build in the HTML page using the :put() method of the HTML object and four tags are required along with their end tags: <TABLE>, <TH>, <TR> and <TD>. The first table row displays the column headings (lines #6-14). The following rows display field data and are created in the FOR..NEXT loop which skips in each iteration to the next record.

This code can be adapted easily to display fields of any database. In the showroom of the Klondike Computer Shop, however, there are two particular table columns that need some special attention. Let us first see how the table column containing JPEG images is created. For this you must know that the images are stored in a binary memo field of the COMPUTER.DBF database:

01: STATIC PROCEDURE BuildShopTable( oHtml, oContext ) 
02:    LOCAL i, cJpgFile 
03: 
04:    <... table column headings ...> 
05: 
06:    FOR i:=1 TO RECORDS_PER_PAGE 
07:       <... columns for fields ...> 
08: 
09:       cJpgFile := "Cp"+StrZero( Comp->(Recno()), 6 ) + ".jpg"
10:       oHtml:putData( Comp->JPG_IMAGE, "\temp\" + cJpgFile )
11: 
12:       oHtml:put( '<TD ALIGN=CENTER>') 
13:       oHtml:put( '<IMG SRC=/temp/' + cJpgFile + ; 
14:                  ' ALIGN=CENTER ALT=' + Trim( Comp->COMPUTER ) +'>' ) 
15:       oHtml:put( '</TD>') 
16:    NEXT 
17: RETURN 

The basic mechanism is that the contents of a database field (Comp->JPG_IMAGE) is copied by the HTML object to a temporary file visible for the HTTP server. This is accomplished in line #10 by the :putData() method which accepts as parameters a string (file contents) and a file name. The latter is created in line #9 and used to reference the JPEG image in the <IMG> tag added to the HTML page in line #13. This follows the same program logic as discussed for the :img() method in the chapter The menu, except that the temporary file is not created from another file but from a database field. It is, therefore, important that the file name is choosen relative to the document root directory of the HTTP server, and that the image data of each record is copied to a file having a unique name.

Selecting multiple records

When a database is displayed across several HTML pages and each page allows for selecting one ore more records, there are three problems to be resolved. We must choose an appropriate input control that identifies a single record, we must know which records are displayed on the current page sent to a Web browser, and we must be able to identify the selected records when a user submits the current page.

A solution to these three problems is demonstrated in the showroom of the Klondike Computer Shop where for each record displayed in the HTML page a checkbox is included as input control. The state of the checkboxes is maintained via session management and each checkbox is assigned a unique, dynamically created variable name. The entire program logic is split into the procedures BuildShopTable(), which creates the HTML page displaying multiple records, and ResumeShopping() for maintaining the state of the checkboxes while the session is open. The PRG code for both procedures follows, but keep in mind that only the relevant lines of code are listed (see SHOWROOM.PRG for the entire code). Also, we must distinguish the situations where the showroom page is accessed for the first time (no record is selected), and repeatedly (some records may be selected). Just assume for now that the page is accessed for the first time:

01: STATIC PROCEDURE BuildShopTable( oHtml, oContext ) 
02:    LOCAL i, nRecno, aRecords, aSelected 
03: 
04:    ResumeShopping( oHtml, oContext, @aRecords, @aSelected )
05: 
06:    FOR i:=1 TO RECORDS_PER_PAGE 
07:       nRecno := Comp->(Recno()) 
08:       AAdd( aRecords, nRecno ) 
09: 
10:       IF AScan( aSelected, nRecno ) == 0 
11:          oHtml:checkBox( PadL( nRecno, 8, "0" ), , "" ) 
12:       ELSE 
13:          oHtml:checkBox( PadL( nRecno, 8, "0" ), , "", "checked" ) 
14:       ENDIF 
15:       SKIP 
16:    NEXT 
17: 
18:    oHtml:setVar( "USER_SELECTION", "YES" ) 
19: 
20:    oContext:setCargo( "REC_INPAGE", aRecords  ) 
21:    oContext:setCargo( "REC_SELECT", aSelected ) 
22: RETURN 

The session is opened in ResumeShopping() and the two variables aRecords and aSelected are initialized with empty arrays when the page is accessed for the first time. The numbers of the records displayed in the HTML page are added to theaRecords array in line #8, and a checkbox in Unchecked state is created for each record in line #11 by the :checkBox() method of the HTML object. Each checkbox receives a name created from the record number with the PadL() function.

When the FOR..NEXT loop is complete, the state variable USER_SELECTION is bound to the HTML page so that we can detect later if the page contains user-selectable items. Finally, the two arrays holding the record numbers of the displayed and selected records are attached to the Context object with :setCargo(). Then, the complete HTML page is sent to the Web browser where a user can mark records by clicking the checkboxes. The selection is submitted when a user clicks "Next Page" or "Previous Page" and this leads to a repeated call of BuildShopTable().

In this case, the procedure ResumeShopping() becomes important, because it detects which records are marked for selection. For this, the session is re-opened, and the two arrays aRecords and aSelected are retrieved from the Context object:

23: PROCEDURE ResumeShopping( oHtml, oContext, aRecords, aSelected ) 
24:    LOCAL cValue, nSelect, i, imax 
25: 
26:    oContext:openSession() 
27: 
28:    aRecords  := oContext:getCargo( "REC_INPAGE" ) 
29:    aSelected := oContext:getCargo( "REC_SELECT" ) 
30: 
31:    IF aRecords == NIL 
32:      aRecords := {} 
33:    ENDIF 
34: 
35:    IF aSelected == NIL 
36:      aSelected := {} 
37:    ENDIF 
38: 
39:    cValue := oHtml:getVar( "USER_SELECTION" ) 
40: 
41:    IF .NOT. cValue == "YES" 
42:       RETURN 
43:    ENDIF 
44: 
45:    imax := Len( aRecords ) 
46:    FOR i:=1 TO imax 
47:       cValue  := oHtml:getVar( PadL( aRecords[i], 8, "0" ) ) 
48:       nSelect := AScan( aSelected, aRecords[i] ) 
49: 
50:       IF Empty( cValue ) 
51:          IF nSelect > 0 
52:             ADel ( aSelected, nSelect ) 
53:             ASize( aSelected, Len(aSelected)-1 ) 
54:          ENDIF 
55:       ELSE 
56:          IF nSelect == 0 
57:             AAdd( aSelected, aRecords[i] ) 
58:          ENDIF 
59:       ENDIF 
60:    NEXT 
61: RETURN 

The procedure determines selected records only if the state variable USER_SELECTION is set to "YES" (line #41). Then, it iterates through the array aRecords which holds the record numbers of the records currently being displayed on the client side of the Web application. Since each record number corresponds with the name of a checkbox for that record, the state of a checkbox is detected in line #47 where the value of this variable is retrieved with :getVar(). If the checkbox is Unchecked, the method returns a null string. This is used in line #50 to find out whether or not the user has (un)marked a record for selection.

A checkbox can have assigned a value, but it is usually not relevant since an Empty vs. a Not Empty string returned from :getVar() is sufficient to detect a checkbox's state.

In the lines #51-59, the record numbers of selected records are added to, or deleted from the aSelected array, depending on whether the user changed the state of the corresponding checkbox from Unchecked to Checked or vice versa. As a result, the numbers of all marked records are stored in the aSelected array and this, in turn, is used in line #10 of BuildShopTable() to detect the selection state of an arbitrary record, and to create a checkbox with the correct state when a new HTML page is returned to the Web browser (lines #11 and #13). This way, the selection state of all records of the database is maintained.

It is possible to monitor the selection state of records using state variables for each record. However, this is not a recommended technique since a number of LastRec() state variables needs to be bound to an HTML page. This can lead to a huge amount of hidden data being transferred to the client station. The session approach discussed here is more adequate since the effective volume of transferred data is determined by the number of records displayed in one HTML page, not by the total number of records in the database.

Feedback

If you see anything in the documentation that is not correct, does not match your experience with the particular feature or requires further clarification, please use this form to report a documentation issue.