I started thinking about an application framework back in 1992, when the users of OOPS begun complaining that it was too mastodontic (you had to go six or seven levels down a menu before getting to do something, and had to go all the way up plus some more levels down to do something else); in the meantime I was starting to develop, requested by the local university radiology department, Sfinge, a system which could generate a set of multiple choice questions that could be used to prepare exam papers and as a self assessment tool.
Developing OOPS had made a few things clear:
A few things were in order:
By popular demand, the new OOPS was to be
"Never more than two keystrokes away from what you want to do" was the motto.
For starters, OOPS (and the application framework, for that matter) needed
to have one monolithic menu structure accessed by every viewer, and not
a 4gl menu for each of them, like in "traditional" 4gl apps.
This alone allowed for easier to maintain, shorter code.
The menu structure behavior would be to directly invoke services common
to all viewers, leaving them to deal with their specific user request
("Insert new record", etc).
The next move was to have the services tell the viewers what to do upon regaining control, rather that putting in the viewers themselves the knowledge of the possible behaviors (new record inserted, tbl locked, row deleted, screen defaced, etc.) of all the services.
Third, a way was needed to let the viewers know from menus and services what to do next: 4glWorks was born.
What follows is a block diagram of a typical 4glWorks application:
The idea is quite simple: the application is made up of a number of modules that call each other and exchange messages in order to communicate what each wants the other to do.
Messages are a couple of smallints that travel upwards (as parameters passed to the next module) and downwards, as values returned.
4gl doesn't offer constant definition, so probably strings would have offered more readability. However messages use the stack, and 4gl suffers from TSS aches. Smallints seemed a much safer choice.
More important, smallints allow for efficient message masking, whereas strings don't.
I quickly got accustomed to using integer values for messages. Recently, when things got out of control, I switched to my own cpp like precompiler.
This section briefly describe the three main components of a 4glWorks
application, viewers, services and the menu structure.
Viewers ultimate scope is handle completely a particular subset of your
DB, however complex. Viewers duties therefore comprise the retrieval of
data and its display on the screen and, optionally, the invocation of
viewers related services.
Viewers input comes obviously from the menu structure and services (and, as of
2.0b5, from the system clock too), and it essentially falls in two main categories:
state restoration & user requests.
State restoration requests are the only mechanism a viewer has to keep
track of what is going on. Application correct functionality is based
on the fact that services correctly notify the viewers about the action
they need to take to restore a correct state in the following areas:
User requests come from the menu structure. Depending on viewer capabilities, such messages might deal with:
The only user request a viewer must be able to service is "pack up & quit".
The menu structure interfaces directly with the user, allowing him to
choose the operations he wishes to perform.
Menus wait for the user to select an action and then either call a service
to perform whatever the user wants to be done, or pass the request back
to the current viewer.
Any given message can be associated with any of the following:
Note that a message can be associated to more than a menu entry at the same time (this is typically used for accelerator keys) and that menus can be freely cascaded. This scenario allows for user interface facelifts without modifications to viewers or services.
The role of vertical menu is clear enough, but why have an horizontal
menu library?
In fact, up to beta 0.7 there was no such thing. The horizontal menu was
built using a standard 4gl menu, made dynamic using variable command definitions.
Things were changed for both aesthetic and practical reasons.
Aesthetically speaking, 4glWorks menu can be made to consume less space that
4gl menus; also options remain highlighted (like they did in 4gl menus back in
v1.01.03) for the user to have clear what he's doing.
But the nicest thing they do is not to switch to the first on screen option
in the menu whenever the user chooses an option only associated with a
key (this is a behaviour I've always disliked in 4gl menus).
Turning to more important things, the horizontal menu library was written
mainly because 4gl menus don't allow for dynamic key assignments, and
a tighter integration with vertical menus was needed. 4glWorks menu libraries
allow to mask & filter messages irrespective of where the associated command
has been put, and how it has been defined.
(It should alse be noted that the horizontal menu library simplified a great deal
the implementation of timer generated messages support)
Services are the modules that actually change the state of the DB (locking
a table or the DB, writing a row) and/or the application (viewer configuration,
active set selection). They can be called, at the programmer's wish, either
by viewers or by menus.
Unlike other modules, services only produce messages
The current implementation of 4glWorks uses a SDI: only one viewer (your typical application will most certainly have more than one) active at a given time, using the screen. The main program is therefore only a viewer spawner: just a while statement with a big case inside.
The system described here is in fact a small port of an event driven system
to a character based programming language. The beauty of the system is
that each module needs only to be able to perform a small number of actions
and need not have a clue about what other modules do or where user requests
come from. Whenever active, the message received will tell the module
what it needs to do to return to a safe state and/or which actions it
is requested to perform.
4glWorks application are obviously modal, the current mode of operation
being set by the viewer or service active at a given time.
One of the nice things of messages is that they can be filtered; that
is to say a message can be fed to a black box that does something with
it and on exit returns an entirely different one.
4glWorks has two breeds of filters: plain common ones, and scrollers.
A scroller is a filter explicitly placed between the menu structure and
the viewer that uses it. It typically contains all the data retrieval
& display functionality the viewer needs.
The scroller receives messages from both the menu structure and the viewer
and trap all the ones for which it has built in functionality. All the
other are passed to their intended destination, possibly filtered.
Scrollers give viewers a common behaviour leaving them to deal only with
messages related to what the viewer is all about.
And, if a scroller (or a filter, for that matter) doesn't exactly do what
you want (say you want a nice header over the data), you can enhance its
behaviour by putting another one in front of it.
Encapsulation and inheritance!
If it isn't clear by now, I don't consider 4gl as being fit for applications with multiple screen elements, the most obvious reason being the lack of pointing device support.
Nonetheless there are times in which it is useful to present the user with more
than one pane (the sql interpreter included in 4glWorks has an edit and a result pane,
for instance), and 4glWorks gives limited support for this.
For just a moment lets go back to the typical scroller structure:
function typical_scroller(im, ip) define im, ip, om, op smallint #input & output messages call upstream(im, ip) returning om, op while true call menu_get(om, op) returning im, ip call downstream(im, ip) returning om, op if (om not in [system_messages]) then return om, op end if end while end function
(forgive the pseudo-code) where upstream
and
downstream
are filters that handle messages going from the
viewer to the menu structure and viceversa.
Apply this kind of code to viewers, i.e. let them call the upstream and downstream
filters directly, like thus:
function multi_pane_viewer() define im, ip, om, op smallint #input & output messages let om=do_whatever_initialization_is_needed let op=same_as_above while true call upstream_1(om, op) returning om, op .... call upstream_n(om, op) returning om, op call menu_get(om, op) returning im, ip call downstream_1(im, ip) returning im, ip .... call downstream_n(im, ip) returning om, op # # optionally, deal with messages, here # if (om=pack_up) then return op end if end while end function
and presto, a multiple pane viewer has been built.
Obviously, there are certain rules the up/downstream filter must respect when dealing
with messages. If they don't, the next filter will most certainly fail to present the
user with the correct data, or invoke a service it shouldn't.
For now, it goes by itself that panes not currently focussed should only handle redisplay
& exit messages, and leave all others untouched. More on this topic in the
programmer's manual.
Limitations. 4gl has only static screen elements (i.e. you can't reference a screen array or field via a char variable) and does not support screen matrices (as opposed to screen arrays). The only way left for this scheme to work is to create a separate screen array for each pane and to handle it with brute force.
In short:
Also this kind of approach is not entirely compatible with the scroller approach, but
don't despair.
To ease the use of existing scroller code, many 4glworks scrollers come using the
upstream / downstream paradigm.
The only limitation, due to the previously exposed reasons, is that with the exception of
the uni_scroller (which resorts to dirty hacks) any one scroller can
be used by only one pane within the same viewer. Also, note that no particular effort has
been made to write stateless scrollers.
Looking to the bright side, if you take the time to control your windows, with this same approach, you can write a MDI viewer.
The support 4glWorks gives for multiple panes viewer development consists in
For everything else, you are on your own.
As of 2.0b5, 4glWorks permits to specify that a message should be generated after a specified
amount of time has elapsed, or if the user hasn't pressed a key in some time.
This, of course can be used to refresh a viewer display every so often, or to check for new mail, or
to quit the application and regain a valuable user licence if the user hasn't pressed a key in some
time (and this might help to avoid zombie engine processes too).
A less obvious use could be to refresh the detail pane in a dual pane master-detail viewer whenever
the user hasn't asked to move the screen cursor in the master pane for a certain amount of time.
The ratio of this would clearly be to speed up the cursor motion in the master pane, since this
doesn't have to wait for the detail pane to redisplay data every time the current row moves.
I think it's about time you have a look at the feature list of 4glWorks, or the programmer's manual.