Tuesday, 28 October 2014

Fields based on subwindows

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/2014

Introduction

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.
Add an entry field onto our subwindow and name it "Contents". Again this is just a convention I follow, you can decide on your own name if you wish but I'm assuming below it is called Contents.
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
Notice that I've not set $dataname just yet. It is much better to copy our $dataname from our subwindow field and we simply doing this by adding some code to our $construct:
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
Now there are a few things we notice when doing this:
  1. 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
  2. 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
  3. 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
Omnis however has a very elegant solution, one that is arguably better then the build in $event approach (and yes, I've put in an enhancement request:))

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.
For this last point I'm using the naming convention that any class method starting with $ev is an event that will be supported when implementing on the subwindow field.

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:

  1. we are going to assume it holds a numeric value
  2. we are going to implement key events that will increase/decrease the value
  3. 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.

Sunday, 27 April 2014

Getting back into playing with OpenGL

The single most influential thing that got me into programming where games..

My father worked in IT and we had computers at home since before I was born which may not seem special today, but in the late 70ies, in the Netherlands, people owning their own Personal Computer was rare, very rare indeed.

My first memories are playing simple text based computer games on a TRS-80 or somewhat better looking games on an Atari 2600. While I started learning to program in Basic on the TRS-80 at a very young age it wasn't until my father bought an intel 286 based PC with a EGA graphics card that I started getting really interested in graphics programming. Initially still sparked by a wish to learn how to write games but by the later 80ies I would be introduced to the so called Demo Scene and get hooked purely on doing graphics programming for the sake of making cool effects onscreen.

Eventually I would get a job doing "normal" programming (which in an entirely different way is exciting and rewarding in its own right) and doing graphics programming was put onto the back burner. Occasionally I would play around again, learn some of the newer technologies and forget about it for awhile again. Some of my friends did move into writing games and I've stayed in touch which is great as they do not mind letting me know a bit on what goes on under the hood of modern graphics programming.

My interest has recently been sparked again, purely as a hobby mind you, mostly with the popularity of the new Oculus Rift and my age old love of VR. Combine that with having bought a TV that allows stereoscopic viewer and a Retina Macbook that has an HDMI port and voile, the game is set.

So I've dived back into OpenGL. Why OpenGL and not Direct X (which seems the better choice if I ever actually get my hands on a Rift)? Well simply put, I have a Macbook pro and OpenGL is natively supported and the graphics card in my little Macbook is beefy enough for what I do.
I have since read that while OpenGL is well supported on the Linux platform, Microsoft seems to not give it any love because it competes with Direct X and as there is a large game market using that API it always seems to run a generation in front of OpenGL. Alas.

It isn't a big problem as long as this stays a hobby, if something comes out of this in years in the future, well we'll deal with it then:)

The last time I played with OpenGL was, uhm, 10 years or so ago? Yeah probably about that if I don't count my stint with WebGL a few years back, so I decided to start from scratch. Learned a few new things about vertex buffer attributes and brushed up on my GLSL skills and I have my starting point.

I've got a landscape thingy that I'm still working on, but I've put that on ice for a little while. The thing that has driven my wife nuts for the last few days as I've spend hours deep into the night while everyone is asleep was getting a simple wavefront object loader up and running and doing a simple texture mapped shader on which I can start building some cooler things.

I pulled out a trusty old model from one of my favourite games Homeworld to test things with. Here is a nice little screenshot :)

And for those that are able to view things in stereo, a nice little splitscreen screenshot:


This is a good article (imho) on which I based most of the stereo scopic code: Stereo geometry in OpenGL

Thursday, 27 March 2014

Developing externals for Omnis Studio

For the past few years I've been building externals for Omnis Classic and later Omnis Studio. Initially relatively small things but as time progressed I ended up having to build larger and more complex externals.

In the Omnis Classic days externals where relatively limited but the SDK that comes with Omnis Studio allows for a lot of functionality to be implemented as is proven by some of the commercially available externals such as Brainy Data's excellent oWrite.

Unfortunately the documentation often leaves much to be desired, the sample externals are over simplified and support steers clear from any issues involving external development.

Both at my previous employer and my current employer I've been blessed to have worked on some interesting, yet proprietary externals. That hasn't stopped me from doodling around in my spare time, one of the more notorious albeit long forgotten, more public externals I did in the classic days was one that allowed you to "skin" the Omnis interface much like the then popular WinAmp did.

I have for a long time been working, mostly in my spare time, on a framework for creating externals that takes care of a lot of the nitty gritty stuff that ends up in each and every external and just gets in the way of making the external do what you want it to do.  I've started again a few times and still haven't gotten it right but it is slowly coming of age.

I've since published the work that has been done so far on my GitHub page: https://github.com/BastiaanOlij

As I add things onto this project I'll try and keep up blogging about what I've been up to and what things where worth learning.

If you find use of the source code that has been made public, please be so kind as to credit the author (thats me). Other then that use it as you see fit, except for evil, I don't like evil.

On that note, I'd like to take the time to credit and thank the following people:
Kelly Burgess, for his excellent sessions at EurOmnis and his frequent emails with advise
Stefan Csomor, for always offering a helping hand and insight in the more lower level stuff
Michael Monschau, for showing us just how far you can take this
David McKeone, for also contributing to the public knowledge with his excellent GitHub page.

Tuesday, 28 May 2013

Part 5: Adding multi lingual support

This is a repost of my old tutorial I wrote in late 2007. I'm presenting them as they where.

Now that our application is still small its easy to add multi language support into it. I always suggest doing this when you start building your application, not later down the line.
For multi lingual support Omnis internally uses string tables. String table translate a label to the current language in the background so you can build your application using the labels. As the string table is stored separately from your library you can have someone translate the labels while you continue working on your application.
Well be using Omnis string table editor to create our string table. This is a fine tool to use when youre a single developer but when youre working in a team you may want to take a different approach. Ive build my own version of the string table editor that uses a database backend so a team of developers can maintain a single collection of translations and build string tables from there. However I would probably not go down that route today but instead let each developer maintain their own IDs and merge string tables together.
For our tutorial well use a single string table file, that is enough to show you how it works. If you have a big application, or think your application may become big in the future, I do advise you to create multiple string table files. What I do and what works fine for me is that I place all the string tables I need in a subfolder and when my library loads, I load all string tables within that subfolder into memory using the string table file name as a guide.
The reason I suggest using multiple string tables is two fold:
1) very very big string tables are hard to maintain and can have a performance penalty
2) you can add string tables with translations that only apply for a certain customer or module
You can also choose not to use string table files but store the translations separately. For instance the string table module contains functions from loading translations from a list that you can build from a database. But Ill leave that up to your own imagination.
You can find the string table editor by going to Tools->Add Ons->String Table Editor. Each string table has an "ID" column and one or more language columns. You can open my example string table and take a look at its contents. There are translations in there for 3 languages at the moment.
The ID column is whats important to our application. If you use a single string table you can use this ID field directly but if you have multiple string tables loaded, its smart to prefix the ID with the string table name when using it in your application. So a ID called "Username" you would use as "demolib.Username" in the application. I always prefix it because I may be using a single string table today, but maybe tomorrow I will add a second or third.
Now that we have created our string table we need to load it into memory to be able to use it. Add the following code to your startup_task:

