Tech News Back Issues Issue: 121202

Introduction to the Omnis Web Client: Part 6, Minimising the number of instance variables using iRow.Field as datanames.

By Dr Caroline Wilkins
iB2B Systems Ltd

www.ib2bsystems.com
caroline@ib2bsystems.com

In the previous three newsletters, we created an Omnis library, an Omnis database, programmed a startup task that will open a session to that database and created a schema in the library to represent a table in the database. We built a web client application that consists of a Sidebar controlling the pagenumber of a PagedPane component that enables a user to enter records into a table from a remote form. Last time, we added a facility to upload JPEG images to the web server using the remote form.

If you want to skip the stages in the previous newsletter, you can download the zip file RPJPEG.zip containing RescuePet.lbs and RescuePet.df1 (libraries & datafiles may need to be converted in Omnis). The numbering in the this issue follows on directly from the last issue. [Note that the table class T_Dog had gone missing in the last newsletters download, so please download RPJPEG.zip again if you havent been following the whole tutorial.] Make sure that you either put your library and datafile in c:\RescuePet\ directory or change the path to the datafile in the Startup_Task $construct method:

Set hostname {C:\RescuePet\RescuePet.df1}

In this newsletter, we will look at a way to improve the application.

So far, we have used a seperate instance variable for each field in the "Add New Dog" screen. Whilst this demonstrates how a row variable can be assembled from a collection of instance variables, it is not very practical for a larger application (where there might be a large number of data fields) and entails quite a bit more code than is strictly necessary. Instead, we are going to replace those instance variables with iRow.Field.

24. Variables

24.1 Confirm that you have already created a variable called iDogRow. You should have done this in order to facilitate the insert method previously.
24.2 Click on the Id, Name, Age, Gender and Size data fields in turn and change their datanames to iDogRow.Id, iDogRow.Name, iDogRow.Age, iDogRow.Gender, iDogRow.Size and iDogRow.ShelterId respectively
24.3 Leave the dataname for the Picture field as it is for now.

25. Row variables

25.1 The code behind the "Enter" button currently looks like:

