Delegates are a critical aspect of the Microsoft® .NET Framework and are required learning for many programmers. They will take some time to master, but once you learn how to program with delegates, you'll be thrilled with the possibilities they offer.
Let's dig in. First, you should know that events in the .NET Framework are layered on top of delegates. When you use an event-driven application framework such as Windows® Forms or ASP.NET, your knowledge of delegates will make you a much stronger developer. Delegates provide the primary means in .NET for executing a method on a secondary thread in an asynchronous fashion. Therefore, delegates open the door to multithreading.
Delegates represent a .NET innovation intended to address application designs that involve callback notifications. First we will see how to implement callbacks using delegates. However, before I jump into programming with delegates, I want to explain why and how callback notifications have been implemented in the past.
Callback Notifications
Application software is often designed around the notion of callbacks. The use of callbacks is a programming technique in which one part of an application sends out notifications to alert other parts of the application whenever something interesting has occurred. To be more specific, a callback is a method that is implemented by one or more handlers and is then executed by a notification source.
Most programmers who use Visual Basic® should be familiar with the concept of callbacks because of the manner in which Visual Basic has always supported event handling. An event handler is a simple example of a callback method. When you write an event handler for the Click event of a command button, you are really writing the implementation for a callback method. However, you are not required to explicitly call the event handler. Instead, the Form class of the Windows Forms framework acts as a notification source because it automatically executes your event handler method at exactly the right time.
It can be helpful to make an analogy to something that might occur in everyday life to fully appreciate the value of a design based on callbacks. Imagine your boss has just assigned you a task. Assume that this task will take several hours to complete and that your boss wants to know the minute that you are finished. It would probably become very annoying if your boss called you on the phone every few minutes to ask if you had finished the task. It would be far more reasonable for you to make the following suggestion to your boss. "Don't call me; I'll call you when I'm finished."
As long as you notify your boss the moment you've completed the task, your boss can react and take whatever actions are appropriate in a timely fashion. As you can imagine, the use of a callback in this particular scenario is going to be more efficient (and far less annoying) than having your boss poll you every few minutes to see if you've finished your work.
If you are using Visual Basic .NET or an earlier version such as Visual Basic 6.0, you can implement a callback design using an interface. For example, you can use an interface to define a callback method. Then you can create a listener class that implements this interface. In doing so, your listener class will provide an implementation for the handler method defined by the interface. Finally, you can design another object to act as a notification source. The notification source object should be designed to track an interface-based reference to a listener object. Once the notification source object has a reference to the listener object, it can execute the handler method whenever it needs to send out callback notification.
C++ programmers have been implementing callbacks in their applications long before the .NET Framework came along. However, a callback in C++ is often implemented without the use of an interface. Instead, it can be implemented using something called a function pointer. While it's not important for you to know the low-level details of how to program using function pointers, it's valuable to understand them at a higher level because of how much influence they have had on the architecture of the .NET Framework.
A function pointer is an in-memory address that points to a method implementation. At the physical level, a function pointer points to a set of instructions that represents the executable logic for a method. To use a function pointer, one part of the application must initialize it to point to a particular method. Another part of an application can then use this function pointer to execute the method to which it points.
Function pointers are useful in C++ because they provide an efficient way to implement a callback between a notification source and a listener. As long as a notification source can obtain a function pointer that points to the implementation for a handler method, it can send out notifications in an anonymous fashion.
When it comes to modeling callbacks, however, the use of function pointers offers a few distinct advantages over the use of interfaces. First, function pointers don't require that a notification source and its listeners agree on the names of callback methods. Second, the use of function pointers can provide a higher degree of granularity than interfaces because you can register handlers to receive callbacks on a method-by-method basis. Function pointers also make it possible to use shared (static) methods in addition to instance methods.
Introducing Delegates
When architects on the .NET team began to design their new framework, they knew they wanted to provide rich support for implementing callbacks. They weighed the pros and cons of implementing callback methods with interfaces versus function pointers. In the end, they decided to create a new hybrid technique that combines the type safety and polymorphism of using interfaces together with the efficiency and flexibility of using function pointers. This new technique involves an innovation known as a delegate.
A delegate is a special kind of type within the programming model of the .NET Framework. The architects of the .NET Framework added delegates to provide a convenient binding mechanism to wire up one or more handler methods to a notification source. As you will see, delegates can be used to implement the same kinds of callback designs that you can implement using interfaces or function pointers. However, a callback design based on a delegate often requires less code and provides more features than a callback design based on an interface or function pointers.
You can define a delegate type in Visual Basic .NET using the Delegate keyword. Each delegate definition you create must include a type name and the calling signature for a handler method. Here's an example of three delegate type definitions:
Delegate Sub BaggageHandler()
Delegate Sub MailHandler(ItemID As Integer)
Delegate Function QuoteOfTheDayHandler(Funny As Boolean) As String
As you can see, you define the calling signature of a delegate type using the standard Visual Basic syntax for method definitions. Like a method, a delegate type must be defined using either the Sub keyword or the Function keyword. A delegate definition is also similar to a method definition in that it can optionally define a parameter list and a return value.
When you compile code that contains a delegate type definition, the Visual Basic .NET compiler does some extra work for you behind the scenes. In particular, the compiler generates a class definition for each delegate type. The class that is generated for each delegate type is a creatable class that inherits from the System.Multicast delegate. Figure 1 shows what the preceding three delegate types look like after compilation from the perspective of ILDASM.EXE.
Figure 1 Delegate Types After Compilation
Take a moment to examine the definition of the delegate type QuoteOfTheDayHandler in Figure 1. You can see that in addition to creating a class that inherits from MulticastDelegate, the Visual Basic .NET compiler has also added a public constructor and three public methods named Invoke, BeginInvoke, and EndInvoke.
The Visual Basic .NET compiler adds a public constructor to each delegate definition to make it a creatable type. The public constructor is the member with the name .ctor. Once you acknowledge that a delegate is a creatable type, it is easier to understand how it's used in an application. Programming with delegates requires creating delegate objects from delegate types. Furthermore, each delegate object must be created in such a way so that it gets initialized to point to a target method implementation. In just a moment, you will see how to do this by writing the code to create a delegate object that gets bound to a target handler method.
In addition to a public constructor, the compiler automatically adds a public method named Invoke whenever it generates a delegate type definition. The Invoke method is supplied so that a notification source can execute the target handler method to which the delegate object is bound. When you call Invoke on a delegate object, it simply forwards the call to the target handler method.
The calling signature for the Invoke method of QuoteOfTheDayHandler matches the calling signature of the delegate type itself. This is always the case with a delegate type definition. The compiler will always generate an Invoke method whose calling signature matches that of the containing delegate type. Any input parameters that you pass in a call to Invoke will be forwarded in the call to the target handler method. If the target handler method has any output parameters or a return value, these values will be returned to the caller of the delegate object's Invoke method.
You have probably also noticed that the definition for type QuoteOfTheDayHandler contains two additional methods generated by the compiler, named BeginInvoke and EndInvoke. These two methods provide a basis for executing a delegate object's target handler method on a separate thread in an asynchronous fashion. The fact that delegates can be used to execute certain tasks asynchronously makes them that much more powerful. However, I will defer a discussion of how to use BeginInvoke and EndInvoke to execute methods asynchronously for a future column.
Creating a Delegate Object
When you want to create a delegate object, you typically use the New operator followed by the name of the delegate type. When you create a delegate object using the New operator, you must provide the information that's needed to bind the new delegate object to the implementation of a target handler method. Let's step through an example using the QuoteOfTheDayHandler delegate.
Before you can create a delegate object, you must determine what handler method you'd like to bind it to. A handler method can be either a shared method or an instance method. Keep in mind that the name of the handler method doesn't really matter. The only requirement is that a handler method be defined with a calling signature that matches the delegate type. For the first example, imagine you have written a class named JennysHandlers that contains a shared method that was written to be a handler method for the QuoteOfTheDayHandler delegate type, as shown in the following code:
Class JennysHandlers
Shared Function GetQuote(ByVal Funny As Boolean) As String
'*** custom handler implementation
End Function
End Class
As you can see, the shared method GetQuote has the correct calling signature to be used with the QuoteOfTheDayHandler delegate. When you want to create a delegate object, you can use the New operator and pass a parameter value with the information that allows the CLR to bind the new delegate object to a target method implementation. The way to accomplish this in Visual Basic .NET is by using the AddressOf operator followed by the method name. For example, you can create a delegate object that is bound to the GetQuote method using the following code:
Dim handler1 As QuoteOfTheDayHandler
handler1 = New QuoteOfTheDayHandler(AddressOf JennysHandlers.GetQuote)
Note that the Visual Basic .NET compiler will generate a compile-time error if the calling signature of the GetQuote method does not match the calling signature of the delegate type QuoteOfTheDayHandler. These compile-time checks are what makes programming with delegates far less error-prone than programming with function pointers. The strongly typed nature of delegates ensures that a notification source and its handler methods all agree on a particular calling signature.
You might remember the AddressOf operator from previous versions of Visual Basic. This operator was introduced in Visual Basic 5.0 to provide a means for passing actual function pointers to low-level functions in the Win32® API. However, the role of AddressOf is different in Visual Basic .NET. Its primary purpose is now to initialize delegate objects.
Note that the Visual Basic .NET language provides a convenient shorthand syntax for creating a delegate object. The previous code sample can be rewritten more concisely:
Dim handler1 As QuoteOfTheDayHandler
handler1 = AddressOf JennysHandlers.GetQuote
As you can see, the call to the New operator can be omitted for convenience. In this example, the Visual Basic .NET compiler is designed to create a new object from the QuoteOfTheDayHandler delegate type and assign a reference to this delegate object back to the variable handler1. What you should see is that whenever the compiler expects a delegate object of type QuoteOfTheDayHandler, you can simply pass the following expression:
AddressOf JennysHandlers.GetQuote
The Visual Basic .NET compiler expands this expression into code that creates a new QuoteOfTheDayHandler object and binds it to the handler method GetQuote. While the longer syntax might make what's actually going on more obvious, you might find the shorthand syntax more concise and easier to write.
Now that you know how to bind a new delegate object to a handler method, it's time to see how to execute the handler method that the delegate object is bound to. You can execute the handler method by simply calling the Invoke method on the delegate object, as shown in the following code:
'*** create and bind delegate object
Dim handler1 As QuoteOfTheDayHandler
handler1 = AddressOf _
JennysHandlers.GetQuote
'*** execute target method
Dim quote As String = _
handler1.Invoke(True)
Note that the call to Invoke in this example requires a single parameter of type Boolean and has a return value based on the String type. When you call Invoke on the delegate object, it forwards the call by executing the target handler method GetQuote. You should also see that the call to Invoke returns the same value that was returned from the call to GetQuote.
Visual Basic .NET also provides you with another convenient shorthand syntax when programming delegates. You have the option of omitting the call to the delegate's Invoke method. If you don't provide an explicit call to Invoke, the Visual Basic .NET compiler will automatically add the call for you. Look at the following lines of code:
'*** this code
Dim quote1 As String = handler1.Invoke(True)
'*** is the same as this code
Dim quote2 As String = handler1(True)
As you can see, the call to Invoke can be made either explicitly or implicitly. When you replace the syntax handler1.Invoke(True) with the syntax handler1(True), the Visual Basic .NET compiler automatically adds the call to Invoke for you. Therefore, you can simply treat a reference variable or a field based on a delegate type as if it were the name of an actual method.
Whether you make calls to the Invoke method explicitly or implicitly comes down to a stylistic preference on your part. It has no effect on your code once it is compiled. Some programmers prefer explicit calls to Invoke because they feel it makes their code easier to read. Others prefer implicit calls to Invoke because this results in a little less typing.
It is interesting to note that the C# compiler doesn't allow for explicit calls to a delegate's Invoke method. Instead, it requires that programmers use the implicit style in which a delegate reference is treated as though it were an actual method name. The C# compiler always adds the call to Invoke during compilation. If you are planning to switch back and forth between Visual Basic .NET and C#, you might consider using the implicit style of calling Invoke because it promotes greater consistency across languages.
Binding a Delegate to an Instance Method
Up to this point you have seen how to bind a delegate object to a shared method. As you already know, this is accomplished using the AddressOf operator followed by the class name together with the shared method name. However, it's also possible to bind a delegate to an instance method. Let's look at a code example so you can see how binding a delegate object to an instance method differs from binding to a shared method.
Instance methods are different from shared methods because they must execute within the context of an object created from the class in which they are defined. Therefore, binding a delegate object to an instance method requires the delegate object to track a target object in addition to tracking its target method implementation. After all, it would not be possible for a delegate object to execute an instance method unless it knew what object to use for the method's execution context.
In order to create a delegate object that is bound to an instance method, you must start by creating or acquiring a reference to an object created from the class that defines the instance method. For example, suppose you have created a class named JerrysQuotes that contains an instance method named NextQuote, as shown in this code snippet:
Class JerrysQuotes
Function NextQuote(ByVal Funny As Boolean) As String
'*** implementation
End Function
End Class
In order to bind a delegate object to the instance method NextQuote, you must first create an object from the class JerrysQuotes. Once you have acquired an object reference, you can create and bind a delegate object to one of its instance methods. You do this by using the AddressOf operator together with the object reference followed by the method name:
Dim quotes As New JerrysQuotes()
Dim handler2 As QuoteOfTheDayHandler = AddressOf quotes.NextQuote
The difference between this example and the previous one involving a shared method is that the delegate object must now keep track of the target object. Figure 2 reveals some of the private implementation details of a delegate object. As you can see, every delegate object contains a private field that holds a function pointer to a method implementation. Delegate objects that are bound to an instance method also track a reference to a target object that will be used when the target instance method is executed.
Figure 2 Implementing Delegate Objects
When you think of how things work at a lower level, a delegate object is really nothing more than a friendly, type-safe wrapper around a function pointer. However, when you think about delegates at a higher level, it's important to see that they open up many possibilities when it comes to designing an application that requires callbacks. Delegates provide a flexible and efficient way to implement a loosely coupled design in which a notification source must send notifications to a set of handler methods.
Conclusion
This month's column has provided you with an introductory look at delegates. As you have seen, delegates were introduced in the .NET Framework to assist designers and programmers with implementing callback notifications. Delegates provide a hybrid technique for implementing callbacks that combine the type safety of using interfaces together with the efficiency and flexibility of using function pointers.
Now you have seen what is required to define a delegate type and how to create a delegate object and initialize it to bind to either a shared method or an instance method. You've also seen how to fire the handler method by calling the Invoke method supplied by the delegate.
In a future column I'll continue this discussion of delegates and will show you how to create a loosely coupled design using a custom delegate type in which a class has been designed to send out callback notifications. I'll also explain how delegates seamlessly support binding a notification source to multiple handler methods through a feature known as multicasting.
Implementing Callbacks with Delegates
Imagine you are designing an application with a class named BankAccount that contains a method named Withdraw. Let's say you want to make it possible for another part of the application to react whenever a BankAccount object experiences a withdrawal of an amount greater than $5000. The starting point for your class might look something like this:
Class BankAccount
Sub Withdraw(ByVal Amount As Decimal)
If (Amount > 5000) Then
'*** send notification to interested parties
End If
'*** perform withdrawal
End Sub
End Class
In this example, a BankAccount object is going to act as a notification source. That means a BankAccount object must provide a way for a listener to express its interest in receiving notifications. In other words, a BankAccount object must allow a listener to register a handler method for a callback. Let's start by defining a new delegate type named LargeWithdrawHandler:
Delegate Sub LargeWithdrawHandler(ByVal Amount As Decimal)
Now it's time to modify the BankAccount class to act as a notification source. I can do this by adding two members. First, I'll add a private field named "handler" defined in terms of the delegate type LargeWithdrawHandler. Second, I will add a method named RegisterHandler that allows other code to register a delegate object to receive callback notifications:
Class BankAccount
Private handler As LargeWithdrawHandler
Sub RegisterHandler(ByVal handler As LargeWithdrawHandler)
Me.handler = handler
End Sub
'*** other class members omitted
End Class
As you can see, the RegisterHandler method accepts a single parameter of type LargeWithdrawHandler. The implementation of RegisterHandler assigns this value to the field named handler so that a BankAccount object can track a delegate object and execute its handler method.
Once the RegisterHandler method has been called with a delegate object, any method within the BankAccount class can execute the handler method by calling Invoke on the registered delegate object. Here's how you execute the Invoke method from within the Withdraw method of the BankAccount class:
Sub Withdraw(ByVal Amount As Decimal)
'*** send notifications if required
If (Amount > 5000) AndAlso (Not handler Is Nothing) Then
handler.Invoke(Amount)
End If
'*** perform withdrawal
End Sub
The Withdraw method conducts a check to make sure the handler field contains a valid reference rather than a value of Nothing. Remember that it will have a value of Nothing until there is a call to RegisterHandler, so you must prevent your code from attempting to execute the Invoke method on an uninitialized reference.
The BankAccount class has been written to send out a delegate-based notification whenever a withdrawal is made for an amount that exceeds $5000. Now let's create a handler method that can be wired up to respond to these notifications. Take a look at the class shown in the following code:
Class AccountHandlers
Shared Sub GetApproval(ByVal Amount As Decimal)
'*** block until manager approves withdrawal amount
End Sub
Shared Sub LogWithdrawToDB(ByVal Amount As Decimal)
'*** write withdrawal info to a database
End Sub
Shared Sub LogWithdrawToFile(ByVal Amount As Decimal)
'*** write withdrawal info to a log file
End Sub
End Class
The GetApproval, LogWithdrawToDB, and LogWithdrawToFile methods have been written with the proper calling signatures so they can serve as handler methods for the notifications sent by a BankAccount object.
Now, let's write a simple application that ties everything together. First, the application must create a BankAccount object. Next, the application must create a delegate object that is bound to a target handler method such as GetApproval. The final step in hooking everything up is to call the RegisterHandler method, passing a reference to the delegate object.
Module MyApp
Sub Main()
'*** create bank account object
Dim acc1 As New BankAccount()
'*** create delegate object and register callback method
acc1.RegisterHandler(AddressOf AccountHandlers.GetApproval)
'*** do something that triggers callback
acc1.Withdraw(5001)
End Sub
End Module
This example doesn't use the New operator when creating a delegate object. Instead, it uses the more convenient shorthand syntax. Since the RegisterHandler method expects a parameter of type LargeWithdrawHandler, you can simply use the AddressOf operator followed by the method name, as shown in the code.
At this point, I have a simple application that performs a callback notification using a delegate. When the Main method calls the Withdraw method on the BankAccount object and passes a parameter value of 5001, the implementation of the Withdraw method uses the delegate object held by the handler field to execute the GetApproval method.
Note that this application provides a good example of a loosely coupled design. The BankAccount class doesn't know or care about what type of handler method is used. It would be very easy to replace the GetApproval handler method with the handler methods LogWithdrawToDB or LogWithdrawToFile. It would also be easy to create another method in a different class and use that one as a callback method instead. From this, you should be able to conclude that a delegate-based design can provide polymorphism in the same fashion as an interface-based design.
There is still one more important design issue that needs to be addressed. The current implementation of BankAccount can only provide callbacks to a single handler method. It would be better if the BankAccount class could be modified to provide callbacks to more than one handler method at a time. Fortunately, delegates provide built-in support for dealing with multiple handler methods through a feature known as multicasting.
Multicasting
Every delegate type has built-in support for dealing with multiple handler methods. Delegate types gain this support by inheriting from the MulticastDelegate class that's defined in the System namespace. The benefit of multicasting is that you can combine several handler methods in a list so they are all bound to a single delegate object. When Invoke is called on the delegate object, the MulticastDelegate class provides the code to execute every handler method in the list.
An example should help you see exactly how this works. Let's say you'd like to bind two different handler methods to a single delegate object. You can accomplish this by calling a shared method of the System.Delegate class, Combine. If you call the Combine method and pass two delegate objects, this method will return a new delegate object that is a multicast of the other two. Here's an example of taking the handler methods for two different delegate objects and combining them into a multicast delegate object:
'*** create two individual delegates
Dim handler1, handler2 As LargeWithdrawHandler
handler1 = AddressOf AccountHandlers.GetApproval
handler2 = AddressOf AccountHandlers.LogWithdrawToDB
'*** combine delegates into multicast delegate
Dim result As [Delegate] = [Delegate].Combine(handler1, handler2)
'*** convert reference to LargeWithdrawHandler type
Dim handlers As LargeWithdrawHandler
handlers = CType(result, LargeWithdrawHandler)
'*** execute handler methods in multicast list
handlers.Invoke(5001)
Please take a look at the square brackets that have been placed around the name of the [Delegate] class. They are required because Delegate is also a keyword. The brackets are used as escape characters to tell the Visual Basic® .NET compiler that you intend to use Delegate as a class name.
The call to Combine returns a reference to a newly created multicast delegate object. Note that the Combine method has a generic return type of Delegate that must be converted to the more specific delegate type, LargeWithdrawHandler. This is only required if you want to call the Invoke method.
Let's take a moment to discuss how multicast delegates are implemented. A multicast delegate is simply a linked list of delegate objects. The private implementation of each delegate object contains a field designed to hold a reference to the previous delegate object in the list. You might think it would be more intuitive if a delegate held a reference to the next delegate in the list as opposed to the previous delegate. However, the multicast delegate design uses the notion of the previous delegate because of the sequence in which the handler methods are executed. The reason that each delegate object tracks a previous delegate will be explained in more detail later in this column.
When you have created a multicast delegate that contains multiple handler methods, the delegate at the head of the list holds a reference to the previous delegate. That delegate also holds a reference to the previous delegate. For the delegate object at the tail of the list, the field for the previous delegate will have a value of Nothing. As you will see, the position in which a delegate object is placed in the list is important.
When you call Combine, it links two or more delegate objects together and returns a reference to the delegate object at the head of the list. Note that the previous example called the overloaded implementation of Combine that accepts two delegate parameters. This implementation of Combine creates a multicast list that places the delegate object passed as the second parameter at the head. For example, in the preceding code example the delegate at the head of the list is bound to the LogWithdrawToDB method. This delegate object contains a private field that references the previous delegate object that's bound to the GetApproval method.
Note that there is another overloaded version of Combine which accepts an array of delegate objects. When you pass an array of delegate objects to the Combine method, the method places the first delegate object in the array at position 0, at the head of the list. You can call whichever overloaded version of Combine you'd like. Just make sure you pay attention to how each delegate object is being placed in the list. You have control over which handler methods get executed first.
When you call Invoke on the delegate object that's at the head of the list, the MulticastDelegate class provides the code that's needed to enumerate through the list and execute the Invoke method of each delegate object in a chain. It's important to observe that the execution of individual handler methods proceeds in a serialized and synchronous fashion. You should also take note of which handler methods are executed first.
The delegate object at the head of a multicast list does not execute its handler method until after it has called the Invoke method on the previous delegate. This is why the multicast delegate design pattern refers to it as the previous delegate object as opposed to the next delegate object. You should see that control passes from the delegate object at the head of the list to the delegate object at the tail before any handler methods are executed.
The delegate object at the tail of a multicast list always executes its handler method first. Therefore, execution always occurs from back to front. You should observe that the delegate object at the head of the multicast list always executes its handler last. In my example involving a multicasting of two delegate objects, the GetApproval method is going to execute before the LogWithdrawToDB method.
Callbacks with a Multicast Delegate
Now let's revisit the example involving the BankAccount class and add support for multicasting. I'm going to modify the class implementation so that a BankAccount object can make callbacks to a list of handler methods. See the class definition in Figure 1.
First, you should notice that the handler field has been renamed to "handlers" to signify that the field can be used to hold a reference to a multicast delegate object. However, this is nothing more than a renaming issue since the field is still based on the LargeWithdrawHandler delegate type.
The implementation of the RegisterHandler method has also been updated to support multicasting. The implementation of RegisterHandler now calls the Combine method to add the new delegate object to the existing list of delegate objects. Note that the implementation of RegisterHandler passes the new delegate object as the second parameter in its call to Combine. That means the new delegate object will become the head of the list and will, therefore, be executed last in the chain. If you'd prefer, you could easily rewrite RegisterHandler to place a new handler method at the tail of the multicast list where it would execute before any handler method that was previously registered:
Sub RegisterHandler(ByVal handler As LargeWithdrawHandler)
'*** add new handler to tail of multicast list
Dim NewList As [Delegate] = [Delegate].Combine(handler, handlers)
handlers = CType(NewList, LargeWithdrawHandler)
End Sub
You might have also observed that the Withdraw method did not require any modifications other than updating the name of the handler field to "handlers". The call to Invoke is made in the exact same way as before. This illustrates one of the most valuable aspects of using multicast delegates. A multicast delegate doesn't have to be concerned with how many target methods are bound to a delegate object. A notification source simply calls Invoke and every handler method is automatically executed.
Now that the BankAccount class has been updated to support multicasting, the application can be rewritten to register three different handler methods to respond to large withdrawal notifications, as shown in the following code:
Sub Main()
'*** create bank account object
Dim acc1 As New BankAccount()
'*** create register handler methods
acc1.RegisterHandler(AddressOf AccountHandlers.GetApproval)
acc1.RegisterHandler(AddressOf AccountHandlers.LogWithdrawToDB)
acc1.RegisterHandler(AddressOf AccountHandlers.LogWithdrawToFile)
'*** do something that triggers callback
acc1.Withdraw(5001)
End Sub
You should see from this design that multicasting allows you to place the handler methods so they execute in a predictable sequence. Figure 2 gives a high-level view of how things are laid out. The GetApproval method will execute first because it has been placed at the tail of the list. Next, the LogWithdrawFromDB method will execute. Since the LogWithdrawToFile method was registered last, it will be placed at the head of the list and, consequently, will execute last.
Figure 2 Multicast Delegate Executing Handlers
Figure 3 shows a complete application based on the design I just walked though. Before moving on, you should understand what's been created here with delegates: a loosely coupled design for implementing callbacks with support for multicasting. It would be easy to further customize this application by creating even more handler methods and using delegates to register them.
Calling the GetInvocationList Method
In many cases, a notification source can simply call Invoke to execute all the target handler methods associated with a multicast delegate object. However, there are other times when you need more control. For example, you might need to determine how many target handler methods have been added to a multicast delegate list. You also might be required to write code that can gracefully deal with the exceptions thrown by handler methods in the list.
The Delegate class provides a public instance method named GetInvocationList. When you call this method on a multicast delegate, it returns an array of references to individual delegate objects. This array makes it possible to determine how many handler methods are currently bound to a multicast delegate:
'*** determine number of target handler methods
Dim HandlerCount As Integer = handlers.GetInvocationList().Length
A call to GetInvocationList also makes it relatively simple to enumerate through the individual delegate objects and explicitly execute their handler methods one at a time. Since a call to GetInvocationList returns an array of references to delegate objects, it makes it easy to structure your code to enumerate through the delegate objects using a For Each loop:
Sub Withdraw(ByVal Amount As Decimal)
'*** send notifications if required
If Not (Amount > 5000) AndAlso (Not handlers Is Nothing) Then
Dim handler As LargeWithdrawHandler
For Each handler In handlers.GetInvocationList()
handler.Invoke(Amount)
Next
End If
'*** perform withdrawal
End Sub
The code you've just seen doesn't really provide any more control than executing the Invoke method on the handlers field. So you might be asking why you would ever need to call GetInvocationList to enumerate through the delegate objects in the list. One reason is that you might need more control if one of the handler methods throws an exception during its execution.
Let's look at an example. Imagine you are holding onto a multicast delegate object that is bound to 10 handler methods. What happens if you call Invoke, and the seventh handler method throws an exception? The first six handler methods have already executed successfully. The exception thrown by the seventh handler causes the Invoke method to terminate unexpectedly. Therefore, the eighth, ninth, and tenth handler methods never execute at all.
The problem is that there's really no way to tell which handler methods executed successfully, which handler method failed, and which handler methods were never executed. You'll have more control if you restructure your code to explicitly call the Invoke method on each delegate object within a Try block:
Dim handler As LargeWithdrawHandler
For Each handler In handlers.GetInvocationList()
Try
handler.Invoke(Amount)
Catch ex As Exception
'*** deal with exception and continue
End Try
Next
A second reason you might need to call GetInvocationList and enumerate through each delegate object individually is that it makes it possible to retrieve return values and output parameters from more than one handler method. If you call Invoke on a multicast delegate object and it involves an output parameter or a return value, the results you get are somewhat arbitrary. The output parameter and the return value you receive are the ones supplied by the last handler method to execute. Remember, that's the delegate object at the head of the list. However, you can capture a separate output parameter and return value for each handler method if you write the extra code to enumerate through the list and explicitly call Invoke on each delegate object.
Note that GetInvocationList returns an array that represents a snapshot in time. In other words, GetInvocationList returns the list delegate objects that were present at the time the method was called. If you add a new delegate object to a multicast delegate, an array generated with an earlier call to GetInvocationList will not be in sync. You must call GetInvocationList again to create a new array that represents the updated list of delegate objects.
Wrap-up
This concludes my two-part discussion of programming with delegates. As you have seen, a delegate is a programmable binding mechanism for implementing a callback between a notification source and one or more handler methods. Delegates provide an attractive means for implementing callbacks because they combine the type safety and polymorphic capabilities of interfaces with the efficiency and flexibility of function pointers.
Understanding the fundamentals of programming with delegates is a prerequisite to becoming an advanced user of Visual Basic .NET. I say this for two reasons. First, event handling in the .NET Framework is entirely based on delegates. If you really want to make the most of an event-driven application framework such as Windows® Forms or ASP.NET, you'd better be prepared to drop down to a lower level and program in terms of delegates when it's required. Delegates also provide the primary means for executing a method on a secondary thread in an asynchronous fashion.
This month's Basic Instincts column builds upon my last two columns in which I talked about concepts and programming techniques associated with delegates. I will assume you have read the last two installments of this column and that you understand the role that delegates play within the Microsoft® .NET Framework. If you haven't read the last two columns, see Implementing Callback Notifications Using Delegates and Implementing Callbacks with a Multicast Delegate. You should also know how to design and write a simple application that uses multicast delegates to send callback notifications to a set of handler methods.
While you have probably been programming with events for years, migrating to the .NET Framework requires you to reexamine their inner workings because events in the .NET Framework are layered on top of delegates. The more you know about delegates, the more power you can harness when you program events. Your understanding of how events work at a lower level is critical when you start to work with one of the event-driven frameworks of the common language runtime (CLR) such as Windows® Forms or ASP.NET. My goal this month is to give you an understanding of how events work at a lower level.
What Exactly is an Event?
An event is just a formalized software pattern in which a notification source makes callbacks to one or more handler methods. Events are therefore similar to interfaces and delegates because they provide a means to design applications that use callback methods. However, events add a valuable degree of productivity because they are easier to use than interfaces or delegates. Events allow the compiler and the Visual Studio® .NET IDE to do much of the work for you behind the scenes.
A design that involves events is based on an event source and one or more event handlers. An event source can be either a class or an object. An event handler is a delegate object that's bound to a handler method. Figure 1 shows a high-level view of an event source wired up to its handler methods.
Figure 1 Event Source and Handlers
Every event is defined in terms of a particular delegate type. For each event defined by an event source, there is a private field that is based on the event's underlying delegate type. This field is used to track a multicast delegate object. An event source also provides a public registration method that allows you to register as many event handlers as you'd like.
When you create an event handler (a delegate object) and register it with an event source, the event source simply appends the new event handler to the end of the list. An event source can then use the private field to call Invoke on the multicast delegate which, in turn, will execute all the registered event handlers.
What's really nice about events is that much of the work to set them up is already done for you. As you will soon see, the Visual Basic® .NET compiler assists you by automatically adding a private delegate field and a public registration method whenever you define an event. You will also see that Visual Studio .NET provides even more assistance with a code generator that can automatically emit the skeleton definitions for your handler methods.
Programming with Events
Because events in .NET are built on top of delegates, their underlying plumbing details are very different from the way things used to work in previous versions of Visual Basic. However, the language designers of Visual Basic .NET did a good job in keeping the syntax for programming events consistent with earlier versions of Visual Basic. In many cases, programming events involves the same old familiar syntax you're used to using. For example, you will use keywords such as Event, RaiseEvent, and WithEvents, and they will behave almost identically to the way they have behaved in previous versions of Visual Basic.
Let's start by creating a simple callback design based on an event. First, I need to define an event within a class definition by using the Event keyword. Every event must be defined in terms of a specific delegate type. Here's an example of defining both a custom delegate type and a class that uses it to define an event:
Delegate Sub LargeWithdrawHandler(ByVal Amount As Decimal)
Class BankAccount
Public Event LargeWithdraw As LargeWithdrawHandler
'*** other members omitted
End Class
In this example, the LargeWithdraw event has been defined as an instance member. In this design, a BankAccount object will act as the event source. If you want a class instead of an object to act as an event source, you should define events as shared members using the Shared keyword.
When you program with events, it's important to acknowledge that the compiler is doing a good deal of extra work for you behind the scenes. For example, what do you think the compiler does when you compile the definition of the BankAccount class that I just showed you into an assembly? Figure 2 shows what the resulting class definition would look like when inspected with ILDasm.exe, the intermediate language disassembler. This view provides a revealing look at how much the Visual Basic .NET compiler is doing behind the scenes to assist you.
Figure 2 Class Definition in ILDasm
When you define an event, the compiler generates four members inside the class definition. The first member is a private field based on the delegate type. This field is used to track a reference to a delegate object. The compiler generates the name for this private field by taking the name of the event itself and adding the suffix "Event". This means that creating an event named LargeWithdraw results in the creation of a private field named LargeWithdrawEvent.
The compiler also generates two methods to assist with the registration and unregistration of delegate objects that are to serve as event handlers. These two methods are named using a standard naming convention. The method for registering an event handler is named after the event along with a prefix of "add_". The method for unregistering an event handler is named after the event along with a prefix of "remove_". Therefore, the two methods created for the LargeWithdraw event are named add_LargeWithdraw and remove_LargeWithdraw.
The Visual Basic .NET compiler generates an implementation for add_LargeWithdraw that accepts a delegate object as a parameter and adds it to the list of handlers by calling the Combine method of the Delegate class. The compiler generates an implementation for remove_LargeWithdraw that removes a handler method from the list by calling the Remove method in the Delegate class.
The fourth and final member that is added to the class definition is one that represents the event itself. You should be able to locate the event member named LargeWithdraw in Figure 2. It is the member with an upside-down triangle next to it. However, you should note that this event member isn't really a physical member like the other three. Instead, it's a metadata-only member.
This metadata-only event member is valuable because it can inform compilers and other development tools that the class supports the standard pattern for event registration in the .NET Framework. The event member also contains the names of the registration method and the unregistration method. This allows compilers for managed languages such as Visual Basic .NET and C# to discover the name of the registration method at compile time.
Visual Studio .NET is another good example of a development tool that looks for this metadata-only event member. When Visual Studio .NET sees that a class definition contains events, it automatically generates the skeleton definitions for handler methods as well as the code to register them as event handlers.
Before I move on to a discussion of raising events, I'd like to cover a restriction involved with creating a delegate type that's to be used for defining events. A delegate type used to define an event cannot have a return value. You must define the delegate type using the Sub keyword instead of the Function keyword, as shown here:
'*** can be used for events
Delegate Sub BaggageHandler()
Delegate Sub MailHandler(ItemID As Integer)
'*** cannot be used for events
Delegate Function QuoteOfTheDayHandler(Funny As Boolean) As String
There's a good reason for this restriction. It's far more difficult to work with return values in a case involving a multicast delegate that's bound to several handler methods. A call to Invoke on a multicast delegate returns the same value as the last handler method in the invocation list. However, capturing the return value of handler methods that appear earlier in the list isn't so straightforward. Eliminating the need to capture multiple return values simply makes events easier to use.
Raising an Event
Now let's modify the BankAccount class so that it's able to raise an event when a withdrawal is made for an amount that exceeds a $5000 threshold. The easiest way to fire the LargeWithdraw event is to use the RaiseEvent keyword within the implementation of a method, property, or constructor. This syntax is probably familiar because it's similar to what you have used in earlier versions of Visual Basic. Here's an example of firing the LargeWithdraw event from the Withdraw method:
Class BankAccount
Public Event LargeWithdraw As LargeWithdrawHandler
Sub Withdraw(ByVal Amount As Decimal)
'*** send notifications if required
If (Amount > 5000) Then
RaiseEvent LargeWithdraw(Amount)
End If
'*** perform withdrawal
End Sub
End Class
While the syntax remains the same from previous versions of Visual Basic, what happens when you raise an event is very different now. When you use the RaiseEvent keyword to fire an event, the Visual Basic .NET compiler generates the code required to execute each and every event handler. For example, what do you think happens when you compile the following code?
RaiseEvent LargeWithdraw(Amount)
The Visual Basic .NET compiler expands this expression to code that calls Invoke on the private field that holds the multicast delegate object. In other words, using the RaiseEvent keyword has the very same effect as writing the code in the following snippet:
If (Not LargeWithdrawEvent Is Nothing) Then
LargeWithdrawEvent.Invoke(Amount)
End If
Note that the code generated by the Visual Basic .NET compiler conducts a check to make sure that the LargeWithdrawEvent field contains a valid reference to an object. That's because the LargeWithdrawEvent field will have a value of Nothing until the first handler method is registered. Therefore, the generated code doesn't attempt to call Invoke unless at least one handler method is currently registered.
You should be able to make an observation about raising an event. It usually doesn't matter whether you use the RaiseEvent keyword or you program directly against the private LargeWithdrawEvent field that's automatically generated by the compiler. Both approaches produce equivalent code:
'*** this code
RaiseEvent LargeWithdraw(Amount)
'*** is the same as this code
If (Not LargeWithdrawEvent Is Nothing) Then
LargeWithdrawEvent.Invoke(Amount)
End If
In many cases, it's likely that you will prefer the syntax of the RaiseEvent keyword because it requires less typing and it results in code that is more concise. However, in certain situations where you need more control, it might make sense to explicitly program against the private LargeWithdrawEvent field. Let's look at an example of when this would be the case.
Imagine a scenario in which a BankAccount object has three event handlers that have been registered to receive notifications for the LargeWithdraw event. What would happen if you triggered the event using the RaiseEvent keyword and the second event handler in the invocation list threw an exception? The line of code containing the RaiseEvent statement would receive a runtime exception, but you would have no way to determine which event handler threw it. Furthermore, there would be no way to handle the exception thrown by the second event handler and to continue where the third event handler is executed as expected.
However, if you are willing to program in terms of the private LargeWithdrawEvent field, you can deal with an exception thrown by an event handler in a more graceful manner. Examine the code in Figure 3. As you can see, dropping down to a lower level and programming against the private delegate field provides an extra degree of control. You can gracefully handle an exception and then go on to execute event handlers that appear later in the list. This technique has obvious benefits over the RaiseEvent syntax in which an exception thrown by an event handler prevents the execution of any event handlers that appear later in the invocation list.
Creating and Registering an Event Handler
Now that you've seen how to define and raise an event, it's time to discuss how to create an event handler and register it with a given source. There are two different ways to accomplish this in Visual Basic .NET. The first technique is known as dynamic event binding and involves the use of the AddHandler keyword. The second technique is called static event binding and involves the use of the familiar Visual Basic keyword WithEvents. I plan to cover static event binding in a future column. So for now, let's examine how dynamic event binding works.
Remember that an event handler is a delegate object. Therefore, you create one by instantiating a delegate object from the delegate type on which the event is based. When you create this delegate object, you must bind it to a target handler method that you want to serve as an event handler.
Once you have created an event handler, you must register it with a specific event by calling the special registration method on the event source. Recall that the registration method for the LargeWithdraw event is named add_LargeWithdraw. When you call the add_LargeWithdraw method and pass a delegate object as a parameter, the event source adds the delegate object to the list of event handlers that are to receive event notifications.
What's confusing about event registration is that you never directly call a registration method such as add_LargeWithdraw. In fact, the Visual Basic .NET compiler will raise a compile-time error if you try to access an event registration method by name. Instead, you use an alternate syntax involving the AddHandler statement. When you use the AddHandler statement, the Visual Basic .NET compiler generates the code to call the event registration method for you.
Let's look at an example of wiring up a few event handlers using dynamic event registration. Imagine you have written the following set of shared methods in the AccountHandlers class:
Class AccountHandlers
Shared Sub LogWithdraw(ByVal Amount As Decimal)
'*** write withdrawal info to log file
End Sub
Shared Sub GetApproval(ByVal Amount As Decimal)
'*** block until manager approval
End Sub
End Class
What should you do if you'd like to employ these methods as event handlers for the LargeWithdraw event of the BankAccount class? Let's start by creating an event handler that's bound to the handler LogWithdraw. First, you must create the delegate object that's going to serve as an event handler:
Dim handler1 As LargeWithdrawHandler
handler1 = AddressOf AccountHandlers.LogWithdraw
Next, you must register this new delegate object with an event source using an AddHandler statement. When you register an event handler using the AddHandler statement, you are required to pass two parameters, like this:
AddHandler
The first parameter required by AddHandler is an expression that evaluates to an event of a class or object. The second parameter is a reference to the delegate object that is going to be wired up as an event handler. Here's an example of using an AddHandler statement to register an event handler with the LargeWithdraw event of a BankAccount object:
'*** create bank account object
Dim account1 As New BankAccount()
'*** create and register event handler
Dim handler1 As LargeWithdrawHandler
handler1 = AddressOf AccountHandlers.LogWithdraw
AddHandler account1.LargeWithdraw, handler1
When you use the AddHandler keyword to register an event handler for the LargeWithdraw event, the Visual Basic .NET compiler expands this code to call the registration method add_LargeWithdraw. Once the code containing the AddHandler statement has been executed, your event handler is in place and ready for notifications. Therefore, the LogWithdraw method will execute whenever the BankAccount object raises a LargeWithdraw event.
In the last example, I used a longer form of syntax to illustrate exactly what happens when you create and register an event handler. However, once you understand how things work, you might appreciate using a more concise syntax to accomplish the same goal, as shown here:
'*** create bank account object
Dim account1 As New BankAccount()
'*** register event handlers
AddHandler account1.LargeWithdraw, AddressOf AccountHandlers.LogWithdraw
AddHandler account1.LargeWithdraw, AddressOf AccountHandlers.GetApproval
Since the AddHandler statement expects a reference to a delegate object as the second parameter, you can use the shorthand syntax of the AddressOf operator followed by the name of the target handler method. When it sees this, the Visual Basic .NET compiler then generates the extra code to create the delegate object that is going to serve as the event handler.
The AddHandler statement of the Visual Basic .NET language is complemented by the RemoveHandler statement. RemoveHandler requires the same two parameters as AddHandler, yet it has the opposite effect. It removes the target handler method from the list of registered handlers by calling the remove_LargeWithdraw method supplied by the event source:
Dim account1 As New BankAccount()
'*** register event handler
AddHandler account1.LargeWithdraw, AddressOf AccountHandlers.LogWithdraw
'*** unregister event handler
RemoveHandler account1.LargeWithdraw, AddressOf AccountHandlers.LogWithdraw
Now you have seen all the steps required to implement a callback design using events. The code in Figure 4 shows a complete application in which two event handlers have been registered to receive callback notifications from the LargeWithdraw event of a BankAccount object.
Conclusion
While the motivation for using events and some of the syntax remains unchanged from previous versions of Visual Basic, you have to admit that things are rather different now. As you can see, you have far more control over how you respond to events than you've ever had before. This is especially true if you're willing to drop down and program in terms of delegates.
In the next installment of the Basic Instincts column I plan to continue this discussion of events. I'll show you how Visual Basic .NET supports static event binding through the familiar syntax of the WithEvents keyword, and I'll discuss the Handles clause. In order to really master events, you must be comfortable with both dynamic event registration and static event registration.
No comments:
Post a Comment