[Omnis-Newsletter] Omnis Technical Newsletter
omnis-news-admin@omnis.net
omnis-news-admin@omnis.net
Wed, 26 Jun 2002 17:25:20 +0100
June 26th, 2002
========================================
UNSUBSCRIBE OPTIONS: You have been sent this email because you have directly
signed up for, or expressed an interest in receiving a technical newsletter
when you downloaded an evaluation of Omnis Studio, or registered Omnis
Studio via a magazine promotion. If, however you feel you have received this
email in error you can unsubscribe as well as change your subscription
options at www.omnis.net/newsletter.
N.B. If you subscribed by checking a box on one of our forms, you will not
have received a password. You will need to submit your email address at
www.omnis.net/newsletter and select the 'email me my password' option on the
next page in order to receive this.
========================================
WELCOME!
Welcome and thank you for subscribing to the Omnis Technical Newsletter.
Published fortnightly, it is intended for Omnis developers of all levels and
experience, for those people evaluating Omnis Studio, or for developers
moving from a similar tool. We think you'll find the content both
interesting and useful for your Omnis development needs and hopefully it
will help you become more productive in Omnis application design.
In the first article in this newsletter, Geir Fjaerli continues with his
exploration of 'Triple O' (Omnis Object Orientation) by looking at
inheritance and shows how this can save you time and effort in the long run.
In the second part, David Swain looks at ways to switch the functions of the
'Tab' key and the 'Enter/Return' key in response to a very particular
customer request.
You may find it easier to work through the exercises and examples in this
newsletter by printing it out before you begin.
CONTENTS:
-About the Authors
-Triple O - Object Oriented Omnis, Part 4, by Geir Fjaerli
-Omnis Training: new European dates.
-Alternative Data Entry Strategies, by David Swain
-Copyright and Unsubscribe details
========================================
About the Authors
The Omnis Technical Newsletter contains high quality content from two
leading and well respected Omnis developers, Geir Fjaerli and David Swain.
Geir Fjaerli is based in Norway and has been an Omnis professional developer
for many years as well as a Regional Sales Manager for Omnis. He is
currently working freelance again developing a range of products (in Omnis
of course) including his Prophet5 sales and customer relationship management
solution, now available for Mac OS X. Geir is a regular contributor to the
annual Omnis Developer conferences in the US and Europe, speaking about
Object-Oriented programming in Omnis and SQL development, amongst other
things.
David Swain is the founder and president of Polymath Business Systems, for
many years a leading provider of Omnis training. His expertise in Omnis
programming and his ability to make complex concepts understandable are
recognized throughout the worldwide Omnis community. His current schedule of
Omnis Studio training classes can be found on his web site at
http://www.polymath-bus-sys.com.
========================================
Triple O - Object Oriented Omnis, Part 4.
By Geir Fjaerli, Sunshine Data
Email: geir@sunshinedata.net
Web: www.sunshinedata.net
In our previous OO chapter we looked at messaging in theory and in Studio.
Today it is time to look at the final of our six requirements:
THE CLASS TREE IS AN INHERITANCE HIERARCHY.
Having exploited the subject of messaging and instantiation, it is now time
to move to yet another of the fundamental concepts of Object Orientation.
This brings us to requirement 6: "The class tree is an inheritance
hierarchy."
This means that a class inherits one or more properties from another class.
We call the parent class Superclass, and the child a Subclass. So the
Subclass inherits from a Superclass. Typically the subclass will inherit
methods and variables, for GUI classes also fields.
In its purest sense requirement 6 implies that every class is a descendant
of the same origin, the root of the inheritance tree. Languages like Delphi
and Smalltalk will have a master class that all other classes are derived
from.
Omnis as you know operates with distinct class types, like windows, reports,
menus, toolbars etc. So even if they all share a common set of properties,
we do not have access to any common master class. (There still is one, but
we do not really have any use for it at the 4GL level.)
One important consequence is that Omnis does not support mixed inheritance,
a class of one type cannot be a subclass of a class of another type. So you
cannot make a menu inherit from a window, or a report from an object class.
SO WHY INHERIT?
Well, basically we inherit because it makes things easier in some way.
Either easier to write, or easier to manage, or whatever. The keyword is
reuse. If some functionality or design is used in many circumstances, it
makes sense to isolate the common part, and then just add the variations as
needed in each case.
Some of the obvious benefits of inheritance are:
* You can reuse code, rather than have to rewrite it every time you need it.
* Several modules can share common code
* It is easier to enforce a common interface for all modules
* It enables you to build reusable components
* It allows for very fast prototyping, because you can build on an existing
class and don't have to bother with the trivial stuff.
* It eases information hiding: The developer need not be concerned with how
the superclass is constructed, as long as he understands the interface.
WHEN DO YOU INHERIT?
Obviously, for inheritance to make sense, two factors need to be present:
1. The subclass must use something from the superclass. If it doesn't, there
is no purpose in using the superclass.
2. The subclass must add something to the superclass, or change something in
it. If not we might just as well use the superclass itself.
So usually either the subclass extends the functionality of the superclass,
or it specializes it for a certain purpose. Actually, there are a number of
subtly different reasons for subclassing, including but not limited to:
Specialization: The subclass is the superclass specialized for a specific
purpose. Say your superclass does basic file read & writes. Your subclass
may add data types, say the ability to write a text string.
Specification: The subclass implements behavior defined in the superclass.
This allows the superclass to define a common interface to be shared by a
number of different classes. The superclass then doesn't hold any
functionality.
Construction: The subclass constructs new functionality using superclass
functionality.
Extension: The subclass adds new (related) functionality to that of the
superclass.
Limitation: When certain functions of the superclass are needed, but others
aren't suitable for the subclass. This is usually only desirable when
someone else has supplied the superclass, if you write the code yourself you
should rather put the common parts in a new superclass, and make both the
others subclasses of that one.
INHERITANCE SEMANTICS
There are two different approaches to behavior inheritance. One is called
American semantics, because it is used in some of the languages of American
origin, like Smalltalk and C++. Here the child class methods replace the
parent ones.
The other is used in Simula and Beta, and is therefore called Scandinavian
Semantics. In these languages the child method refines or adds to the
parent.
Omnis uses the American approach. Normally the child method replaces the
parent one. Arguably the complexity added by the Scandinavian semantics is
rarely ever worth it.
However, in Omnis the command "Do inherited" will run the parent method
anywhere within the subclass method. You can also force a message to go to
the superclass by using "Do $cinst.$inherited.$methodname()". Similar
options are available in other "American" languages, like C++ and Java.
THE PRINCIPLE OF SUBSTITUTABILITY.
Even if a class is indeed a subclass of another, there is nothing that says
it has to behave in the same way, since it may override the superclass
behavior. But one may argue that ideally it should do so. This ideal is
known as the Principle of substitutability. It says that the subclass should
always be usable instead of the superclass in any situation. This implies
that the subclass, while adding to the superclass functionality, still
should support the superclass one fully. The reverse is naturally not true,
since the subclass adds to the superclass, which means it has functionality
that the superclass hasn't.
If indeed it is substitutable for the superclass, the subclass is known as a
subtype. Note that there is no relationship necessary between these two, you
can very well build a subtype without using subclassing by building the
subtype from scratch. Obviously you won't very likely want to.
To find what kind of relationship there is between your super- and subclass,
you can use the following tests:
If you can say that your subclass IS A superclass, then your subclass is a
specialized form of the superclass. Just as you could say, "A sedan is a
car", "Mount Everest is a mountain".
Here you would probably use a subclass in the traditional sense.
The other alternative is that your subclass is different from the
superclass, but uses parts of the superclass. This would be a HAS A
relationship. "A car has an engine", but clearly the engine is not a car,
and the car is not an engine. Here it would make more sense to use a
subwindow or an object class to slot in the "engine" into the main class or
car.
STUDIO - WHAT IS INHERITED?
So, what can you actually inherit from a superclass? In Omnis there are some
limitations imposed by the tool. It depends on the type of class you are
subclassing. The following simple table suggests what is inherited for each
type.
Methods Variables Properties Fields
Window x x x *
Menu x x x *
Toolbar x x x *
Report x x x
Task x x
Table x x some
Object x x
* Fields are inherited, but cannot be overridden.
As you can see, there are some important limitations to remember. First of
all, a window that inherits fields (buttons, entry fields etc.) cannot
override the properties of those fields. So you cannot move them around or
change their methods. If you need to do this, consider using subwindows
instead. Second, reports do not inherit fields at all, unfortunately. It
would make sense to set up a master report holding say logos and letterhead
etc., and then let other reports inherit this layout. Hopefully this
enhancement will find its way into later versions of Studio.
INHERITANCE: STUDIO - HOW TO CREATE?
Studio actually offers two ways of making a subclass, and they produce
slightly different results.
1. You can take a class you have already created, and later make it a
subclass of another class by setting its superclass property. If you do
this, the subclass will inherit methods, vars and fields from the
superclass, but not the properties. The reason is why assume that for an
existing class you will have set these properties the way you want them, and
do not want these to be overwritten by the ones of the new superclass. You
may of course select a property and choose to inherit it anyway.
2. The other approach is to start with your superclass, and make a fresh
subclass by selecting "Make subclass" from the class menu or context menu
for that class. This way you will also inherit the relevant properties of
the superclass, as the subclass is new it won't have any set yet. Again, you
can later choose to override any individual property.
MULTIPLE INHERITANCE.
One last item on inheritance. On the subject of inheritance we often hear
the expression "multiple inheritance." This means that any subclass inherits
from more than one superclass. Say combining two menus into one.
Omnis does not support multiple inheritance. A class can only inherit from
one superclass.
It is quite possible to work round this, for example, for window classes by
using subwindows rather than subclassing.
IS OMNIS OBJECT ORIENTED?
So, that concludes our quick intro to Object Orientation and Omnis Studio. I
believe we have made a pretty good case for our claim to "OO'ness". While
there are shortcomings in Studio, they are not a major drawback for the kind
of things we want to use it for. And also remember that many of these
shortcomings are not architectural, but rather a result of practical
evaluations of the usefulness vs. the resources it would take to implement
them. It is not so much a case of "cannot do that" as "have not yet done
that".
DON'T USE IT.
OK, so now we have looked on how to construct OO stuff in Studio. Now let us
look on the Dos and Don'ts. These are obviously my own personal opinions and
experiences, people will object - and have - to much of this. It is kind of
like politics or religion. ;-)
First let us focus on some of the parts of Studio that don't lend themselves
well to OOP.
CURRENT RECORD BUFFER (CRB).
First of all there is the CRB. The CRB is a global memory for the entire
library, and simply cannot be instantiated. I know that a lot of people
struggle with this one. Messages to the Omnis Underground mail list describe
how people build instances of the CRB by pushing values in and out of lists.
Trust me on one thing: Trying to make this work is a lot more work than
doing it the right way, using Schemas and Tables and Instance vars.
(Note: Some people may object. They will also point out that when using the
web client, each get a unique CRB. True, but that is under Studio control,
you cannot do the same in your own code.)
So you do not want to build your OO applications using the CRB. This means
bye-bye to file classes and search classes. Both are for all practical
purposes identical to their Omnis 7 file and search format ancestors, and
directly linked to the CRB, so don't use them.
CODE CLASSES.
The second thing you want to avoid is Code Classes. I would for the sake of
OO purity argument go so far as to say that Code classes should be banned
from the product, and never implemented in the first place. Their only valid
purpose is for people who want to develop new non-OO Omnis 7 type code in
Studio, and if that is what you want, you may be reading this by mistake.
Code classes do nothing that object classes don't do better, except help you
continue any bad programming habits you may have.
Lets move on to other global structures and references to avoid.
ENTER DATA.
First of all: Enter Data. I shall not start a discussion on modal vs.
modeless interfaces, that is a separate discussion beyond this subject. But
Omnis only has a single global Enter Data, and there is no sensible way you
can combine this with multiple instances. Consider having two windows. Omnis
will allow you to start an Enter Data from each. Only problem is that the
Enter Data is not linked to the window, instead they're nested on the global
stack. So if you initiate an Enter Data from Window 1, then another Enter
Data from Window 2, if you return to Window 1 and hit OK, it is still the
second Enter Data you terminate.
Instead you should use the new $enterable property in Studio 2.0. This
allows you to control the enterable property for individual window
instances. Note that this is not a modal enter data, so it won't stop method
execution like the original enter data.
CALL METHOD.
Second, do not use Call method. This is the old global procedure call of
Omnis 7, and will not observe your difference between public and private
methods.
GLOBAL REFERENCES.
Third, avoid any global references like $topwind etc. More on this in a
minute.
GLOBAL VARIABLES. And finally, resist the temptation to use global variables
to pass value between instances. Reserve your class variables for
housekeeping purposes, if any. Any other state info should be instance vars,
and parameters and values should be passed though messages.
So, these were a few "don't use them" items. On to the Do's.
COMPONENTS: THE PURPOSE OF OBJECT ORIENTATION.
Of all my advice, this is the simple ground rule: Think components even if
you don't plan on reusing them. A well-constructed component makes sense in
any environment, and you will appreciate why when you return to your code
later.
COUPLING AND COHESION.
A final piece of definition belongs with our Dos and Don'ts: When writing
objects and modules, we have two important goals. To make the module as
independent as possible, and to make it as focused as possible. Our aim is
to make something that is easily manageable, maintainable and easily
portable to new environments.
The terms coupling and cohesion describes these two goals. Coupling
describes the relationships between modules, cohesion describes the
relationships within a module.
We want our modules to be as independent of the environment as possible.
Ideally a component should be slotted into any application and perform as
intended. So we want minimal coupling.
Cohesion on the other hand, means that any part of our module should be
related to one specific purpose. If our module consists of different
functions that do not have any common parts (variables, methods or other),
they probably should be split into two or more separate modules. So we aim
for strong cohesion.
COMPONENT SUMMARY.
So consider the following rules:
In object orientation, big is not beautiful. You should make each part of
your app small and focused.
You should avoid duplication of code and functionality whenever possible, if
you find the same function in multiple parts of your app, start
investigating the use of an object class or inheritance.
You should aim for substitutability, and avoid partial inheritance. The
ideal as we defined earlier was that that wherever you would use the
superclass, you could just as well use the subclass, without any noticeable
difference.
And: Don't peek! Don't use shortcuts or globals. Don't access the internals,
that be methods or variables, directly from the outside. Observe the
principles of messaging and information hiding.
COMPONENT INDEPENDENCE.
A component should not have to know its environment to function. Ideally a
component is assigned its task, does its job and returns eventual values.
Obviously there are cases where the component needs to know who assigned the
task. Say if you make a toolbar or subwindow that holds controls for a
window, it needs to know that window. If so the clean approach is to send
the window ID or ref as a parameter to the $construct of the component. This
way it will work whether the toolbar installed in the window or on the main
toolbar.
Also, observe the application chain of command. Think of your application as
an army. The soldier reports to the corporal, who reports to the sergeant,
who...
A solider does not go directly to a general, (unless he is that general's
driver or something) or to another troops sergeant. One sergeant may request
the assistance of another sergeant and his troops, but he can only ask, only
the general (or other higher rank) can tell them to do something. You tell
your subordinate, you agree with your peer, you request (and accept a no)
from your superiors.
Your components should behave in the same way. A window can instruct its
subordinates, say fields and buttons in a window, but should send polite
messages to other windows and other instances. And if a window wants to
close the whole application, that should be done by sending a message up to
the controlling task which then sends the relevant messages and registers
any protests. This is especially important when considering functions like
$sendall.
OBJECT ORIENTATION: WHAT DOES IT TAKE?
So, we've seen now what Object Orientation is, and how it works in Omnis.
All that remains now is the "sales pitch". That is, why should you prefer
Object Oriented programming techniques to other methods? Must be because you
can see some advantage. Like any gain it doesn't come for free, but it is
worth the effort.
Remember, some of you are clearly biased. You have been using Omnis 7 for
many years, or maybe other tools. And what you know is always easier. This
is why kids learn faster than grown ups, they do not have to 'unlearn' a lot
of old habits, prejudices and plain stubbornness. And why it may in fact be
easier to learn Studio for an 18 year old VB developer than an Omnis 7
developer.
But fear not, friends, there is still hope for us as well. I have been using
Omnis since '87, and I am hooked on Studio. You just have to focus on what
you gain, instead of what you seem to loose. If not, you end up comparing
apples and oranges. Like at the end of a Studio SQL session one developer
said: "I have to do all this instead of simply saying 'Single file find'"?
Clearly he was focusing of the definition and 2-3 lines of code this would
take compared to a single line. What he failed to see was the major
advantages coming from the Object Oriented bit, a general SQL object with a
clear interface, easily maintained and used everywhere. We've all been
through the "search my whole app for 'Find on KSEQ' and rewrite that code."
So you need to first look at the benefits, then you can measure these
against any disadvantages. If you start with the problems, that is probably
were it stops.
So you need to invest in OO, it takes time. Time to unlearn old habits and
learn new tricks, time to write new object-oriented modules.
WRITE FOR OTHERS.
This brings us to my main principle and final piece of advice:
You should always write your components assuming that a complete stranger
will have to take that component and use it somewhere, without having you
around to ask or help! Even if that is not the case, using that ideal will
produce better modules.
So, that was my little Object Oriented introduction. I hope it has been
educational and informative. As usual, comments and questions are welcome.
Geir :)
========================================
Omnis Training: new European dates.
Raining Data provides hands-on training for all levels of developer, from
beginners to experienced users. Omnis Studio training is provided in North
America, the UK and mainland Europe, including courses for Omnis Web Client.
We have recently updated our schedule for training in Europe, which includes
dates up to the end of 2002. In Europe, Omnis training is available in
Hamburg, Munich, Zurich, and Utrecht. Please go to the following pages to
check out the new training schedule and read course details.
Germany: Hamburg, Munich
http://www.omnis.net/training/gereng.html
Switzerland: Zurich
http://www.omnis.net/training/zurichschedeng.html
Benelux: Utrecht
http://www.omnis.net/training/benelux.html
========================================
Alternative Data Entry Strategies
By David Swain, Polymath Business Systems Inc
Email: dataguru@polymath-bus-sys.com
Web: www.polymath-bus-sys.com
I frequently get requests for assistance from other developers. (I am a
consultant, after all - it's my job!) A while ago I got one from fellow
Omnis developer Ron Harden who wrote:
"Do you know of a way to switch the functions of the 'Tab' key and the
'Enter/Return' key? I have a situation where a long series of numbers need
to be entered. The operators would like to do that with a 10 key number pad
with a 'Enter/Return' to change fields between number entries. And, the
'Tab' key would work OK for the 'Termination' of Enter Data Mode."
I found this to be both a reasonable request and a good subject for a Tech
News article, since many other developers will face similar problems at some
point in their work.
Notice that we are not simply being asked to convert the use of the "Enter"
key on the numeric keypad to a "Tab" action (a relatively simple thing to
do), but to "switch the functions" of the "Tab" key with those of the
"Enter/Return" keys. Apparently, some users at the client site prefer to use
the "Return" key (the "Enter" key on the main keyboard - the one above the
"Shift" key) with their thumb instead of using the numeric keypad "Enter"
key with their little or ring finger. Since the "Tab" key will not be used
to move the focus from field to field, it makes a good candidate for
signaling acceptance of a record. Interesting challenge!
The Client Is Occasionally Right
The immediate reaction to this request by some developers might be along the
line of "Why don't you teach those people modern data entry techniques?
Don't you know there are 'Human Interface Guidelines' that firmly state we
should use the Tab key to move the focus from field to field and the
Enter/Return key to signal acceptance of the entered data? Don't you think
you might confuse your more competent and properly trained users by
deviating from this standard?(!!!)"
While I might very well agree with these statements-posed-as-questions in
the most general case, this specific one highlights the need for "interface
consistency" on an even more basic level - that of practicality. If our
basic job is to create applications that make the work of our users easier,
we have to be open to understanding the way in which they actually work and
the historic reasons behind those techniques. While "computerizing" a
company can lead to many improvements in workflow efficiency, "their" way is
sometimes better than "our" way. Strict adherence to arbitrary (however well
thought out they may be) guidelines can sometimes make an application less
useful.
The very reason for the layout of a 10-key numeric entry device is to make
numeric data entry as fast as possible for a competent data entry person.
It's laid out the way electronic adding machines always have been (ever
since we stopped having to pull a lever to complete an entry and move on to
the next number). If you haven't experienced the exhilarating rush of being
able to enter numbers almost as fast as you can read them, this may not seem
like a big deal. But to a highly skilled numeric data entry artist, having
to move the hand containing the finger that is used to maintain (operator)
focus on the current figure being entered and use it instead to hit the Tab
key (or move the "entry" hand way over to the other side of the keyboard!)
to move the (computer) focus to the next field is an unnecessary and
outrageous burden to have imposed by an insensitive database application
designer.
The use of the "Tab" key to signal record acceptance is an interesting
twist, but it makes perfect sense in this situation. I can easily envision a
workflow where the user keys data with the right hand while using the left
hand to 1) mark their place on the current form being processed, 2) press
the "Tab" key to complete entry for the current form and 3) flip the page to
go to the next form. From a "time and motion" point of view, this seems very
reasonable.
Insisting on doing this the "right" way can get us into trouble. The (formal
or informal) operators' union will see to it that the application isn't
accepted and the client will simply choose another consultant - with good
reason! So let's work WITH our client on this one...
The Obvious Way Doesn't Work
At first glance it would seem that all we have to do is trap the
"Enter/Return" version of an "evOK" event and change it to an "evTab" event
and vice versa. We might even be so clever as to not trap these events at
the field level, but use the "$control" method of the window class to
universally trap those field level events in a single method for easier
maintenance.
Let's begin by creating a new window class named "numberEntry". We'll place
ten Entry Fields on the window, a "user defined" pushbutton labeled "Begin"
and "OK" and "Cancel" pushbuttons. Make the "dataname" property value of
each Entry Field the name of a numeric variable. (It's OK to use Hash
Variables here - I'm using #1 through #10 for convenience in testing - but
you're welcome to use Instance, Class or Task Variables, variables from a
Memory-Only File Class, or columns from a Row variable defined at any of
these scope levels if you prefer to create variables just for this test.) We
might also want to name these Entry Fields something consistent, like
"fld01" through "fld10".
For greater control, we'll use "modal" data entry (the "Enter data"
command). For this kind of entry we will most likely want to clear all
existing values on the window when the user begins a new record. This in
itself is a "mode", so "modal" data entry seems appropriate. Let's set the
"modelessdata" property of the window to "kFalse" and use the "Begin"
pushbutton to launch into "data entry" mode. (And if we set the
"canfocusbuttons" property of our library to "kFocus", the "space" bar can
be used to "click" the pushbutton to launch the process when the window is
"at rest" - more user convenience!) In this case, the "$event" method of the
"Begin" pushbutton should read:
On evClick
; method line(s) to clear existing data (depends on variables used)
Do $cinst.$redraw() ;; or your favorite redraw command
Enter data
; method lines to process finished entry
Our "OK" and "Cancel" pushbuttons will be disabled and grayed when not in
data entry mode and enabled and normal-looking when data entry mode is in
effect. This will help us gauge some measure of the success or failure of
our attempts at a solution to our problem. (If we are truly paranoid - a
reasonable trait for a programmer occasionally - we might test our window at
this point to be sure the "right" way of entering data works as expected
before we go messing around with it...)
We then turn to our class-level event handling methods. "evTab" is a
"field-level" event, but we can trap it "universally" in the "$control"
method of the class. The "Enter/Return" event is a subset of the "evOK"
event, which is a "window-level" event. This must be trapped in the "$event"
method of the class. In our first attempt to turn one event into the other
we might try the following:
$control
--------
On evTab
Queue OK
$event
------
On evOK
If #RETURN|#ENTER
Queue tab
End If
Now let's open a test instance of our window class, click the "Begin" button
and see how we did.
The immediate effect we will notice when testing our trial solution is that
we end up in an infinite loop! In fact, it's sort of a "pinball machine"
real-time effect that makes all fields and pushbuttons "light up" in their
turn. The only thing that stops this is closing the window instance, then
opening an instance of a different window with "OK" and "Cancel" buttons on
it and clicking one of those buttons to end the "Enter data" mode.
The problem with this technique is that each event produces the opposite
event, which produces the first event, etc. in "ping-pong ball" fashion
forever. While the result is entertaining, it isn't very useful, so we have
to try again.
But wait! Maybe if we discard the original event; that might somehow bypass
our problem. Since we're trying to "redirect" the flow of events, we want to
get rid of the original event and replace it with the new one. Maybe that
will help. (But admittedly we're just guessing here...) We can change the
code to:
$control
--------
On evTab
Queue OK
Quit event handler (Discard event)
$event
------
On evOK
If #RETURN|#ENTER
Queue tab
Quit event handler (Discard event)
End If
When we test this...it still doesn't work. This time the focus just remains
in the first field and flickers. In fact, the focus only leaves the field if
we manually click into another one with the mouse. This isn't even
entertaining, so we have to find a different way...
Can We Be Happy With "evAfter"?
Maybe we just chose the wrong point to redirect these events. After all,
they are both preceded by the "evAfter" event. Maybe we can just redirect
events based on the value of "pNextCode" during "evAfter". (But again, we're
just guessing...)
We can do all this trapping in the "$control" method of the window class
since the only "event" we are concerned with now is "evAfter" (a field-level
event). So let's give it another try.
For educational purposes, let's make a duplicate of our "numberEntry" window
class, name it "pinballEntry" and set it aside. Now we'll go back and modify
our original window class.
First let's remove the code we put in the "$event" method above. Then let's
replace the code in "$control" with the following:
$control
--------
On evAfter
If pNextCode=evTab
Queue OK
Quit event handler (Discard event)
Else If pNextCode=evOK
If #RETURN|#ENTER
Queue tab
Quit event handler (Discard event)
End If
End If
Now when we test our project...we get another failure. Trapping one event
and converting it to another still puts us into an infinite loop. Maybe we
need to trap the event a different way...
Trapping Keystrokes
Instead of trapping the actual events, perhaps we could trap the keystrokes
that yield those events. This requires setting the "$keyevents" property of
either the Entry Fields on our window or the entire library to "kTrue". This
turns on the ability to trap the "evKey" event.
Note that this is strictly a "field-level" event. A field must currently
have the focus (be the "$cobj") to receive this event. A window cannot
directly receive "evKey", but we can trap this event in the "$control"
method of the window class as we can for any other "field-level" event.
We can either turn this ability on globally (at the library level) or for
individual fields. As long as we use the "Calculate" command to set this
property's value to "kTrue", we can set the "global" property reversibly.
This allows us to only turn it on for a specific window instance, for
example. Do whichever suits you best.
The "evKey" event has two parameters in addition to the "pEventCode"
parameter (which all events possess): "pKey" and "pSystemKey". The value of
"pKey" is the ASCII character typed by the user if a "normal" key or key
combination (one associated with a printable character) was the last
pressed. Modifier keys (Shift on all platforms and Option on MacOS) help
determine this, but do not register as a keystroke by themselves (or we'd
have to be VERY precise about simultaneity!).
But if a "system" key is pressed, "pKey" is left empty (pressing the "Tab"
key does not give "pKey" an ASCII "9" value, for example) and we must look
to the value of "pSystemKey" to determine what the user did. The value of
"pSystemKey" is an integer that Omnis Studio associates with the "system"
keystroke. It is equal to zero if "pKey" has a non-empty value. Here is a
list of all the "system" key codes I have been able to determine:
1-15 the "F" keys (not all keyboards have all 15 and some operating systems
and auxiliary software may map these for special purposes, in which case
they don't return anything to "pSystemKey")
17 up arrow
18 down arrow
19 left arrow
20 right arrow
21 page up
22 page down
25 home
26 end
27 tab
28 return (sometimes "enter" on the main keyboard)
29 enter (either on the numeric keypad or next to the space bar on some
laptops)
30 backspace (sometimes "delete" on the main keyboard)
31 clear (on the numeric keypad)
32 escape
34 forward delete
35 insert (sometimes labeled "Help")
Of course, we are only interested in the "Return", "Enter" and "Tab"
keystrokes in this article, but I thought this might be a useful list for
you. Yes, there are a few gaps in the numbering and there may be some
obscure keys on some keyboards that correspond to those missing numbers, but
these are the ones I could locate (and even some of these are not
universally available, so don't base your application around them).
Using this facility, we can change the "$control" method of our window class
to read as follows:
On evKey
Switch pSystemKey
Case 28,29 ;; Return or Enter
Queue tab
Quit event handler (Discard event)
Case 27 ;; Tab
Queue OK
Quit event handler (Discard event)
End Switch
But even when we test this, we meet with failure. In this case, the window
appears to be "frozen" and we must go to some lengths to recover without
restarting Omnis Studio or rebooting the computer! (You did read this before
testing the code, didn't you?)
There are many permutations of the above techniques that we could try, but
they all give us similar results. They either yield a little light show like
the ones we've already seen, cause the cursor to remain in the same field
forever, or create a very quick "stack overload" condition that essentially
freezes the computer and requires at least a restart of Omnis Studio, if not
the entire computer! Our specific problem of converting a "Return/Enter" to
a "Tab" and vice versa seems doomed...
The Real Problem
The underlying problem is that setting the current event to a different one
causes the queued event to be triggered. If that event is also trapped by
our event handling methods in a way that triggers some other event that gets
trapped (ad infinitum), we get caught in an infinite loop. What we really
need to do is redirect to "different" events that have the same ultimate
effect but are not trapped as such. In this case, we need a way to "emulate"
a "Tab" event, an "OK" event and, possibly, a "Cancel" event. If we can do
this in other ways, perhaps we have a solution to our problem.
Queue Set Current Field
Rather than causing a "Tab" event to occur, why don't we just put the focus
in the appropriate field? We have a command named "Queue set current field"
that causes the focus to shift to a specified field, maybe we can cast this
in such a way that the "next" field is always chosen.
If we set up our window so that all the Entry Fields are clustered by their
"order" property values, there is a "$next()" operation we can perform on
the group of field objects on our window instance. If "$cobj" is a reference
to the "current object" receiving the "current event", then
"$cinst.$objs.$next($cobj)" points to the next object in field number order.
(We need to supply a notational reference to the "starting point" member of
the group as the parameter of the "$next()" method.) We can then use this
notation to retrieve any property value for the next field, such as the
value of "$name". So the command line
Queue set current field {[$cinst.$objs.$next($cobj).$name]}
will put the focus into the next field after the current one. If we want to
deal with the special case of an "Enter/Return" occurring while the final
entry field ("fld10" in our example) has the focus, we can properly redirect
the focus with the following conditional block (with which we will replace
our "Queue tab" command line):
If $cobj.$name='fld10'
Queue set current field {fld01}
Else
Queue set current field {[$cinst.$objs.$next($cobj).$name]}
End If
Quit event handler (Discard event)
Notice that we must still discard the current event.
So the first part of our "$control" method now looks like this:
On evKey
Switch pSystemKey
Case 28,29 ;; Return or Enter
If $cobj.$name='fld10'
Queue set current field {fld01}
Else
Queue set current field {[$cinst.$objs.$next($cobj).$name]}
End If
Quit event handler (Discard event)
End Switch
We aren't quite ready to test it, though, because we still have to work out
how to change a "Tab" keystroke to something that signals Omnis Studio that
we want to accept the entered data.
Enter Data With Condition
The thing we can do here is to set new acceptance and rejection conditions
for the data entry cycle. By default, the "Enter data" command responds to
"OK" and "Cancel" events (generated by either clicks on specialized
pushbutton fields or from keyboard equivalents), setting an internal global
variable known as "The Flag" to "1" or "0" as a result. But "Enter data" now
has an option that allows us to set our own conditions for termination of
data entry mode and we can use this option to great advantage here.
The optional "termination condition" of the "Enter data" command is an
expression used to terminate the data entry cycle as soon as it evaluates to
"kTrue" or a non-zero numeric value. For our project, we could set up two
Boolean variables of "instance" scope named "accepted" and "rejected" to act
as our data entry termination flags and, instead of a simple "Enter data"
command, our "Begin" pushbuttons "$event" method would include a line like:
Enter data until accepted|rejected
We also have to initialize these variables to "kFalse" as we begin the
method and deal with the "rejected" value as a special case after "Enter
data" as we usually do with an "If flag false" code block. The resulting
"$event" method for the "Begin" pushbutton then becomes:
On evClick
; method line(s) to clear existing data (depends on variables used)
Calculate accepted as kFalse
Calculate rejected as kFalse
Do $cinst.$redraw() ;; or your favorite redraw command
Enter data until accepted|rejected
If rejected
; data rejection and cleanup operations
Quit method
End If
; data acceptance and storage operations
Now all we have to do is determine where within our event handling methods
to set the value of each of these variables to "kTrue". The most obvious
place is where we previously tried to queue an "evOK" event from a "Tab"
keystroke in the "$control" method of our window class. A simple
modification of the code and it now looks like this:
On evKey
Switch pSystemKey
Case 28,29 ;; Return or Enter
If $cobj.$name='fld10'
Queue set current field {fld01}
Else
Queue set current field {[$cinst.$objs.$next($cobj).$name]}
End If
Quit event handler (Discard event)
Case 27 ;; Tab
Calculate accepted as kTrue
Quit event handler (Discard event)
End Switch
And if we now test it...it works great! We have now achieved the basic
functionality our client requires. But there are still a couple of loose
ends...
So What If They Click The "OK" Button?
Since "OK" and "Cancel" events no longer signal the end of data entry, we
need to also redirect them by setting a "kTrue" value for the appropriate
termination condition variable. We can do this in the "$event" method of the
window class as follows:
On evOK
If not(#RETURN|#ENTER)
Calculate accepted as kTrue
End If
On evCancel
Calculate rejected as kTrue
Now the novice user of our window also has a means of accepting or rejecting
data entry using the mouse. The "evOK" and "evCancel" events are still
reported, they just don't directly effect "Enter data" when we use the
optional termination condition expression.
Other Options
We might include some other useful options for our users while we're at it.
For example, we might decide to set up the "Escape" key to emulate clicking
the "Cancel" button. Or we might set up the "Clear" key on the numeric
keypad to zero out the value of the current field (in case the user notices
they made a mistake). Here are those two additional blocks of code that go
in the "Switch/Case" block of the windows "$control" method:
Case 32 ;; Escape
Calculate rejected as kTrue
Quit event handler (Discard event)
Case 31 ;; Clear
Calculate [$cobj.$dataname] as 0
Do $cobj.$redraw()
Quit event handler (Discard event)
Once we get a technique that works, we're only limited by our imagination
(and common sense) as to what options we offer the user.
Go Ahead And Try It
Whether or not you're a whiz at numeric data entry, try entering a set of
numbers onto the window to see how easily this can be done using this
technique. And don't just type a bunch of random digits on the numeric
keypad trying to "prove" that users could use the "Tab" key as easily as the
"Enter" key - find a form with some numbers on it and do your best to enter
them as skilled numeric data entry operators would: by reading the numbers
from a form (which may require one hand to keep your place on the hard copy)
and entering the data with one hand on the numeric keypad.
(Notice the little bump on the "5" key. This helps us "center" our fingers
over the keypad. Feel for this with your middle finger and use that finger
for the "8", "5" and "2" digits. Use the index finger for the "7-4-1" column
and the ring finger for the "9-6-3" column. The "0" is usually positioned so
you can easily use your thumb for that key and the "decimal" is usually in
line with the "9-6-3" column.)
In Closing
If my leading you down the garden path in the early examples of this article
concerned you, I apologize. A bit of positive feedback I often get in my
classes is that many students enjoy watching the "problem-solving" process
and seeing how mistakes can be made as well as how solutions evolve. I
merely attempted to emulate that experience here.
Again, I would like to thank Ron Harden for the precise wording of his
request that launched this little odyssey. It certainly gave us a lot to
discuss! I hope it was worthwhile for you.
========================================
I hope you've found this issue of the Omnis Tech Newsletter both interesting
and informative. Please send me your comments and feedback, and include
suggestions for future articles if you like. We would like to hear from
you...
Regards,
--Andrew Smith.
Omnis Technical Newsletter Editor
Email: editor@omnis.net
========================================
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 Raining Data.
(c) Copyright Raining Data, Inc., and its licensors 2002. All rights
reserved.
Omnis(r) is a registered trademark and Omnis 7(tm), and Omnis Studio are
trademarks of Raining Data UK Ltd. Other products mentioned are trademarks
or registered trademarks of their corporations.
========================================
To unsubscribe from this newsletter or change your subscription options,
please go to:
www.omnis.net/newsletter