Dual-Hybrid Apps: The Details

by Steve Parker

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.

  • Digression: It takes another programmer to really appreciate these solutions. Over the past six years, I’ve had the privilege of sharing many of them with you while simultaneously paying my debt to Ken Weiss, Mike Hanson, Randy Goodhew, Louis Berman and Ron Eisner, among others. Thank you for giving me this opportunity.

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.

Say, what?

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.

 Figure 1
Figure 1

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.

 Figure 2
Figure 2

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.)

Design Considerations

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").

 Figure 3
Figure 3

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.

Queues

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:

  • · set up the page
  • · determine if a re-sort is needed
  • · determine if a filter is needed
  • · determine where records are relative to what is displayed

Data

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).

Setting up the Page

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.

Did Anything Change? The Apply Button

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).

Sort?

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

Filter?

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:

Where am I?

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

Summary

"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…

Printer-friendly version

 
 

Search

 

Advanced Search
Topical Index

Related Articles

Subscribe to
ClarionMag

One year: $184

(includes all back issues since '99)

Renewals from $134

Two years: $274

Renewals from $224

More Info

Subscribe Now!

ClarionMag Blog

RSS Feeds

Updates via Email

Enter your Email


Powered by FeedBlitz

Quick Links