Omnis and subwindows
I've been way to busy lately to do any personal posts however today there was a topic on the Omnis discussion list about subwindows and communication between them and their parent windows that made me want to write a little something about it. The topic has come up many times in the past and since it is something I've done a lot with it seemed like a good idea to do a little sample library and add some explanation.
Subwindows are basically windows that are contained within another window. They are Omnis Studio's answer to be able to reuse visual components in multiple places in your application. This can be anything from a subwindow containing a single component/field to a subwindow that allows you to enter an entire record consisting of a dozen entry fields.
The problem herein is how to properly communicate between the subwindow and its parent window.
Object Orientation states that any object should not assume or directly influence another object that contains/uses it. That is the only way to guarantee an object can be reused in multiple places without any restrictions in the behavior of the object that uses it. A subwindow and it's parent however always have a symbiotic relationship in one form or another and some measure of communication between them is required. The parent window communicating with the subwindow is easy as this is simply done through public methods, but the subwindow (as it interacts directly with the user) communicating back to the parent, now their lies the problem.
As I stated the subwindow can be an implementation as simple as containing a single component to one of many. For my example I'm going for a single component implementation as this makes our lives simpler but the same rules apply to any other subwindow. This example also addresses another short coming of Omnis Studio in that there is no way to subclass a component to extent its behavior. Using a subwindow gives you the required functionality but at a price (both performance and in event handling). The other option would be to create an xcomp in C++ but that often is overkill and requires a completely different skill set.
First, I'm not going to present all the code here, instead you can download the sample library here (Omnis Studio 4.3.1).
When you run the sample library it installs a menu from which you can access 4 sample windows. You will also find that there are four folders inside of the library (Example 1 though 4) each containing the objects used in each example, these folders are self contained for clearity, I'm not using any object between examples.
Then an important distinction:
- When I speak of a subwindow I mean the windows class that I'm using as a subwindow (or it's instance)
- When I speak of a subwindow FIELD I mean the field on the parent window that contains (or will contain) the instance of the subwindow.
The first example window simply shows us our objective. It is an implementation of the logic without using a subwindow. The logic is an entry field that has some auto completion magic going on in the background. The $construct of the window builds a list of words from a piece of text I copied off of the web somewhere. The text (and the list of words derived thereof) is completely useless but shows the principle. The entry field has an evKey event handler that tries to complete the word the user is typing by using the list of words as a dictionary. Only words from the list are allowed.
It is a handy bit of logic that I would like to reuse in my application in many different places, anywhere the user is trying to type in something that I want restricted or want to make the life of the user easier by looking up the word he/she is trying to type but I don't want to copy the logic again and again...
The second example shows our first attempt at moving the logic into a subwindow so we can reuse it. As I want the subwindow to be used for any set of lookup values the list of words is build outside of my subwindow and then passed to it but it is the subwindow that holds all the behavior for looking up words. When the user leaves the field there is a callback into my parent window that informs the parent window the word has changed (if this is indeed the case).
This already shows the first omission within Omnis. Although my subwindow field has a $dataname property it doesn't actually give me access to a variable on the parent window. Instead I pass the field I would like to use with $dataname as a parameter and remember a reference to it. There is a trick I can use with $dataname but I found it very inconsistent. Some may argue this is against OO principles but remember I'm trying to mimic subclassing a component and window components all implement this type of behavior through $dataname.
In this sample I'm communicating back to my parent window by also passing a reference to the instance of my parent window as a parameter. In this particular case I could do without this reference and instead use $cwind however then I would loose the ability to use my subwindow on another subwindow as $cwind would no longer be pointing to my subwindows parent but the main window.
There is however a weakness to the approach in example 2. If I would like to have more then one auto completion field on my parent window they would all call the same method on this parent window. Now I could also pass a method name to my subwindow or add an additional parameter to my callback method but it would all get very confusing very fast.
This is where example #3 comes in, on this window there are 3 subwindow fields, each using the same class. These will end up containing 3 different instances of my one subwindow class. I've changed my example only slightly by doing two things:
1) I've implemented an $evModified class method in my subwindow
2) I've implemented an $evModified field method for each of my subwindow fields
I've left out the $evModified on the 3rd field on purpose to demonstrate what is happening behind the scenes.
If you look at my $event's "On evAfter" you will see that I am calling $cinst.$evModified. You would assume that this would call the class method for my subwindow but you would only be partly right. Omnis actually first tries to find this method on the subwindow field on my parent window!
Hence for the first two subwindow fields the correct method on the parent is called establishing the communication I want and only for the 3rd subwindow field (the one I didn't implement an $evModified for) calls the method within my subwindow. This last method nicely informs the developer he/she (=me) forgot to implement that method.
Example 4 takes this logic one step further.
First it moves the lookup list back into the parent window and instead introduces another callback called $evGetWord that requests the completed word. When a lookup list is available this actually introduces some unwanted overhead, but assume for a moment we're not using a list but instead getting the data from another source we suddenly have an implementation where we can customize loading the data to our needs (for instance, doing a lookup on a specific file class).
The second change lies in the implementation of the methods in the class itself. As stated these only get called if the developer has forgotten to implement them on the subwindow field. In this example I've implemented a nice bit of helper code that creates the methods on the parent class as needed. This obviously is not code designed to run in the end product but it is something that helps you remember the interface you need to support. Especially when working in a team where another developer has created the subwindow, he/she ensures you know which methods you need to implement to properly use his/her component. All you need to do is place the component on your window and run it once.
Again I haven't implemented these methods on the second subwindow field to demonstrate Omnis creating the methods on the fly as needed. They could end up having a completely different implementation compared to the first field.
Add to: Digg | Technorati | del.icio.us | Stumbleupon | reddit | Furl