Basic Instincts: Implementing Callbacks with a ...

We were unable to locate this content in de-de.

Here is the same content in en-us.

Basic Instincts
Implementing Callbacks with a Multicast Delegate
Ted Pattison

This month's column is a follow-up to the December 2002 installment in which I introduced the basic concepts and programming techniques associated with delegates. I am going to assume you have already read that column and that you are familiar with the fundamentals of programming delegates. For example, you should know how to define a delegate type, how to create a delegate object that's bound to a handler method, and how to execute the handler method by calling the delegate object's Invoke method.
This month I'll create a delegate-based design that involves callback notifications. I will show how delegates make it possible to create such a design in a loosely coupled fashion. After that, I will explain how delegates 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.
Class BankAccount  
  Private handlers As LargeWithdrawHandler
  Sub RegisterHandler(ByVal handler As LargeWithdrawHandler)
   '*** add new handler to head of multicast list
    Dim NewList As [Delegate] = [Delegate].Combine(handlers, handler)
    handlers = CType(NewList, LargeWithdrawHandler)
  End Sub
  Sub Withdraw(ByVal Amount As Decimal)
    '*** send notifications if required
    If (Amount > 5000) AndAlso (Not handlers Is Nothing) Then
      handlers.Invoke(Amount)
    End If
    '*** perform withdrawal
  End Sub
End Class
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.
Imports System

Delegate Sub LargeWithdrawHandler(ByVal Amount As Decimal)

Class BankAccount
  Private handlers As LargeWithdrawHandler
  Sub RegisterHandler(ByVal handler As LargeWithdrawHandler)
    Dim NewList As [Delegate] = [Delegate].Combine(handlers, handler)
    handlers = CType(NewList, LargeWithdrawHandler)
  End Sub
  Sub Withdraw(ByVal Amount As Decimal)
    '*** send notifications if required
    If (Amount > 5000) AndAlso (Not handlers Is Nothing) Then
      handlers.Invoke(Amount)
    End If
    '*** perform withdrawal
  End Sub
End Class

Class AccountHandlers
  Shared Sub GetApproval(ByVal Amount As Decimal)
    '*** block until manager approval
  End Sub
  Shared Sub LogWithdrawToDB(ByVal Amount As Decimal)
    '*** write withdrawal info to database
  End Sub
  Shared Sub LogWithdrawToFile(ByVal Amount As Decimal)
    '*** write withdrawal info to log file
  End Sub
End Class

Module MyApp
  Sub Main()
    '*** create bank account object
    Dim acc1 As New BankAccount()
    '*** register callback 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
End Module

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.

Send your questions and comments for Ted to instinct@microsoft.com.


Ted Pattisonis an instructor and researcher at DevelopMentor (http://www.develop.com), where he co-manages the Visual Basic curriculum. He is the author of Programming Distributed Applications with COM and Microsoft Visual Basic 6.0 (Microsoft Press, 2000).

Page view tracker