Prologue
Below is a write-up based on one of the talks I did at EurOmnis 2014. Further files related to this can be found on my github page: https://github.com/BastiaanOlij/EurOmnis/tree/master/2014Introduction
We often find ourselves implementing the same behaviour time and time again on entry fields and such, adding additional keyboard shortcuts, adding a button to popup some predefined options, implementing auto completion, etc.One solution to this problem is to create a helper object that centralises the code used in these scenarios. The downside of this approach is that any enhancement you make to the helper object potentially requires you to go past every instance it is used and make modifications.
Omnis has an easy to use solution that does not seem obvious at first but is really powerful once you know the rules by which to play. Before we look into this solution it is important to dispel one big myth.
$dataname
When we assign a variable name to the $dataname property of a field, such as an entry field, the illusion is created that this entry field now display and makes accessible this variable. In fact this is not what is happening as this break the encapsulation rule in OO programming.An entry field actually has its own value and it is that value that is being displayed. For convenience we can directly access this value through the $contents property of the field. Even without the $dataname set, the entry field is fully functional.
Omnis will copy the contents of the variable into the fields contents when a redraw is triggered. This is the responsibility of the window, for the window the variable is within scope and it has access to the field and can thus copy the value without breaking any of the rules.
In the background it is slightly smarter as it first checks if the variable has changed by comparing it with the current contents and if changed copy the value and redraws the field. If you've ever build an XCOMP you'll see the special messages you need to implement on the XCOMP that drives this.
Omnis only copies the contents of the field back into the variable in 1 condition and that is when the field looses focus. It is actually part of the evAfter event where the field informs its parents window focus is leaving it, the parent window copies the contents into the variable (again, both now being in scope) and then runs through the $event code.
This is something you can easily see for yourself if you turn $keyevents to true and capture the key events, you'll see the variable never changes while the contents is updated as a result of the keyboard input. Only at the start of the evAfter event the variable now has its new value.
It is this last piece of knowledge that hinders us the most as with subwindows we may combine a couple of fields to form a single entity and we may have a need to update our value but not being able to push it back out to the variable as our main field hasn't got focus. But I'm running ahead of myself.
Omnis is not alone here, to be exact every development environment I've worked in so far, be it Obj-C Cocoa development, Microsoft Foundation Classes, Javascript, etc. have to deal with the same and each implement data binding differently. The easiest in which to see this probably is Javascript/HTML. The entry fields all exist within a form and each hold their own data. You access that data directly on the fields in Javascript through the fields text property and have to write your own code to copy this into any variables where you need the data.
Omnis' solution is one of the more elegant ones I've come across and excels in it simplicity.
Foundation for a subwindow field
The funky thing in all of this is that Omnis has added one extra little feature that seems counter intuitive but allows us to implement more complex fields as subwindows. As it turns out setting the $dataname on both the subwindow field on the parent name and on the field help within causes the logic to expand to the fields within the subwindow.It is important to realise within our subwindow our variable is out of scope and we do not have access to it. We do however have access to the contents of our field.
We start with building an extremely boring subwindow with just our one field on it to prove the concept and lay down some rules.
One rule is one of a naming convention, you can decide on your own but I tent to prefix these types of subwindows with "wView". "View" comes from the MVC (Model View Controller) approach that is widely adapted today. A view is defined as an entity that visually presents data and enables the users to interact with that data without any further knowledge of the construct the data is a part of nor the business rules that surround that data. But I regress..
Create a new window class and call it wViewExample.
- Set its $issubwindow property to true (this is purely a helper property and has no real effect on the window).
- I also tent to size the subwindow to how I would use it and set its $style to kNoFrame but you may want to leave this until the very last moment as it does hinder with development.
- Set its $backgroundtheme to kBGThemeControl. While this has no effect when using our subwindow it does help when developing our field.
We're going to set a few properties on this field:
- first of all, leave $fieldstyle empty, we'll be inheriting our fieldstyle from our subwindow field
- set $backgroundtheme to kBGThemeParent, this will copy the background settings from our subwindow field
- set $effect to kBorderNone, our subwindow field will already have the required border, no need in doubling it
- set $edgefloat to kEFPosnClient
- set $vertcentertext to kTrue, I generally find this to work better visually but it is an optional.
- set $subwindowstyle (text) to true. This ensures our font, fontsize and fontstyle are all taken from our subwindow field
wViewExample.$construct ----------------------- Calculate $cinst.$objs.Contents.$dataname as $cinst.$dataname Calculate $cinst.$objs.Contents.$tooltip as $cinst.$tooltip
Notice that I've also copied $tooltip. You may find other properties that you can set on your subwindow field that you would want to copy into your field but for this example I'll stick with $tooltip.
Note that if I was to instantiate my view as a window it works fully, these actions are pretty much ignored. While $cinst points to my window instance these properties don't exist. But we'd never use this window as such.
When used as a subwindow it is very important to realise $cinst points to the subwindow field on the parent window not directly to your subwindow instance. This seems a feeble difference but it is important. Your subwindow instance lives 'inside' your subwindow field. There is a runtime property on your subwindow field called $subinst that gives you access to the subwindow instance.
Now we'll sidetrack slightly, one of the things that may happen is that you will want to change the $dataname or $tooltip in runtime. When you do this you will change the property on the subwindow field but not our entry field held within. Luckily there is a simple solution by implementing $assign methods for these properties:
wViewExample.$dataname.$assign(pvNewName) ----------------------------------------- Do default ;; This will assign the dataname on our subwindow field Calculate $cinst.$objs.Contents.$dataname as pvNewName wViewExample.$tooltip.$assign(pvNewTooltip) ------------------------------------------- Do default ;; This will assign the tooltip on our subwindow field Calculate $cinst.$objs.Contents.$tooltip as pvNewTooltip
We do not implement getters for these properties as that would break access to our properties on the subwindow field. We would implement getters for any additional properties we want to add to our subwindow.
Now we're ready to put our new subwindow on a test window.
- we create a window called wExample
- we create an instance variable on our window called ivTest
- we drag our subwindow field from the Subwindows tab in our component star (this is what the $issubwindow property is for)
- we give our subwindow field a name, lets say "MySubwindowField"
- we set the $dataname property of our subwindow field to ivTest
- when the subwindow is dragged from the component store we end up with a field slightly larger then the size we've set our subwindow class too. Hence why I tent to set these conservative
- if you configure your subwindow field either through field styles or by setting its background and text properties you should notice the field within following suit
- if you test your window you should notice that the contents of our variable is now shown and changing the data also changes the contents of the variable (after you tab out of the field)
Adding events
The obvious problem is that our evAfter is now contained within our subwindow. Our parent window is never told the user has tabbed out of the field and thus can't react appropriately. We need a way to send events to the parent window.Omnis does not support a way to fire of standard events. You could call $event but you do not have any control over the event parameters. You could call a method on the containing window through $cwind but this is also not without problems:
- What if your subwindow is within another subwindow, you'd end up calling the wrong parent
- What if you have multiple copies of your subwindow on the window, you only have one method, you'd somehow need to know which subwindow is calling
- What if the developer using your subwindow doesn't know which methods to implement? Or forgets one? You'll run into a nasty error
As I mentioned before $cinst will be pointing to the subwindow field, but in absence of a method in the subwindow fields the method in class methods will be called. Now let that sink in, because in every other situation you would expect the method in the class methods to be called.
Lets create a method in our class methods called $evAfter and call it from our after event:
wViewExample.$evAfter --------------------- ; This is just a stub Quit method kTrue wViewExample.Contents.$events ----------------------------- ... On evAfter ;; Event Parameters - pClickedField, pClickedWindow, pMenuLine, pCommandNumber, pRow If $cinst.$evAfter()=kFalse Quit event handler (Discard event) Else Quit event handler (Pass to next handler) End If ...
Note: I tent to quit true or quit false and then issue a discard or pass event on return (the =kFalse is so my code assumes passing the event if nothing is returned).
Alternatively you could just end your $evAfter code with "Quit event handler..." and call "Do $cinst.$evAfter" in your evAfter event. There is no right or wrong here.
If you test your window now, we're no further then we where. We can type in some text, tab out, the $evAfter is called but our parent window is still non the wiser. However we've already dealt with two issues:
- if our developer using our component doesn't implement the $evAfter method, nothing breaks
- the developer can check the interface manager and see which events are supported.
So our final piece of the puzzle is implementing $evAfter on our parent window:
wExample.MySubwindowField.$evAfter ---------------------------------- OK message MySubwindowField.$evAfter {Our value was changed to [ivMyValue]}
Now when we test our window, type in something in our field and tab out, we get a nice message informing us the value has changed. Our event mechanism works!
Obviously all we have now is a glorified entry field that does a lot less then a normal entry field.
Lets make it a bit more special
A spinner control
Lets change our example into a spinner control. We're going to do a couple of enhancements to our example subwindow:- we are going to assume it holds a numeric value
- we are going to implement key events that will increase/decrease the value
- we are going to add buttons to increase/decrease our value
For point one the only thing we'll change for now is to set our $align property on our contents field to kCenterJst. We can't enforce the user to use a numeric variable. We could go through the trouble to use a masked entry field instead of a normal entry field and set its formatting to numeric. That I'll leave as an assignment to you to do.
To make our key events work we'll need to set the $keyevents property on our Contents field to true. Then we implement our key event code:
wViewExample.Contents.$events ----------------------------- ... On evKey ;; Event Parameters - pKey, pSystemKey If pKey='-'|pKey='_' Calculate $cobj.$contents as $cobj.$contents-1 Quit event handler (Discard event) Else If pKey='='|pKey='+' Calculate $cobj.$contents as $cobj.$contents+1 Quit event handler (Discard event) Else Quit event handler (Pass to next handler) End If ...
Try out your window (don't forget to save or the properties don't always stick!). You should now be able to increase/decrease the value with the + and - keys respectively.
We are updating the $contents of our field so our variable doesn't change but this is fine, as we still have focus on the field as soon as we tab out Omnis will copy our variable and handle the evAfter event.
Now we are going to do the same with buttons. Add two buttons to your window "IncreaseBtn" and "DecreaseBtn". Set the $edgefloat for "IncreaseBtn" to kEFposnRightToolbar and for "DecreaseBtn" to kEFposnLeftToolbar. You'll want to give them an appropriate width but I'll leave it up to your imagination to further style these buttons.
I also tent to set $disablefocus to kTrue but that is a personal preference.
Now implement some code:
wViewExample.DecreaseBtn.$event ------------------------------- On evClick ;; Event Parameters - pRow( Itemreference ) Calculate $cinst.$objs.Contents.$contents as $cinst.$objs.Contents.$contents-1 Quit event handler (Pass to next handler) wViewExample.IncreaseBtn.$event ------------------------------- On evClick ;; Event Parameters - pRow( Itemreference ) Calculate $cinst.$objs.Contents.$contents as $cinst.$objs.Contents.$contents+1 Quit event handler (Pass to next handler)
Well that was easy... or was it?? Sure when I press one of the buttons it increase or decreases the number. But as soon as I do something else it changes back??????
Why?
Remember, Omnis will copy the contents of your variable into the field on a redraw, and only copy back the contents into the variable when our field looses focus.
When we press the button our field has already lost focus, the new contents is not copied back into our variable.
There are two ways to deal with this. The easiest is to ensure our field does have focus.
wViewExample.DecreaseBtn.$event ------------------------------- On evClick ;; Event Parameters - pRow( Itemreference ) Do $ctarget.$assign($cinst.$objs.Contents) Calculate $cinst.$objs.Contents.$contents as $cinst.$objs.Contents.$contents-1 Quit event handler (Pass to next handler)And do the same for the IncreaseBtns $event. This will make our field work. Our entry field get focus, we change our contents, and when the user tabs out all is well.
This technique however doesn't always work. For simple situations like these it does but once your subwindow becomes more complex and starts to popup messages or dropdowns and such it falls hopelessly short. Omnis just won't handle the focus correctly or the already queued events will get in the way.
As mentioned a few times now, this is all about scope. When you leave the field Omnis is sending an event to the parent window, it is the parent window that copies the contents into the variable, and then the evAfter is triggered.
We can do the same but we don't want to rely on the developer to implement the needed method on the parent window so we do this dynamically.
Here is the final bit of code needed to make this work consistently:
wViewExample.doDatanameCallback ------------------------------- ; $cinst points to our subwindow field so this is actually going to look for our method on our parent window! ; Note that this logic will fail if our parent window is locked Set reference lvMethodref to $cinst().$methods.$findname('$tmpUpdateData') If lvMethodref ; Cool Else Set reference lvMethodref to $cinst().$methods.$add('$tmpUpdateData') Do lvMethodref.$lvardefs.$add('pvValue',kInteger,k32bitint,0,kTrue) Do lvMethodref.$methodlines.$add(con('Calculate ',$cinst.$dataname,' as pvDate')) End If ; ; And call Do $cinst.$tmpUpdateData($cinst.$objs.Contents.$contents) wViewExample.DecreaseBtn.$event ------------------------------- On evClick ;; Event Parameters - pRow( Itemreference ) Calculate $cinst.$objs.Contents.$contents as $cinst.$objs.Contents.$contents-1 Do method doDatanameCallback Do $cinst.$evAfter() Quit event handler (Pass to next handler) wViewExample.IncreaseBtn.$event ------------------------------- On evClick ;; Event Parameters - pRow( Itemreference ) Calculate $cinst.$objs.Contents.$contents as $cinst.$objs.Contents.$contents+1 Do method doDatanameCallback Do $cinst.$evAfter() Quit event handler (Pass to next handler)
I've also asked TigerLogic for an enhancement to make this push a feature, it really is the only missing ingredient.
Now things are working correctly. We have a reusable field that lets us increase/decrease our value.
A few enhancements that I can think off of the top of my head that would be nice to implement:
- implement an $evModified method that gets called before $evAfter but only if the value has changed
- implement a $stepvalue property that allows you to set by what value we increment
- react to mouse input to increase/decrease the step value
There are plenty more examples in the widgets library, some a lot more complex then this.
No comments:
Post a Comment