by Jim Karabatsos - GUI Computing
One of the things I really love about VB4 is its support for classes. These allow you to create objects that encapsulate complex functionality so that it can be used as a black box in your applications. Furthermore, because it is so simple to convert a VB Class into an OLE server, it is possible to make that same functionality available to other applications that can act as OLE controllers such as Access, Project or Excel.
One of the things I really hate about VB4 is that there is no support for the generation and handling of events from a class object. This means that all instances of a class by definition must act in exactly the same way. Imagine VBXs without events — that's pretty much what you are limited to with VB classes.
I have been thinking about this dilemma for a while now (ever since the VB4 beta program, in fact) and have tried several alternatives. I decided that there were certain requirements that needed to be met if I was going to have a truly workable solution, and I am rather happy to report that such a solution is possible. There are still a few rough edges, mainly caused by the "all or nothing" nature of polymorphism in Visual Basic's object model; either you specify an exact match for an object, or you specify the totally generic "Object" type. More on this later.
What I wanted to achieve was a generic mechanism that could be used to communicate events from an object back to the user of that object, in much the same way that events in a VBX are reflected back into the application that uses it. The OLE specification does not support the concept of events in ordinary OLE objects; instead, the event mechanism is introduced with OLE Controls a.k.a. ActiveX controls which VB cannot produce, at least not in version 4.
The other very important requirement was that the event handlers could be coded on a per-instance basis. What I mean by this is that a single application can create multiple instances of a particular object type, then code different event handlers for each instance. Again, going back to the VBX analogy, if you have two separate buttons on a form, there are two separate Click events and you can write completely different code in each of them. Going a step further, it would be nice if I could retain the option to designate the same event handler to handle events from a number of object instances (which is like the much cleaner event model in Delphi) but that would be a bonus (one that it turns out was easy to achieve).
Finally, it had to work even while a modal form was displayed. Those who have read previous articles will appreciate the problems modality introduces, but we all know that modal forms are often required (and often totally appropriate). OLE servers can quite effectively encapsulate a modal form. Typically, this is done by calling a method of the server object to display the form modally and then return the results. One of my initial designs involved returning from that method with a status indicating an event and with a requirement that the method be called again in a loop until the status indicated completion. Unfortunately, this architecture could not handle modal forms so an alternative was called for.
The architecture I have come up with is to design a standard interface that can be implemented by any object and consists of a single function. That function is always declared like this :
Public Function NotifyEvent(IN_objSource As Object, _ IN_sEvent As String, _ Optional INOUT_vParam1 As Variant, _ Optional INOUT_vParam2 As Variant) _ As Variant
The NotifyEvent function is designed to be called from the OLE server in order to signal that an event has occurred. The first parameter, IN_objSender, is the object instance that is generating the event. This allows an event handler to access properties and methods of that object and also allows a single event handler to service multiple objects if that is what you want to do (although this is NOT a requirement of the architecture).
The second parameter, IN_sEvent, is a string that indicates the event that is being raised. I did experiment with creating an Event object to encapsulate this, but I found that a simple string was adequate for what we wanted to communicate. It is up to the server to specify what events can be raised; typically, they will be strings like "CLICK" or "BEFORECOMMIT" or whatever.
The final two parameters are generic variants; the meaning can be different depending on what a particular event requires. These are a little bit like the old wParam and lParam parameters in a Windows message. Being variants, anything can be passed either way and, because variants can contain arrays or indeed objects there is no real limit is to what can be passed each way.
The procedure is a function and it is defined as returning a variant. It has been defined this way in particular so that it can return a value in a convenient manner to the server, in much the same way that SendMessage can return a value to the caller.
OK, so we have a cute function definition. How do we use this and how does it address the design goals that we set?
Every server object implements a public property as follows :
Public Property Set EventHandler(IN_vEventHandler) Set mobjEventHandler = IN_vEventHandler End Property
As you can see, this property simply saves a reference to an object in a private class-level variable called mobjEventHandler which is defined "As Object". What I would really have liked is to define it as a more specific object type; unfortunately this would require that all instances of a server object have exactly the same event handlers, so we need the generic "object" type. This is just one example where a fuller implementation of OOP would be very useful.
Whenever we create an instance of a server object, one of the first things we need to do is to assign an event handler object to this property of the server. An event handler object is any object that implements the public NotifyEvent function we have described above. If we want multiple server object instances to share the same event handlers, we assign the same event handler object to this property of those servers. On the other hand, we can define a different event handler object type for each instance of the server object and assign each server its own, unique handler. As long as each handler object supports the NotifyEvent function as defined, it all hangs together because VB objects are polymorphic on a property by property (or method by method) basis.
The following diagram shows how the architecture hangs together.
So far, so good. You can see that the event handler object could be any object because the EventHandler property of the server object will accept any object type. However, that object type had better have a public NotifyEvent property, or else a run-time error will be generated later when the server tries to call that method.
Each individual event handler object is free to implement whatever logic it wants to in the NotifyEvent function. I have adopted the following standard format :
Public Function NotifyEvent(IN_objSource As Object, _ IN_sEvent As String, _ Optional INOUT_vParam1 As Variant, _ Optional INOUT_vParam2 As Variant) _ As Variant Dim vRetVal As Variant Select Case UCase$(IN_sEvent) Case "LOAD": vRetVal = OnLoad(IN_objSource) Case "UNLOAD": vRetVal = OnUnload(IN_objSource) Case "CLICK": vRetVal = OnClick(IN_objSource, _ CLng(INOUT_vParam1), _ CLng(INOUT_vParam2) ) Case Else: vRetVal = OnUnexpectedEvent(IN_objSource, _ IN_sEvent, _ INOUT_vParam1, _ INOUT_vParam2) End Select NotifyEvent = vRetVal End Function
I then code individual event handler functions that have names that start with "On", as in OnClick. Notice that I can customise which additional parameters are passed to each OnXXX event handler function. In the example above, the OnClick event might be passed the X and Y coordinates of the mouse - so it might be defined like this :
Private Function OnClick(IN_objSource As Object, _ ByVal IN_X As Long, ByVal IN_Y As Long) As Variant
Note too that all event handler functions are passed the object that raised the event as the first parameter - so that they can call upon the object's methods and properties to obtain further information - or to initiate whatever processing that object makes available, and that they are all functions that return a variant.
The final requirement that has not yet been addressed is the ability of a modal form to raise events back in the client program, while the modal form continues to be displayed. This is actually not too hard - once you get over the conceptual hurdle of realising at a deep-down, fundamental level that a form is a class, give or take a little. (Actually, give the visual interface, take the OLE server conversion.) All we need to do is to define a property of the form that can be set to point to the object that owns it (or, more accurately, that invoked it). I prefer to set this property to point to the server object. While this requires a further level of redirection, it does make the server object addressable from within the form. If a server method needs to display a modal form, it needs to do the following :
Public Sub ShowSomeModalForm() Set frmModal.Owner = Me ' ME is the object instance frmModal.Show vbModal ' THIS WILL SUSPEND UNTIL FORM HIDDEN !!! Unload frmModal End Sub
Remember that this is a method of the server object and that it has been called from the client program, so when the Show statement suspends for the duration of the modal form, the client task is also suspended. However, because the form has been passed a reference to the server object instance, it can call the client's NotifyEvent function by referencing the EventHandler property of its Owner. (Of course, this requires that a Get function be made available by the server object for the EventHandler property so that the form can get at it.)
|The accompanying code is a simple demo that displays a form at startup with a button and a list box.|
When the button is pressed, a server and event-handler object are created and a method called to display a modal form. The MouseUp events on that modal form are reflected back to the form as Click events and the X and Y coordinates are added to the list box.
In the following screen shot, Form2 is the modal form that is generating Click events that are being reflected back to Form1.
This is still work in progress. There is some hope that Microsoft will improve the object-oriented facilities provided by Visual Basic in future versions, so that a more self-checking architecture can be devised. For now, this set of conventions will get the job done and will work well provided that they are followed carefully.