Why are event orders different across platforms?

| | Comments (8)

This is something that I just assumed was such a basic concept that I'd never have to explain it. But I've seen a number of people comment on what a horrible problem this is, so I felt I should discuss this a bit.

In case you're just coming in from the cold -- event orders are different on different platforms. This is a given IMHO. Just like the widgets look different between platforms, the order that the OS alerts you of events can be different. And this isn't as huge of a problem as people tend to make it out to be -- though it certainly can be uncomfortable if you're making assumptions.

You should never, ever, rely on the order of events being the same from platform to platform. Heck, I'd even go so far as to assert that you shouldn't rely on the order of events from OS version to OS version (such as 10.4 to 10.4.1). This is because it's the operating system that ultimately decides when to fire an event to the REALbasic framework -- the framework simply responds to the event and passes the information along to you in turn. Since the operating system doesn't define the order of the events in many instances, then we're unable to do the same for you. Furthermore, different OSes may have different event ordering schemes altogether. So what may result in Event A->Event B->Event C on one OS may be Event B->Event D->Event C on another (skipping over A and adding D). There's simply no feasible way to allow the OS to control your app (read: create a truly native application) and keep the event ordering 100% the same on all platforms.

We do not document the order of *any* event firing. However, there are some common sense ones that you are 99.999% assured will continue to fire in this order.

1) Open will fire before Close.
2) CancelClose will fire before Close.
3) Resizing will fire before Resize.
4) MouseUp will happen after MouseDown.