Startup_Task.$reloadStringTable()
----
;  Get the location of our library, our string table should be in the same directory
Split path name (sys(10),tmpDrive,tmpDir) 

;  Get the current selected language (if we are reloading)
Calculate tmpColumn as StringTable.$getcolumnnumber(demolib)
If tmpColumn=-22     ;; No columns set?
Calculate tmpColumn as 2     ;; 2 is the first language!
End If

;  Unload string table just in case
Do StringTable.$unloadstringtable(demolib)

;  Now load our string table
Do StringTable.$loadstringtable(demolib,con(tmpDrive,tmpDir,demolib.stb))

;  Make the language current, note that if you have multiple string tables you need to do this for each one!
Do StringTable.$setcolumn(con(demolib.,tmpColumn))

We need to call this method from our startup tasks $construct. Note that its called $reloadStringTable, not $loadStringTable and that the code allows you to call it at a later time again. As youre developing youll be altering the string table and you will want to load the changes into memory without restarting your whole application. Adding a menu item or toolbar button that is only available during development and calls $ctask.$reloadStringTable will give you the ability to quickly load the updated version of the string table.
Most of the time you will be using the string table to translate labels on your window. For this Raining Data has created a nifty little background object called a string label field. Its in the last pane of your component store. Well start by making our logon window multi lingual and replace all our label fields with string label fields.
Remove the username label from the logon window and drag a string label field onto the logon window and place it where our username used to be. Then rightclick on the string label and select properties, the property manager should popup.
In the property manager there is a new tab called "Custom". In this tab there is a property called rowid. In this you should put the ID of the string table, type in "demolib.Username".
Note that the text isnt translated until runtime.
Also assign CtrlLabel to its $fieldstyle (note, in early versions of the string label $fieldstyle didnt work and you had to set all the properties manually!).
Now do the same for the password and hostname fields.
We also need to do the logon button. Now there is no string table version of the pushbutton, instead well need to handle the translation ourselves. This is easy. Select the $text property of the logon pushbutton and clear it, now press F9.
Select the StringTable pane in the catalog and doubleclick the logon label. Omnis will insert the correct text, all we need to do is place this text between brackets so $text becomes: [StringTable.$gettext("demolib.Logon")]
We will do the same with the window title. I have a standard that I always name my label the same as my window, so wLogon.$title becomes: [StringTable.$gettext("demolib.wLogon")].
Now when you open the logon window it should show all the labels in Dutch. Since Dutch is my first column in my StringTable, it has become my default.
However we want the user to select a different language. You might want to make this part of your applications configuration or maybe store it along with the user details and not set it until after the user logs on. For our tutorial well make the language selectable by the user on the logon window as that is the first window the user sees. Well add a dropdown list field to our logon window and add the following two methods to it:

wLogon.iLanguageList kList

wLogon.iLanguageList.$construct
----
;  the stringtable framework should always be installed so we can use that..
Calculate tmpCurrentColumn as StringTable.$getcolumnnumber(demolib)

Do iLanguageList.$define()
Do iLanguageList.$cols.$add("Description",kCharacter,0,250)

For tmpColNumber from 2 to StringTable.$colcnt(demolib) step 1
  ;  Make the column current so we can get the column name!
  Do StringTable.$setcolumn(con(demolib.,tmpColNumber))
  Calculate tmpColName as StringTable.$getcolumnname(demolib)
  Do iLanguageList.$add(tmpColName)
End For

;  Change it back to what it was
Do StringTable.$setcolumn(con(demolib.,tmpCurrentColumn))
Calculate iLanguageList.$line as tmpCurrentColumn-1

wLogon.iLanguageList.$event
-----
On evClick     ;; Event Parameters - pRow ( Itemreference )
  Do StringTable.$setcolumn(con(demolib.,iLanguageList.$line+1))
  Do StringTable.$redraw($cwind.$hwnd)
  Do $cinst.$redraw(kTrue,kTrue)
  ;  Title is not reset automatically....
  Calculate $cinst.$title as StringTable.$gettext(demolib.wLogon)

Now when you open the logon window you should be able to select from the 3 languages and the interface should change itself.
So resume:
  • Use string labels for your labels and they will automatically translate themselves
  • Use the method stringtable.$gettext(stringtable.label), between [ and ] where needed, to translate on the fly
  • Always prefix your labels with the name of your stringtable.
Two last things that remain to be said:
  • The standard $gettext is fun, but I always implement a Startup_task.$gettext(pLabel, p1, p2, p3, ... p9) method that translates the label pLabel and then does a replaceall function for each specified parameter. This is great for translating error messages. My error label "mandatoryError" translates to "%1 is a mandatory field". Not I can simply do a $ctask.$gettext("mandatoryError", $ctask.$gettext("Username")) and it will come back with a nicely translated "Username is a mandatory field". Ill leave it up to you to make a nice one for yourself.
  • I dont particularly like the placement of some of my routines. Proper OO programming, if there is such a thing, would dictate that I would subclass the StringTable object and implement a $reloadStringTable, $buildLangList, $setLanguage, $getLanguage and $gettext (overridden) method in there with my logic. But I can not subclass from the StringTable object in the way that I want and so I generally dont bother because its only the $gettext that I frequently use. Still it may be nicer to create a single object that encapsulates this logic so its in one place and then call the logic from the places its needed.
Thanks to Rainer R. Greim for the German translations.
----
Disclaimer: Im sure some of the listers will recognize their ideas in this tutorial in one form or another. Some ideas are truly mine, some are inspired by what Ive learned in over 10 years of conferencing and reading the list. It would be an impossible task for me to list the many many people who I owe thanks to. Im sure you know who you are my friends. I take only credit for taking the time to put this series together. Use the code you find in this tutorials and the library/libraries attached freely and as you see fit. I take no responsibility and will not be liable for any damages resulting directly or indirectly from using the information I present here.

Part 4: Adding a broker

This is a repost of my old tutorial I wrote in late 2007. I'm presenting them as they where.

Simply looking at our current application we already see a problem prop up. I try to enter a new contact and I find out there is no contact type that suits my needs. I open up the window where I can add a contact type and I do this but when I return to my contact window, my new contact type is nowhere to be found.
A broker is one way to solve this. A broker is a mechanism where objects can subscribe to messages (events) that they would like to know about and to which they can send messages when a certain action has been performed.
Our contact maintenance window can tell the broker system it wants to be informed whenever the content of the contact type table changes, and our contact type maintenance window can tell the broker system when its changed it.
The contact maintenance window and the contact type maintenance window do not need to know anything about each other nor about their individual needs. The broker system needs to know nothing about how contact types are created or what the contact maintenance window requires to do when a contact type changes. All it needs to do is figure out who needs to be told about what.
Before we implement our broker system however, we're first going to readdress and old issue :) we're going to auto number the contact number. So how to implement this? Well very simple, we'll handle it at the source.
Our tContact table class handles the insert logic for our contact records so every time we need to insert a new record, we call its $insert, so every time we need a new contact number, we call our $insert, so why not put the logic to determine a new contact number in our $insert of our tContact table class.
First we change our $validateRecord and remove the check that validates the number, the user won't be filling in the contact number. Then we change our $insert

