Providing Global Resources
in Multiple Task Applications
By David Swain
Polymath Business Systems
This article continues our exploration
of the use of tasks in Omnis Studio. In previous articles, we have
discussed what task instances are and how they can be used to manage
aspects of our applications. We have also seen how Omnis Studio
deals with changes of context as either the user or the execution
of our methods switches from one task instance to another. We have
learned the difference between the current task and the
active task and have seen how to make certain instances
within a task local to that task. We have even learned
that we can launch a new instance into a task from a method executing
inside a different task instance, indicating that there is perhaps
more flexibility and power at our disposal than we might have imagined
at first glance.
So let's assume that we have decided to use multiple tasks within
an application. Now we are faced with the problem of how to provide
various application resources that can be accessed from anywhere
within the application. In a single task application, this is easy
to do - we can create task variables that can be accessed from within
any instance contained in the task. But now we must find a way to
share such resources across task boundaries.
What Do We Mean By "Global Resources"?
Global resources are items that we may need to access from anywhere
within our application. For example, Omnis Studio provides many
global resources for us: The date and time variables (#D
and #T ) are obvious cases. But all of the built-in
and external functions and constants, class and component object
type definitions and even localized character sets and label strings
are available to us throughout an application without our having
to instantiate them for each new window instance - or even for each
new task instance. They are just "there" for us to use.
Most applications we build will have similar resource needs that
are more application specific and that could not have been anticipated
by the people who created Omnis Studio. An application with even
rudimentary security will need to keep track of certain information
about the current user and their current session
within the application, for example. Access to SQL data sources
may be through a session object that we desire to make
global. There may be many custom functions that are required
in diverse parts of the application for performing common calculations
or other operations. The question is: How do we provide such resources
so that they can be shared among the instances of our application,
regardless of which task is the current or active one?
Inheritance of Task Superclasses
A partial solution can be found by using a "master task"
superclass containing such resources. We could create this superclass
to contain all our global methods directly or to contain task variables
of object type that hold collections of such methods. We could also
include other task variables that we may need to access "globally"
as well, such as custom "constants" for certain calculations
(such as an interest rate or engineering factor) or "states"
that are specific to the current runtime (like the path to a local
output directory). Then the task classes for our application could
be subclassed from this superclass and all of them will contain
the same methods and variables automatically.
But there is a problem with this approach. Yes, all these tasks
contain the same variables and methods, inherited from the superclass,
so they can be accessed from any location in any instance with the
same code. But the variables of each task are completely independent
of their counterparts in the other tasks. The subclasses do
not share the task variables of the superclass, but simply
make copies of them - actually, only copies of their definitions,
so assigned values (other than default calculations) aren't even
copied. If we chose to house most of our custom functions in object
classes for the flexibility and portability they offer, then we
have created multiple copies of these methods as well. Variable
values contained within these object instances are not shared either,
so changes made to such values inside one task instance are not
automatically reflected in other task instances. The inheritance
technique only gets us part of the way to a solution.
A partial solution to this problem exists. We could create our
resources in a single task instance (perhaps the startup task),
create a notational reference to this task instance and then pass
that reference to our other tasks. This will work for many cases,
but it limits our flexibility. There could still be scoping problems
with this approach, for example. Still, for Omnis Studio 3 and earlier,
this can be used effectively.
But users of Omnis Studio version 4 have another option...
Object Reference Variables at the Task Level
Object instances are wonderful for use as variable and method repositories.
In fact, these combinations of variables and methods can be very
useful for retaining and managing data (like a security object that
can retain current user information and create new user tracking
records for significant events we wish to monitor). But passing
objects around can be a cumbersome chore. We all soon found that
a more flexible technique for working with objects was needed for
many cases.
Omnis Studio version 4.0 introduced the object reference
data type. While object variables contain an instance
of an object class, object reference variables only point
to an instance of an object class. This is an important distinction.
When an object variable is passed as a parameter, a new
object instance is created which is a duplicate of the instance
contained in the original object variable whose value was passed.
That object then goes on to have a completely independent life of
its own. When an object reference variable is passed as
a parameter, the new object reference variable points to the same
object instance as does the original object reference. No new object
instance is created. That is, there remains only one object
instance no matter the scope of either of the object reference
variables. But how do we get this reference into an object reference
variable in the first place?
An object reference variable generally comes into being without
containing a valid reference (unless it is a parameter variable
and is passed a valid reference as it is spawned). So it must be
assigned a reference, either from another object reference
variable or from a method that launches a suitable object instance
and returns a reference to it. Only references to an object instance
launched in a certain way can be assigned to an object reference
variable. (That is, we can't use a reference to an object variable
because it could easily go out of scope.) A little background information
would be useful here...
Object Reference Basics
We will present a more thorough treatment of object references
in a future article, but here is some basic information on how they
are populated. Object classes in Omnis Studio version 4 and later
contain a number of methods relating to their use with object reference
variables. The method in which we are interested here is the $newref()
method. When applied to an object class, this method launches a
non-scoped instance of the object into the current task and returns
a reference to that instance suitable for use in an object reference
variable. We can populate the object reference variable in this
way:
Calculate objRefVar as $clib.$objects.<objclassname>.$newref()
Parameters can be passed to the $construct() method of
the object instance within the parentheses that follow the name
of the method. Notice that we are not given options for assigning
a name to the object instance. It certainly has a name (as do all
instances within Omnis Studio), but we have no control over it because
we generally access such instances through object reference variables.
By default, Omnis Studio assigns the first instance of a given object
class that is launched into a task by the $newref() method
the same name as its class name (assuming that name has not yet
been used). If subsequent instances of that same object class are
launched into the same task instance while the first object instance
still exists, each is given a name consisting of the class name,
an underscore character and a randomly assigned integer (e.g., classname_256 ).
But an object instance of that same class launched into a different
task instance is again given the class name alone as its instance
name. (I only point this out in this article because we will be
using the Notation Inspector to view such object instances and I
want to preempt any possible confusion that might arise.) So apparently
uniqueness of object instance names is only required within a task
and not within the entire application.
We can pass an object reference as a parameter to another object
reference variable in the target method and that variable will also
refer to the same object instance. If we need to pass that reference
to a more "permanent" variable, like an instance variable,
we can do so using a simple calculation:
Calculate ivObjRef as pvObjRef
We can also assign this reference to a cell in a list variable
and all manner of other useful things, but those are subjects for
another time. We have plenty of work to do here on our multiple
task situation...
Setting Up the Task Superclass
So it would seem that all we need to do is to create the object
reference variables we need in our task superclass, include code
to populate them with an object instance (as outlined above) in
the $construct() method of that task and then create all
the task classes we need as subclasses of that superclass. Well,
it's not quite that simple.
The trick is to create the new reference once in the first
task to launch (the startup task) and then pass that reference to
each new task that we instantiate. Setting up the task variables
in the superclass is fine. That way we will have consistently named
task variables guaranteed! But if we were to populate those variables
using the $newref() technique in the superclass $construct()
method, we would end up with multiple, independent instances of
our object classes - just what we want to avoid! Does this mean
this technique is another dead end? What options do we have?
There are at least two viable options. In the first case, we could
simply override the $construct() method in each subclass
and perform this operation only in our startup task. Then whenever
we launch a new task instance, we can pass the necessary object
references as parameters to the $construct() method of
that instance. In the (overridden) $construct method of each task
launched in this manner, we would then assign the reference held
in this parameter to the appropriate task variable. Each of those
will point back to the original instance launched in the startup
task.
A second option is to also create a method in the task superclass
named something like $setup() or $configure()
and invoke that at some point within the $construct() method,
passing appropriate parameters on to this subroutine. This is a
little more involved, but it may appeal to some Omnis Studio programmers.
That subroutine would then be overridden in the subclasses so that
specific method lines could be included to address the configuration
needs of each task.
I use a combination of these techniques in my own work. I use the
first technique for establishing the object reference task variable(s)
and the second technique for a number of other configuration operations.
So my task superclass contains:
- Task variables of object reference type (and possibly others)
to which I want to have global access throughout the application
- A $construct() method that performs basic, universal construction
operations. This includes calling:
- A $setup() method that is intended to be overridden in all subclasses,
so it contains no code in the superclass
- Other methods that may be universal or may need to be overridden
in the subclasses (or may not be used at all in some applications),
such as $destruct(), $control(), $activate(), $deactivate(), $suspend()
and $resume()
Here is the code for the $construct() method of the task
superclass (which I named taskmaster ):
Do method $cinst.$setup
; other code as needed for specific application
Not much to it, really. The real tricks are in the parts that we
override in the subclasses.
Setting Up the Task Subclasses
The first subclass to create is the startup task. We can simply
use the default that was created when we first made our library
and set its superclass property value to taskmaster
(or whatever the name is of our task superclass). Any existing $construct()
code remains untouched by this operation, which is fine since we
would override that method anyway. We then just add a line to the
beginning of that method (or to an appropriate position within the
method if the beginning is not quite right) to perform the inherited
bit. Our $construct() method would then look like this:
Do inherited
Install menu mainMenu
Set 'About...' method aboutCode/About Task Demo
; other code as needed
We next need to establish the object reference variable value and
the object instance that it points to for each such task variable.
For this task, we will do this in the $setup() method.
The $setup method would then contain:
Calculate fn as $clib.$objects.commonMethods.$newref()
; other code as needed
Since the object instance is created in code executed within the
startup task, that instance belongs to this task. This implies that
the startup task must remain in existence for this object
to continue to exist. I only mention this because I have seen "techniques"
where the programmer launches another "main" task for
the application and then closes the startup task. This is entirely
unnecessary, but if a programmer feels that it is, then I would
suggest not bothering to perform the operations here until the "main"
task is established.
In most multiple task applications I create, I launch the various
task instances for the application at startup. This way, the user
only has to switch from one task to another and I don't have to
repeatedly establish and destroy the resources for any specific
task. But it is equally viable to launch a task when it is needed
and then close it again when the user is finished working in that
area. The technique I demonstrate here for launching them all at
startup can be easily modified to accommodate other styles of programming.
Either the $construct() method or the $setup()
method of the startup task can be used to launch the other task
instances of the application. As always, there are two techniques
that we can use to do this: the method command technique
and the notational technique. Suppose that our application
contains two tasks in addition to the startup task: memberTask
and reportTask . Here is the code I would add to either
the $construct() or $setup() method after
the line(s) where the original object reference variable setup is
performed:
Open task instance memberTask (fn)
Do $clib.$tasks.reportTask.$open(,fn)
Each of these method lines will launch a task instance with the
same name as the class from which it was spawned. Notice that the
first parameter of the notational method $open() is reserved for
the name we wish to assign. This is one of the rare parameters that
can be left absolutely empty in Omnis Studio. We can instead put
an empty string (two quotes with nothing between them) as the value
for this parameter and get the same effect. This tells Omnis Studio
to use the class name as the name for the task instance. The second
parameter for the notational method is the same as the first parameter
for the command method. It is the first parameter that will be received
by the $construct() method of the task instance.
This means that we must add a parameter variable of object reference
type to the $construct() method of each task for each such
variable we wish to pass from the startup task. (Our example here
has just the one, but we could very well decide to have more in
a real application.) I have used the name pFn for this
parameter in each non-startup task (which makes copying and pasting
code easier). The $construct() method of the memberTask
might then look like this:
Do inherited
Calculate fn as pFn
Install menu memberMenu
Calculate $imenus.memberMenu.$local as kTrue
This assures that the reference to the original object instance
is passed to our object reference variables in each task rather
than establishing new object instances. The beauty of this is that
any reference can be passed since they all point back to
the same object instance. So it is not necessary to launch the other
tasks all from the startup task, but only from a task that has received
a reference that originated from there. The most important rule
is that the startup task instance (or whichever task contains the
object instance) must remain open for the reference to be valid.
Checking Our Handiwork
Once our application has been opened with this code in place, we
should be able to verify that we have one, and only one, instance
of our object class. We can do this in our development copy of Omnis
Studio using the Notation Inspector.
It is important to understand that there is no "iObjects"
group at the root level as there is for other types of instance.
Neither can we drill down through the iTasks group to see
the object instances contained within a task (which would only be
the "non-scoped" instances we discussed in this article
anyway, since those associated with object variables would not be
contained so directly within a task instance). So where would we
find this information?
We have to instead drill down through the $libs node,
the library that contains the original object class, the $objects
group within that library, the object class itself and
finally the $insts group for that class. There we will
see all the instances that are suitable for use with object reference
variables. Note that we will not see object instances used
with object variables in this listing. It may be that they
are out of scope or it may be that they just aren't included in
this prestigious group. Whatever the reason, we only see object
instances launched by $newref() here.
This listing gives us a means of ascertaining whether we have inadvertently
launched additional instances rather than passing references as
we intended. It also lets us test to see whether we have successfully
closed such an instance... but that is a topic for another time.
Next Time
In the next article, the final one on tasks for now, we will examine
the process of closing a task instance. There are some interesting
and useful twists to this process that make it worthy of closer
scrutiny.
The third article in a series for MacTech
magazine is nearing completion. The first two were in their March
and May issues and there should be at least two more in June and
July. Publication has lagged a bit from the real world calendar,
but it is nice to be able to represent Omnis Studio in a trade publication
of any sort - and these people have become very enthusiastic about
Omnis Studio from reading my articles for editorial purposes.
|