Filtering Browses on the Fly

by Steve Parker

Published 1999-01-01    Printer-friendly version

This should be easy but many Clarion developers are still having trouble implementing dynamic filters in ABC. There are too many scenarios, too many different ways to filter browses for a comprehensive treatment of the subject. Instead of trying to cover every possibility, we shall examine the core techniques. Using these as a basis, you should be positioned to apply them to you own ... ah, particular circumstances.

But, first a word from our interface ...

Too many developers get hung up worrying about the user interface for runtime filtering. Not counting the QBE Template in C5, there are, in fact, only three user interfaces.

First, you may set up a multi-tabbed browse, each with its own unique filter. Second, you may place entry fields (including file drops and radio buttons) on the Tabs. Third, you may create a separate window procedure to capture filter criteria. That's it.

The first doesn't really count as runtime filtering since the user doesn't have any control over the display.

On-Tab entry fields are suitable for simple filters. For example, using a file drop of job types, a user can view all jobs in "Accounting" or "Civil Engineering" (assuming, of course, that the data entry form used the same file drop to ensure that the strings will match). Using INSTRING() behind an entry control, you can do a free-form (a/k/a brute force) keyword search.

A separate window (or, even, set of windows) is suitable for more complex filters, with larger, more variable expressions. You may call such a window before your browse, in ThisWindow.Init after the parent call or from a button on the browse.