tContact.$insert
----
local variable tmpNewID kCharacter(25)
local variable tmpResult kBoolean

;  Get a counter value for our contact number

Calculate tmpNewID as $cinst.$getNextValue('ContactNumber')
If tmpNewID<0
  Quit method kFalse
End If

Calculate $cinst.Number as jst(tmpNewID,'-8P0')

Do inherited Returns tmpResult
If not(tmpResult)
  Calculate $cinst.Number as ''
End If

Quit method tmpResult

We also change the contact maintenance window so the contact number field is always disabled and we change the record browser base window to redraw on a successful save.
Now it's time to build our broker. Now this broker is going to be really simple, it's only going to support windows and it's only going to support messages to inform of record updates. You can build a more complex broker if you want.
I've created an object base class with some basic error handling that we can extend later on. I won't detail it here right now as we're not really using it for the broker.
We start with the constructor for the broker.

class variable cSubscribeList kList
oBroker.$construct
----
Do inherited 

If cSubscribeList.$cols.$count=0     ;; only init our subscription list once
  Do cSubscribeList.$cols.$add("instanceName",kCharacter,0,250)
  Do cSubscribeList.$cols.$add("tableName",kCharacter,0,250)
End If

Our broker class contains a class variable in which we keep the information about the windows that have subscribed to the broker. This is a class list since we will have an instance of our broker within our window that is subscribing and an instance of our broker in the window that will be sending the messages. You can think of the broker object as a doorway to our subscription list that we can construct and throw away as we please.

oBroker.$subscribe(pInstanceName, pTableName)
----
If cSubscribeList.$search($ref.instanceName=pInstanceName&$ref.tableName=pTableName,kTrue,kFalse,kFalse,kFalse)=0
  Do cSubscribeList.$add(pInstanceName,pTableName)
End If

Quit method kTrue
This method allows us to subscribe to our broker. It tells the broker we want to receive information about changes to a specific table.

oBroker.$unsubscribe(pInstanceName[, pTableName])
----
If cSubscribeList.$search($ref.instanceName=pInstanceName&($ref.tableName=pTableName|pTableName=''),kTrue,kFalse,kTrue,kTrue)>0
  Do cSubscribeList.$remove(kListDeleteSelected)
End If

Quit method kTrue

Our unsubscribe method unsubscribes our window. We can unsubscribe all our subscriptions at once by only specifying the instance name. Note that in the window base class $destruct method I've put the following code:

wBase.$destruct()
----
local variable tmpBrokerObj kObject(oBroker)
Do tmpBrokerObj.$unsubscribe($cinst().$name)
This will ensure any subscriptions still active for our window will be unsubscribed when a window closes, I never have to worry about this anymore.
Now for the code that will actually send the messages around:

oBroker.$sendmsg(pSenderName, pTablename, pDataList)
----
;  pDataList could be a list of primary keys that have been effected
For cSubscribeList.$line from 1 to cSubscribeList.$linecount step 1
  If cSubscribeList.instanceName<>pSenderName&cSubscribeList.TableName=pTableName
    ;  inform each of the subscribed windows
    If $iwindows.[cSubscribeList.Instancename].$methods.$findname('$receiveBrokerMsg')
      Do $iwindows.[cSubscribeList.instanceName].$receiveBrokerMsg(pSenderName,pTableName,pDataList)
    End If
  End If
End For

Our send logic simply loops through the subscription list and sends the message to any instance that has subscribed to the table that has been changed unless it's the window that send the message.
pDataList is a list variable that should contain at least the primary key column of each record that was changed in the table.
For our example however we'll make it easy for ourselves. We'll change our wBaseRecordBrowser.$evSave method to automatically call the $sendmsg method of the broker when we save a record and just send the row being changed in its completeness:

wBaseRecordBrowser.$evSave
----
local variable tmpBrokerObj kObject(oBroker)
[...]
If tmpSuccess
  Do gDatabase.$commit()
  ;  Tell our broker, we updated something...
  Do tmpBrokerObj.$sendmsg($cinst().$name,iRecordRow.$servertablenames,iRecordRow)
[...]

Now all we need to do is use our message. When a user changes an exist or adds a new contact type, we want our contact window to reflect this. So we handle the message on our contact window. Two changes are needed:

local variable tmpBrokerObj kObject(oBroker)
wContact.$construct
----
;  First define our record row for the correct type!
Do iRecordRow.$definefromsqlclass('tContact')

;  Tell the broker we want to know about ContactType changes
Do tmpBrokerObj.$subscribe($cinst().$name,'ContactType')

;  Then do the default construct!
Do inherited

wContact.$receiveBrokerMsg(pSenderName, pTableName)
----
Switch pTableName
  Case 'ContactType'
    ;  Rebuild our contact type list!
    Do $cinst.$objs.iContactTypeList.$buildList()
  Default
End Switch

Note that we've also changed the $construct code and added a $buildList method to the iContactTypeList field on this window. The $buildList will rebuild the contents of the contact type list and make sure the correct line is reselected. In this case its easier to just retrieve the updated data from the database but we could also have looked up the data within the contact list using the pDataList parameter.
Now open both windows, look up a contact record and change the description of the contact type for that contact and you'll see it gets updated.

Part 3: Transactions

This is a repost of my old tutorial I wrote in late 2007. I'm presenting them as they where.

Transactions can be a very complex subject and we'll only touch it a little deeper here then we've done so far. We'll look at why transactions are needed and how Omnis implements transactions cross platform. We'll then look into the short comings of what we've done with transactions so far and see how we can improve on them.
Now keep in mind, not all databases that we can use with Omnis support transactions. Our DAM object has a property called $allowtransactions that will tell us if the DAM thinks if transaction support is available. I generally make it a rule not to build applications based on databases that don't support transactions. It simply gives me to many headaches :D But you can't always avoid this.
So what is a transaction? Well a transaction is a mechanism that ensures that any action that happens with in that transaction is seen as a single unit of work. That means that until you finish (commit) the transaction all actions that have happened within that transaction can be undone if something goes wrong. The transaction always either succeeds or fails in its entirety.
An example, let's say that you've created a window where you can enter orders into your system. Your order consists of an order header record that contains an order number, the link to the customer and the total amount for that order, and you have a number of order detail records that specify the individual items that make up this order and their price.
When you save this order you want to save the order in its completeness. If you safe the header but there is a problem saving the details, the data in your database suddenly is inconsistent. You have an order header stating you sold $100 worth of goods, but if only half of the details are saved, the details only add up to $50.
A transaction lets you protect this. If you start a transaction before you save any changes to the database, then insert/update your order header, then insert/update/delete your order details and finally commit the transaction you will be certain the database will not make those changes permanent until they have all happened. If anything happens in between, you can simply do a rollback and all changes since the start of the transaction are undone.
A transaction is also rolled back if something unexpected happens such as when the database connection is lost, or your application crashes or your customer trips over his/her power cables.
You can be sure you're changes are not applied until you execute your commit and that certainly safes a few nightmares and helps keep your database consistent.
Note that some database platforms require you to always start and commit a transaction to execute a query. Oracle I believe automatically starts a new transaction when you commit the last transaction. If you forget to execute a commit your transaction stays open and when you later close your application, all your changes will be lost (hurrah). Omnis luckily helps you there (autocommit).
Sybase ASE, which I have the most experience with mostly does not require a transaction to run queries, any query run when no transaction is started is automatically committed.
To support transactions Omnis has a number of different transaction modes that are important to look at first. You can change/check the transaction mode through the $transactionmode property of the DAM object. It has the following values:
  • kSessionTranAutomatic, this leaves it up to Omnis, Omnis will automatically start a transaction for each query executed if the platform requires this, and commit the transaction afterwards (similar to the old "autocommit on" in Classic). This means you do not need to, and I believe can not, start your own transactions.
  • kSessionTranManual, Omnis now lets you do your own thing and Omnis will not automatically start or commit a transaction. You now need to call $begin, $commit and $rollback to do session control (this is similar to the old "autocommit off" in Classic). So now you have to do your own transaction control.
  • kSessionTranServer, basically means hands off to Omnis. This means that you take over from Omnis and do everything yourself by executing the right SQL statements on the database. There is a big difference between kSessionTranManual and Oracle is a good example here. Note that I just mentioned that Oracle automatically starts a new transaction when you do a commit? That means that if you execute a "begin transaction" SQL command you actually get a nested transaction. That in turn means that you need to execute two commit statements to commit your transaction, one for you own transaction, and one for the Oracle one! So you really need to know the ins and outs of your platform to properly handle this mode, but it does give you the most flexibility
