Using classes to handle events
While reviewing the code for WindowResizerClass (in an attempt to get more agreeable resizing behavior for ClarionTest) I came across a usage of RegisterEvent that I hadn't noticed before:
REGISTEREVENT (EVENT:DoResize, ADDRESS(SELF.TakeResize), ADDRESS(SELF))
The help says this about Register, which is a synonym for RegisterEvent:
REGISTER registers an event handler PROCEDURE called internally by the currently active ACCEPT loop of the specified window whenever the specified event occurs. This may be a User-defined event, or any other event. User-defined event numbers can be defined as any integer between 400h and 0FFFh.
RegisterEvent installs a callback procedure for your accept loop, to be called whenever the specified event occurs. You can install event handlers for windows and for individual controls.
The bit I hadn't noticed was the ability to specify a class method as the callback by passing in the address of the method and the address of the class.
Using class methods as callbacks in Clarion is often problematic because internally the first parameter to any method is the class itself. This is why using Omitted(parameternumber) is unwise with class methods that have optional parameters - if the first parameter of a method is omittable, you have to specify Omitted(2) rather than Omitted(1). Just use Omitted(parametername) - it will save you a lot of trouble.
Similarly, you can't use class methods as WinAPI callbacks because of that hidden first parameter.
But RegisterEvent is part of the Clarion runtime, and it's smart enough to handle a method if that's what you throw at it.
Here's a small program to demonstrate:
PROGRAM MAP END EventQueue queue,type EventTime long end EventHandler class EventQ &EventQueue Construct procedure Destruct procedure TakeEvent procedure,byte end Window WINDOW('Timer event callback'),AT(,,177,129),GRAY,FONT('Microsoft Sans Serif',8), | TIMER(100) LIST,AT(3,2,171,124),USE(?LIST1),VSCROLL,FROM(EventHandler.EventQ), | FORMAT('20L(2)|M@t4@') END CODE open(window) accept case event() of EVENT:OpenWindow RegisterEvent(event:timer,address(EventHandler.TakeEvent),address(EventHandler)) end end UnRegisterEvent(event:timer,address(EventHandler.TakeEvent),address(EventHandler)) close(window) EventHandler.Construct procedure code self.EventQ &= new EventQueue EventHandler.Destruct procedure code free(self.EventQ) dispose(self.EventQ) EventHandler.TakeEvent procedure code self.EventQ.EventTime = clock() add(self.EventQ,-self.EventQ.EventTime) return level:benign
On EVENT:OpenWindow registers the EventHander.TakeEvent method as a timer event handler. TakeEvent simply adds a record to a queue each time the timer event fires.
Here's the result:
The help has some conflicting information on the event handler prototype:
The handler procedure MUST have 1 parameter: when the handler is called the runtime library is passing the object value (the 3rd parameter in the call to REGISTER) as its parameter.
The handler PROCEDURE must not take any parameters and must return a BYTE containing one of the following EQUATEd values (these EQUATEs are defined in the ABERROR.INC file):
I'm not completely sure what the first statement is referring to, but the second statement is accurate. The handler procedure does not have any parameters and must return a byte.
The return value can be one of the following:
Level:Benign | Calls any other handlers and the ACCEPT loop, if available. |
Level:Notify | Doesn't call other handlers or the ACCEPT loop. This is like executing CYCLE when processing the event in an ACCEPT loop. |
Level:Fatal | Doesn't call other handlers or the ACCEPT loop. This is like executing BREAK when processing the event in an ACCEPT loop. |
On further experimentation I found that I could reduce the program code to just four lines:
open(window) RegisterEvent(event:timer,address(EventHandler.TakeEvent),address(EventHandler)) accept end
As long as RegisterEvent is called after the window is opened it will work - it doesn't have to be called inside the Accept loop. And since all registered handlers are automatically unregistered when the window closes, and the window is closed when the program exits (this also happens at the procedure level if the window is local to the procedure), I don't have to call UnRegister explicitly.
It doesn't end here...
Although this sample program is short, it demonstrates several things that I think are important for the future of Clarion development. For a few years now I've been harping on the need to extract business logic from embeds and into classes, where it can be tested and reused. But that leaves a gap; how do you get that business logic to interact with the user interface? Does that code need to be put in embed points?
This example shows two ways classes can be used to manage user interface interaction. The first is obvious: you can register a class method as event handler. But take a closer look at the window structure, specifically the list statement:
LIST,AT(3,2,171,124),USE(?LIST1),VSCROLL,FROM(EventHandler.EventQ),FORMAT('20L(2)|M@t4@')
The From attribute points directly to a queue that's a property of the class. That means that not only can use use classes to manage events, you can use classes to supply data that's bound to the user interface. There isn't even any need to issue a Display() after adding a queue record: the data simply appears on screen as it is added.
It's still important to keep a separation between business logic and UI code, but clearly there's a role for classes in managing UI behavior. More interestingly, you can also apply unit testing to some of that UI behavior. I'll have more to say about that in the near future.