The point at which you call this criterion window determines whether or not your user can create a second query. If called before the browse, the user will return to the criterion window when exiting the browse (though, if you so decide, this can be short-circuited). A button on the browse clearly functions in a similar manner. Only calling the window from ThisWindow.Init inherently bars the user from multiple queries. (You will find an excellent example of this sort of implementation on Arnor Baldvinsson's knowledge base at www.icetips.com .)

What you are going to do with such a window is to prime global variables to be read in the browse procedure, create parameters to be passed to the browse procedure, or create the entire expression to be passed to the browse. Which method you select is entirely up to you.

So much for the user interface.

Implementation: Fast and Dirty

So, you've created your interface and have a mechanism to capture an expression. How do you inform your browse that you want to use that expression as a filter?

Without a doubt, the easiest way is to use the built-in Record Filter prompt, if your filter can be written as a non-conditional expression (or function) that resolves to True or False. Found behind the Browse Box Behavior button on the Procedure Properties worksheet, you may use this prompt in a variety of ways.

If you have a fully formatted expression, you may create a string containing it and type that variable's label here. More typical is to type the expression into the filter prompt and use your interface to prime the variables in it.

Filter1.gif

Variables and user created functions named here must be bound. Standard Clarion functions do not require a BIND.

The big drawback to this technique is that you cannot use the Embeditor to edit your filter. A big plus is that your filter is guaranteed to be generated at the right place in your final code and the browse will be refreshed as required.

There is an interesting variation on this technique (I originally got this from Mike Hanson, early in CFD3.0's development). Suppose your filter expression is something like:

LIS:Job_Interest1 = JCO:Job_Interest

and thatLIS:Job_Interest1 was originally filled fromJCO:Job_Interest (i.e., the two strings, if there is a match at all, will match exactly). Typically your browse will be empty when first called or whenever there is no value inJCO:Job_Interest. You can make the browse show all records when the source variable is empty:

NOT JCO:Job_Interest or (LIS:JOB_INTEREST1 = JCO:JOB_INTEREST)

Complex filters

More complex filters can be handled in BRW1.ValidateRecord (ABC templates -- where "BRW1" is the browse object -- Local Objects ... BRW1 ... ValidateRecord) or in the ValidateRecord embed (Clarion templates). (In Process template procedures, the corresponding ABC method is ThisProcess.ValidateRecord, ThisReport.ValidateRecord in Reports; the TakeRecord method may also be used but is better suited for processing the record)

Filter3.gif

Filter2.gif

Because you have full access to text editing, you can create filtering code as complex as you wish (or can read and make sense of in several months time). Notice in both figures, a function call is used to filter records: IncludeRecord() and IsTagged(). Because these functions are called within the body of the procedure's code, they do not need to be bound. Only variables and functions in the Record Filter prompt require BINDing.

BINDing is required for variables and developer-created functions "used in an expression string for either the EVALUATE procedure or a VIEW structure's FILTER attribute" according to the LRM. These embeds do not fit this requirement. Making life much easier and avoiding the dreaded "801 - No Bind" error message.

The important thing to remember is that the ValidateRecord embed (Clarion templates) is in a Routine and that BRW1.ValidateRecord (ABC templates) is a procedure (ah, method). This means that when a record fails to meet your filter criterion in the ValidateRecord Routine, you EXIT. In the ValidateRecord method, you

RETURN RECORD:FILTERED

or

RETURN RECORD:OUTOFRANGE.

The obvious benefit of this technique is that your filters can be just as Case- or condition-ridden as you want. The downside? Lots of typing (I have created filters, using this method, one-half to two-thirds of a page in length).

The ABC way

Both simple and moderately complex filters can be created using the .SetFilter method. Because the expression will be used in the filter attribute of a View, you do need to BIND variables and functions referenced here.

If you use the Record Filter prompt in an ABC browse and check the generated code, you will find your filter as the parameter of a SetFilter method call. And, if you create a multi-Tabbed browse with multiple filters, you will find a CASE structure of SetFilters. (This is what is known as "efficient use of resources.")

This suggests a ready method for creating "button filters," that is, filters triggered by a button press. Indeed, if you check out my java-free FAQ app, you will infer (correctly) that this is just what I do.

In the button press, I set the filter variable, e.g.:

FilterString = 'C4/ABC/.html'
Do CheckKeyWord

Of course, any kind of entry field, anything that posts an Event:Accepted can be used.

Then, invoke the filter:

CheckKeyWord  Routine
 If KeyWord <> ''
   FilterString = |

'Instring(Clip(Upper(Keyword)),Upper(FAQ:Description),1,1)'
   BRW1.SetFilter(FilterString)
   !ShowAll = False
   StatusString = 'Selected FAQs'
 Else
   BRW1.SetFilter('')
   StatusString = 'All FAQs'
   Set(FAQ:ProductKey)
   Case FilterString
   Of 'CW2.x'
     BRW1.SetFilter('(Clip(Upper(FAQ:Product)) = ''CW2.X'')')
     StatusString = 'CW2 FAQs'
   Of 'IC'
     BRW1.SetFilter('(Clip(Upper(FAQ:Product)) = ''IC'')')
     StatusString = 'IC FAQs'
   Of 'C4/ABC/.html'
     BRW1.SetFilter('(Clip(Upper(FAQ:Product)) = ''C4/ABC/.html'')')
     StatusString = 'C4/ABC FAQs'
   Of 'DCT'
     BRW1.SetFilter('(Clip(Upper(FAQ:Product)) = ''DCT'')')
     StatusString = 'DCT FAQs'
   Of 'SQL'
     BRW1.SetFilter('(Clip(Upper(FAQ:Product)) = ''SQL/ODBC/.html'')')
     StatusString = 'SQL/ODBC FAQs'
   Of 'C5/EE/.html'
     BRW1.SetFilter('(Clip(Upper(FAQ:Product)) = ''C5/EE/.html'')')
     StatusString = 'C5/EE FAQs'
   End
 End
 ThisWindow.Reset(1)

This particular code segment also recognizes a filter set by an entry field (If KeyWord <> '') and sets the filter to do a brute force search for the keyword. If there is no KeyWord, it checks for the value set by the last button pressed and filters accordingly.

At the same time, you will note, it also sets up a display string. Because the app from which this code is taken does not use Tabs (if it did, the templates would do all of this for me), this provides the kind of user notification that would normally be provided by the Tab label.

A "ShowAll" condition is also easily implemented with the code above. If ShowAll (a Byte variable) is checked:

Clear(KeyWord)
Clear(FilterString)
Do CheckKeyWord

The KeyWord condition will fail, as will the Case FilterString. So, no filter will be set and the window will be refreshed. Voila, all records!

No Records

By default, if there are no qualifying records to display, the browse will be empty. While not user hostile, this isn't exactly user friendly either. Presenting a MESSAGE() is more considerate.

The problem is "Where do I present the MESSAGE()?" It won't do, for example, to present a no records message when the browse is first opened and/or filter criteria have not yet been entered.

This is complicated by the fact that if your application will use the Internet Connect extensions, some embeds that work in Windows do not perform as expected when running in a browser.

The messaging issue is further complicated because there is no obvious ABC embed for that purpose.

My most common circumstance is an entry field on the browse. For years, I've used the Control Event Handling After Generated Code ... Accepted embed for that control successfully:

Select(?Browse:1);ThisWindow.Reset
If ~Records(Queue:Browse:1)
  Message('No jobs matching selected criteria were found.' &|
         ' Make another selection or try another view.', |
         'No Match')
Else
  Post(Event:ScrollTop,?Browse:1)
  Enable(?Change:2)
End

The appeal of this embed is that there are no conditions to check or Cases to worry about. When the control is completed, either the queue contains records or it doesn't. And the code can be simply copied to embeds for entry fields on other tabs.

This actually still works in ABC. But, it works only in Windows, not in IC. In IC, if you switch to a Tab filtered on an entry field, everything appears to work. But, if you switch to a second Tab filtered this way, you will get the "No Records" message before getting a chance to complete the entry field. You will also get the message on closing the browse.

Thinking this through, there is an embed that is available only after the queue has been filled. This is the After Browse Total Loop. There are, however, two significant issues with using this embed.

First, performance. If your filter triggers a key, the total loop will execute quickly. If not, performance is likely to degrade in proportion to file size as the view engine may reset by reading the file from disk.

Second, you now need to check for more than Records(Queue:Browse:1) or BRW1.Records(). You need to know which Tab is active and which entry field was completed. Why? Because this method (the total loop is inside the ResetFromView method) is called whenever the browse changes (e.g., when the filter changes). But, it does not know which filter is active.

Case Choice(?CurrentTab)
Of 2
  If Clip(GLO:JobInterest1) <> Clip(SaveString) and |
    ~Records(Queue:Browse:1)
    Message(' ...
  End
  SaveString = GLO:JobInterest1
Of 3
  If Clip(GLO:Job_Type) <> Clip(SaveType) and |
     ~Records(Queue:Browse:1)
    Message(' ...
  End
  SaveType = GLO:Job_Type
End

In the code shown, I use an additional check to ensure that the browse is refreshed only when the filter condition changes, not simply when the Tab changes.

Finally, there is the RTFM solution. "Making the Transition to ABC" spoon feeds us the correct embed:

filter4.gif

Local Objects ... BRW1 ... ResetQueue, after Parent call.

Again, because this method is ignorant of the currently active Tab and the most recently completed entry field, the CASE structure shown above is necessary. But, because we are filtering the View, we do not risk having to read the entire file from disk as the Total Loop might.

This embed works in both Windows and IC. Further, it has the virtue of being findable; six months from now, should you need to maintain this code, the Total Loop is probably one of the last places you'd look for it.

Summary

One of the standard bits of advice is "look at the generated code." ABC may not be as simple as "ABC," but a peek at the generated code certainly makes dynamic filtering a lot easier. Yes, there are i's to dot and t's to cross, but done once, you'll find somewhere between few and no limits to what you can do.

Printer-friendly version

Reader Comments

To add a comment to this article you must log in.

 
 

Search

 

Advanced Search
Topical Index

Related Articles

Subscribe to
ClarionMag

One year: $169

(includes all back issues since '99)

Renewals from $119

Two years: $269

Renewals from $219

More Info

Subscribe Now!

ClarionMag Blog

RSS Feeds

Updates via Email

Enter your Email


Powered by FeedBlitz

Quick Links