Raising and Handling Events
This column appeared in the August 1998 edition of EXE,
and was concerned with adding support for Events in Visual Basic applications
through the use of the WithEvents and RaiseEvent keywords.
Windows is an event-driven environment. This means that a fair
proportion of the code that is written for an application needs to be
responsive to messages sent from various sources. In the CUI (Common User
Interface) days before Windows, applications written in such languages as
Clipper typically only needed to be on the lookout for straightforward keyboard
input. These days, of course, code is written much more as tiers of
functionality using object components. Different mechanisms exist to cater for
communication between these components, so in this months column I shall
discuss the use of Events to this end.
An Event is a means of informing an application that something
interesting has happened. In all versions of Visual Basic up to and including
number 4 the only Events that you could program with were those provided by
Visual Basic itself. Because Visual Basic was originally intended to simplify
Windows programming the Events that were catered for were presented as Subs
rather than Events. In these cases the name of the sub was made up of the
control name and the event name. Consequently if you draw a command button onto
a form and double click it from the development environment then you are
presented with
Private
Sub Command1_Click(). For each Windows control that is offered as standard with Visual
Basic, you are given the means to provide code for the Events that are most
likely to be of interest to you.
RaiseEvent and WithEvents
After many requests from the development community Visual
Basic 5 finally offered the ability to add custom Events to forms, classes, and
controls. From the design point of view Events are called outgoing interfaces
because they originate from within the object and are handled elsewhere.
Conversely, properties and methods are called incoming interfaces because they
are invoked from outside the object. The object that generates Events is called
the event source, while the receiving object is called the event sink.
For a class to generate an event, it must first introduce it
in the Declarations section in the form [Public] Event procedurename
[(arglist)]. The keyword Public is cosmetic; events are always public. The
event can actually be raised in code through the use of the RaiseEvent command.
Listing 1 demonstrates the bare bones of a simple class, CFile, which performs
some kind of processing on a file. An event, called BatchNotify, is fired for
every 1000 records that are processed to give the front-end application the
opportunity to provide some form of visual feedback. A further event,
Completed, is fired when the task finishes. Note that the BatchNotify event
also passes a piece of data with it, as could the Completed event of course.
Event arguments mainly follow the same rules as procedure arguments, except
that Events cannot have named arguments, Optional arguments, or ParamArray
arguments. As in all other cases arguments can be declared as ByVal or ByRef
(the default), the latter giving the client the option to modify the contents
of the variable for later use by the event source.
Listing 1: Simple CFile class showing Events
declarations
Public Event BatchNotify(ByVal lNumber
As Long)
Public Event Completion()
Public Sub ProcessFile()
iReadFileNum = FreeFile
Open "c:\demo.txt" For Random Access Read Lock Read
As #iReadFileNum
iWriteFileNum = FreeFile
Open "output.txt" For Output As #iWriteFileNum
Do While Not EOF(iReadFileNum)
If lRecordCount Mod 1000 = 0 Then
RaiseEvent BatchNotify(lRecordCount)
End If
Loop
RaiseEvent Completion
Close #iReadFileNum
Close #iWriteFileNum
End Sub
In order that the calling piece of code can trap these Events
as they occur it is necessary to declare the class in the Declarations section
of the calling code module. This declaration must be of the form Private
WithEvents varname As type. The requirement here is to create an instance of
the class object at the Declaration level rather than, for example, from within
a single Sub or Function. The visibility scope of the class might now be wider
than you would ideally want it to be, but thats the trade off for getting
this functionality. Once the event handler has been installed via the
WithEvents declaration the IDE will automatically add entries to the Object
list box (the control immediately above and on the left-hand side of the main
edit window) for each event that it finds registered for the class in question.
Listing 2 provides another piece of simple code that receives the Events and
acts accordingly. Its worth noting that the class declaration didnt
also include an actual instantiation. The current implementation doesnt
allow the New keyword together with the WithEvents keyword. Therefore Ive
provided the creation and destruction in the form load and unload handlers
respectively. Whether these are the best places for them depends very much on
each specific requirement.
Listing 2: Simple form showing WithEvents declaration
Private WithEvents moFile As CFile
Private Sub Form_Load()
Set moFile = New File
Me.Show
moFile.ProcessFile
End Sub
Private Sub Form_Unload(Cancel As Integer)
Set moFile = Nothing
End Sub
Private Sub moFile_BatchNotify(ByVal lNumber As Long)
lblWriteCount.Caption = CStr(lNumber)
lblWriteCount.Refresh
End Sub
Private Sub moFile_Completion()
MsgBox "File conversion completed!"
End Sub
A further coding requirement that WithEvents imposes is that
the variable cannot be a generic object. That is, you cannot declare it As
Object. This is because the IDE needs to instantly accommodate a WithEvents
declaration by adding the appropriate event handler entries to the code editor
window. If the variable is declared As Object then the IDE will not be able to
scan the relevant type library in order to extract the provided events. You
also cannot create arrays of WithEvents variables.
Events provide a simple way of providing a one-way form of
communication between two code objects. Although the potential uses for an
Event are theoretically limitless they are often used to provide feedback on
long running processing tasks, as the examples in Listings 1 and 2 demonstrate.
However the ability to pass parameters ByRef gives the event sink the ability
to return useful information back to the event source. For example a boolean
Cancel parameter could be set to false by default, but could be altered to True
by the event sink if the user had requested the current operation to be
aborted.
Events or callbacks?
A callback will provide, more or less, the same ability as an
event, but there are important differences to consider in the way that the two
mechanisms work. When a server raises an event the notification is sequentially
sent to each client that is connected to it. If there are many clients
connected to a single server (for example a remote multiuse business object)
then the event message will need to be handled by each client in turn before
control is given back to the server. As a result Events are dispatched and
received in a random servicing order that might not always suit business
requirements. Within more sophisticated systems there could be sound reasons
for ensuring that certain clients with a higher degree of priority receive
event notifications before the other clients.
Callbacks on the other hand exist on a one-to-one relationship
with the server. This means that more specific messages can be sent to a single
client rather than the "tell everybody" approach employed by Events.
A client that has created a callback to the server can be sure that the server
will definitely receive a modified ByRef parameter. In the case of an event
broadcast to multiple clients the ByRef parameter could be modified at each
client stop point resulting in only one modification - the last one - arriving
at the server.
Programming considerations
-
It is not possible for two objects to each have a WithEvents reference to each
other. The compiler will reject such an intention with a circular
references warning
-
Attempting to pass Events over a DCOM implementation doesnt work very
well. If youre building for this kind of delivery platform then you are
better to go to the extra effort of writing a callback routine.
-
If an error occurs in the client application (event sink) then the event source
is not notified of the problem. Depending upon the nature of the application
this will or wont be a problem. If it will cause a problem then a
callback server is a better solution because it will automatically receive the
error when it is passed up the call chain. This is standard behaviour within
Visual Basic.
In summary, custom Events are not always
the best method for passing messages between one object and another,
but when they are appropriate then they are very straightforward
to program. They should probably be the first consideration when
designing a method of one-way communication between objects, but
discarded if the disadvantages would be an issue.