Implementing Callback Notifications Using Delegates
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. In this month's column I am going to show you 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.
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.
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: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.
Delegate Sub BaggageHandler() Delegate Sub MailHandler(ItemID As Integer) Delegate Function QuoteOfTheDayHandler(Funny As Boolean) As String
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: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.
Dim handler1 As QuoteOfTheDayHandler handler1 = New QuoteOfTheDayHandler(AddressOf JennysHandlers.GetQuote)
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: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: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.
'*** create and bind delegate object Dim handler1 As QuoteOfTheDayHandler handler1 = AddressOf _ JennysHandlers.GetQuote '*** execute target method Dim quote As String = _ handler1.Invoke(True)
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: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.
'*** this code Dim quote1 As String = handler1.Invoke(True) '*** is the same as this code Dim quote2 As String = handler1(True)
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: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:
Class JerrysQuotes Function NextQuote(ByVal Funny As Boolean) As String '*** implementation End Function End Class
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.
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.
Send questions and comments for Ted to firstname.lastname@example.org.
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).