Don't Miss the Main Event!

Mike Helland

Visual FoxPro is a great object-oriented language—there's very, very little the language can't do. But there is one feature that VFP's object model doesn't handle very well—raising custom events from objects. Mike Helland has seen several attempts at accomplishing such a feat in native VFP classes; his article presents another solution.

First of all, what do I mean by "raise an event"? Don't the FoxPro base classes have events like Click(), Init(), and Destroy()? They do, but they're not very useful when you want to add events to your business logic classes. For example, in a class that processes orders, an invalid order may come through. The application that's using the object may want to display these problems as they occur in a user interface, or, if there's no user interface, the invalid transaction should be logged. Or the application may decide that the order can be fixed and even resubmitted without user intervention. What I want to do is give the class the ability to raise an event, or, in other words, notify the application that something has happened, and give the application the ability to respond to that event intelligently.

Before I take on that task, let me present the class I'll be using throughout this article. It simply creates a string, dumps the string to a file, and then deletes the file. I know—it's not a very productive use of these speedy CPUs, but at least it takes more than a second or two to run. Here's the class:

DEFINE CLASS routine AS Session

Iterations = 500

fileName = 'file.txt'

fileContents = ''

FUNCTION Run

* iterate the specified number of times

LOCAL iteration

FOR iteration = 1 TO this.iterations

* create a file

this.createFileContents()

* add it to disk

this.sendFileToDisk()

* delete the file

this.deleteFile()

ENDFOR

RETURN

FUNCTION createFileContents

* add a random string to another string 1000 times

this.fileContents = ''

LOCAL i

FOR i = 1 TO 1000

this.fileContents = this.fileContents + SYS(2015)

ENDFOR

RETURN

FUNCTION sendFileToDisk

STRTOFILE(this.fileContents, this.fileName)

RETURN

FUNCTION deleteFile

ERASE (this.fileName)

RETURN

ENDDEFINE

Now, I need to find some way to log what happens during this process and inform the user. The first try would be to change the Run() method by including method calls to perform the logging and informing, like so:

FUNCTION Run

* iterate the specified number of times

LOCAL iteration

FOR iteration = 1 TO this.iterations

* create a file

this.createFileContents()

*MH log that the string has been created

this.Log(iteration)

* add it to disk

this.sendFileToDisk()

*MH tell the user the file has been put on disk

this.Progress(iteration)

* delete the file

this.deleteFile()

ENDFOR

RETURN

Now I'd have to add my Log() and Progress() functions to my class and actually implement them.

But something doesn't feel right. It seems that the class now contains functionality that goes beyond what the Process class should contain. I suppose what I could do is not put any code into the Log() and Progress() functions, and leave that up to a subclass to implement. That way, I can create different subclasses for Web apps or Windows apps... Wait, that would mean that I'd have to duplicate code for the logging if the progress indicator had to be different. I'm not sure I like that either.

Another idea that's crossed my mind is reversing the flow of the program and letting the user interface call the parts of my process routine separately. By that, I mean a form would do the FOR loop and call the createFileContents() and sendFileToDisk() methods, allowing for custom code in between. Still, that doesn't sit well with me. Call me hard to please, but ideally my progress logic, logger logic, and business logic would all be in their own classes, and each class would have absolutely no idea that the other classes exist.

So I found away to do that by using the functionality of raising custom events and delegates. Delegates are the method of choice for handling events in .NET languages like C# and VB.NET, and I attempted to re-create that functionality in Visual FoxPro 7.0.

Preparing the Process()

First, I'm going to modify my Run() method to raise some events by calling the RaiseEvent of an event processor. In this case, my event processor is just an object that can be referenced through the global variable "oE." When you implement this, you can do the same thing, or add the event processor to another object, or even create an event processor instance on every object you use. That's up to you. Anyway, here's what the new Run() code looks like:

* create a file

this.createFileContents()

oE.RaiseEvent(this, 'STRINGCREATED', ;

TRANSFORM(iteration))

* add it to disk

this.sendFileToDisk()

oE.RaiseEvent(this, 'FILEONDISK', TRANSFORM(iteration))

The two new lines simply "raise" the events StringCreated and FileOnDisk of the object passed in the first parameter. The last parameter to RaiseEvent is a string representing any parameters that will be passed on to the event processor. Next, there needs to be a way to respond to those events; this is where delegates come into play.

The BindEvent() method of the event processor class has four parameters. The first two parameters describe what event to watch for (the name of the event and the object that raised it), and the last two parameters describe how to handle the event (an object and method to execute when the event is raised). Because the code that actually handles the event is delegated to a method of another object, these last two parameters make up what is known as the delegate. Here's how a typical BindEvent() would look:

oE.BindEvent(oO, 'STRINGCREATED', oX, 'Update')