Transactions have another benefit to them. To be able to undo your changes in case of a rollback your database will lock any record to which you've made changes. This means no other user can touch those records until you are done with it. Look at our order example again, no user can see or touch (the changes to) our order until we've fully created/updated it, so there is no possibility that another user is running an order rapport at the same time and suddenly find the total order amount to include our new order amount, but is missing the detail information.
There is an important difference between platforms to know about though. Again looking at the difference between Oracle and Sybase they handle locking differently when another user tries to select the data. If we look at the default behaviour of these platforms we'll see that with Oracle, when a user is running an order rapport, and another user is changing an order at that same time, the rapport will be run as if no changes have happened. Oracle will see that the other user has locked the record(s) and simply retrieve the data as it was before the user started modifying the record(s).
Sybase on the other hand will wait until the user modifying the record is finished and either commits or rolls back his/her transaction. This means that the user running the order rapport will have to wait a little longer but after waiting he/she will get the rapport including the change made by the other user (personally I like Oracle's approach better).
You can do, what is often referred to as, a dirty read if you really want to. Again the different platforms will have different ways of implementing this and different rules apply (which is why I generally keep my hands off of it). A dirty read lets you select locked data in the database. That means that I can run my rapport and I actually get my results including the changes the other user has made even if a possibility exists that that user may rollback his/her transaction. On Sybase this is the only way to get my rapport without waiting for the other user to finish, but if this is your reason, I suggest you go back to the drawing board, you're doing something wrong already. I must add to this that there are some viable situations where you may want to do this. For instance in a search window you really don't care that you might get a result once in a while that may change again before you use it. But our order rapport example is one where I would never use dirty reads simply because it could make my order rapport incorrect if I read only part of the changes a user is making while my rapport is running (my order header amounts may not add up to my order detail amounts!).
Note: there are some databases out there that either allow a default to be set to, or always perform dirty reads.
When two users try to update the same record the first user will lock that record and the second user will always have to wait until the first user has finished. As you may remember we used this nice little fact to protect our auto numbering approach in tBaseCounted.$getNextValue().
Finally, I can lock records on selects but this is usually not done by default, again different databases implement this with slightly different SQL syntax. Any select on a table will result in the record being retrieved being locked. I generally don't use it.
So what about nested transactions? What are they and why do I need them? Well lets start by saying that the way Omnis implements transactions they are not supported by default, you will have to create support for them. Look at our tBaseCounted.$getNextValue() method. It starts a transaction, updates our counter table, gets the result and commits the transaction. Since there is only one update we're not really using our transaction to safeguard multiple changes here, but we're using the locking side effect from transactions to safeguard giving out unique values.
However, now I want to implement my order saving logic. My order saving logic looks like this:
1) get the next value for the order header
2) insert the order header
3) get the next value for the order detail
4) save the order detail
5) repeat 3 and 4 for each order detail
So what if I start a transaction before I execute these commands? Well we then go to step one, and in step one, uhm, I get a begin transaction? But I already have one! Hmmm.. Well we don't support nested transactions so it gets ignored, then it gets my next value, and it commits.. Oh oh.... That's the end of our transaction, right then and there.
The rest of our save thus happens outside of my transaction, if somewhere down the line a problem happens, well bad luck, there is no more transaction and its not going to rollback the way I expect it to.
Nested transactions allow me to do this, whenever a begin transaction is encountered I nest the transaction. This means that when a commit is encountered, it doesn't actually commit anything, it just goes back one transaction, until finally there are an equal amount of commits as there where begins and then my unit of work actually gets committed. If anywhere between the first begin and where I am right now there is a rollback, all changes are undone regardless of any previous commits.
Okay, now there is a catch here. Again different platforms implement nesting differently, some don't support it. Most will rollback all nested transactions on a rollback so you have to be careful that after a rollback, you know that you are no longer in a transaction. You don't need to do an equal number of rollbacks to begins.
We do not want to assume anything about the way that our platform would handle this, so we'll have to handle this within Omnis. How can we implement this?

class variable oDatabase.cNestCountList (kList)
instance variable oDatabase.iNestIdx (kLongInt)

oDatabase.$construct
----
;  Start in automatic mode (should be the default)
Calculate $cinst.$transactionmode as kSessionTranAutomatic

;  When we copy our database connection object around we share the same database connection
;  But we do not share the same instance variables as a copy is a new instance
;  So if we did our nesting count with an instance variable we're in trouble.
;  Class variables are shared amongst instances so we need to use those but we do
;  Want a counter per database connection. This is the way we work around that :)
If cNestCountList.$cols.$count=0
  Do cNestCountList.$cols.$add("NestCount",kInteger,kLongint)
End If
Do cNestCountList.$add(0)
Calculate iNestIdx as cNestCountList.$linecount
;  iNestIdx is copied to each copy of our database connection and thus they all point to
;  the same entry in our counting list!

