Tuesday, 07 September 2010
PicoSearch

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

‘ cfile.cls
'==========

‘ Declare the events that this class will raise
Public Event BatchNotify(ByVal lNumber As Long)
Public Event Completion()

Public Sub ProcessFile()

  ‘ Open input and output files
  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)
    ‘ Do some processing here and then
    If lRecordCount Mod 1000 = 0 Then
      ‘ Raise an event every 1000th record
      RaiseEvent BatchNotify(lRecordCount)
    End If
  Loop

  ‘ Raise a message to flag completion
  RaiseEvent Completion

  ‘ Close files
  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 that’s 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. It’s worth noting that the class declaration didn’t also include an actual instantiation. The current implementation doesn’t allow the New keyword together with the WithEvents keyword. Therefore I’ve 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

‘ form1.frm

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)
  ‘ Update label on form
  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 doesn’t work very well. If you’re 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 won’t 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.

Copyright ©2002 Jon Perkins I, Jon Michael Perkins, hereby assert and give notice of my right under section 77 of the Copyright, Designs, and Patents Act 1988 to be identified as the author of the foregoing article.