Creating Events and Delegates in VB.NET
By Dan Fox
Date: Aug 20, 2001
Article is provided courtesy of Sams.
In this article, learn event handling and the major uses of delegates in VB.NET. Dan Fox discusses mapping events to delegates, function substitution with delegates, and asychronous processing with delegates.
This article assumes that the reader has some experience with VB, the Windows environment, event-based programming, basic HTML, and scripting.
The basic event handling syntax has not changed significantly from previous versions of VB, although it has been extended (as you'll see shortly). As in previous versions, events can be declared within a class using the Event keyword. An event can be Public, Private, Protected, Friend, and Protected Friend to the class, can pass arguments either ByVal or ByRef, and cannot return values. Events can then be raised using the RaiseEvent keyword from within the class and passing the required arguments. A partial sample from a class called RegistrationWatch is shown in Listing 4.2.
Listing 4.2—Simple events. This class implements the simple event NewRegistrations using the Event keyword and fires it using RaiseEvent.
Public Class RegistrationWatch
Public Event NewRegistrations(ByVal pStudents As DataSet)
' Other methods and properties
Public Sub Look()
Dim dsStuds As DataSet
Dim flNew As Boolean
' Method that fires on a timer to look for new registrations
' since the last invocation of the method
' If one is found then create a DataSet with the new students
' and raise the event
flNew = True
dsStuds = New DataSet()
If flNew Then
RaiseEvent NewRegistrations(dsStuds)
End If
End Sub
End Class
Notice that the class defines a NewRegistrations event as Public so that consumers of the class will be able to catch it. The event passes back to the consumer a variable containing an ADO.NET DataSet that stores information on the new registrations found. The event is raised in the Look method using the RaiseEvent statement.
To catch the event, a consumer can declare the RegistrationWatcher class using the WithEvents keyword (termed declarative event handling). Note that, as in VB 6.0, variables declared using WithEvents may be either Public or Private. However, in VB.NET, WithEvents can be declared at the module or class level, rather than only in classes and forms as in VB 6.0. The syntax for using WithEvents is as follows:
Private WithEvents mobjReg As RegistrationWatch
To set up the event handler (or event sink), you can then create a procedure that handles the event within the class or module, as shown in the following example:
Public Sub RegFound(ByVal pStudents As System.Data.DataSet) _
Handles mobjReg.NewRegistrations
MsgBox("New Students Found!")
End Sub
Note that VB.NET uses the new Handles keyword to indicate precisely which event the procedure handles rather than simply relying on the naming convention used by the event handler as in previous versions. Declarative event handling is certainly the most convenient way to handle events, although as mentioned, it requires the object variable to be scoped correctly and cannot be used to dynamically turn an event handler on or off.
TIP
Although it is true that declarative event handling does not allow you to turn events on and off, if the class raising the event is a custom class, you can implement your own EnableRaisingEvents property. The client can then use this property to stop the raising of events. You would then wrap your RaiseEvent statements in a check of this property.
When dealing with inheritance, keep in mind that if a class acting as a consumer does not implement the event handlers for a particular object, classes derived from it will not be able to implement them later. In addition, as with other methods, the event handlers can be specified with the Overridable, NotOverridable, and MustOverride keywords.
Events can also be marked as Shared, which allows all consumers of a class to receive an event notification when the RaiseEvent method is executed in any instance of the class. This might be useful for notifying several consumers when shared data changes, for example, in a service application that periodically queries a database and exposes the data through shared properties.
Dynamic Event Handling
VB.NET expands the ability to use events by implementing dynamic event handling in addition to the declarative approach discussed earlier. Dynamic event handling can be very useful when you want to turn event handling on or off (called hooking and unhooking an event) at a specific time or when the object variable that you want to hook does not reside at the module or class level. To hook and unhook events at run-time, use the AddHandler and RemoveHandler statements.
As an example, consider the RegistrationWatch class shown in Listing 4.3. In this example, we want to create client code in a class that hooks and unhooks the NewRegistrations event based on the setting of the public property. The client class that does this is shown in Listing 4.3.
Listing 4.3—Dynamic events. This class hooks and unhooks events dynamically using the AddHandler and RemoveHandler statements.
Public Class Registrations
Private mRec As Boolean = False
Private mRegWatch As RegistrationWatch
Public Property ReceiveNotifications() As Boolean
Get
Return mRec
End Get
Set
mRec = Value
If mRec = True Then
' Add the event handler
AddHandler mRegWatch.NewRegistrations, _
AddressOf Me.NewRegistrations
Else
' Remove the event handler
RemoveHandler mRegWatch.NewRegistrations, _
AddressOf Me.NewRegistrations
End If
End Set
End Property
Public Sub NewRegistrations(ByVal ds As DataSet)
' New Registrations found
MsgBox("New registrations have been added!")
End Sub
Public Sub New()
' Instantiate the class level object
mRegWatch = New RegistrationWatch()
End Sub
Public Sub TestNotify()
' Test method to simulate repeated queries for new registrations
mRegWatch.Look()
End Sub
End Class
In Listing 4.3, the property ReceiveNotifications is used to determine whether the class is to receive notifications. The Set block of the property then reads the value and calls the AddHandler and RemoveHandler statements accordingly. Both of the statements accept two parameters:
A reference to the event to be hooked (in this case, the NewRegistrations event of the module level mRegWatch variable)
The address of the method to use as the event handler
Note that the AddressOf and Me keywords are used to create a pointer to the method and specify a method internal to the class, respectively.
From the client's perspective, the code differs only in that the ReceiveNotification property can be set to True when notifications are desired (because the mRec class level variable defaults to False), as shown in the following example:
Dim objReg As New Registrations() objReg.TestNotify() ' No notification objReg.ReceiveNotifications = True objReg.TestNotify() ' Notification receive
Mapping Events to Delegates
As mentioned in Chapter 1, the infrastructure for events is based on the concept of delegates, and so it is not surprising that the event keywords (such as Event, RaiseEvent, AddHandler, and RemoveHandler) simply abstract the creation and processing of delegates. However, VB.NET developers can also access delegates directly through the Delegate keyword. To understand delegates, let's first explore how delegates are used to implement events in VB.NET.
Remember first and foremost that delegates are simply types that hold references to functions in other objects (type-safe function pointers). There are two basic types of delegates: single-cast and multi-cast. The former allows a single function pointer to be stored and the latter creates a linked-list of pointers (events are implemented as multi-cast delegates). In addition to the function pointer, the delegate stores the arguments that the function will accept. As a result, from the developer's view, the delegate can be thought of simply as a method signature.
The basic idea behind using a delegate is that a program (called A for this example) creates a delegate that points to one of its own functions and then passes the delegate to some other program (B). At some time in the future, B executes A's function, making sure to push the appropriate arguments on the stack, by running the code at the address provided by the delegate. Of course, this is exactly the model used when dealing with events.
In the example in Listing 4.3, what happens when the Event keyword and RaiseEvent statements are added to the class is this. Behind the scenes, the VB compiler creates a Delegate with the same signature as the event and stores it in a field of the class as follows:
Delegate Sub NewRegistrations(ByVal pStudents As DataSet)
The compiler also creates add and remove methods for the delegate that take as an argument a reference to a delegate defined (in this case, NewRegistration). In addition, the RaiseEvent is replaced with a call to the delegate's Invoke method, which accepts the single argument as defined in the Delegate.
The consumer then uses the WithEvents keyword and implements the event handler with the Handles statement. At run-time, the delegate is instantiated (it is actually a class derived from System.Delegate) and a reference to the event handler is passed to the add method in RegistrationWatch. When the delegate is invoked, the function pointer to the event handler is used to call the method. This simple mapping of delegates to events should also explain why it is easy for VB.NET to support the AddHandler and RemoveHandler statements. They simply call the add and remove methods implemented by the compiler at specified times rather than upon instantiation and deallocation.
To make this a little clearer, examine the code in Listing 4.4 that shows the RegistrationWatcher class rewritten with a delegate in place of the event.
Listing 4.4—Simple delegate. This class uses a delegate in place of an event to perform simple notification.
Public Class RegistrationWatch
Delegate Sub NewRegistrations(ByVal pStudents As DataSet)
' Other methods and properties
Private mfound as NewRegistrations
Public Sub RegisterClient(ByVal found As NewRegistrations)
Mfound = found
End Sub
Public Sub Look()
Dim dsStuds As DataSet
Dim flNew As Boolean
' Method that fires on a timer to look for new registration
' If one is found then create a DataSet with the new students
' and raise the event
flNew = True
dsStuds = New DataSet()
If flNew Then
mfound.Invoke(dsStuds) 'invoke the delegate
End If
End Sub
End Class
The differences between Listing 4.4 and Listing 4.3 can be summarized as follows:
The Event statement has been replaced with Delegate.
The RaiseEvent statement has been replaced with Delegate.Invoke.
The RegisterClient method is now used to pass in the reference to the delegate stored in a private class variable, whereas the Look method simply invokes the delegate at the appropriate time.
Notice also you don't have to explicitly create the add and remove methods, even when specifying the delegate yourself; the VB.NET compiler will add these automatically.
The client code also changes in order to instantiate the delegate as it is being passed to the Look method. Note that the address of the event handler is passed as the only argument in the constructor of the delegate, as shown in the following example:
Private mobjReg As RegistrationWatch ' in a class or module mobjReg = New RegistrationWatch() mobjReg.RegisterClient(New RegistrationWatch.NewRegistrations( _ AddressOf NewRegistrations)) mobjReg.Look() Private Sub NewRegistrations(ByVal ds As DataSet) End Sub
Events can also work with delegates as a kind of shortcut for declaring the event signature. For example, rather than declaring an event as
Event NewRegistrations(ByVal ds As DataSet)
you could make the declarations
Delegate Sub NewRegistrations(ByVal ds As DataSet) Event NewReg as NewRegistrations
This allows you to reuse the definition of the delegate inside the event. This can be useful when you have many events that require the same arguments.
Obviously, using delegates in place of events might be construed as overkill because events work quite nicely for scenarios where an event model is required. However, because delegates are function pointers, they also provide other capabilities that you can take advantage of, such as function substitution and asynchronous operation.
Function Substitution with Delegates
The idea of function substitution is exactly as it sounds: A section of client code may call one of several functions depending on the delegate it is passed. This promotes the idea of polymorphism because it allows you to write code that is generic as it doesn't know at compile-time which function it is going to call. As an example, consider the Registration class shown in Figure 4.1. Suppose that it contains a RegisterStudent method that implements the business rules necessary to register a student to take a course. As a small part of that process, the cost of the course must be calculated. However, the algorithm for calculating the cost varies depending on how the student was registered and could include a phone call, Web, and registrations received directly from a vendor or partner through a Web service.
To implement this requirement, the Registration class could declare a delegate called CalcCourseCost as follows:
Delegate Function CalcCourseCost(ByVal CourseID As String) As Decimal
Note that delegates can also return values and therefore be declared as a function. The RegisterStudent method is shown in Listing 4.5.
Listing 4.5Skeleton code for the RegisterStudent method. This method uses the delegate passed in as the first parameter to invoke the appropriate calculation function.
Public Sub RegisterStudent(ByVal pCalc As CalcCourseCost, _ ByVal pStud As DataSet) ' Implement the business process for registering the student ' 1. make sure the student has provided enough info ' 2. make sure the student has not been blacklisted ' 3. possibly suggest another class that is closer based on ' geographic data: raise exception ' 4. make sure the class is not full ' 5. calculate the cost Dim curCost As Decimal Dim strCourseID As String curCost = pCalc(strCourseID) ' 6. persist the registration (database or queue) ' 7. notify the appropriate internal staff of a new registration ' 8. email verification to student End Sub
The method takes both the delegate and the student information packed in a DataSet as parameters. After completing the preliminary business rules, the course cost can be calculated simply by invoking the delegate pCalc and passing the required CourseID argument. Note that this example illustrates that the Invoke method of the delegate is actually the default method of a delegate object. The invocation of the calculation function could also be written as pCalc.Invoke(strCourseID).
On the client side, the appropriate delegate must be instantiated. The skeleton code in Listing 4.6 shows code residing in a module or class that first collects the registration method (stored in RegType) and uses a Select Case statement to instantiate the correct delegate. The delegate is then passed to the RegisterStudent method.
Listing 4.6Client code using a delegate. This code determines the appropriate delegate at run-time and passes it to the RegisterStudent method. Note that the procedures used as the delegates are shown later.
Dim objReg As New Registrations() Dim delCalc As Registrations.CalcCourseCost Dim RegType As Integer Dim ds As DataSet ' Determine the registration type ' Create the delegate based on the registration type Select Case RegType Case 1 delCalc = New Registrations.CalcCourseCost(AddressOf CalcWeb) Case 2 delCalc = New Registrations.CalcCourseCost(AddressOf CalcPhone) Case 3 delCalc = New Registrations.CalcCourseCost(AddressOf CalcService) End Select ' Register the student objReg.RegisterStudent(delCalc, ds) ' Other code goes here Public Function CalcWeb(ByVal strCourseID As String) As Decimal ' Calculate cost based on web registration End Function Public Function CalcPhone(ByVal strCourseID As String) As Decimal ' Calculate cost based on phone registration End Function Public Function CalcService(ByVal strCourseID As String) As Decimal ' Calculate cost based on service registration End Function
NOTE
Experienced VB developers might have noticed that this technique is similar to using the CallByName function in VB 6.0. The difference is that CallByName could be used only on internal classes or COM objects, whereas delegates can be used with any procedure (in a module or a class) that fits the signature of the delegate.
What About Interfaces?
Developers who have done interface-based programming in VB 6.0 will note that the polymorphism shown in the CalcCourseCost example could also be implemented simply by creating separate classes for each calculation method and implementing a common interface (ICalc) that exposes a Calculate method. Different classes that implement the ICalc interface could then be passed to RegisterStudent at run-time to call the Calculate method. Although this technique would also work, it might be more complicated and less efficient. For example, by using a delegate, you don't have to create separate classes because any number of functions (as long as they have the same signature) in the calling code can be used to implement the delegate, and you don't have to pass a full object reference to RegisterStudent, only a delegate. As a rule of thumb, create interfaces when there is a collection of related members that you want to implement across classes and when the particular function will be implemented only once. Use delegates when you have only a single function to implement, the class implementing the delegate doesn't require a reference to the object, or you want to create a delegate for a shared method (all methods defined for the interface are always instance methods).
As mentioned in the What About Interfaces? sidebar, using delegates for function substitution can also take the place of using interfaces (discussed later in the chapter). This pattern can be approximated by instantiating a delegate inside a class and then using a private function to invoke the delegate. For example, consider the case in which disparate classes each support the ability to persist their state. In this case, client applications could work with each of these classes polymorphically through the use of a delegate rather than requiring them to implement the same interface or belong to the same inheritance hierarchy. The pattern for such a class is shown in Listing 4.7.
Listing 4.7Delegates as interfaces. This class shows how a delegate can be used to expose functionality to client applications polymorphically. It is functionally equivalent to using an interface.
Delegate Function Persist() As String Public Class Registrations Public ReadOnly myPersist As Persist Public Sub New() myPersist = New Persist(AddressOf Me.SaveToDisk) End Sub Private Function SaveToDisk() As String Dim strFile as String ' save the contents to disk and return the path name Return strFile End Function End Class
Note that the delegate is declared external to the class and that the class then instantiates a read-only variable in the constructor, passing it the address of the private SaveToDisk method (which will actually save the results and return the file name). The client application can then call any class that supports the Persist delegate by implementing a function that accepts the delegate as parameter, as shown here:
Public Function Save(ByVal pPersist As Persist) As String Return pPersist.Invoke End Function
The client then invokes the function to save the contents to a file by passing the delegate of the Registrations class to the Save function, as shown here:
Dim objRegister As New Registrations Dim strPath As String ' ...work with the class strPath = Save(objRegister.myPersist)
In this way, each class can decide internally how to implement the code to save its contents and yet provide a public interface through the Persist delegate that client applications can call. Even though this code is slightly more complex from the client application's perspective, it is more flexible because interfaces and inheritance are not required.
Asynchronous Processing with Delegates
A second major use of delegates in .NET is for handling asynchronous processing. One of the benefits of using delegates for asynchronous processing is that they can be used both in conjunction with .NET Remoting (discussed in Chapter 8, "Building Components,") to facilitate communication between processes and machines and with code that resides in the same application domain. This common programming model allows you to write code that is location transparent as well.
Delegates are appropriate for asynchronous calls because they were designed with BeginInvoke and EndInvoke methods that allow the client to invoke a method asynchronously and then catch its results respectively. You can think of BeginInvoke and EndInvoke as partitioning the execution of a method from a client's perspective. To specify which procedure to call upon completion of the asynchronous method, you use the AsyncCallback object and IAsyncResult interface from the System namespace. When using delegates with the asynchronous classes and interfaces in the System namespace, the CLR provides the underlying services needed to support this programming model including a thread pool and the ability to call objects that reside inside synchronized contexts such as those using COM+ services. (We'll explore more about thread support in the CLR in Chapter 11, "Accessing System Services," as well.) One of the side benefits of this integration is to make it fairly simple to create multi-threaded applications in VB.NET.
NOTE
VB 6.0 developers will recall that because the VB 6.0 runtime was single-threaded, it was very difficult to create any asynchronous code without resorting to using the Win32 API directly (which led to unstable code that was tricky to debug) or tricking the COM library into creating an object in a new single-threaded apartment. Needless to say, neither technique was particularly attractive.
To understand the pattern for asynchronous programming in .NET, consider a method of the Registration class that is used to send reminder e-mail notifications to all students who are to attend a class the following week. This RemindStudent method might take some time to complete because it must read a list of students from the database, construct an e-mail message for each one, send the e-mail, and update the database appropriately. For the client application's thread to avoid being blocked when the method is called, the method can be called using a delegate (in this case often termed an asynchronous delegate).
Because the .NET asynchronous model allows the client to decide whether the called code will run asynchronously, the code for the RemindStudent method does not have to be written explicitly to handle asynchronous calls. You can simply declare it and write the code as if it were synchronous as follows:
Public Function RemindStudent(ByVal pDaysAhead As Integer, _ ByRef pErrors() as String) As Boolean ' Read from the database and send out the email notification End Sub
Note that the method requires an argument to specify how many days in advance the e-mail should be sent out and returns an array of strings containing any error messages. However, the client code must significantly change. The code in Listing 4.8 is used to call the RemindStudent method.
Listing 4.8Code used to implement asynchronous programming using delegates.
delegate Delegate Function RemStudentCallback(ByVal pDaysAhead As Integer, _ ByRef pErrors() as String) As Boolean ' Async' Client code inside a class or module ' Declare variables Dim temp() As String Dim intDays As Integer = 7 ' Create the Registration class and the delegate Dim objReg As Registration = New Registration() Dim StudCb As RemStudentCallback = New RemStudentCallback(AddressOf _ objReg.RemindStudent) ' Define the AsyncCallback delegate Dim cb As AsyncCallback = New AsyncCallback(AddressOf RemindResult) ' Can create any object as the state object Dim state As Object = New Object() ' Asynchronously invoke the RemindStudent method on objReg StudCb.BeginInvoke(intDays, temp, cb, state) ' Do some other useful work Public Sub RemindResult(ByVal ar As IAsyncResult) Dim strErrors() As String Dim StudCb As RemStudentCallback Dim flResult As Boolean Dim objResult As AsyncResult ' Extract the delegate from the AsyncResult objResult = CType(ar, AsyncResult) StudCb = objResult.AsyncDelegate ' Obtain the result flResult = StudCb.EndInvoke(strErrors, ar) ' Output results End Sub
To begin, notice that the client code first creates a new instance of the Registration class as normal. However, it then creates a new RemStudentCallback delegate and places in it the address of the RemindStudent method of the newly created object instance (objReg). Once again, the delegate must have the same signature as the RemindStudent method.
Now the code must create an address that can be called back into upon completion of the RemindStudent method. To do this, a system delegate class called AsyncCallback is instantiated and passed the address of a procedure called RemindResult. You'll notice that RemindResult must accept one argument: an object that supports the IAsyncResult interface.
After the callback is set up, the RemindStudent method is actually invoked using the BeginInvoke method of the delegate that was created earlier (StudCb). The BeginInvoke method accepts the same arguments as the delegate it was instantiated as in addition to the delegate that contains the callback and an object in which you can place additional state information required by the method.
At this point, your code can continue on the current execution path. The delegate will invoke the RemindStudent method (on a separate thread if in the same application) and when finished will call the procedure defined in the delegate passed to BeginInvoke (in this case, the RemindResult). As mentioned previously, this procedure accepts an argument of type IAsyncResult. To retrieve the instance of the delegate RemStudentCallback from it, you first need to convert it to an object of type AsyncResult using the CType function. The AsyncDelegate property of the resulting AsyncResult object contains the reference to the RemStudentCallback delegate. The return value and any arguments passed ByRef are then captured using the return value and arguments passed to the EndInvoke method of the delegate.