Two thing happen in our new $construct method for our database. First we set the transaction mode to automatic so we don't have to worry about it when we're not using transactions yet. Second we initialise a class variable list to keep nested levels. Remember that in our table class we assign our database object instance to $sessionobject of our tableclass? This actually creates a copy of our object (a second instance) that shares the same database connection but otherwise has a copy of the instance variables our original object instance has. Once we start altering those instance variables their value becomes different from the original. This is not wanted behaviour!
Class variables are shared amongst all instances of our object however simply using a nested counter directly doesn't work either. If we can assume we only have one database connection or we never start transactions in parallel on different database connections I guess we could but I find that assumption very dangerous. That's why I keep a list. Every time I instantiate a connection object it adds a line to this list and the instance variable iNestIdx points to this line in the list. Every time the connection instance gets copied it copies that same iNestIdx value and thus points to the same line in my class list. Each line in my class list thus uniquely represents a database connection.
(note: with object references we should be able to fix this as well and just use instance variables, which is much nicer, but I am not sure if object references work with $sessionobject and haven't experimented enough with it to answer that)

oDatabase.$begin()
----
If cNestCountList.[iNestIdx].NestCount=0
  ;  Set manual transaction mode (autocommit off).
  Calculate $cinst.$transactionmode as kSessionTranManual

  ;  No transaction started, so we start one!
  Do $inherited.$begin()

  Send to trace log (Diagnostic message) {oDatabase.$begin(): Transaction started}
End If

Calculate cNestCountList.[iNestIdx].NestCount as cNestCountList.[iNestIdx].NestCount+1

Send to trace log (Diagnostic message) {oDatabase.$begin(): Transaction nest level is now [cNestCountList.[iNestIdx].NestCount]}

Quit method kTrue

We've overridden our $begin method and changed it so we only start a transaction when no transaction is started. Before we start the transaction we switch the transaction mode to manual.
Notice the use of trace log with diagnostic message turned on. This means these message do not get logged to the trace log until you rightclick in the trace log and select "Log diagnostic messages". The trace log is a great tool for checking if your code runs correctly. In this case it is always important to check if your save logic correctly handles nested transactions.
Also notice the way the classlist is used for keeping the nested transaction count.

oDatabase.$commit()
----
If cNestCountList.[iNestIdx].NestCount>0     ;; Still transactions open?
  Calculate cNestCountList.[iNestIdx].NestCount as cNestCountList.[iNestIdx].NestCount-1

  Send to trace log (Diagnostic message) {oDatabase.$commit(): Transaction nest level is now [cNestCountList.[iNestIdx].NestCount]}

  If cNestCountList.[iNestIdx].NestCount=0
    Do $inherited.$commit()

    ;  Return to automatic mode...
    Calculate $cinst.$transactionmode as kSessionTranAutomatic

    Send to trace log (Diagnostic message) {oDatabase.$commit(): Successful commit}
  End If

  Quit method kTrue
Else
  Send to trace log (Diagnostic message) {oDatabase.$commit(): Warning: Commit without a transaction!}

  Calculate iLastError as "Warning: Commit without a transaction!"

  Quit method kFalse     ;; Very likely we've already had an auto commit but we may still want to inform the user!
End If

The commit logic will decrease the nested transaction counter and only when 0 is reached actually commit the transaction. After commit we bring the transaction mode back to automatic.
If we get a commit when there is no transaction open, we'll log a warning.

oDatabase.$rollback()
----
;  $rollback() Rollback session transaction

If cNestCountList.[iNestIdx].NestCount>0     ;; Is there a transaction open?
  Calculate cNestCountList.[iNestIdx].NestCount as 0
  Send to trace log (Diagnostic message) {oDatabase.$rollback(): Transaction nest level is now [cNestCountList.[iNestIdx].NestCount]}

  Do $inherited.$rollback()

  Send to trace log (Diagnostic message) {oDatabase.$rollback(): Successful rollback}

  ;  Go back to automatic mode!
  Calculate $cinst.$transactionmode as kSessionTranAutomatic

  Quit method kTrue
Else
  ; Could be that we already rolled back the transaction.
  Send to trace log (Diagnostic message) {oDatabase.$rollback(): Warning: Rollback without a transaction!}

  Quit method kTrue     ;; We do return true because most likely we just encounter a rollback from a lower nest level.
End If

The rollback is always immediately performed and brings the nested count back to 0. If the rollback is performed when no transaction is run we log this but do not see this as an error.
This last bit is very important because it allows us to do some defensive programming. When I use transactions within an object I can not assume that we're going to use this transaction within another transaction or not. I will always end any begin with a commit or rollback. This means that if I nest transactions I will have an equal number of commits+rollbacks (a combination is possible) even if I do not need all the rollbacks. But it makes my code safe, I don't need to make any assumptions about the code calling my method or about any method I call from my method that also use transactions.

;  Always structure transaction handling like this:
Do $cinst.$sessionobject.$begin()  ;; using $cinst.$sessionobject as an example but could also be any other connection instance.

;  Perform any changes, calls whatever but do not escape from the code!

;  We should always reach this point, every $begin has its $commit or $rollback!
if tmpSuccess ;; on success we commit
  Do $cinst.$sessionobject.$commit()
  Quit method kTrue
else ;; on failure we rollback
  Do $cinst.$sessionobject.$rollback()
  Quit method kFalse
end if 

So lets put this into practise. We revisit our wBaseRecordBrowser. First I've made a few changes to this window that I will only mention. $updateDetail has been renamed to $$updateDetail to emphasise this is an internal method that can be overridden. Again note that this is not enforced by Omnis but my own naming convention for protected methods.
I've also added a $$saveDetail method that we don't really need right now but I've introduced them for later. This handles saving of any detail records so if we ever use our wBaseRecordBrowser for an order header/order detail window we can handle the saving of order details in this.
Finally I've added a $$detailInTransaction method that specifies whether saving the details should be part of the main transaction or not. With our order header/detail example this should be answered with a "true" but for saving customers with for instance address details we may return a false allowing the customer to be created but the address information to fail.
Some of the other methods have been adjusted to take these changes into account. Also note I did a small fix in tBaseCounted.$loadPrev() and in tBase.$newRecord().
Our new transaction handling we've placed here:

wBaseRecordBrowser.$evSave()
----
If iRecordRow.$validateRecord()
  Calculate tmpSuccess as kTrue     ;; Assume we'll be successful
  Calculate tmpNewRecord as kFalse     ;; Assume this is an existing record

  ;  Begin a transaction on our global database connection, this one is used by our record row aswell!
  Do gDatabase.$begin()

  If iRecordRow.C1>0
    If not(iRecordRow.$update(iRecordRow))
      Calculate tmpErrorMsg as iRecordRow.$GetLastError()
      Calculate tmpSuccess as kFalse
    End If
  Else
    If not(iRecordRow.$insert())
      Calculate tmpErrorMsg as iRecordRow.$GetLastError()
      Calculate tmpSuccess as kFalse
    Else
      Calculate tmpNewRecord as kTrue
    End If
  End If

  If tmpSuccess     ;; Only save details if our main record was saved
    If $cinst.$$detailInTransaction()
      If not($cinst.$$saveDetail(tmpErrorMsg))
        If tmpNewRecord
          Calculate iRecordRow.C1 as 0     ;; Reset the ID, with the rollback it will be undone...
        End If

        Calculate tmpSuccess as kFalse
      End If
    End If
  End If

  If tmpSuccess
    Do gDatabase.$commit()

    ;  Save of details should be outside of our transaction?
    If not($cinst.$$detailInTransaction())
      If not($cinst.$$saveDetail(tmpErrorMsg))
        OK message Error {[tmpErrorMsg]}
        Quit method kFalse
      End If
    End If

    Do $cinst.$setState(1)
    Quit method kTrue
  Else
    ;  Always rollback before informing the user or the records will be locked until 
    ;  the user returns from his/her coffee break!
    ;  Note, its possible we've already rolled back, actually we should assume this, 
    ; but better safe then sorry..
    Do gDatabase.$rollback()
    OK message Error {[tmpErrorMsg]}
    Quit method kFalse
  End If
Else
  OK message Error {[iRecordRow.$getLastError()]}
  Quit method kFalse
End If

Our save has chanced a bit, first we now start a transaction before we do any changes, then we do the changes, and when we're done, we do either a commit or a rollback depending on success or failure. Now on failure it is possible that we've already done a rollback, but we're not sure, so we always do the rollback. In our case, we will have gotten a rollback if our changes failed on getting a new ID value, but won't if the failure happened insert data into our database.
Note that we do not give any feedback to the user until we've done our commit or our rollback.
Open up the trace log (Tools->Trace log) and enabled logging of diagnostic messages.
Now try changing or creating some records with one of our two windows. We should see in our trace log that the transaction is started, nested and eventually committed.
Lets have some fun with this. Edit wContact and override its $$saveDetail. Change its code to:

wContact.$$saveDetail(pErrorText)
----
Calculate pErrorText as "We're just testing.."

Quit method kFalse
This will ensure any save on our contact details will fail :D Note that $$detailInTransaction is set to kTrue by default.
Before you begin, do a select * from Counter using an SQL client tool for your database to see the current contents of the Counter table.
Now try creating a new contact record. First we should see in our trace log that it will start a new transaction, it will nest a transaction (for out $getNextValue) it will commit that transaction, now it creates the contact record but we don't see that, finally it rollback the transaction due to our simulated error.
If we execute our selection in our SQL client we should see that nothing in this table has changed. If we cancel our editing and we browse through the records in our table, we should not be able to find our contact record. It is as if we've never tried to save our contact.
Now lets have some more fun, lets do something we should never ever do!

wContact.$$saveDetail(pErrorText)
----
OK message Keeping our transaction open {Go drink some coffee and make your collegues very upset....}

Quit method kTrue
Try and create a new contact again but leave the OK message on the screen!
Our transaction is still open, our Counter table record should now be locked, our newly inserted record is also still locked (though nobody will notice that most likely).
Now try and do your select in your SQL client again. Three possibilities here:
1) you get results but you should notice that your counter has not increased (Oracle, etc.)
2) you're now waiting.. and waiting... and waiting.. (Sybase ASE, MSSQL, etc.)
3) your database defaults to dirty reads and you can see the change after all (Sybase Anywhere as it turns out)
Press OK in your OK message.
Only after you press OK and the changes are committed then either your other connection should be unblocked and you should see the end results, or you can rerun the query and now see the table having been updated.
If you start the application twice (start Studio twice) and logon to the same database, and you insert a contact with one of the applications but do not press the OK button, and you then try to insert a contact with the other. Or if you try and edit the same record, you should also notice that one application waits for the other.
This is also why you should never put an OK message within a transaction :)
Finally, deadlocks, the achillis heel of transactions. Take the above example where one user is waiting for the other user to finish his or her changes. What if the other user is also waiting for the first? Then they are waiting for each other and they can wait till the end of time. This is called a deadlock.
Now in our little application so far deadlocks are not very likely, our transactions are far to simple. But take our order header/detail example and we have introduced a risk, well at least with Sybase ASE in page locking mode :D Here it is possible that a page on the counter table is locked that contains both the counter for the header and detail table. If user one updates an order header, and user two insert a new order header (and thus updates the counter) then it is possible that the insert from user #2 is locked by the update of user #1 (page locking, the insert page may be the update page!) and user #1 gets locked by user #2 as he/she tries to add a detail to the order and thus needs access to the already locked page of the counter.
Luckily Sybase these days supports row locking as do most major database vendors and this would work fine. But as transactions become more complex and effect more records, the risk of deadlocking increases.
Two important rules to follow:
  • Always insert/update your tables in the same order. Don't create your header and then your details, but in your update first update your details and then your header.
  • Try and keep transactions small, don't overdo it. For instance, if you make a quick entry window where a new customer can be entered together with the first order he/she places it makes little sense to put that entire action in a transaction. When the customer is created, commit that, if the order creation fails, only rollback the order creation, then treat the customer as a returning customer from that point onwards.
