![]() |
|
Published 1999-01-01 Printer-friendly version
After solving a particularly knotty problem with some unique or clever block of code, I am often (insufferably) proud of my accomplishment. It is rare however that I can point to an entire application with that same kind of glow. The CWICWeb Download application is one of them (http://www.cwicweb.com/apps/cwlauch.dll/download.exe.0).
This application was the subject of "Dual-Dual-Mode Apps" (Clarion Online, 2, 4, November 1998) and apparently it excited a number of TopSpeeders as much as it did me.
On reflection, it now seems to me that I missed the real import of Download. I had been captivated by the fact that I could create a Clarion application that looked and behaved like the best Web apps, was cleaner than most and, in fact, could not readily be distinguished from them. That I also fooled some first-class Topspeeders in the bargain (no mean feat, that), didn't exactly go down too hard, either.
The real significance of this app, however, lies in the fact that I used Clarion as a differential HTML writer.
When I did photography, I did not use zoom lenses. Zooms are convenient but their image quality is always inferior to fixed length lenses, even in snapshot sized prints. In the argot of the time, I opted for "best of breed" rather than what was popular or convenient (explains why we use Clarion, doesn't it?).
PC software is somewhat the same way.
If you want to do a Web site, an HTML generator seems the obvious choice. And there are a number of very good ones available. And, if you want to do databases ... well, that's what we've got Clarion for. But what if we want to do both?
There are, to be sure, some megabuck tools out there that will do both. Keyword is "megabuck." There are also some reasonably priced tools available that do a credible job. The problem is that either you must make some important compromises or you must learn a new, complex tool. Usually it's both, but it's the compromises that cause problems.
Using a visual HTML tool makes it easy to determine what the HTML should look like. After that, it is back to straight Clarion, letting Clarion write the HTML, as needed and only as much as needed. As I said, a differential HTML writer.
At its foundation this simply takes a page from Clarion's own history, memory mapped video handling. Comprehending what memory mapped video means, Clarion treats screens and windows as data. Similarly, HTML is contained in an ASCII file. Therefore, we can then treat it as data. After all, a record is a record, isn't it? Specifically, I used it like a series of constants, very much the way Clarion handles screens and windows.

The HTML, then, is no longer an issue. That leaves data access to be dealt with. Data access, of course, is something Clarion does well, so we can simply select from the standard Clarion tools and techniques we already know.
Since the HTML is now just a series of constants and database access is a known, what is left is the mechanics of the user interface. That is, all that really had to be decided was how the application should work for the end user. (It only took two days to build the application with all its functionalities. It took several weeks to debug an operating problem when Records(file) % NumberPerPage = 1. QED.)
The defining parameter is that the app uses Tony Goldstein's template modifications to run the application Java-free.
HTML behaves significantly differently from what we are used to - it does not post events the way that Windows does, with each control completion. By removing the Java, all options provided by IC become moot. You cannot set a control to refresh the page (i.e., trigger the ACCEPT() loop), either partially or fully, because these options require the Java Support Libraries. That is, on the Web, the ACCEPT loop starts off hobbled and, running Java-free, is entirely crippled; all IC remedies are closed off.
HTML provides only one way to perform what we would call an update, a Submit. To affect a Submit, you place a button on the window. In turn, this means that almost all of your embedded code will be behind this button - while there may be values associated with other controls, there are no other events to invoke in the ACCEPT() loop. You might think that code embedded behind other controls should execute when the Submit button triggers the ACCEPT() loop. But not all of those controls will have ACCEPT events pending. So, code that must execute needs to be in this one place.
Proof: Run the app and change the filter or sort order but do not press the Apply button. Instead, press Next Page.
I knew that I was going to need a filter so that users could limit selections by category (this is the File Loaded Drop control, "Select").

The radio buttons for "Sort" and the entry field for "Items/page" were added later (on the second development day). What is important is that each of these controls sets a value and requires a refresh of the page.
To repeat, because I am not using the Java Classes: the ACCEPT() loop cannot be made to CYCLE when expected, but when it is triggered on an HTML Submit, will capture the values. This is why there is an "Apply" button (which submits to the server and calls any required code) forcing ACCEPT() to cycle.
(See "Proof," above.)
It is also important to remember that, in either case, Clarion is still providing event handling. This is why I used Clarion for this application and not some other tool: I see no reason to reinvent the (event handler) wheel. It also explains why I retained the standard Clarion window "Close" button to take care of properly closing the app.
To implement forward and back buttons, we need to know where we are in the file; we need an index of first and last records displayed and the position of each relative to both ends of the file. Queues would allow fast filtering and sorting while providing a reliable pointer that allows retrieval of the required records:
Get(FileQueue,FQ:ID)
FIL:ID = FQ:ID
Access:Files.Fetch(FIL:ID_Key)
But if you do not FREE() the queue at every conceivable place your program might exit and account for all methods of exiting, you will cause a memory leak and your server will lock. Particularly, I worried about what would happen to the memory used by the queue if users simply abandoned the app and it times out. (Since most of you using this app do just that, let it time out, this concern was very real.)
Then, another important fact about IC struck me: the app is actually running on the server. Granted, it runs without displaying its windows, but it is running in and on Windows. Therefore, if I put
Free(FileQueue)
in every embed where the window is closing/closed, the mechanism to terminate the app on timeout would probably trigger the code to return the queue's memory
So, queues could (probably) be used safely. Further reflection confirmed this reasoning: standard browse boxes use queues, are standard IC controls and are properly disposed of on timeout.
The issues involved in providing filters, alternate sort orders, a variable number of lines per page while providing forward and back buttons as needed, suddenly where under control. They are no longer Web issues, but standard Clarion issues.
All that now needs to be done is:
What are the things we need to track?
We need to know if the user wants to filter the page. A variable, FilterString, holds the user's selection from the "Select" File Drop. It defaults to "ALL."
We need to know the "pointer" to the low end of the page being displayed as well as to the high end. MinNDX and MaxNDX (old friends from CPD) handle this.
Similarly, we need to know how many records to display per page (NbrPerPage with a default value of 7).
We do not need to know the select sort order. The Option Group can be checked for its current value. However, a user might change only the sort order...
What I decided to do is save the filter so that I could check if it was changed (SaveFilterString). I did the same for the sort order so that I could check if it was changed (SaveSortOrder).
These two variables allow checking whether the user only changed the filter or only changed the sort or both. When the filter is changed, I automatically sort because sorting a queue is so fast that processing time is negligible.
Oh, yes. We need a queue.
There are three choices: (1) define the queue so that it stores only a unique ID for the record. We could use the code shown above to retrieve the actual records required. The selected sort order could be used to determine which file key to use. (2) Have the queue store record IDs and data required for sorting. This could significantly reduce code by not having to check for keys. (3) Define the queue so that it contained all record fields. This would allow all processing to work against the queue.
Because the actual size of each record is small, under 200 bytes/record, and I did not expect more than a few hundred records (only "the best of the best"), I went with option (3).
When the app is first opened, there is no filter, so
FilterString = 'ALL' SaveFilterString = 'ALL'
And,
MinNDX = 1 MaxNDX = NbrPerPage
While we could have hard-coded MaxNDX to its default value, using the NbrPerPage variable gives the option at some later time of using a Cookie to store and retrieve the user's preference.
The only thing to worry about is the case where there are fewer records that MaxNDX/NbrPerPage.
If MaxNDX > Records(FileQueue) MaxNDX = Records(FileQueue) End
Next, since the app is just opening, there can be no "previous" records, so:
Disable(?Previous)
and
If Records(FileQueue) <= NbrPerPage Disable(?Next) End
prevents the Next button from appearing when there are few records in the file. (Remember, in IC, a disabled control is hidden.)
One more thing we need to take of...
Building the Queue
Ensuring the queue is built is standard Clarion stuff:
BuildQueue Routine
!Create processing queue
Free(FileQueue)
Set(FIL:ID_Key)
Loop Until Access:Files.Next()
Clear(FIL:Record)
If Upper(FilterString) <> 'ALL' !FILTERED?
If Upper(Clip(FIL:Category)) <> Upper(Clip(FilterString))
Cycle
End
End
FQ:ID = FIL:ID
FQ:Description = FIL:Description
FQ:Author = FIL:Author
FQ:URLPath = FIL:URLPath
FQ:FileName = FIL:FileName
FQ:FileType = FIL:FileType
FQ:FileDate = FIL:FileDate
FQ:Category = FIL:Category
Add(FileQueue)
End
Do SortQueue
A few things here are worth pointing out.
First, because I decided to build the queue whenever the filter changes, I need to be prepared to build the queue multiple times. So the code is placed in a routine (write once, execute many).
Second, I need to handle the user switching from a filtered view to an unfiltered view. "ALL" indicates a no filter selection; so, if FilterString is anything other than "ALL," check the file record against the selection criterion. Thus, a single routine handles all possible cases of filtering.
Third, by re-creating the queue on a filter change, I do not need to account for records in the queue that do not qualify for display. All queue records are eligible for display. This makes calculating whether the forward and/or back button should be displayed much easier.
Actually, the app originally built the queue only once and filtered directly from the queue. It ran like that for a few months. There was, of course, a very substantial increase in the amount of code required to keep everything in sync. But in tracking down a bug, I found that rebuilding the queue did not result in any performance penalty. Given the small size of the file and the large size of the server's memory, this isn't really unexpected.
Finally, the queue is sorted. The user may have changed both filters and sort orders at the same time.
Even if not, the overhead of sorting a few hundred records in a queue is negligible.
This is the core of making the application work, knowing that the user selected something and what was selected. Did the user press the Apply button? If so, ACCEPT() cycles and we can check whether there is a new filter, a new sort order, a new number of records per page or the user just pressed Apply for fun.
Since we know the previous filter (stored in SaveFilterString) and the previous sort order (in SaveSortOrder), it is easy to determine:
!rebuild page and/or queue MinNDX = 1 If FilterString <> SaveFilterString Do BuildQueue End SaveFilterString = FilterString If SortOrder_ <> SaveSortOrder Do SortQueue End SaveSortOrder = SortOrder_ Do BuildPage
Note that the filter and sort order are "re-"saved each time Apply is pressed. This sets up the next button press (if the user actually does change a variable).
I don't actually check whether NbrPerPage is changed here (I suppose I really should). So, the penalty for pressing Apply without changing anything is a page build and a round-trip to the server. NbrPerPage is checked in the BuildPage Routine.
BuildPage is not a new routine, though you may not recognize the name. This is the loop that writes the HTML discussed in the November article (the final code is available for download).
Very standard stuff, here, just pick up the value of the Option Structure (SortOrder_):
SortQueue Routine Case SortOrder_ Of 'Date' Sort(FileQueue,-FQ:FileDate) Of 'Developer' Sort(FileQueue,FQ:Author) Of 'Description' Sort(FileQueue,FQ:Description) End
This routine is only call if a new filter has been selected or if
SortOrder_ <> SaveSortOrder
Code re-use may be very efficient, but not having to write code at all is ultimately efficient. Filtering was simply built into the queue build. All we need to determine is:
This is the hard part; lots of "i" dotting and "t" crossing to be done.
This is hard because there are three places we need to check the relative location in the queue/file and determine the contents of a page: at initialization, before building the page and after building it.
Initialization was discussed above. Straightforward stuff here.
Why do we need to check before building the page? This is required by the fact that we change the value of MinNDX (the starting point for the next page) whenever the forward or back buttons are pressed. We need to know if this value still in range.
After the page is built, we need to check whether (and which) buttons should remain visible.
Before building the page: When the back button is pressed:
MinNDX -= NbrPerPage Do BuildPage
And, when the forward button is pressed:
MinNDX += NbrPerPage Do BuildPage
If the user pressed the back button, MinNDX is decremented by NbrPerPage. Thus, MinNDX could become less than 1. If the user pressed the forward button, MinNDX is incremented and could become greater than the number of records, as could MaxNDX. In either case, if a variable goes out of range ... GPF city. So:
If MinNDX < 1 or MinNDX > Records(FileQueue) MinNDX = 1 End
re-displaying from the beginning of the file.
Now we can set MaxNDX and, again, check that there are sufficient records:
MaxNDX = MinNDX + NbrPerPage - 1 If MaxNDX > Records(FileQueue) MaxNDX = Records(FileQueue) End
This calculation is based on the fact that simple addition (MinNDX + NbrPerPage) does not account for the initial value of the starting point (MinNDX). For example, at program load (when MinNDX is 1 and NbrPerPage is 7), MaxNDX would be eight (8). That is, addition works on cardinals but we need ordinals.
After building the page: If MinNDX is greater than 1, then there are records in the queue before the records displayed on the current page. So, the back button should display:
If MinNDX > 1
?Prev{Prop:Enable} = False
Else
?Prev{Prop:Enable} = True
End
If the calculated MaxNDX is less than the total records in the queue, there are records after those displayed on the current page. So, the forward button should display.
If MaxNDX < Records(FileQueue
?Next{Prop:Enable} = False
Else
?Next{Prop:Enable} = True
End
"Best of breed." An excellent concept (Ok, I admit that there was one zoom I did use. The original Minolta 40-80 was an exception - I heard it was designed by Leitz).
Use an HTML designer to create an HTML template. Then forget about it.
Use Clarion to access data and provide event handling. Then forget about this, too.
Using Clarion to write your HTML for you allows you to concentrate on how the interface should work.
Sounds an awful lot like how most of us use Clarion for creating standard Windows apps...
Copyright © 1999-2009 by CoveComm Inc. All Rights Reserved. Reproduction in any form without the express written consent of CoveComm Inc., except as described in the subscription agreement, is prohibited.
Clarion Magazine ISSN 1718-9942
One year: $169
(includes all back issues since '99)
Renewals from $119
Two years: $269
Renewals from $219