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.