On evClick
  Do iDogRow.$definefromsqlclass('Dog')
  Calculate iDogRow.Id as iId
  Calculate iDogRow.Name as iName
  Calculate iDogRow.Age as iAge
  Calculate iDogRow.Gender as iGender
  Calculate iDogRow.Size as iSize
  Calculate iDogRow.ShelterId as iShelterId
  Do iDogRow.$insert()
  Calculate lTempPath as con('C:\InetPub\wwwroot\rescuepet\images\',iDogRow.Id,'.jpg')
  WriteBinFile (lTempPath,iImage)

As you are not going to be using the iId, iName, iAge, iGender, iSize and iShelterId variables, you should cut this down (or just comment out the unnecessary lines) to:

On evClick
  Do iDogRow.$insert()
  Calculate lTempPath as con('C:\InetPub\wwwroot\rescuepet\images\',iDogRow.Id,'.jpg')
  WriteBinFile (lTempPath,iImage)

26. The secret to success for this approach

26.1 If you were to test the form and try and insert data at this point, the insert would not work. This is because the iDogRow variable has not been defined. If you put a breakpoint on the insert line and inspect the iDogRow variable, you will verify that the variable has no column structure and certainly no data. You might try reinstating this line:

Do iDogRow.$definefromsqlclass('Dog')

Whilst this will give the iDogRow variable column structure, there will be no data. Comment the line out.

26.2 The secret to making this iRow.Field approach work is to define the row variable in the construct of the remote form. It is then defined before it gets used in the application. Go to the construct method of the remote form and add these two lines after the block of code that defines iSideBarList:

Do $tables.T_Dog.$sqlclassname.$assign('Dog') Returns #F
Do iDogRow.$definefromsqlclass('Dog')

26.3 Make sure you have removed or commented out this line behind the 'Enter' button:

Do iDogRow.$definefromsqlclass('Dog')

If you leave it in, it will wipe the contents of iDogRow and the user data will be lost before the insert gets a chance!

27. Tidying up

27.1 Test that the insert method now works. If it doesn't, make sure that the two lines of code defining iDogRow are in the construct method. Check also that the table class T_Dog exists in your library and that its sqlclassname property is set to 'Dog'. Make sure the button has the evClick event enabled. Set a breakpoint just after the 'On evClick' line and step through. If there is a problem, hopefully stepping through the method will give some indicator as to where the problem lies.

27.2 Once you are satisfied that the insert is working correctly using just iDogRow.Field technique, you can delete the unused instance variables. Open up the method editor and right click in the 'Init Val/Calc' column of the Instance Variable tab pane. Select 'Delete Unusued Variables'.

27.3 Click 'Select All' and then 'Cancel'

The iRow.Field technique is an elegant and efficient way of handling the data from a large number of data fields. I would like to thank Mike Ternasky for showing me this technique.I think you will find it preferable to having lots of individual instance variables.

If all has gone to plan, you should have a library that looks something like RPRow.zip. (Libraries & datafiles may need to be converted in Omnis.)

 


 

Keystroke Events

By David Swain
Polymath Business Systems
www.polymath-bus-sys.com
dataguru@polymath-bus-sys.com

This article is a continuation of a series started three issues ago on "Special Event Types". These are event handling options that we must "turn on" to use at either the library or the individual field level. For more background, read these articles first if you haven't already. This article focuses on keystroke events.

Breaking Down Data Entry

Just as the mouse event types we examined in the last three issues of Omnis Tech News allow us to break a click into component parts (mouse up and mouse down) and respond to them more precisely (with the help of other functions, event parameters and constants), keystroke events allow us to work with the component actions of data entry to a field. The primary actions we perform during data entry are the keystrokes we type, but we can also place an insertion point somewhere in an entry field and we can select a portion of the fields contents using the mouse or the keyboard (the arrow keys in combination with the Shift key). There are runtime properties of an entry field that we can use to specify insertion point and text selection details that work well with our keystroke event handling techniques.

The keyevents Property

We turn on the ability to detect keystroke events by setting the keyevents property of a field (or a library if we feel we must use this kind of event globally) to "kTrue". As with mouse events, it is not enough to simply put an "On evKey" block in the fields $event method if this event category has not been activated for a field. This was made an "optional" event tracking mechanism because of the tremendous amount of event "traffic" it can produce (an event cycle for every keystroke the user types!). While the computers that are currently on the market (new) have no problem handling this, older, slower machines may notice the additional load. I find it best to use this facility on an "as-needed" basis for only selected fields in my work for this reason.

On evKey

As with any other event type, we detect the keystroke event, evKey, using an On command in a $event or $control event handling method. This one event handling variable is used to detect any kind of "effective" keystroke, that is, one that can perform some action on its own. Simply pressing and releasing the Shift, Control or other "modifier" keys does not trigger such an event. We do, however, have tools for detecting that such keys are also held down when an "effective" keystroke is performed. We will examine these a little later on.

pKey

The evKey event is accompanied by two event parameters besides the ubiquitous pEventCode: pKey and pSystemKey. If a "normal" (printable) keystroke is pressed, the pKey parameter contains the actual character associated with that keystroke. Think of it as an "inspection area" for the character the user is trying to enter. So it has a one character string value and we must work with it as such. We can use asc(pKey,1) to determine its decimal ASCII value. We can concatenate it with string we wish or perform any other string-related operation with it.

Since this character is still held "in detention" awaiting the decision of our code as to whether it can join its fellows in the contents of the current entry field, those contents have not yet been affected by the keystroke. We will discuss the implications of this a bit further on in this article.

pSystemKey

If a "non-printable" keystroke is performed, pKey remains empty, but pSystemKey is populated with a value. this value is slightly different from that we discussed above for pKey. The value generated for pSystemKey is a number that is used to indicate which "system" key was pressed. It is not an ASCII value, as many system keys don't have an ASCII equivalent, and some are even platform-specific or not available on all keyboard configurations.

Some, but not all, of these keystrokes have "Keyboard" constants associated with them. These can be found under the "Constants" tab of the Catalog.

Here is a summary of the pSystemKey values I have detected and their associated keystrokes and constants:

Value Keystroke Constant (if available)
1-15 F1-F15 Function keys (when not otherwise used)  
17-20 Up, Down, Left and Right Arrow keys kUp, kDown, kLeft and kRight
21-24 PageUp, PageDown, PageLeft and Page Right kPUp, kPDown, kPLeft and kPRight
25 Home kHome
26 End kEnd
27 Tab  
28 Return (sometimes labeled "Enter")  
29 Enter (often on numeric keyboard)  
30 Backspace/Delete kBack
31 Clear (numeric kaypad) kClear
32 Escape  
34 Forward Delete kFwdDel
35 Insert ("Help" on my Mac keyboard) kInsert
53 Context key (Windows only) kContext

Some of these keystrokes (Tab, Return, Enter) generate their own events (evTab, evShiftTab and evAfter), but the evKey event is detected before those other events.

Modifier Key Variables vs. Modifier Property Constants

To further define exactly what takes place during a keystroke event, there are a number of hash variables that can be used to detect the state of each modifier key. These are keys like Shift, Control, Alt, etc. that have no utility on their own, but are used to access other characters or actions in association with a "basic" keystroke. These variables act like Boolean variables (although they are actually numeric). They have a value of "0" when their corresponding modifier key is not in use and a value of "1" when it is held down. These variables are sometimes called "key modifier flags" as well as "key modifier event variables" because they "signal" the state of a modifier key.

For those who have thoroughly succumbed to the siren song of Omnis Notation and who have also been convinced by others that Omnis "hash" variables are intrinsically "bad" and should not be used, let me save you many hours of frustration and spurious bug reports. Yes, there is a set of "Key modifier" property constants (kControl, kShift, etc.), but they are used for an entirely different purpose. They are used to specify the modifiers needed for menu line keyboard equivalents when we use Notation to dynamically add or modify menu items. They have very large, fixed integer values that are not affected by any user action. They are not suited for use in detecting whether a modifier key is held down when a keystroke event is generated. I have also seen it expressed by some that the creators of Omnis Studio "should" give us event parameters to use for this rather than those filthy hash variables. Let me be clear: There is nothing wrong with hash variables and they ARE the tools we must use for this purpose. While I may (minimally) agree that a "pControl" or "pCommand" might be useful additions (if only for the sake of consistency), we already have tools that work brilliantly for this purpose, and the reality is, this is what we've got.

That said, there is only a subset of these "modifier key event variables" that we need to consider for use with keystroke events. We don't need to detect "Shift" since the keystroke itself is a specific ASCII character (either upper or lower case for alpha characters), so detecting "Shift" in addition to the keystroke would be redundant. On the Macintosh, the "Option" key (the exact equivalent to the "Alt" key) is used to access "higher" ASCII values directly through the keyboard, so the #OPTION and #ALT key modifier flags (which can be used interchangeably) are also excluded from this discussion on that platform. This leaves us with the #COMMAND and #CONTROL key modifier flags (also interchangeable), which are necessary to distinguish "command" keystrokes from "data entry" keystrokes so we don't unnecessarily exclude legitimate "power user" actions while filtering which characters are allowed for a field. The first example below demonstrates this.

Before or After the Fact

As with most other kinds of events, keystroke events trap the users intention or attempt to perform an action, allowing our code to determine whether or not to allow the action to proceed, to redirect it to some other action, or to act as though it never happened. But sometimes we may want to perform some additional action in our code as a result of a certain action actually taking place. While this may seem a subtle distinction, it has real consequences in our programming. To allow the action to proceed but keep us in the event handling method for that action, the creators of Omnis Studio have given us the Process event and continue command. As the name indicates, this command allows the user action to actually take place, but keeps method execution going in the current event cycle. The result is that we are then in the position of dealing with the result of the user action rather than deciding whether the action should be allowed.

We will use this to advantage in the final example in this article.

Field Content vs. Variable Value

The keystroke event, when reporting a "printable" keystroke for an entry field, indicates an attempt to modify the contents of the current field. This does not directly affect the value held in that fields associated variable. These two items (the contents of the field and the value of the associated variable) are separate and distinct entities, although they are tied to each other through the use of the dataname property of the field. The entire first issue of the "new" OmniScience technical journal is devoted to explaining all the implications of this, but this fact must be mentioned here as it affects how we must approach one of the examples later in this article.

Information is transferred between these two containers in two ways: When the focus leaves an entry field (on an "evAfter" event), the contents of the field are copied into the value of the fields associated variable. If any conversion is required (since the field contents is always a string value), this is automatically performed by Omnis Studio. If this conversion changes what must be displayed in the field to properly represent the converted value (for example, if the associated variable is a date or numeric variable), the field contents are automatically reassigned to reflect the proper format for this value. Going the other direction, if the value of a variable is changed "in the background" (by a calculation or record location accomplished by a method command, for example), the field contents that must represent this value are updated using a "redraw" command (either the actual Redraw method command or the notational $redraw() method applied to the appropriate item or group).

The implications of this knowledge are that if we begin manipulating the field contents in a way that requires the field to be redrawn (like changing the selection of part of those contents), we must also involve the associated variable in our manipulations by calculating a new value for it that reflects what is happening with the contents of the field. Otherwise we will be transferring the value of that variable back into the field contents before the user's data entry has had any affect on that value.

With greater power comes greater responsibility...

Controlling Insertion Point Position and Selection of Field Content

We also have tools for manipulating the cursor within a data entry field. While this is not strictly part of "keystroke events", it is related closely enough to our use of those events to warrant a brief mention here. There are two runtime properties of an entry field named "$firstsel" and "$lastsel". These can be used to specify either a discrete insertion point or to select a range of characters within the fields contents. Unlike most notational properties that take immediate effect, these properties (when used to gether for highlighting characters) require that the target field be redrawn. "$firstsel", when used with an "entry" field, indicates the first character position in a selection block. "$lastsel" (as you might suspect) indicates the last character position in the selection. We can set them in either order, but we must then issue some sort of "redraw" command for the resulting selection highlight to be displayed. Since the redraw facility also updates the field contents with the value of the associated variable, use of these properties requires the use of the associated variable as well. (You'll see what I mean as we build the final example for this article...)

So What Can We Do With All This?

Our use of this technology is only limited by our imagination. There are many things we can potentially do with it. Let's start simple and work our way into some really interesting techniques.

Disallow Inappropriate Keystrokes

Consider an entry window that contains a field that requires one of a limited range of characters. For example, we might have a "gender" field that should only accept an "F" for "Female" or an "M" for "Male". (To maintain consistent data entry, we might also set the uppercase property to "kTrue".) All other keystrokes are invalid. A traditional way to handle this is to test the value entered into the field and only allow the value entered to remain if it passes muster by testing it during an "evAfter" event. But this still allows invalid values to be entered. What if we just don't allow them?!

To do this, we can tesst the keystroke before it actually hits the field using keystroke events. We first set the keyevents property for the field (or the global one for the library) to "kTrue". We can then include the following code (or some variation) in the $event method of that field:

On evKey
 If len(pKey)&not(#COMMAND)&not(pos(pKey,'FfMm'))
  Sound bell
  Quit event handler (Discard event)
 End If

Notice that there are certain keystrokes that we want to allow even though they are not an "F" or an "M". For example, we want to allow system keystrokes (like "Backspace"), so we insist that pKey contains something before we test the keystroke. We also want to allow command keystrokes, so we allow any keystroke accompanied by the #COMMAND (or #CONTROL) flag. But if a keystroke is a "normal" key and is not accompanied by the #COMMAND modifier, it must be an upper or lower case "f" or "m" or it will simply be discarded (with a little "beep" to notify the user there is a problem with their action. Even if we have set the uppercase property to "kTrue" (which converts a lower case keystroke to an upper case character in the field contents), we must still make sure that the lower case keystrokes are allowed.

Keyboard Control of a Tab Pane

Entry fields are not the only ones that can react to keystroke events. Consider a tab pane field. Wouldn't it be nice to allow our users to select a tab just by pressing a key? We can do this! All it takes is a little ingenuity.

First, we must make our tab pane field able to receive the focus on all platforms. We do this by setting the showfocus property (and, of course, the enabled property) of the tab pane to "kTrue". We then turn on the keyevents property as well and put the following code in the fields $event method (assuming we have four tabs in the field):

On evKey
 Switch pKey
  Case 1
   Calculate $cfield.$currenttab as 1
  Case 2
   Calculate $cfield.$currenttab as 2
  Case 3
   Calculate $cfield.$currenttab as 3
  Case 4
   Calculate $cfield.$currenttab as 4
 End Switch

Yes, we could have done this instead:

On evKey
 If pos(pKey,'1234')
  Calculate $cfield.$currenttab as pKey
 End if

But the first construct allows us to perform other actions on a case-by-case basis (like setting the current field, assigning default values, etc.) which might prove more clumsy using the second construct. The user can now tab into the tab pane field and select a tab by entering its number from the keyboard.

'Clairvoyant' Data Entry

Now for something a little more challenging! A full explanation of this important technique, including its setup and an array of variations, is a highlight of the first issue of my "new" OmniScience technical journal. I have demonstrated this technique at all the international Omnis conferences this past year and in many of my classes long before that, so it has "made the rounds" and has popped up in a variety of places. If you haven't seen how to perform this technique, here is the simple version:

Perhaps I had first better explain what this technique does. "Clairvoyant" data entry simply means that the application "anticipates" what the user intends to enter into a field and fills in its "best guess" as to the rest of the value on each user keystroke. This "guess" is based upon a list or database in the background that either contains "standard" entries for the field or "recent" or "past" entries that have been made into such a field (depending on the needs of the application). The substring that comprises the "guess" portion of the entry is "selected" so that the next user keystroke replaces it, thereby maintaining a consistent string of the exact keystrokes actually entered by the user. We can see this in action in many web browsers and other applications today, but the most interesting and extensive use of this is found in a financial management program named "Quicken". While we enter the name of a "Payee" in this system, not only does Quicken populate that field with the first Payee (from a list of previous used Payee entries) whose name begins with the character string we have typed so far, but it fills in most of the fields on the form with the contents of the most recent involving that Payee, so entering periodic identical (or nearly so) transactions requires only a few keystrokes!

Our example here is not quite so extensive, but will give you a feeling for the power of this technique. First we need to list the tools we will need:

This technique requires a "source" list of anticipated entries and a method that can search the list to find the first line where a specific column value begins with a string supplied as a parameter to the method. It also requires a window with an entry field that has keyevents set to "kTrue". In our simple example, we will supply a clairvoyant guess if one is available, but still allow any string to be entered into the field. One variation would only allow standard entries and would disallow anything not on the list. Another variation would add any new entry to the list for use at a later time (the Quicken technique). Refer to OmniScience for these and many other variations.

Let's begin by creating a new Window class named "clairvoyantEntry". We then place an entry field on the window and set its keyevents property value to "kTrue". We need two instance variables for this window: unit (Character 20) and unitSourceList (List). to make this example most portable, let's put all the code in the entry field:

Create a $construct method for the entry field and include this code:

Begin reversible block
 Set current list unitSourceList
 Clear sort fields
 Set sort field unit (Upper case)
End reversible block
Define list {unit}
Add line to list {('inch')}
Add line to list {('foot')}
Add line to list {('yard')}
Add line to list {('meter')}
Add line to list {('mile')}
Add line to list {('kilometer')}
Add line to list {('centimeter')}
Sort list

This established the "background" list used to supply the clairvoyant "guesses". In the $event method for the field, put this code:

On evKey
 Process event and continue
 If pSystemKey=27|pSystemKey=kBack ;; tab or backspace
  Quit method
 Else
  Calculate original as $cfield.$contents
  Calculate currLength as len(original)
  Do method $lookupunit (original) Returns scratchpad
  Calculate unit as con(original,mid(scratchpad,currLength+1,len(scratchpad)))
  Do $cfield.$redraw() ;; sets $contents
  Calculate $cfield.$firstsel as currLength
  Calculate $cfield.$lastsel as len(unit)
  Do $cfield.$redraw() ;; displays selection within $contents
 End If

This is used to both capture and process each user keystroke. Let's break this down. Since we want to allow each keystroke, we use the Process event and continue command to let the keystroke be included in the field contents before we go any further. This just makes our job easier as it avoids our having to concatenate the value of pKey with the field contents at every turn (which we would have to do if we didn't use this command). if the user keystroke had been either a "tab" or a "baclspace", we simply quit the method as there is no other processing required for these actions. (You may find a few others that also fit in this category.)

But for the "normal" keystrokes, we continue. We now require three local variables: original (Character), currLength (Short integer) and scratchpad (Character). These are used to manipulate intermediate values in the following way: We first place the current field contents (which includes the most recent keystroke that triggered this event cycle) into original for later use. We also put the length of original into currLength (more for demonstration and readability purposes than anything else). We then call a method that looks up a unit in our source list and returns a value (either a string that begins with the value in original or an empty value).

We use this returned value to add the "guess" substring to the end of original. We do it in this way to preserve the exact string of characters entered by the user rather than imposing the specific use of case from the source list. (We'll do that as we leave the field.) since we must eventually redraw the field to set the $firstsel and $lastsel selection state around the "guess" substring, we need our associated variable (unit) to be in synch with the field contents. This is because when we redraw the field to set the selection, the very act of redrawing puts the current value of unit into the field contents. If it is not the same string, our technique won't work! So we calculate a new value for unit combining the value set aside in original with the necessary substring from scratchpad. We then redraw the field to update the contents. Now we assign values to $firstsel and $lastsel to encompass the "guess" substring and then redraw the field a second time to set the selection highlight.

The subroutine we call to search the source list might look something like this:

Begin reversible block
 Set current list unitSourceList
 Set search as calculation {upp(searchSource)=upp(mid(unit,1,len(searchSource)))}
End reversible block
Search list (From start,Do Not Load Line)
If flag true
 Quit method lst(unit)
End If

It has a parameter named searchSource that receives the value of original passed to it from the $event method. This is then used to locate a line in the source list. If such a line is located, the value of unit from that line is returned. Otherwise an empty return value will result.

It is a good idea to perform a final selection on evAfter that this time uses the exact value (including case) from the source list for final display in the field (and storage in the associated variable unit). This code is a bit simpler than that in the On evKey portion of $event:

On evAfter
 Do method $lookupunit ($cfield.$contents) Returns scratchpad
 If len(scratchpad)
  Calculate unit as scratchpad
  Do $cfield.$redraw()
 End If

This does not touch the value of unit if the user has entered a value not found in the source list, but puts the value in "standard form" (the exact value from the source list) if the final value is found there.

Further Reading and Examples

I will post an example library within the next few days with these and other exercises on my web site (http://www.polymath-bus-sys.com/oscihome.html) under the "Free Example Code" heading. I hope you will find these examples useful.

If this subject interests you, you may wish to explore it further in my technical journal, OmniScience. This journal is returning to publication for those who have an interest in understanding the inner workings of Omnis Studio and the techniques we can derive from this knowledge. The issue on "Variables and Fields" is due to be completed any day now. For more information and details on how to sign up for automatic delivery (this is a fee-based publication), check my web site. Have no fear, I will certainly continue to contribute the same quality of content here in Omnis Tech News as always!

Next Time

In the next issue of Omnis Tech News after the holiday break, we will explore "Status" events. Enjoy your holidays!

 
© 2002-2003 Copyright of the text and images herein remains with the respective author. No part of this newsletter may be reproduced, transmitted, stored in a retrieval system or translated into any language in any form by any means without the written permission of the author or Omnis Software.
Omnis® and Omnis Studio® are registered trademarks, and Omnis 7™ is a trademark of Omnis Software Ltd. Other products mentioned are trademarks or registered trademarks of their corporations. All rights reserved.