The attached library is again an Omnis Studio 4.2 Unicode library, the script to create the required database is attached to tutorial part 2.
----
Disclaimer: I'm sure some of the listers will recognise their ideas in this tutorial in one form or another. Some ideas are truly mine, some are inspired by what I've learned in over 10 years of conferencing and reading the list. It would be an impossible task for me to list the many many people who I owe thanks to. I'm sure you know who you are my friends. I take only credit for taking the time to put this series together. Use the code you find in this tutorials and the library/libraries attached freely and as you see fit. I take no responsibility and will not be liable for any damages resulting directly or indirectly from using the information I present here.

Part 2: Creating base classes to speed up development of your application

This is a repost of my old tutorial I wrote in late 2007. I'm presenting them as they where.

Okay, it's time for part two:)
We'll continue where we left off with our library, I've attached the end result to this blog entry again. If you open it you will see I've made a few changes to part one that go outside of our tutorial.
I've put some of my classes into folders. Folders are a great way to organise your library. Before you know it your library will consist of dozens of classes and you'll find yourself getting lost in them.
I've also removed our test window and test schema and table and replaced them with some schema and table classes we'll be using.
Finally I've altered my table base class slightly. The $sqlerror has been changed to retrieve all errors and we have a method called $newRecord. On hindsight I named $newRecord a little bit wrong because I originally implemented it for row variables but it works just the same for list values. It clears the values of the current row but its meant to be a little more intelligent. Right now it simply sets the value based on the column type, but in most of my applications I enhance this by conventions such as setting a column called "createdBy" to the user name or "createdOn" to the current date. It saves me the trouble of writing that logic time and time again.
Note: Please note the $$ prefix I use in the $$setFieldState method. This is a personal convention I use meaning this is a protected method. A protected method can be called from any subclass of this class but it is not allowed to call the method from outside. Omnis does not enforce this, from Omnis' perspective, this is a public method.
Attached to this blog is a small SQL file with some SQL code that will create the needed tables on the database. I like working with scripts like these to install databases because they allow me much greater control in creating constraints and indexs but they do tent to be platform dependent. You can off course also drag the schema classes in the library to the SQL browser (the reverse of what we did in the first section) and Omnis will create the tables for you. I'll show you some time later how you can write some code that will create all the tables in an empty database using the schema classes in the library.
A little information about this script. It creates 3 tables, Counter, ContactType and Contact. Counter is a special table as it allows us to keep a number of counters for auto numbers. Many SQL databases offer great native support for this, Sybase and Microsoft have identity fields, MySQL has the autonumber default property, Oracle has sequence objects, etc. If you commit to a certain platform I suggest you use this functionality. But since I can not assume anything about your platform choice, I'll show a solution that is cross platform that uses a counter table.
The other two tables, ContactType and Contact are my two data tables. All my data tables have something in common. They all start with an ID field. This is a field I generally do not show to the end user but that for my application is vital. Its a unique number that identifies my record and that I use as the primary key. This would be the field I would apply the auto numbering to. In our example I've used an integer field, which is fine for a small demonstration app like this, but for larger apps I'd suggest using a larger field type such as a numeric(15) field (maximum for a number 0dp field, at least when using Sybase).
My ContactType table and Contact table are related to each other through the field FkTypeID in the Contact table. In my script I also create a constraint that informs my database about this relation. This serves a number of functions:
  • It makes sure that my database gives an error if I violate this relationship
  • it documents this relation meaning someone looking at my database that is not formiliar with it, immediately knows this column points to the ContactType table
  • I can query this information and build logic on it enabling me to automate things, though this is harder to do cross platform
