[Omnis-Newsletter] Omnis Technical Newsletter
omnis-news-admin@omnis.net
omnis-news-admin@omnis.net
Wed, 19 Sep 2001 11:18:47 +0100
Omnis Technical Newsletter September 19th, 2001
========================================
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 the Lite
version of Omnis Studio. 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 describes how you can
build a window from scratch using the fields and labels in the Omnis
Component Store. In the second article, David Swain explores display and
calculated fields in conjunction with the pick() and pos() functions. 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
-Building part 9: The task window, by Geir Fjaerli.
-EurOmnis 2001: the European Omnis Developers conference
-Display Fields, 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.
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.
========================================
Building part 9: The task window.
By Geir Fjaerli, Sunshine Data
Email: geir@sunshinedata.net
Web: www.sunshinedata.net
----------------------------------------------------------------------
Hi, and welcome back to our Omnis Studio tutorial.
Here is our work so far:
Part 1: Hello World, our first little Studio application
Part 2: The Studio Classes. The classes are the building blocks of an
application, and include windows, reports, menus etc.
Part 3: The Omnis Integrated Development Environment. Here we looked at the
Component store, the Property Manager and the Catalog.
Part 4: The Data Structure. Including basic terminology.
Part 5: Creating schemas and database tables.
Part 6: Adding table classes and using our schemas in application windows.
Part 7: What did the wizard build, and a tour of the method editor.
Part 8: The logon window and object class.
You should have those handy, and your "tasks.lbs" library file.
As usual, I strongly suggest that you refer to these parts if you haven't
already, or if you feel uncertain about the details. From part 5 onwards,
each new part will be based on programming done in previous parts, so you
have to follow them all. (For back issues, please go to
www.omnis.net/newsletter and click on the Newsletter Archive link.)
So far, we haven't done much design work. Both of the windows we have in our
library were created using built-in Studio wizards, complete with fields and
code, and so on. In this issue, we are going to start creating one from
scratch. It is not as daunting a task as it may sound, Omnis is a 4GL -
fourth generation language - and lets us design by using a graphical editor.
We drag window classes into our library, and components into our windows.
Note that as we move on with the tutorial, we speed things up a bit. I won't
explain everything in the same detail anymore. So if you find yourself
saying "how do I do that?" you are most likely facing an issue explained in
an earlier issue. In the first part I would say "Open the Browser from the
View menu and double-click the class wEmployee to open it in design mode",
now I will just say "Open wEmployee in design mode", assuming that you know
how to do that. Also, when I say "right-click", I will assume you know that
this equals "option-click" on the Mac.
First, start Omnis Studio. Then locate and open the tasks.lbs library. If
you completed the previous part, you will now be prompted for a logon. As we
are using OmnisSQL with an Omnis datafile, we do no need a user name or
password. So just click the Logon button. It should log you on successfully.
If not, most likely it cannot find the datafile. When we used the wizard to
create the logon, it hardcoded the database path in the hostname in the
object class. Hardcoding obviously is not a great idea, we may change that
later. In the mean time, check the iHostName calculation in $construct of
oTask against the actual location of your datafile - "tasks.df1".
Note: If the logon fails, Studio may report "bad disk", do not take that
literally. It doesn't mean there is something wrong with your hard disk, it
just means Omnis is not able to locate the file where it expected it. But
that is probably because you moved it or renamed the folder.
So, assuming you have managed to log on, we are going to continue. You do
not need to be logged on to do the following, but you won't be able to test
it unless you are.
We are going to create a window for our task assignments. Following our
naming convention we would call that window wTask. However, if you look at
the classes in the Browser, you will see that there is a class with that
name already. That is our logon window, and was named so by the wizard
because our session is named Task. Class names have to be unique within a
library. That leaves us with two options: either call the task window
something else, or rename our logon window. We will rename the logon window,
because that gives me an opportunity to introduce you to the "Find and
Replace" function in Studio.
To rename the window class, right-click it in the Browser and select Rename
from the context menu. Name it wLogon and hit Return. A Yes/No dialog window
pops up and asks "Do you want to use Find and Replace to check for
occurrences of 'wTASK'?" (We call it a dialog window because you have to
answer yes or no before you can continue working. More on the different
window types later.)
Why do we need to look for this? Well, the name of our window may be used in
several places around the library. The logon window is opened from the
Startup_Task. If we rename the window, and the task still tries to open it
with the old name, it will fail. So we need to make sure that the new name
is used everywhere our logon window is referenced in the library. The Find
and Replace function helps us doing that, saving us the task of looking
through all our code to replace the name manually. Just think of the job
once you have a really big application, with maybe hundreds of classes.
So click Yes, and Omnis opens the "Find & Replace" window. Let us look at
how this window works. (Make sure you do not change anything, the default
settings is what we want for this particular instance. If you do, then no
big deal, you can close and reopen the Find window. If so, enter wTASK for
the Find, and wLogon for Replace with, and leave the rest as is.)
You will see that it has two tabs, one called "Find & Replace", and one
called "Classes". To the right is a number of pushbuttons. In the Find tab
there are two entry fields, one for the word to find, and one for the word
to replace it with. Then there are 4 checkboxes which indicates if you want
to make the Find cases sensitive or not, look for only whole words, and so
on.
The window lets us perform a Find only, either one occurrence at the time,
which will go to that place in the library; or all of them at once, in which
case you will get a list in which you can double-click to get to that place.
Or you can do a Find and Replace, again one instance at the time or all of
them. Using the Selected log lines only you can do a Find all, select the
instances you want to replace and only replace those. If you want to
restrict your Find to certain classes, you can do that by selecting them in
the Classes tab. If you click there now, you will notice that all the
classes in our library are selected, which is what we want. Notice the +
and - signs above the list of classes, clicking them will bring up a list
that lets you include or exclude classes of a given type.
OK, that explains the Find & Replace functionality in theory. Now it is time
to test it. As I said above, the window allows you to either just do a Find,
or to do a Find & Replace in one operation. It is often wise to do a Find
all first, this will bring up the log so that you can see that your replace
will affect the correct bits, then do the Replace all if you are satisfied
with it.
So click Find all. Omnis will open the log, with one found instance only. It
is the Startup_Task $construct, it is a method line and the text is "Open
Window instance wTASK/*" That sounds OK, so I click the "Find & Replace"
button to go back to the Find window. This time we click Replace all. The
log opens again now with an entry showing that it has performed a replace.
Notice the green dot in front of the line, indicating a successful replace.
To check that it actually did it, double-click the log line and Omnis opens
the Startup_Task at the correct position, and we can see that it now says
"Open window instance wLogon/*", as it should.
So now we have renamed the logon window, and can use wTask for our task
assignment window. Close the Startup_Task editor window and the Find and log
windows.
Now (finally) we can get on with making our task window. Open the browser to
show the classes. Then drag a "New Window" from the component store into it.
Name the new window wTask and hit return. Double-click the class to open it
in design mode. You see that it comes up empty, with no fields or buttons.
This time we are going to add them manually. Also the window is quite small.
We can set the window to any size we like, either by dragging its borders
(Windows) or resize box (Mac), or by setting the height and width properties
in the property manager. A couple of things to consider:
a. An application should have a consistent look and feel. Try to make the
users feel at home in all windows.
b. Think about your users hardware. If they all have the luxury of 19 inch
monitors, you have a lot of screen to play with. But if you cannot assume
that, be careful. Some may still have only 640 x 480 resolution, and the
Windows taskbar and Omnis menus etc takes a big chunk of that. I think most
developers assume an 800x600 resolution today. Personally I go for 1024,
knowing I will loose some users.
Our wEmployee window is 280 high by 575 wide, that is how the wizard created
it. So let us initially make wTask the same size. Open the Property Manager
and set the height and width properties. You can do that with the design
window open or while it is closed (by selecting it in the Browser).
Now we have our empty window. Time to fill it with some contents and
functionality. To find out what we need, let us look at the purpose of this
window. This is where we enter new tasks and assign an employee to that
task. So at the very least we need entry fields for the task information,
including task description, dates and type. Then we need a way to select the
employee. Also we do want to be able to edit an existing task, and even
delete one. Finally we need some way to locate a given task.
There are several ways to perform any of these functions. One may be better
suited to a given environment than another. A number of factors contribute
to your decision. Let us consider the best way of finding a record. You
could do that by finding on a given field/column, say last name, or by
building a list of all the records/rows and clicking the one you want. If
the number of record is small, selecting from a list is easy and intuitive,
and requires a minimum of user action. If there are hundreds or thousands of
records, a list becomes unmanageable, and may take too long to build. So a
Find function is better suited here. But a Find requires more user action,
he has to type in the find criteria and select the Find function. Obviously
the two can be combined, if the user types in "Smith" as the last name to
search for, the program could build a list of all persons named Smith.
None of this really affects our data structure, it is all in the user
interface. This means that we can select one solution now, and easily change
to another later should our requirements change. This is what we will do
here. We will make the window rather simple, with entry fields for the task
info, and a list for selecting the employee. The "Find" function will have a
separate list tab pane in the window to show the results of our finds. In
the beginning we will simply list all tasks, later as the number of tasks
grows we will add a Find function to filter them. This is how applications
evolve in real life. No application is complete from the start, if we aimed
for that we would never finish it. And only real life use can tell us what
we need anyway.
Ok, let us start adding the fields. Let us look at them, and see what kind
of field we want:
cID: This is our primary key which needs to be entered. A bit later we are
going to create a routine that automatically generates unique primary key
values for us, but for now we make it an entry field.
cTask: This is the task description, so we make that a wide entry field.
cDatePlanned: A date entry field. We shall see how Omnis aids in formatting
and completing dates for us.
cDateDone: The same.
cType: We made this a number field (integer.) Each number (1, 2, 3...)
corresponds to a given task type (Phone, meeting, letter). For now we are
going to hardcode the types, later we can use the numbers as the foreign key
pointing to a "Type" table.
sEmployeeID: This one is special. It is a foreign key field which holds the
primary key of the employee assigned to this task. (See part 4 of this
tutorial for an explanation of primary and foreign keys.) The key is just a
number and not really interesting to the user. So we won't display this
field on screen. Instead we will show the name of the user, and keep track
of the key behind the scenes.
We also need a way to select the employee. This is best done with a list.
Omnis has a number of different list types. Since we don't really want to
see all the employees, just the one selected for the task, we can use a drop
down list or combo box. The combo box is best suited for the job, as it has
an entry field linked to the actual name field, and a "Drop down" list
attached to make the selection. However, combo boxes are a Windows style
list, and not proper Mac interface. So we will use drop down lists on
Windows, and pop up lists on the Mac. Although they look and feel
differently, they behave the same way as far as our code is concerned.
The fields need to be linked to variables, so we need to set them up first.
In part 7 we examined the code that the wizard built for us. You should
revisit that chapter if you are unsure about any of the following.
We remember that we used variables of type row, defined from a schema or
table class. The row is a list with only one line, and a number of columns
as defined in the schema.
We need at least two row variables, one for the current task record and one
for the "oldrow" which is use in updates and deletes. And we need a list
variable defined from tEmployee to list the employees. So let us start by
adding these. Assuming you have the task window open in design mode,
double-click it to open the method editor. (You can also right click and
select "Class methods", or if the design window is not open, right-click in
the Browser and select Methods.)
In the variable pane in the top of the editor window, select Instance vars.
You will remember that an instance variable is "private" to a given open
window instance, and stays "alive" as long as the window is open. So this is
the appropriate variable type to use for our window fields. Add three
variables as follows:
1. iTaskRow type row, no subtype.
2. iOldRow, type row, no subtype.
3. iEmployeeList, type list, no subtype.
You may refer to the wEmployee window to see how this is done.
Then we need to define these variables from the appropriate table class.
Enter the following code in $construct:
Do iTaskRow.$definefromsqlclass('tTask')
Do iEmployeeList.$definefromsqlclass('tEmployee')
To enter the statements, click in either the method text field (just under
the comment line that say "; Enter your initialisation code here") or in
the commands list at the bottom left. Type "do" (without the quotes). Omnis
will locate the Do command and display the appropriate fields to complete
the command. Click in the Calculation field and type
"iTaskRow.$definefromsqlclass('tTask')" (again without the quotes). Tab out
of it and the command is completed. There is a field for a Return value, but
it isn't used in this case.
Click on the next line in the method text field, and repeat the process for
the employee list definition. We shall later add code to build the employee
list, but we will leave that for now.
The iOldRow does not need to be defined, it is calculated from iTaskRow and
gets both definition and values in that calculation.
Now we can start adding fields. Close the method editor and go back to the
design window. Open the Component Store and make sure you have the first
pane (Standard fields) selected. Click the Entry field icon (the first after
the arrow) and drag it into the window. You can also double-click the
component to add it to the window, and drag it in position. Position it at
around top = 20 and left = 120. You can position it by dragging it around,
by selecting it and using the "arrow" keys on your keyboard, or by using the
Property Manager. The reason we place it at left = 120 is we want to have
room for the label in front of it.
Next we need to link the field to our variable. This is going to be our
primary key field, which we named cID in the schema. We set this in the
Property Manager. Entry fields has a "dataname" property (second from top).
You may remember that when we looked at the wEmployee window, the format of
the dataname was RowVar.ColumnName. The row variable we defined in the
window, the column name is defined in the schemas. If you remember the
names, you can just type them in, in this case iTaskRow.cID. If not, the
Catalog comes to our rescue. While the cursor is placed in the dataname
property, open the Catalog. (From the View menu or F9.) First go to the
"Variables" tab, select "Instance", and double-click iTaskRow to insert it
in the property. Now click behind the variable name in the property and add
the period (.) that separates the row variable name and the column. Open the
Catalog again, this time go to the "Schemas" tab, select sTask and
double-click cID to add the column.
Now our field is linked to the row variable, and knows what information to
display. This is all we have to do to set up the field. There are a number
of other properties for an entry field, we will look at them later.
Now let us add a label. With the design window open, go back to the
Component Store and select the second pane (Background Objects). Select the
"label" component (the one with the "T" icon) and drag it into the window.
Position it in front of the cID field. You may use the small square
"handles" on it (while selected) to resize it, or use the Property Manager.
Make it start at left = 16 and make it about 90 wide.
Note: There are extensive interface guidelines published by Apple and
Microsoft to decide how these things would look and be positioned. Refer to
these for more information. The coming version of Studio, version 3.1, has
extensive support for styles which makes supporting both platforms a lot
easier.
The label is empty, so we need to add text to it. There are two ways to do
it: Either double-click the label to open a mini editor and enter the text,
then click outside it to close it. Or you can enter the text in the "text"
property in the Property Manager. Call it "ID:" (without the quotes).
One remaining issue: The label is white, while the window is grey. Depending
on which operating system you are on, you may want different styles. To set
the label to grey as well, select it and then go to the Property Manager.
Select the "Appearances" tab and click the backcolor property. You will see
a little "drop down" triangle button at the right edge of the property.
Click this to open the color picker window. The color picker is divided in
three. The top part has all the colors available in the current palette. The
bottom left contains predefined and named colors, like kBlue, kRed, and so
on. The bottom right is special. Rather than showing predefined colors, it
shows system colors, named for instance kColor3DFace. These constants map to
the system colors set up in the operating system. If you use one of these
colors, Studio will use the corresponding color chosen by the user in the
system.
For now set the backcolor of the label to grey, to match the window.
Now we added the first entry field, complete with label. Now add entry
fields for cDatePlanned and cDateDone below the cID field. You can use the
same technique for these as for cID, adding the field and then the label. Or
to save some time you can just "drag-copy" that one. To make a duplicate,
first select both the label and the field for cID. (Either click on one of
them and then click the other holding the Shift key down, or click outside
them and - with the mouse button down - drag a selection rectangle around
them.) Once you have selected them both, click and hold the mouse button
down on one of them, press the control key on the keyboard, and drag the
field and label to the new position. When you release the mouse button,
Omnis will create a duplicate in that position, leaving the original intact.
(If you don't hold Ctrl down, you will just move the original.) You can also
do a normal copy and paste from the Edit menu, and then drag the copy into
position.
If you used the drag-copy technique, all you have to do is change the
dataname of the field and the text of the label. Deselect if necessary (if
more than one object is selected) by clicking outside them, then click the
field and open the Property Manager. The dataname property will still show
iTaskRow.cID. Since the date field belongs to the same row, we only need to
replace the column name. So replace cID with cDatePlanned and cDateDone
respectively for the two new fields. Then change the labels by replacing the
text with "Date planned:" and "date done" respectively.
Next to do is the Type. This is different. We don't want to use an entry
field here, because the column just contains a number value, which has no
meaning to the user. We could teach them that 1 = Phone call, 2 = Meeting,
and so on, but that is neither convenient nor user friendly. Instead we will
use radio buttons to represent the alternatives. You may remember from an
earlier chapter that radio buttons are groups of buttons where selecting one
means deselecting all others. So it is the appropriate control where only
one of a predefined number of choices is correct.
You set up a group of radio buttons by adding the appropriate number of
buttons, giving them all the same dataname, and making sure they follow each
other in field order (e.g. 5, 6, 7, 8) on the window. If you create them in
order they will be, if not you can change the order by setting the "order"
attribute in the Property Manager. If you have a "hole" in the order (say 5,
6, 10, 11) you will have two groups.
To create our radio buttons, first drag one in from the Component Store. It
is the little round icon with a dot in the middle. Place it below your date
fields. Select it, open the Property manager and set its dataname to
iTaskRow.Type. Radio buttons like other buttons have a text attribute for
the text of the button, this is usually shown to the right of the icon,
though you can set that. In the text property replace the default "Radio
button" with "Not selected". This will be our default radio button,
indicating that the user has not yet selected a type. Radio buttons
corresponds to a value for the number variable in number order, starting at
zero. So if you have three buttons, the first will represent the value zero,
the second represents 1, the third 2.
Now use the drag-copy technique to add three more radio buttons below the
first one. Change the text to read "Phone call" for the first, "Meeting" for
the second, and "Other" for the third. They already have the correct
dataname since you set that for the first one, and assuming you didn't
create other fields in between, they are in the correct order. Note that if
you later need to change the order, make sure that the buttons are numbered
consecutively, if not they will suddenly represent different values than
those stored in the database, and Meetings may display as Phone calls etc.
Now you can test your radio buttons. Open the window in runtime mode
(right-click and select open, or press Ctrl-T). Try clicking the buttons and
see that only one is selected at a time.
That completed our task information. Next time we are going to add the list
to select the employee for the task, and the SQL functionality to navigate
and update our task database.
Good luck, and welcome back.
========================================
EurOmnis 2001: the European Omnis Developers conference
EurOmnis 2001 is the European Omnis developers' conference, run by
developers, for developers, which will take place from Nov 5th to 10th, 2001
in Valkenburg, The Netherlands. A few places are still available for this
must-see, must-be-there event in the Omnis developer calendar. For full
session descriptions, speaker profiles, and registration details, please go
to: www.euromnis.com
========================================
Display Fields
By David Swain, Polymath Business Systems Inc
Email: dataguru@polymath-bus-sys.com
Web: www.polymath-bus-sys.com
----------------------------------------------------------------------
There are many times when we need to display a value in a field on a window
or remote form, but not allow data entry into that field. (Many of the
techniques presented here are also applicable to "entry" fields on reports,
which are by their very nature display fields.) Sometimes that displayed
value comes directly from the database. At other times, the value displayed
must be derived from a combination of data elements and is not stored as a
separate data element. In some special cases, we may simply want to expand a
value from a coded form to one more intelligible to the user. In any event,
we want to control what is displayed using programming techniques and not
allow direct user modification of these values.
Simplest Case
The most elementary display field is one that displays the value of a
variable but does not allow data entry. To make a field behave in this
manner, we simply set its "enabled" property to "kFalse". The "enabled"
property controls whether an object is accessible during data entry while
the "active" property controls whether it responds to events -- an entirely
separate subject that we won't explore further for now.
A display field needs to be redrawn when the value of its associated
variable changes to keep the variable value and the display in synch. This
can happen if a new record is displayed on a window or if the variable value
is modified as the result of some calculation performed in a method "behind
the scenes". However the change in value occurs, either a "Redraw" command
or a "$redraw()" method involving the field must be executed to keep the
value displayed in synch with the actual value of the associated variable.
This is the only case that applies to Remote Forms. Since they use only
instance variables, the techniques for generating display values given
further in this article are not an issue. (In fact, entry fields on Remote
Forms don't have most of the properties mentioned in the rest of this
article.) There must be an instance variable for each value displayed on a
Remote Form and that value must be assigned within a method.
Window (and to some extent Report) classes have some other intriguing
possibilities, however...
Calculated Fields
The field itself can contain an expression used to derive a value for
display. If a valid expression is assigned to the "text" property of the
field and its "calculated" property is set to "kTrue", the expression is
evaluated and the resulting value displayed each time the field is redrawn.
It is important to note here that the evaluation can be triggered by a
method command that causes the field to be redrawn, but the evaluated
expression is encapsulated within the field's properties and not contained
in any method.
The "calculated" property value of "kTrue" is only in force if the "enabled"
property is set to "kFalse". Furthermore, it can only carry out a
calculation if an expression exists in the "text" property. All of these
elements must be in place for the calculated field to work as desired. The
value derived by the calculation expression is assigned to the variable
whose name is given in the field's "dataname" property.
For example, suppose we have a variable for "height" and one for "width" in
a record and we want to display the resulting "area". We could create an
instance variable for "area" (of the appropriate numeric datatype) and a
field to display its value. We would make this a display field (entry field
with "enabled" set to "kFalse"), set its "calculated" property to "kTrue",
and give its "text" property the value 'height*width'. Each time the field
is redrawn, the "area" variable will be updated to reflect the values of
"height" and "width" from the current record.
"No Name" Calculated Display Fields
If there is no need to retain the value of a calculated display field in a
variable, there is no need to assign the name of a variable to the field's
"dataname" property. (This is only viable for a display field -- an enabled
entry field with no dataname is totally useless!) This will often be the
case for calculated display fields on windows and reports, as any value that
can be derived from existing values usually doesn't have to be separately
stored. So in our example above, an "area" variable isn't really necessary!
Here's another example, in a line item record for an invoice we must store
the quantity sold and the unit price. It is important to see the line item
extension (quantity times unit price) on screen and on the printed invoice,
but it is unnecessary to separately store this value in the line item record
itself as it can be derived at any time from the other values in the record.
On the other hand, this value may be just an intermediate value in a larger
calculation scheme. A line item extension may not need to be stored in the
database, but if we are displaying a list of line items we may want to
assign it to a slot in that list for two reasons:
1) So that the correct value for each line appears in the final display and
2) So that we can generate the sum of these values for another purpose.
(The invoice total may be a desirable value to store in the database even
though technically it can be derived in other data. Since the data from
which the value is derived exists in a different file or table from that in
which the value is to be stored, this practice can eliminate a great deal of
additional database activity when only the invoice record is of interest.)
Even then it is true that a derived value can appear as a column in a list
display field (List Box or Headed List) and a sum of derived values can be
obtained using the totc() function without using an additional variable.
However, if we are using a Complex Grid where values are displayed in
separate fields within the grid, an instance variable will be required for
calculated fields for the reasons given above. Also, if we want to use the
aggregation facility of a report, we would need to put the derived value in
a variable for this example.
Translating Values for Display
There may be times (for example, in reports) when we need to display an
understandable value for a variable controlled elsewhere in the application
by a set of radio buttons. A variable so controlled stores an integer value
corresponding to the radio button selected, but this value will be
meaningless to the casual observer without a translation table -- and using
such a table slows down the absorption of the information represented by the
data. A calculated field can be used to translate the value directly for
display, making display-only windows and reports easier to understand.
The same principle applies to the display of Boolean or integer status flag
values (which are set by background calculations) and Boolean values entered
using checkbox fields. A report entry showing the status directly rather
than a label associated with a "YES" or "NO" (or with an integer and a
translation table) is again easier to absorb as there is less visual data to
process. It also usually requires less space to display.
We simplify data entry with a checkbox field. In the same way we can
simplify reporting by using calculated fields instead of a label and a raw
Boolean or integer value. Let's examine some of the calculations we might
use for this purpose.
Commonly Used Functions
mid(master string, first extracted character position, number of characters)
Among its many other uses, the mid() function can be used to turn a display
string on or off. Rather than use the function to extract a substring from a
master string, we can use it to display or suppress an "exception" type
label. For example, a product might be flagged as "Discontinued". This is
only important if the value is actually true. If an item is not
discontinued, displaying this fact only tends to clutter the screen or page.
We can use a calculated display field with a mid() function to display this
value when needed and hide it when it is not. (We might also make the
"textcolor" property of such a field something special - like red - to make
it stand out even more.)
Consider the three parameters of this function. The first parameter is an
expression that determines what is to be displayed. This can be either a
static literal or some expression that derives a string value from other
data -- in our example, we will use the string "***DISCONTINUED***" (which
we obviously want to stand out).
The second and third parameters have the power to completely switch off any
display string. For example, the second parameter determines the character
position in the master string to begin extraction. If the second parameter
is zero, negative or greater than the number of characters generated by the
first parameter, no characters are extracted for display since this would
mean starting outside the boundaries of the master string.
The third parameter determines how many characters to extract. If the value
of the third parameter is zero or negative, no characters are extracted for
display (Omnis will not count characters backward from the beginning
position). If the value of the third parameter is greater than or equal to
the number of characters from the beginning extraction position to the end
of the master string, all such characters are selected for display.
So we have two choices for switching the master string value on or off:
1) use the second parameter as the switch (generating either a zero or a
one) with the third parameter set to the length of the master string (or to
some arbitrarily large value) or
2) set the second parameter to one and use a pick() function in the third
parameter to select either zero or a value that is at least the length of
the master string
My personal preference is to use the second parameter of the mid() function
as my switch. If the variable value I wish to represent is a Boolean, it
evaluates numerically to either a zero or a one -- an ideal situation for
setting an all-or-nothing initial extraction position. My finished
expression for the "text" property of this field would then be:
mid("***DISCONTINUED***",product.discontinued,1000)
If I did a poor job of database design and arranged for the exception value
(the one requiring the string to be displayed) as the "NO" value, I can
simply use the not() function within the mid() function's second parameter
to get the desired result. Using a pick() function in the third parameter to
do the switching job seems less efficient to me. There are other places,
though, where it is just the right tool...
pick(picking parameter, choice0, choice1, choice2, ...)
The pick() function allows us to easily convert an integer or Boolean coded
value to an expanded string for display. A calculated report field using
this function is an ideal counterpart to a set of radio buttons on a window,
for example. Here is how it works:
The syntax of the pick() function is:
pick(selector,p0,p1,p2,p3,...)
The first parameter of the pick() function is the "picking" or "selector"
parameter. It is an expression that evaluates to an integer value (or, at
least, the value it generates will be USED as an integer). In the case of a
variable that was set by a group of radio buttons, only the variable name is
needed in this expression, but more complex expressions can be used as the
need arises. This value then determines which of the following parameters
will be used to derive the result of the function. If the value of the
selector is zero, the "p0" parameter is used. If the selector yields a one,
the "p1" parameter is used, and so on. Parameters must be supplied for all
possible values of the selector. An empty string will be returned if there
is no parameter corresponding to the selector value.
Consider a simple example: We have created a file for storing information on
college undergraduates. We have included a column for "year" of integer
value and control it with radio buttons on an entry window. The possible
values are 0-3 for freshman, sophomore, junior and senior, but we also
default the value to 99 when a new record is first created so that no button
is selected without the user doing so. (And we then trap for a "99" value on
"OK" to require a selection.) Here is how the pick() function might be cast
to expand the display for this value:
pick(student.year,"Freshman","Sophomore","Junior","Senior")
If I were concerned that a "99" default value might sneak through, I can get
a little more sophisticated with my picking expression to convert it to a
"4". In fact, I could be completely paranoid and trap for other than
expected values as well...
pick(student.year*(student.year<=3)+4*(student.year=99)+(student.year>3&stud
ent.year<>99),"Freshman","Sophomore","Junior","Senior","Default","Wierd!")
Notice how each Boolean sub-expression acts as a switch for its
corresponding operand. This allows us to deal with discontinuous ranges
using the pick() function without having to supply large numbers of empty
parameters.
The pick() function does not return a value of a set datatype, rather the
datatype is determined by the parameter selected. Omnis then attempts to
translate this to the datatype of the receiving variable or use it in the
enclosing expression. To illustrate this, consider a parameter that
evaluates to a Date value. I will use the expression "dat('12/31/2000')" (US
date string where the month number comes before the day number -- sorry if
it reads convoluted to you) to force the result to be a Date value, but
simply the name of a Date variable would work as well. Consider that the
target variable for the result of our pick() function has a Numeric datatype
(not short integer, but any other will do). The variable will contain
"36525" when the dust settles (which is the number of days in the last
century, if you're curious...).
Now let us change the expression in our selected parameter to
"con(dat('12/31/2000'))". This has the effect of converting the resulting
value to a string since the con() function always returns a string value. If
our target variable had been of Character type (or of Date type), we would
have obtained "DEC 31 2000" in either case, but since it's a number, we now
notice that the variable contains "0". This is because the numeric
equivalent of a string value is "0" unless that string looks exactly like a
number. Bottom line: It is important to understand both the datatype of the
value you are generating and how the context in which the expression is used
will affect that value.
pos()
If we were a bit more traditional and had our users memorize letter codes
for certain variables, we can still expand these to make them easy for
management to read by using the pos() function as a picking parameter for
the pick() function. The syntax of the pos() function is:
pos(substring,master string)
The function scans the master string looking for the first place within it
where the substring exists -- sort of like holding a piece from a jigsaw
puzzle and moving it across the edges of the partially assembled puzzle
looking for a pattern match. The pos() function returns an integer value
which is the character position of the first character of the substring
within the first instance of that substring within the master string.
This may sound complicated, but our use of it here is quite simple since we
are working with single character coded values and our master string will be
a list of all possible codes. In that case, the pos() function simply
reports to us the character position of the code. If for some reason the
value we are seeking is not in the master string we supply, the pos()
function returns a zero.
Let's suppose we had used such codes in our student example above. The
"year" column is now a character column of length 1. Our users now have to
type "F" for freshman, "S" for sophomore, "J" for junior and "R" for senior.
(Can't use "S" twice - one of many reasons why character codes aren't a good
idea now that we have modern alternatives). It doesn't matter in what order
we place these in our master string, as long as we are consistent with the
way we match codes and results. There is no longer a "default" value as the
field is empty until a value is entered, but we can still treat this
differently from an erroneous value as follows:
pick(pos(student.year,"FSJR")+4*(not(len(student.year))),con("Unknown entry
'",student.year,"'"),"Freshman","Sophomore","Junior","Senior","Empty value")
Notice how the empty value was converted into a 4 to distinguish it from an
erroneous value. Also note how the erroneous value was also made part of the
output. These are only necessary if we are concerned that data entry event
management processes haven't disallowed such values. If we are absolutely
certain this can't happen, the picking parameter can be shortened to just
the pos() function. (It's still a good idea to provide for an "Error" item
for the zero parameter. It's there anyway so we may as well use it...)
Substitution Strings (Lookup Tables)
There is another way Omnis Studio can convert a coded value into a more
verbose string. Long before Omnis had a pick() function, it had a facility
called a "Lookup Table". Although the Omnis Studio manuals no longer speak
of this facility, it still exists and can be used to advantage in certain
cases.
The "Lookup Table" facility is an alternate use of a display field -- more
specifically, it is an alternate use of the "text" property of an entry
field. To use the facility, the "enabled" property of the entry field must
be set to "kFalse". The "calculated" property of the field must also be set
to "kFalse" so that Omnis doesn't try to use the contents of the "text"
property as though it were an expression (which would always fail, as we
will see). The "dataname" property must contain the name of a variable,
since it is the value of that variable that the "text" property uses as a
source for substitution.
The "text" property is then given a string in a very special format, as
follows:
Default value (optional)/Code1=Value1/Code2=Value2/
Omnis assumes a string here so no quotes should be used unless you want them
to appear in the output. The initial and final forward slash characters are
required. Any characters before the first forward slash are assumed to be a
"default value" (which will be used if the value of the variable does not
match any of the coded values given in the string -- like the zero value in
the pick() function above that used a pos() function as its picking
parameter).
Here's how it works: Omnis compares the value of the variable given in the
"dataname" property of the field with each of the "CodeX" values given in
the substitution string and substitutes the corresponding descriptive string
for the coded value. Read this as "Translate CodeX to ValueX". The order in
which the Code and Substitution Value items are given is EXTREMELY
important -- the code PRECEDES the substitution value in each pair. Here are
some examples for further illustration:
For a Boolean variable: Unknown/NO=False/YES=True/ ("Unknown" is returned
for empty and NULL values)
For the Discontinued Boolean value from an earlier example:
/NO=/YES=***DISCONTINUED***/ (An empty string is returned for NO, empty and
NULL values)
For an integer variable corresponding to seasons:
***ERROR***/0=Summer/1=Fall/2=Winter/3=Spring/
Notice how we can return an empty value by not supplying a value
corresponding to a code (Discontinued example). This allows us to "turn off"
the display as we did in that earlier example.
The substitution string also supports square bracket notation. Usually we
would use this for the returned values. Consider this example:
A common technique involving radio buttons is to include an "Other" option
to cover the possibility that we have not supplied every conceivable
real-world choice for a data item. The astute programmer would also supply a
string variable for the "Other" choice, making the corresponding entry field
disappear (and clearing the variable's value) unless the "Other" radio
button is selected. To display this value as entered by the user, we could
use a substitution string like this one for the type of a part:
/0=Widget/1=Doohickey/2=Thingumabob/3=Watchamacallit/4=[part.otherType]/
The current value of "part.otherType" will appear if "part.type" equals 4.
Some earlier versions of Omnis Studio did not allow these substitution
strings to be directly entered into the "text" property of an entry field
since they are not valid calculation strings. That meant that such strings
converted from previous versions of Omnis could not even be edited as the
cursor could not leave the entry field in the Property Manager (even though
they still worked!). Studio 3.x has corrected this problem (at least for
fields on a window class) and it now allows direct entry of a substitution
string to the "text" property in the Property Manager for window entry
fields.
Report entry fields are still problematic. The "Lookup Table" facility still
exists, but we cannot directly enter the substitution string into the "text"
property using the Property Manager. Instead, we must use notation, either
with a Calculate command or using the $assign() method with the Do command.
Since we want to affect a property of an object in the Class, our command
would look like this:
Calculate $cclass().$objs.discontinuedLabel.$text as
'/NO=/YES=***DISCONTINUED***/'
Then be extra careful NOT to allow the "text" property value in the Property
Manager get the design focus!
I have avoided using the term "Lookup Table" while describing this facility
here, preferring to use the term "Substitution String". There is a very
different feature of Omnis Studio named "Lookup" that I will describe in the
next issue and I don't want to confuse them.
Local
I said earlier in this article that a calculated field on a window must be
redrawn whenever we want its expression re-evaluated. This is certainly true
for those times when we retrieve a new record or when we have modified some
value that affects the displayed result in a method-based calculation
somewhere, but there is another facility in Omnis Studio that we can employ
during data entry that often proves more convenient (not to mention a bit
faster).
We can set up a display field to automatically be re-evaluated and redrawn
directly after the data entry focus leaves another entry field. There are
strict guidelines to be followed for this, but this facility is very useful.
This is the job of the "local" property.
When a field's "local" property is set to "kTrue", is falls under the
influence of the most immediately preceding field according to the fields'
"order" property values. More specifically, when the focus leaves the
immediately preceding field (by whatever means), the "local" field is
automatically re-evaluated and redrawn. It is important to note that this
happens BEFORE any event methods take place, so it can't be avoided even by
discarding the event. (The rule is that all properties must be satisfied
before the event methods come into play.) The field becomes an "extension"
of the entry field that precedes it.
Furthermore, we can "chain" this facility. If we have a number of fields in
sequential "order" and all but the first are set as "local", all display
fields from the current field to the end of the chain are re-evaluated and
redrawn when the focus leaves the current field. Consider our "area" example
from earlier in this article. If the "order" of the fields is "height"-1,
"width"-2 and "area"-3, and we set the "local" property of both fields 2 and
3 to "kTrue", tabbing out of EITHER the "height" or the "width" field will
automatically cause the "area" field to be re-evaluated and redrawn. The
"local" nature extends right through the "width" field (which is unaffected
by it), so field 3 is "local" to both fields 1 and 2.
I find this feature nothing short of miraculous -- to execute and redraw the
result of a calculation without code! This is not to say that the execution
is out of our hands as programmers (as I have heard some people claim) -- we
just have to understand how it works to take advantage of it. This is a
facility I have enjoyed using for two decades now and I still find it
preferable to event-based calculates and redraws in simple cases like those
mentioned here.
There is another useful aspect to the "local" property. Fields that are
"local" to a List Box whose datanames are columns in the definition of the
list so displayed will automatically be redrawn to display their contents
from the current line of that list when a new line is clicked upon. The same
basic principle applies: the "local" fields "belong" to the list. It is not
necessary for the Current List to be set for this facility to work. Data
entry into such fields (they don't have to be display only) is data entry
directly into the current line of that list as well. It works like an
automatic $loadcols() and/or $assigncols(). This is a technique we used long
before "Table Fields" (now Complex Grids) or notation. Local
re-evaluations/redraws still work as before within such a group of fields
(for calculations of line item extensions, for example).
Fields held within a Complex Grid (Table Field in O7) exhibit this same
behavior regardless of their "local" property values, but the "local"
property must be turned on for re-evaluations/redraws to occur.
I hope this discussion has proved useful to you or has given you some ideas
to ponder. In the next issue I will explore the concept of "Lookup Channels"
and how Omnis/mvDesigner programmers can take advantage of them.
========================================
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 2001. 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