This can really be read as:

"When the StringCreated event on the oO object

is raised, call oX.Update()"

BindEvent() is typically called outside of the class that's raising events. As a result, code that uses classes capable of events will often look like this:

* Create the event processor and the routine

oE = CREATEOBJECT('eventprocessor')

oO = CREATEOBJECT('routine')

* Create the event handlers

oX = NEWOBJECT('logger')

oY = NEWOBJECT('progress')

* Bind the raised events to the event handlers

oE.BindEvent(oO, 'STRINGCREATED', oX, 'Update')

oE.BindEvent(oO, 'FILEONDISK', oY, 'Update')

* Do the dirty deed

oO.Run()

The comments in the code explain what's happening. Four objects are created: the event processor, the routine I started out with, and two event handlers that I'll explain later. Finally, the fifth and sixth lines tie everything together. With all that in place, Run() sets everything into motion.

There's only a little more code to explore: the event processor and the two event handler classes.

The event handlers and event processor

The two objects that handle the events (oX and oY) raised from on the routine class (oO) are pretty simple. They're both classes with an Update function that displayed the passed parameter:

DEFINE CLASS logger AS Session

FUNCTION Update

LPARAMETERS iteration

? iteration

RETURN

ENDDEFINE

DEFINE CLASS progress AS Session

FUNCTION Update

LPARAMETERS iteration

WAIT WINDOW TRANSFORM(iteration) NOWAIT

RETURN

ENDDEFINE

That's basic enough. Now for the glue: the event processor itself.

DEFINE CLASS eventProcessor AS custom

DIMENSION delegates[1, 3]

delegateCount = 0

FUNCTION BindEvent(object,event,eventHandler,delegate)

WITH this

.delegateCount = .delegateCount + 1

DIMENSION .delegates[.delegateCount, 4]

.delegates[.delegateCount, 1] = object

.delegates[.delegateCount, 2] = event

.delegates[.delegateCount, 3] = eventHandler

.delegates[.delegateCount, 4] = delegate

ENDWITH

RETURN

FUNCTION RaiseEvent(object, event, parameters)

IF EMPTY(parameters)

parameters = ''

ENDIF

event = UPPER(event)

WITH this

FOR i = 1 TO .delegateCount

IF .delegates[i, 2] == event AND ;

.delegates[i, 1] = object

.handleEvent(.delegates[i, 3], ;

.delegates[i, 4], parameters)

ENDIF

ENDFOR

ENDWITH

RETURN

PROTECTED FUNCTION handleEvent(object, ;

delegate, parameters)

LOCAL expression

expression = 'object.'+delegate+'('+parameters+')'

EVALUATE(expression)

RETURN

ENDDEFINE

Here, we see that the BindEvent() method simply stores all delegates in an array, and the RaiseEvent() method simply looks in that array to see whether there are any delegates defined for that event. If there are, the protected handleEvent() method executes the delegate.

Before I go any further, where would it make sense to use events and delegates? As you can see from my simple example, I can create rather encapsulated classes for running a routine, logging the status of a routine, and displaying the progress of a routine without coding myself into restraints and rules, creating a rather flexible setup. I could use this technique for more basic functions, too. For example, I can create a menu object that raises events when its menu bars are selected. From there, the application could bind that event to the opening of a form.

I could use my event processor in more complex ways as well. Say I have an order fulfillment routine. When an order comes in, the order receiver raises an event, and an order processor picks it up to do the dirty work. Now, say that when an order comes in at over $1,000, the boss wants an e-mail of the details of the order. That's a pretty simple enhancement—just use BindEvent() to set that up, and the order receiver and the order processor code can remain unchanged.

Time to wait in the queue

I want to throw one more thing out there before I wrap this article up. I often have a need to raise events that are, for the most part, trivial. The progress indicator is a good example of that. It may not be important that the user see every iteration as it processes. In fact, seeing the counter constantly updated is more difficult to read than slower, incremental counters.

In the next code block, I've modified the event processor. This enhanced version accepts more parameters to the BindEvent() method that indicates how long in seconds that an event can be "delayed." Basically, if an event is instructed to wait one second before being processed, the event processor will let multiple events pile up for one second before it can process all of them at once. Here's the new event processor:

DEFINE CLASS eventProcessor AS custom

DIMENSION queuedEvents[1, 3]

DIMENSION delegates[1, 3]

eventCount = 0

delegateCount = 0

FUNCTION BindEvent(object, event, eventHandler, ;

delegate, delay, afterBatch)

WITH this

.delegateCount = .delegateCount + 1

DIMENSION .delegates[.delegateCount, 8]

.delegates[.delegateCount, 1] = object

.delegates[.delegateCount, 2] = event

.delegates[.delegateCount, 3] = eventHandler