Note: This time I'm not going to put every line of code in this blog entry, that would be crazy, I'll just be putting the important bits in.
I started by creating a new table base class called tBaseCounted. We set the $superclass of tBaseCounter to tBase. There are several methods in this class but I want to hilite a few of them:

tBaseCounted.$getNextValue(pCounterName)
----
Do $cinst.$sessionobject.$begin()     ;; Begin a transaction, very important!
Begin statement (Carriage return,Linefeed)
Sta: if not exists (select 1 from Counter where Name = '[pCounterName]') begin
Sta:   insert into Counter (Name, LastID) values ('[pCounterName]', 1)
Sta: end else begin
Sta:   update Counter set LastID = LastID + 1 where Name = '[pCounterName]'
Sta: end
Sta: select LastID from Counter where Name = '[pCounterName]'
End statement
If not($cinst.$statementobject.$execdirect())
    Do $cinst.$sqlerror('$getNextValue',$cinst.$statementobject.$nativeerrorcode,$cinst.$statementobject.$nativeerrortext)
    Do $cinst.$sessionobject.$rollback()
    Quit method -1     ;; Something went wrong!
End If
Do $cinst.$statementobject.$fetch(tmpResult,kFetchAll)
If tmpResult.$linecount=0
    Do $cinst.$sessionobject.$rollback()
    Calculate iLastError as "Couldn't retrieve counter value for an unknown reason!"
    Quit method -2     ;; No value?
Else
    Do $cinst.$sessionobject.$commit()     ;; We're good
    Quit method tmpResult.1.LastID     ;; We should have only 1 result!
End If

This does a couple of things. First it starts a transaction, now not all platforms will support this, and there is an issue with nesting that we'll re-address much much later, but its very important we do this if we're building a multiuser application because of the second thing we do.
We execute a query batch, through means of a statement block, that in one go either inserts a new counter or updates the existing counter and then returns the new value. Because we are doing this in a transaction the insert or update action will lock the record we're after. That means that by the time we select our value it is impossible for another user to do the same, they will be locked by us until we're done selecting our value and committing our transaction. This ensures we do not give out the same number twice.
There is a tiny tiny tiny theoretical risk that the first time a counter is used two users try to create the same counter but this will lead into one user encountering a primary key constraint error. In real life however you'll probably end up creating all counters from the get go and users will only be updating counters.
One more important thing I want to note. I'm using $cinst.$sessionobject and $cinst.$statementobject. During the $construct of my table class my global gDatabase was assigned to the $sessionobject property of my list and at that moment a statementobject was created. I am using these here however by accessing them this way I am making my code safe in case I later decide to use this object using a different database connection. My global database connection has become my default, but not my only means of using my code!

tBaseCounted.$insert()
----
Calculate tmpNewID as $cinst.$getNextValue($cinst.$servertablenames)     ;; Get a counter value based on our server table name
If tmpNewID<0
    Quit method kFalse
End If
Calculate $cinst.C1 as tmpNewID     ;; Assume the first column in our table (and thus in our list) is the primary key!
;  Do the original insert!
Do default Returns tmpResult
If not(tmpResult)
  Calculate $cinst.C1 as 0
End If

Quit method tmpResult

Here we've overridden the standard Omnis $insert method. Whenever we insert a record we now first get a new value from our counter table and assign it to the primary key. Since I've made a rule for myself that the first column of my table is always my auto numbering primary key I can make life easy and select the first column for this (you can use C1, C2, C3, etc to access columns regardless of their name).

tBaseCounted.$validateRecord
----
;  Override this method to do any validation on the data of the current record
Quit method kTrue

Ok, this is important, this method does absolutely nothing. Its there because I want to make sure my developers are aware that this method needs to exist in subclasses of this base class. As validation rules can be very different per table, I need to override this logic per subclass. Also by implementing this method in my base class I can build logic that assumes this method exists even though when I build the logic, I know nothing about how it will work. Also if after months I decide I do implement some generic validation logic, I have already created a place to put it.
There are also a number of other methods added to tBaseCounted that work together and that are specifically meant to be used for row variables:
  • $loadRecord, loads a record with a certain ID
  • $loadFirst, loads the first record in our database
  • $loadLast, loads the last record in our database
  • $loadPrev, loads the record previous to the current record
  • $loadNext, loads the record next to the current record
Take a look at how these methods work, the code pretty much explains itself.
Now we create our schema and table classes for our ContactType and Contact tables by dragging them from our SQL browser into our library. We don't need a schema and table class for our Counter table as we won't access that directly.
Tip: Click on SQL Browser, select Options, go to the drag and drop tab pane and select the "create table and schema class" option to create both schema and table classes when you drag a table into your library. Note that Omnis creates the table classes with a T_ prefix, I tent to use t as a prefix, pick a standard you like but I do advise you to use a naming convention.
We assign tBaseCounted to the $superclass property of our two new table classes. In our two new table classes we only override the $validateRecord method, you can check the code but for now, it's really simple.
Now its time for some windows. We're going to make a couple of really simple maintenance windows for our two tables. Really straight forward. Our windows will be standard insert/edit/delete windows with a save/undo button and some browse buttons to allow us to browse through the records. The idea will be to place as much logic as we can in a base class and keep our final windows as clean as possible. Once we're finished creating another maintenance window for a new table in our database will be a matter of minutes. This is our payback on our investment.
I started with creating a window class called wBase. This will be a window class that will form the basis of all windows I'll be making. I've implemented only some basic logic within this class but it is very important logic. I've implemented what we call a state engine. This current state engine currently recognises 5 states:
  • View state, no record selected
  • View state, record selected
  • Edit state, new record
  • Edit state, existing record
  • Direct editing
