There are many times when objects can perform some action in response to a property being set or a method being called. This has traditionally been difficult to implement, since an object has usually needed to notify the calling program that the action has taken place.
In the previous section, we discussed the invoice object and how it can detect when a video is added or removed from the list of rentals. Either of these conditions should cause the subtotal, tax and total amounts to be recalculated. The challenge is that the UI needs to be notified that these values have changed so that the display can be updated.
Traditionally, this would have been handled either by having the UI refresh the values when it added or removed a video, or by calling a special Calculate method to force the calculation.
If the UI updates the values when adding or removing a video, we've effectively coded some business logic into the UI. After all, the UI would have to know all the conditions under which the values might change, so that it could know when to refresh the display.
Using an explicit Calculate method is also far from ideal. If we have such a method, then we're relying on the UI code to call it, rather than recalculating our values automatically. If the UI doesn't call the method when appropriate, we could end up in a situation where the subtotal, tax, and total properties on the object would be incorrect. This solution can not only lead to bugs, but it also requires that the UI programmer must have more knowledge about how our object works than we might otherwise prefer (such as a more appropriate time to call the Calculate method).
A more complex solution to the problem would be to have the invoice object call a method of another object and have that object update the UI. This solution solves most of our problems, but it's definitely harder to implement for the UI developer.
To implement this, the UI itself would have to create an object to receive the message sent by the invoice object. Of course, the invoice object would need to know about the UI's object so that it could call it; we'd therefore need to add a method to the invoice object so that the UI could pass a reference to its object!
Once the invoice object had a reference to the UI's object (let's call it Displayer), it could call some predefined method on the Displayer object whenever a message needed to be sent to the UI. This is shown in this diagram:
Let's look through some code that could handle this.
First off, our UI developer will need to provide an object to update the display. Create an ActiveX DLL project, create a new class module called Displayer
, and add this sample code:
Option Explicit
Public Sub UpdateValues()
' in this routine we'd update our display
' with the properties from the Invoice
' object
End Sub
This example assumes that we've implemented our business objects in an ActiveX DLL, and that the UI is in a different EXE or DLL. We'll keep our Displayer
class in a separate ActiveX DLL for now, which will allow us to create a simple UI test harness in a Standard EXE. (Alternatively, if we had created our UI in a ActiveX DLL, then the Displayer
class could have been included within the main UI project.)
The Displayer
class must also have its Instancing
property set to 5 - MultiUse so that we can make it available to the Invoice
object.
The upshot of all this is that if we did want our Displayer
class to be within the main UI program, then our UI project would need to be an ActiveX server - whereas it may have only needed to be a regular program prior to the introduction of the callback mechanism.
Within the main DLL project for our business objects, the Invoice
class also needs some special code to support the callback. It needs a method that will allow the UI to give it a reference to the Displayer
object.
For our current example, add a sample DLL for our business objects, using the File-Add Project menu option, then add a class module, using the Project-Add Class Module menu option, and call that class module Invoice
. Now add the following sample code to Invoice
:
Option Explicit
Private objCallback As Object
Public Sub SetCallback(Display As Object)
Set objCallback = Display
End Sub
Then, in the routine where the Invoice
object does the calculations, we need to call the UpdateValues
method of the Displayer
object:
Public Sub Calculate()
' here we'd do the calculations
objCallback.UpdateValues
End Sub
Finally, we need to make sure that the Invoice
object releases its reference to the Displayer
object before the Invoice
goes away. If we don't do this, we run the risk of having objects hanging around in memory after we've tried to close down the program:
Private Sub Class_Terminate()
Set objCallback = Nothing
End Sub
Now that all that has been set up, we can write some UI code that creates the Invoice
object. Since we're going to use a Standard EXE to hold our sample UI and test our callback mechanism, add a Standard EXE project using the File-Add Project menu option, add a standard module to that project, using the Project-Add Module menu option, and enter this example code:
Private objInvoice As Invoice
Private objDisplayer As Displayer
Set objDisplayer = New Displayer
Set objInvoice = New Invoice
objInvoice.SetCallback objDisplayer
objInvoice.Calculate
Notice that, as well as creating an Invoice
object, we create a Displayer
object, and give the Invoice
object a reference to it.
You'll need to make a reference to the Invoice
object from the UI project. Select the UI project in the Project window, and use the Project-References menu option to select the project containing our Invoice
object.
Since we've created a separate EXE for the UI test harness, we also need to add references to the projects containing the Displayer
object and the Invoice
object. Select the UI EXE project in the Project window, and use the Project-References menu option to select the appropriate references to the other projects.
If you run the sample program as it stands, you won't see much happen: but if you step through the program execution, you'll see the callback mechanism alive and well. The UI code successfully establishes the Displayer
callback method to itself before it calls the Invoice
object's Calculate
method. Within the Calculate
method, the callback is then made to the Displayer
class's UpdateValues
routine.
This solves our problem, in that it allows an object to notify our program when something has happened. In our example, the Invoice
object notifies the UI object that Calculate
method has been called. In some ways, this is very attractive, since the interaction between the two objects is very well defined, and the UI can give the Invoice
object a reference to any object that implements an UpdateValues
method. Since this technique uses regular method calls between the two objects, we can write our code to pass any parameters that our program might need.
On the other hand, this solution is somewhat complex: it forces the UI developer to add a new class to the project, or make the UI program an ActiveX server. It also tightly couples the business object to the UI implementation, since it requires that the UI provide an object with a specific method.
Visual Basic 5.0 introduced the ability to raise events from our objects. This means that we can write code in our class module that will raise an event in the calling code, just as a button control can raise a Click
event in a form.
This is done by introducing three new keywords to the Visual Basic 5.0 language. In our class module, we need to use the Event
and RaiseEvent
keywords, respectively, to declare and raise events. In the calling code (Form or Class module), we need to declare the object using the new WithEvents
keyword.
Sticking with the Invoice
object example, we could have the Invoice
object raise an event when the subtotal, tax and total amounts have changed. Then the UI programmer would just need to write code behind the event to update the display. This is a very nice model, since all Visual Basic programmers are familiar with writing code to respond to events - so the learning curve for the UI programmer is very small.
Inside our Invoice
object, we need to have a line to declare our event:
Public Event UpdateValues()
Then in the routine where we calculate the amounts, we can add a line to raise the event:
Private Sub Calculate()
' here we'd do the calculations
RaiseEvent UpdateValues
End Sub
All references to our objCallback
object are now unnecessary, and we no longer need our SetCallback
routine, since we've managed to create the same mechanism using events.
The UI programmer just needs to make a change to the declaration of the Invoice
object to include the WithEvents
keyword:
Private WithEvents objInvoice As Invoice
At this point, the objInvoice
object will appear in the Object dropdown in the upper-left of the code window, along with any other controls or objects available to the module. If we choose objInvoice
, then the Procedure dropdown in the upper right will list all the events that are declared in the Invoice
class - just UpdateValues
in this case. This means we can write a regular event procedure:
Private Sub objInvoice_UpdateValues()
' here we'd put the code to update the
' values on the display
End Sub
All references to our objDisplayer
object are now unnecessary; and, since we no longer need our Displayer
class module, the UI no longer needs to be an ActiveX server.
This solution is not very different from implementing a callback, but it does have some significant advantages. The UI programmer doesn't need to implement a special class. In fact, the UI programmer can draw on existing knowledge and techniques to write code behind the object's events - just as if they were events from a control on a form. Furthermore, the Invoice
object author doesn't need to worry about being passed some external object, or any specifics about the calling program at all. They may appreciate that.
The bottom line is that the RaiseEvents
command works independently of whether anyone actually receives the event that's raised. This means that we've effectively separated the UI programmer's tasks from the object developer's tasks.