.delegates[.delegateCount, 4] = delegate

.delegates[.delegateCount, 5] = IIF(EMPTY(delay), ;

0, delay)

.delegates[.delegateCount, 6] = 0 & Next run

* this is the delegateID

.delegates[.delegateCount, 7] = SYS(2015)

.delegates[.delegateCount, 8] = afterBatch

ENDWITH

RETURN

FUNCTION RaiseEvent(object, event, parameters)

IF EMPTY(parameters)

parameters = ''

ENDIF

event = UPPER(event)

WITH this

FOR i = 1 TO .delegateCount

IF .delegates[i, 2] == event AND ;

.delegates[i, 1] = object

* if this there is a value in the delay column,

* queue the event for a further time

IF .delegates[i, 5] > 0

.queueEvent(.delegates[i, 7], .delegates[i, 3], ;

.delegates[i, 4], parameters)

* If there is no 6th element (the next run),

* set that now

IF .delegates[i, 6] = 0

.delegates[i, 6] = SECONDS() ;

+ .delegates[i, 5]

ENDIF

ELSE

* otherwise handle it immediately

.handleEvent(.delegates[i, 3], ;

.delegates[i, 4], parameters)

ENDIF

ENDIF

ENDFOR

ENDWITH

RETURN

PROTECTED FUNCTION handleEvent(object, ;

delegate, parameters)

LOCAL expression

expression = 'object.'+delegate+'('+parameters+')'

EVALUATE(expression)

RETURN

PROTECTED FUNCTION queueEvent(delegateId, object, ;

delegate, parameters)

WITH this

* add this to the queued event stack

.eventCount = .eventCount + 1

DIMENSION .queuedEvents[.eventCount, 4]

.queuedEvents[.eventCount, 1] = delegateId

.queuedEvents[.eventCount, 2] = object

.queuedEvents[.eventCount, 3] = delegate

.queuedEvents[.eventCount, 4] = parameters

ENDWITH

RETURN

FUNCTION DoEvents(forceEvents)

LOCAL i, seconds

seconds = SECONDS()

* when the timer fires, it should know what

* events to call

WITH this

FOR i = 1 TO .delegateCount

IF forceEvents OR .delegates[i, 6] <= seconds

* Handle all the events queued for the deletegate

.doEventsForDelegate(.delegates[i, 7])

.delegates[i, 6] = 0

IF NOT EMPTY(.delegates[i, 8])

.handleEvent(.delegates[i, 3], ;

.delegates[i, 8], '')

ENDIF

ENDIF

ENDFOR

ENDWITH

RETURN

PROTECTED FUNCTION doEventsForDelegate(delegateId)

LOCAL i, j

j = 1

FOR i = 1 TO .eventCount

IF .queuedEvents[j, 1] == delegateID

.handleEvent(.queuedEvents[j, 2], ;

.queuedEvents[j, 3], .queuedEvents[j, 4])

.removeQueuedEvent(j)

ELSE

j = j + 1

ENDIF

ENDFOR

RETURN

PROTECTED FUNCTION removeQueuedEvent(eventIndex)

WITH this

ADEL(.queuedEvents, eventIndex)

.eventCount = .eventCount - 1

IF .eventCount > 0

DIMENSION .queuedEvents[.eventCount, 4]

ENDIF

ENDWITH

RETURN

ENDDEFINE

The new code works a bit differently than what I've previously presented. Here's a quick recap of how it works:

1. When a delegate is created, a "delay" is passed, and a second method can be specified to run after all the delegates in a queue are executed. The new BindEvent() call can look like this:

oE.BindEvent(oO, 'STRINGCREATED', oX, 'Update', ;

.25, 'After_Update')

Here, the .25 means a quarter of a second delay.

2. When an event is raised and a delegate with a delay exists, the event is dropped into the queue.

3. A DoEvents() method has to be called to run through the queue and run any events that have ripened. If a .T. is passed, all the events are forced to execute. That means the Routine.Run() method has to be changed; something like this will do:

* delete the file

this.deleteFile()

oE.DoEvents()

ENDFOR

* Make sure to get any events still in the queue

oE.DoEvents(.T.)

RETURN

4. After a delegate is executed, the "after" delegate is called if one is specified. To take advantage of this, the logger object can be changed like so:

DEFINE CLASS progress AS Session OLEPUBLIC

iteration = 0

FUNCTION Update

LPARAMETERS iteration

this.iteration = iteration

RETURN

FUNCTION After_Update

?this.iteration

RETURN

ENDDEFINE

And that wraps it all up. There are a couple of other reasons you'd want to do something like this. For example, if you wanted to send messages to disk or to a Web Service, instead of sending every message individually, which would require many roundtrips, you could let them build up into larger chunks requiring fewer roundtrips.