Delegates and Events You have seen several ways that classes can establish a rapport with one another. A derived class can converse with its parent through Protected methods and a nonderived class can use the Public or Friend (if in the same assembly) methods. You might have noticed that this level of communication implies a priori knowledge of the classes involved; all classes and their methods are known at compile time. Therefore the lines of communication are drawn, hardcoded into the mix. Consider the following piece of code: Public Class Log Public Sub Write(ByVal msg As String) #If Debug Then Console.WriteLine(msg) #End If End Sub End Class You can imagine that this simple class provides basic debugging capabilities for some application. As you can see, it is wired to write a message to the console. Not very accommodating, is it? What if you wanted to write the message to a text file or database or send it to a remote debugging console? Or worse yet, what if you moved the class to a Windows executable? There wouldn't be a console window anymore. You could go into the class and add new methods for each scenario, but that wouldn't help much if calls to Log.Write were already scattered throughout your object model. You could always rewrite Log.Write every time you wanted to send the message to a different location, but that's not too practical either. OOP involves code reuse, not code rewrite. That wouldn't exactly lead to a class that you could share with your friends and neighbors. It would be nice to tell the Log class which method to call in order to do the logging. Then you could switch methods at runtime—perhaps even incorporating several different logging methods: logging to a local text file or sending debug messages to a remote debug window. For years, C/C++ programmers used callbacks to achieve this capability. In a callback situation, one function receives a pointer to another function through its argument list. The function can then use this pointer to send a notification to the callback. If you could use the pointer in this way with the Log class, you could tell it which function you wanted it to use to handle the writing. The C/C++ callback method has some drawbacks. First, it is not typesafe. There is no way to ensure that the pointer actually refers to a function with the appropriate signature—that it has the same parameters and return type. Second, there is no way to extend the callback to use more than one function without rewriting the method to accommodate the change. What if you wanted to log to the console and the database? 3.8.1 Delegates VB.NET has the function pointer you need, but without the drawbacks just discussed. The pointer is called a delegate. As shown in Example 3-8, it is declared much like a Sub or a Function, except it doesn't have a body. It just defines a function signature. You can think of a delegate as a typesafe function pointer, but it is really much more. A delegate can be associated with one method, or it can refer to several methods, as long as the methods have the same signature and return value. It can also refer to an instance method or a class method. Try that in C++! Example 3-8. Using delegates, Part I Imports System Imports System.IO Public Class Log Public Delegate Sub Writer(ByVal msg As String) Private myWriter As Writer Public Sub New(ByVal writeMethod As Writer) myWriter = writeMethod End Sub Public Sub Write(ByVal msg As String) myWriter(msg) End Sub End Class Notice that once you define a delegate, you can declare a variable that holds that type. This is the case with myWriter, which is declared immediately after the delegate declaration. The constructor for the class is then used to specify which method Log should use to perform its duties. The method is saved in myWriter to be used later during the call to Write. When you declare a delegate, you actually derive a class from System.MulticastDelegate, which itself is derived from the Delegate class. This class contains three methods that are used to handle the callbacks internally: Invoke, BeginInvoke, and EndInvoke. Invoke is used for synchronous callbacks such as the example in this chapter; BeginInvoke and EndInvoke are used for asynchronous callbacks that are typically found in a remoting scenario. Supplying the Log class with a Write method is simple, as Example 3-9 illustrates. You can provide your own version of Write, which outputs to a text file by using the AddressOf operator. This operator returns a delegate that can be passed to the Log class. Example 3-9. Using delegates, Part II Imports System Imports System.IO Public Class Log Public Delegate Sub Writer(ByVal msg As String) Private myWriter As Writer Public Sub New(ByVal writeMethod As Writer) myWriter = writeMethod End Sub Public Sub Write(ByVal msg As String) myWriter(msg) End Sub End Class Friend Class Test Public Shared Sub Write(ByVal msg As String) Dim file As New FileStream("debug.log", _ FileMode.OpenOrCreate, _ FileAccess.Write) Dim stream As New StreamWriter(file) 'Write date/time and message stream.Write("{0} - {1}", DateTime.Now, msg) 'Close the stream (and the file, too) stream.Close( ) End Sub Public Shared Sub Main( ) Dim debugLog As New Log(AddressOf Write) debugLog.Write("Welcome to Visual Basic!") End Sub End Class Try using AddressOf with a method that has a mismatched signature. You won't get far. The compiler checks the signatures, sees that they don't match, and gives you an error. A delegate can refer to more than one method. You can do this with the Delegate.Combine method, which lets you chain any number of methods together using the same delegate. Let's put a new method in Log to house this functionality: Public Class Log Public Sub Add(ByVal writeMethod As Writer) myWriter = myWriter.Combine(myWriter, writeMethod) End Sub If you want to add another method to the delegate, you can do so. Verify this behavior by adding another method to the Test class from Example 3-9: