A simple conundrum posed by Visual Basic 6 -- how to trap and handle events from objects that are not assigned to an individual variable -- was solved by the earliest versions of VB.NET. The solution is the AddHandler command.
.NET provides the flexible handles clause to assign routines to handle events from object variables declared with the WithEvents keyword. There are, however, two common cases where there is no object variable to manipulate -- first, where there is no object instance associated with the event, and second, where the object is stored in a collection or an array.
AddHandler addresses both of these needs, and, as a bonus, permits us to change the handler routine at runtime. It also makes the WithEvents keyword unnecessary. This tip describes an example that demonstrates both cases in the same form.
Case 1: No Object Instance to Handle
The first case occurs with either an event declared in a module or with a shared event in a class.
As an example, error logging functionality was built into a class called ErrLogger. All of Errlogger's members, including its lone LogWritten event, which gets raised whenever the log file is written to, are declared using the shared keyword. ErrLogger is shown in Listing 1.
Listing 1: ErrLogging Class
Imports System
Imports System.IO
Public Class ErrLogger
Private Shared strFilePath As String = "C:\Myapp\bin" ' File path for log file
Public Shared Event _
LogWritten(ByVal Sender As Object, ByVal e As ELLogWrittenEventArgs)
'Append date/time to errMsg, write to log file, & raise the LogWritten Event
Public Shared Sub WriteErrMsg(ByVal errMsg As String)
Dim sR As New StreamWriter(strFilePath, True)
Dim FormattedMsg = String.Format("{0:G}", Now())& " : " & Errmsg & vbCrLf
sR.WriteLine(FormattedMsg)
'Raise the Logwritten event
RaiseEvent LogWritten(Nothing, New ELLogWrittenEventArgs (FormattedMsg))
sR.Close()
End Sub
End Class
'Custom EventArgs Class for the LogWritten event
Public Class ELLogWrittenEventArgs
Inherits System.EventArgs
Public Msg As String ' The message being written to the log
Sub New(ByVal newMsg As String)
Me.Msg = newMsg
End Sub
End Class
When using only shared members of a class, you don't have to instantiate the class. That's like having a global object, without having to track what happens to it. However, without an object instance, the handles clause can't be appended to a subroutine to handle an event.
Fortunately, Addhandler doesn't need an object instance -- only an event name and the address of a handler routine.
The syntax of Addhandler is this:
AddHandler event, AddressOf eventhandler.
Thus, in the form's load event handler, this line of code assigns the subroutine ErrorLogged as a handler to the ErrLogger class' LogWritten event:
AddHandler Errlogger.LogWritten, AddressOf ErrorLogged
Case 2: Handling Events from an Object within a Collection
AddHandler solves the problem of trapping and handling events raised by objects stored in a collection or array with relative ease. Use it to assign a handler to each object prior to adding it to the array or collection.
Example Description
Envision a collection containing one or more collections, each of which contains one or more elements. (The example code was developed for electronic signs, each of which contains a collection of one or more lines of text that are populated at regular intervals from an external data source. The example fits collections of tables with a variable number of chairs or classrooms with a variable number of students just as easily.)
The signs are represented as Group objects that contain collections of Line objects. The Group and Line objects are constructed dynamically (earlier in the application) from data in a database, so a count is not available at design time.
The data members of the Line and Group objects are displayed on a form using separate objects. Groups are displayed using groupDisplay objects that are stored in a collection owned by the form. Similarly, the Lines are displayed (and controlled) by lineDisplay objects, which are stored in a collection that is owned by their parent groupDisplay object.
The form's user can click on a lineDisplay's button to override the display. The goal of this tip is to show how the form can trap and handle the override events raised by individual lineDisplay objects.
The Form
An image of the form, configured for two groupDisplays with three lineDisplays each, is shown in Figure 1.
Figure 1: Form with Three LineDisplays within Two GroupDisplays
Each lineDisplay object has four constituent controls to display and control the text:
- Two labels -- to show the its name and current value,
- A textbox to enter an operator over-ride value, and
- A button to start the over-ride process.
Note that there's no data from the external source in these controls -- but a user can override "nothing" just as easily as "something." Also note that the bottom half of the form displays errors that are logged by the ErrLogger object -- as described above.
Handling the Event
AddHandler is used to assign a handler to the object before it is added to the collection. The object that raised the event is identified by using the sender object in the event's parameters.
The extra layer of collections in the example means that there's an extra required step: the lineDisplay's override event notifies the groupDisplay, which, in turn raises its own override event to notify the form. Each layer has its own handler routine, and the notification bubbles up through each layer.
This is less complex than it sounds. The relevant parts of the lineDisplay and groupDisplay classes are shown in Listings 2 and 3. The "irrelevant" parts of these classes, which create and place constituent controls and do other bookkeeping, have been omitted for clarity.
Listing 2: lineDisplay Class
Public Class LineDisplay
'... code to assign the object to parent group, create & place controls
' as well as the object constructor(s) omitted for clarity.
Public Event Override(ByVal Sender As Object, ByVal e As EventArgs)
Dim WithEvents btn As New Button
'Handle Click of the object's OverRide button
Private Sub btnOverRide_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnOverRide.Click
' *** Raise Event to Inform Client Object - A Group Display Object ****
RaiseEvent Override(Me, New EventArgs)
End Sub
End Class
Listing 3: groupDisplay Class
Public Class GroupDisplay
' Fire an event that delegates the LineDisplay Override event to this Object's Client (a Form Object)
Public Event Override(ByVal sender As Object, ByVal e As GDOREventArgs)
Sub New()
' Code to set the group associated with the display, assign reference to the panel for the group,
' and to instantiate the groupbox for this group omitted for clarity.
Dim Ln As Line
For Each Ln In Me.MyGroup.Lines
' create a lineDisplay
' **** Dynamically add the sub HandleLDButton as a handler ****
' add it to the collection of line displays for this groupdisplay
Dim LD As LineDisplay = New LineDisplay(Me.Grp, Ln)
AddHandler LD.Override, AddressOf HandleLDButton
Me.LDisplays.Add(Ln, LD)
Next
End Sub
' Dynamically added as handler to each LDisplay.Override Event
Sub HandleLDButton(ByVal sender As Object, ByVal e As EventArgs)
' Raise the event in a client form
Dim LD As LineDisplay = CType(sender, LineDisplay)
RaiseEvent Override(Me, New GDOREventArgs(LD))
End Sub
End Class
Public Class GDOREventArgs : Inherits EventArgs
Public MyLD As LineDisplay
Sub New(ByVal LD As LineDisplay)
Me.MyLD = LD
End Sub
End Class
As shown in Listing 2, lineDisplay's override event is raised in the button's click event handler. Meanwhile, the groupDisplay code in Listing 3 shows a lineDisplay object being created for each Line object in the group's Lines collection and the event handler -- (HandleLDButton()) -- being assigned.
When a particular lineDisplay raises its override event, it is handled by the groupDisplay via its HandleLDButton routine. To notify the client form, HandleLDButton raises the groupDisplay's override event.
How does the form know which object raised the event? That is in the lineDisplay's override event, as the sender parameter. That information is passed along when re-raising the event (for the client form) with a System.EventArgs-derived object that incorporates the lineDisplay object as a custom field.
The last piece is the code for the client form's load event, the relevant parts of which are shown in Listing 4.
Listing 4: Relevant Form Load Event Code
Dim gDisplays As Hashtable
Private Sub FormMain_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load
'Make Sub ErrorLogged Handle the LogWritten event of ErrLogger
AddHandler Errlogger.LogWritten, AddressOf ErrorLogged
'Create and set up group displays, assigning the event handler
Dim Grp As Group
For Each Grp In Main.Groups
'create new groupDisplay for this group
Dim GD As New GroupDisplay()
'Add the event handler -bubbles up from lineDisplay to groupDisplay to here
AddHandler GD.Override, AddressOf ButtonHandler
gDisplays.Add(Grp, GD) ' add to the hashtable
Next
End Sub
Private Sub ButtonHandler(ByVal sender As Object, ByVal e As GDOREventArgs)
'code here
End Sub
Note the assignment of the ErrLogger.LogWritten event that was excerpted earlier. After that, there's a simple for loop to create groupDisplay objects and assign the form's ButtonHandler routine as the groupDisplay object's override event's handler.
Conclusion
There are countless real-world applications dealing with events raised by objects stored in collections or non-instantiated class events. Where they occur, Addhandler addresses the inter-object communication problem very neatly.
Bruce D. Neiger is a licensed professional engineer working for Parsons Transportation Group (PTG) in New York City. His 20-year career has included of highway-related air quality and noise modeling, application development, OOP, and object architecture. He can be reached at Bruce.D.Neiger@Parsons.com.
This was first published in February 2007