The way I've implemented this is that you can call the $setState method of the base class to change the state, this method then registers the current state for future reference and checks the objects on the window to enable/disable them according to the state. It also calls the toolbars and any subwindows and informs them about the state change.
The state engine will call the $getEnabled() method of an object if it exists and expects that method to return if that field should be enabled in the current state.
I base my visual settings fully on #STYLES. Go to the system classes folder and double click on #STYLES to look at them. Omnis creates most of the default styles, you will see that I have created copies of some of the styles suffixed with "Disabled". I toggle between the style and its disabled version to visually inform the user that a field is disabled.
(RD if you're reading this, GREAT ENHANCEMENT IDEA, a single style that lets me configure how a field should look when its enabled, and what it should look like when its disabled!).
More about styles some other day, needless to say: Use them, they are great!
I also have a toolbar base class called tbBase, again, this purely implements the state engine. Using this base class I've created two toolbar classes, tbBrowse and tbInsertEditDelete.
tbBrowse is a toolbar that will allow the user to browse through the records in a table:
When we look at the first button on this toolbar we see the following methods:

tbBrowse.FirstBtn.$event()
----
On evClick
    Do $cwind.$evFirst()

tbBrowse.FirstBtn.$getEnabled()
----
Quit method pick(iState,kTrue,kTrue,kFalse,kFalse,kTrue)

Our $event method simply handles a click event that sends a message to the window this toolbar is on. The actual logic thus is present in the window. This way this toolbar can be used for any sort of window that has any sort of browse logic.
Our $getEnabled() method is used by the state engine and we can see how this button reacts to our 5 states. The button is enabled in the two view states and in the direct edit state, but disabled in the two edit states.
The tbInsertEditDelete toolbar is a toolbar that gives us a nice insert, edit and delete button with a save and undo button to confirm our change. Sorry I couldn't find a nice delete icon and didn't feel artistic enough to make one :D

Now that we have our toolbars we can create the window where it all starts to come together. wBaseRecordBrowser is a base class that puts all the pieces of the puzzle together and its become really simple. I've added our two toolbars to this window but there are no fields on this window. There is a new instance variable called iRecordRow but we do not initialise it in our base class yet.
When we look at the code we see that except for one additional method, this class only implements the logic for the calls that the toolbars make to this window. Owh we also override our $construct but all we do here is call our $evFirst method, the same method the first button on our browse toolbar calls. This will ensure that when a subclass of this window opens, the first record of the table is shown.
The additional method is called $updateDetail and there is no logic in there. This method gets called after a new record is loaded into the screen and allows any subclass of our window to load additional details into the window or perform additional logic that is required. We may or may not need it.
When we look at the methods of our first toolbar, our browse toolbar we see that the logic follows a simple pattern:
  • call the appropriate method of our row variable to get the first/last/previous/next record
  • if successful call $updateDetail, set the correct state depending on whether a record was found and redraw the window
  • if we fail, give an error
When we look at the methods of our second toolbar, our insert/edit/delete toolbar, the logic also remains pretty simple.
  • For the insert button we first clear the record and then change the state.
  • For the edit button we simply change the state (note that the edit button is only enabled in a state where a record is available that we can edit!)
  • For the delete button we ask the user if he/she is sure, and the call the $delete method of our row variable and act according to the result
  • For the save we either call the $insert or $update method of our row variable and act according to the result, finally we change the state back to view.
  • Last but not least, for the undo we simply reload the current record and change the state, off course after asking the user if he/she is sure
Okay, at this point we've done all the hard stuff. Our base classes are finished, we've spend alot of work planning our logic and building a structure that will allow us to produce. So lets do that.
Now remember we've already dragged our tables from the SQL browser into our library, in doing so created the schema class and the table classes, we've subclassed our new table classes from tBaseCounted and last but not least, we've implemented the $validateRecord method for our new table classes. These are roughly the steps you would have to repeat for each new table that you want to introduce to your application. Piece of cake right?
Creating our first window is just as easy. Create a subclass of wBaseRecordBrowser and call it wContactType.
Now override the $construct and change the code to this:

wContactType.$construct
----
;  First define our record row for the correct type!
Do iRecordRow.$definefromsqlclass('tContactType')
;  Then do the default construct!
Do inherited

All we do here is define our iRecordRow (remember, it was created in our base class) so it uses our table class (which was subclassed from tBaseCounted and thus contains all that cool logic we implemented).
Now we need to create fields on our window for our table so our window looks like this:
We have 3 fields on this. A background field which is just a label. An entry field where we set the $dataname property to iRecordRow.Description and a checkbox field where we set the $dataname property to iRecordRow.IsPerson and we change the $text property.
Visually we don't need to do anything else.
We do need to implement a $getEnabled() method for the entry field and the checkbox. Both contain a single line of code:

Quit method pick(iState,kFalse,kFalse,kTrue,kTrue,kTrue)
And thats it.
Really.
Thats all. Press CTRL-T and you'll see...
Lets do another one.
Create a subclass of wBaseRecordBrowser and call it wContact. Change the construct so that iRecordRow uses tContact. Add fields to this window so you can edit the record, add a $getEnabled() to each of these fields, and it should work.

Now if you look at the version I made, you can see I made a dropdown list for the column FkTypeID since this is a foreign key to our contact type table. I have to add a little more code for that:

wContact.$updateDetail
----
Calculate tmpFKID as iRecordRow.FkTypeID
Do iContactTypeList.$search($ref.ID=tmpFKID,kTrue,kFalse,kFalse,kFalse)

$UpdateDetail simply sets our dropdown list to the correct line for the current record. If the line can't be found the list will have $line = 0 which is fine by me.

wContact.iContactTypeList.$construct
----
;  Init our contact list
Do iContactTypeList.$definefromsqlclass('tContactType')
If iContactTypeList.$select('order by upper(Description)')
    Do iContactTypeList.$fetch(kFetchAll)
Else
    OK message Error {[iContactTypeList.$getLastError()]}
End If

This is a $construct on our dropdown list. $construct methods on fields get automatically called when a window is opened and are run before the $construct of the window runs. Here we do nothing more then load the values from our contact type table into our list so that our dropdown list is populated.

wContact.iContactTypeList.$event
----
On evClick     ;; Event Parameters - pRow ( Itemreference )
    Calculate iRecordRow.FkTypeID as iContactTypeList.ID
    Do $cinst.$$setFieldState($cinst.$objs.CustomerName)     ;; Changing this dropdownlist could influence the state of this field!
    Do $cinst.$redraw(kTrue,kTrue)

Finally we have to react when the user selects a line from our dropdown list. We update our current record with this information but we also reapply the field state for our customer name field. This is because this field is not only dependent on the state, but also on the value of ContactType.IsPerson.
All that's left now is opening our windows from a menu. We can test our windows by pressing CTRL-T because we are still using a single library and a single task but that's not very use full for a user.
If you look at mMain you will see I'm now opening these windows.
Ok, there are two things missing in this tutorial that I've left out a little bit on purpose. There is enough in this tutorial for you to add this logic yourself, at least for the most part.
First, the user is now still forced to enter a customer number manually. It would be nicer if the system generates a new number for him/her. We can simply disable the customer number field by removing the $getEnabled method and disabling the field. But what to do about auto numbering this field? Hmmm.. where would be the best place? In the window? No that's not it... In the $validateRecord? No that's not it either.....
Second, the user can't search for a record. Our database is still small so there is no problem here, but wouldn't it be nice if the user could search? In one of my next tutorials we'll be building a separate search window but there are other options. Here is a suggestion, add a search button to the browse toolbar and introduce a new state (the search state). When the user presses the search button the window should go into a search state meaning that he/she can no longer browse, he/she can no longer insert/edit/delete/save/undo, he/she can fill in all the information on the screen and finally he/she can press the search button again (or another button) that will cause the system to look for a record in the table that matches what the user has entered.
Let me know where you would put what logic, how you would do this, surprise me :)
I will add a solution myself in the near future but since this is not my preferred way of designing windows I won't max it out.
I hope you enjoyed this part of the tutorial, took me a lot more time then I expected.
----
Disclaimer: I'm sure some of the listers will recognise their ideas in this tutorial in one form or another. Some ideas are truly mine, some are inspired by what I've learned in over 10 years of conferencing and reading the list. It would be an impossible task for me to list the many many people who I owe thanks to. I'm sure you know who you are my friends. I take only credit for taking the time to put this series together. Use the code you find in this tutorials and the library/libraries attached freely and as you see fit. I take no responsibility and will not be liable for any damages resulting directly or indirectly from using the information I present here.