Put VB.NET events in the hands of AddHandler
This technical tip for intermediate VB.NET developers offers a look back at the AddHandler feature and how it addresses scenarios when there is no object variable to manipulate.
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 [email protected]