These are all just common sense items. If you start seeing MouseUp firing before MouseDown for a control, then chances are, there's a bug in the framework somewhere. However, if you see Activate fire before a MouseDown, don't rely on that always happening, because another OS may let you respond to the MouseDown before activating the window (not certain whether this really happens or not -- but it's certainly plausible).

Furthermore, you should never assume that events are fully synchronous. Events generally deal with user interaction, and the user isn't going to sit and wait for one event to finish before beginning another one. And the user doesn't even need to be involved for the issue to rear its ugly head -- you could write some code that causes an event to fire.

For example, in the KeyDown event, you may decide to resize a window. This may (or may not) cause the Window.Resized event to fire before finishing the KeyDown event. This is normal, and it can (and does) behave differently depending on the platform. Some OSes do not fire an event when code resizes a control, and other OSes don't even distinguish between the user resizing and code resizing. This is normal, expected, and it is not going to go away.

So if I had to give you any advice about this topic, it'd be this: assume nothing. When it comes to event ordering, and what can trigger events, you should always be very careful and code accordingly. Let's take a look at a bad example, and how to fix it to be a good example.
[rbcode]
Sub Window1.Open()
mSomeProperty = new SpiffyThinger

me.Width = 100
me.Height = 200

mSomethingElse = new Dictionary

me.Title = "Wahoo, I rule!"
End Sub

Sub Window1.Resized()
if mSomethingElse.HasKey( "Yoiks" ) then
MsgBox "Stop resizing while yoiking"
end if
End Sub
[/rbcode]
The code looks pretty innocuous at first glance, but the astute will notice that the Resized event assumes that mSomethingElse will be non-nil. This is most likely because the coder assumes that the Open event will finish executing before the Resized event fires. This is certainly not the case because it's entirely possible that the Open event is for a visible window, and once the window widget is physically opened, the OS may decide that it's been "resized" and so fire the event. The problem is exacerbated by the fact that the code is resizing the window manually from within the open event, which may trigger the Resized event to fire. So this code is just all-around wrong due to its assumptions -- but how do you fix it?
[rbcode]
Sub Window1.Constructor()
mSomeProperty = new SpiffyThinger
mSomethingElse = new Dictionary

Super.Constructor
End Sub

Sub Window1.Open()
me.Width = 100
me.Height = 200

me.Title = "Wahoo, I rule!"
End Sub

Sub Window1.Resized()
if mSomethingElse = nil then return

if mSomethingElse.HasKey( "Yoiks" ) then
MsgBox "Stop resizing while yoiking"
end if
End Sub
[/rbcode]
Because we know when the Constructor will fire, we can control the order things are executed better. Does this mean that Resized won't fire before the Open event is finished? No way! It simply removes the race condition where Resized can fire before Open has created the mSomethingElse dictionary object. Furthermore, by checking for nil, we're even more safe (you should always, always check for nil -- there's absolutely no valid excuse to not check for nil before accessing an object. Ever!).

As I said, the "good" example does not remove the problem altogether -- it simply removes the assumptions. Not every event ordering issue is as simple to code for though. There are times where you'll run into the event order being different between platforms, such as Activate firing before GotFocus on one platform, and the opposite on another platform. So if you have code that executes in both places, what's the best way to get your application to behave the same on both platforms?

Well, the obvious answer is, don't assume anything about the order the events will be fired in. If that means re-working a concept so that the event order doesn't matter, then so-be-it. However, there will come a time when that's simply not an option. And in that case, I've found its best to keep a simple state machine to tell me when it's ok to do my action. Something like this:
[rbcode]
Dim mState as Integer

Const kFocused = 1
Const kActivated = 2

Sub Window1.GotFocus()
mState = Bitwise.BitOr( mState, kFocused )

CheckState
End Sub

Sub Window.Activate()
mState = Bitwise.BitOr( mState, kActivated )

CheckState
End Sub

Sub CheckState()
if Bitwise.BitAnd( mState, kFocused ) and Bitwise.BitAnd( mState, kActivated ) then
DoTheMagicDance
mState = 0
end if
End Sub
[/rbcode]
The code makes absolutely no assumptions about the order that the events are fired in. Each state simply sets a flag in a bitmask and then calls a helper method that waits for all events to finish firing before executing the code. This way it doesn't matter whether GotFocus fires first, or Activate does -- once both have fired, the code is executed. It's usually quite rare that you need to care to this extent though, so you won't be using this code that often. More often, it's problems like the first one shown that you'll encounter -- and those are pretty easy to eliminate the assumptions from.

Note, the "problems" I discussed are all hypothetical. They may or may not happen, I'm not entirely certain. Don't read this as being "omg, GotFocus and Acticvate fire differently!" since they're just hypothetical examples.

8 Comments

I thought you had to include "Super.Constructor" in Window subclass constructors.

Good catch, you certainly do! I'll fix the code example.

While I do not assume in general that events fire in a particular order, I do assume that a window's Controls Open() events will all fire before the Window Open() event. Is this a correct assumption, or is this false as well?

I'm not certain, to be honest. That's one where we control the order (since we make the controls and the window), but I can see either order making sense. How can a control be open if there's no open window, for example.

This is completely a guess on my part, but I believe that it is how the Super events are coded. If the super Window.Open event looks like this (psudo code):

Sub Open()
Open // calls subclass open event
For k As Integer = 0 To Ubound(Controls)
Control(k).Initialize
Next
End Sub

...then the subclass window event would fire before the control open events.

So my guess is that the Window Open event calls the subclass open event at the end. This guess is based on my own Super/Subclass implementation of events.

IIRC sometime since i bought RB ( at V3) an RS engineer has stated on the NUG that we can count the Window Open event to fire after all the control open events have fired ... and i have depended on that behavior for years.

I am sure that many people are operating under that assumption so if it not something we can count on it should be made generally known.

- karen

Regarding open event ordering, Mars has indicated the order would be as follows:

Window constructor fires
Control open events fire (in undefined order)
Window Open event fires

And he recently verified that the Window Open event was intended for code that needs to run after the window and all controls are set up.

I believe the Window's Open event is pre-defined to fire after all the control Open events.

However, just because we've made a concession about the event order in this instance doesn't mean relying on it is a good programming practice -- it's *always* better to be a defensive programmer and assume that events are unordered, even if we do make some allowances for certain circumstances.

Leave a comment

Disclaimer

I'm currently an employee of REAL Software. My blog is mine. The opinions represented in this blog are mine as well and may not represent my employer's opinions. All original material is copyrighted and property of the author.

REALbasic® is a registered trademark of REAL Software, Inc. REAL SQL Server™ and Lingua™ are pending trademarks of REAL Software, Inc. All rights reserved.