3.8 Delegates and Events

only for RuBoard

3.8 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 runtimeperhaps 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 signaturethat 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:

 Friend Class Test
   
    Public Shared Sub SimpleWrite(ByVal msg As String)
        Console.WriteLine(msg)
    End Sub 

Now you only need to modify Main to add SimpleWrite to the delegate chain:

 Public Shared Sub Main( )
        Dim debugLog As New Log(AddressOf Write)  debugLog.Add(AddressOf SimpleWrite)  debugLog.Write("Welcome to Visual Basic!")
    End Sub 

When Write is called, the message is displayed to the console window and written to debug.log .

As the laws of symmetry demand, you should also provide the functionality to remove a method from a delegate. This can be accomplished by wrapping a call to Delegate.Remove in the Log class:

 Public Class Log
   
    Public Sub Remove(ByVal writeMethod As Writer) 
        myWriter.Remove(myWriter, writeMethod)
    End Sub 

Delegates are much more powerful than traditional function pointers. They can permit sophisticated interactions between the objects in the libraries or frameworks that you write.

The end of this chapter lists the source code for a simple remote debugging console. A class called RemoteDebug is also listed to send messages to the remote console. These listings are provided as an exercise for you. See if you can add the Write method of RemoteDebug to the delegate example you worked with in this section.

3.8.2 Events

Events allow an object to broadcast a message without knowing who will receive the notification. For instance, an AlarmClock object might fire an Alarm event at a designated time, allowing anyone using the object to receive the event and execute code based on the event. AlarmClock just sends a message. It has no idea who will get it. The recipient could be one or 100 objects. It all depends on how many objects subscribed to the event.

Example 3-10 shows a class that could simulate your odds of winning the Texas Lottery. The constructor of the class takes an array of 6 numbers , each between 1 and 54. When the Play method is called, the class randomly generates 6 numbers. If all 6 numbers match, you win! If not, it tries again for 10,000 more attempts (each play costs a dollar). If 3 or more numbers match, a Match event is raised. The example is big, despite the fact that it contains no error handling whatsoever. It is worth stepping through, as you can glean quite a few language features from the code.

Example 3-10. Texas Lottery simulation class
 Option Strict On
   
Imports System
Imports System.Text
   
Public Class TexasLottery
   
  Public Event Match(ByVal msg As String)
   
  Private numbers( ) As Byte
  Private attempts As Integer
   
  Public Sub New(ByVal numbers( ) As Byte)
    Array.Sort(numbers)
    Me.numbers = numbers
  End Sub
   
  Public Sub Play( )
    'Get random number class
    Dim rnd As New Random( )
    'Declare 3 Integers
    Dim i, randomNumber, matches As Integer
   
    Do While attempts < 10000
      matches = 0
      For i = 1 To 6
        'Get random number 1-54 
        randomNumber = rnd.Next(1, 54)
        'Search array for number
        If Array.BinarySearch(numbers, randomNumber) > 0 Then
          matches += 1
        End If
      Next i
   
      If (matches > 2) Then
   
        RaiseEvent Match( _
           String.Format("{0} matches on attempt {1}", _
               matches.ToString( ), attempts.ToString( )))
   
        If (matches = 6) Then
          Dim msg As String
          msg.Format("You won the lottery on attempt {0}", _
                     attempts.ToString( ))
          RaiseEvent Match(msg)
          Exit Do
        End If
   
      End If
   
      attempts += 1 'Increment attempts
   
    Loop
   
  End Sub
   
End Class 

Now that you have a class that generates events, you need a class that can receive them. Several options are available to you at this point. You can subscribe to events at design time or wait until runtime. Example 3-11 demonstrates the former option by using the WithEvents and Handles keywords to connect to an event source.

Example 3-11. WithEvents and Handles
 Imports System
   
Friend Class Test
   
  Private Class LottoTest
   
    Private WithEvents lottery As TexasLottery
   
    Public Sub New( )
      Dim myNumbers( ) As Byte = {6, 11, 23, 31, 32, 44}
      lottery = New TexasLottery(myNumbers)
    End Sub
   
    Public Sub Play( )
      lottery.Play( )
    End Sub
   
    Private Sub OnMatch(ByVal msg As String) Handles lottery.Match
      Console.WriteLine(msg)
    End Sub
   
  End Class
   
  Public Shared Sub Main( )
    Dim test As New LottoTest( )
    test.Play( )
  End Sub
   
End Class 

If you find it necessary, you could use the same event handler to handle multiple events:

 Private WithEvents lottery1 As TexasLottery
Private WithEvents lottery2 As TexasLottery
   
Private Sub OnMatch(msg As String) _  Handles lottery1.Match, lottery2.Match  Console.WriteLine(msg)
End Sub 

3.8.3 Dynamic Event Handling

Example 3-12 shows how to dynamically bind to an event source at runtime rather than at compile time. You do not have to use WithEvents when declaring the TexasLottery object.

Example 3-12. Dynamic event handling
 Imports System
   
Friend Class Test
   
  Public Class LottoTest2
   
    Private lottery As TexasLottery
   
    Public Sub New( )
      Dim myNumbers( ) As Byte = {6, 11, 23, 31, 32, 44}
      lottery = New TexasLottery(myNumbers)
      AddHandler lottery.Match, AddressOf Me.OnMatch
    End Sub
   
    Public Sub Play( )
      lottery.Play( )
      RemoveHandler lottery.Match, AddressOf Me.OnMatch
    End Sub
   
    Private Sub OnMatch(ByVal msg As String)
      Console.WriteLine(msg)
    End Sub
  End Class
   
  Public Shared Sub Main( )
    Dim test As New LottoTest2( )
    test.Play( )
  End Sub
   
End Class 

AddHandler connects to the event source, while RemoveHandler handler disconnects from it. The arguments for both are the same. The first is the event you wish to address, and the second is a delegate pointing to the event handler. You can connect to more than one event by using AddHandler . This process is similar to what you did with the log class using Delegate.Combine and Delegate.Remove .

3.8.4 Delegates Versus Events

At this point, you might wonder what the differences between delegates and events are. After all, Example 3-10 could be rewritten to use delegates in a manner similar to Example 3-8. There really isn't a difference between the two. Internally, events are built with delegates. The various event keywords merely instruct the compiler to inject additional code into a class definition where the events are wired up behind the scenes.

Example 3-13 shows two simple classes that contain minimal event code. Class A exposes a public event called Notify that is raised in the Raise method. Class B contains a private instance of class A and a method that handles the raised event.

Example 3-13. Examining events
 Imports System
   
Public Class A
   
  Public Event Notify(ByVal msg As String)
   
  Public Sub Raise( )
    RaiseEvent Notify("Notifying")
  End Sub
   
End Class
   
Public Class B
   
  Public WithEvents myClassA As A
   
  Public Sub New( )
    myClassA = New A( )
    myClassA.Raise( )
  End Sub
   
  Sub NotifyMessage(ByVal msg As String) Handles myClassA.Notify
    Console.WriteLine("Notified")
  End Sub
   
End Class 

When the compiler sees an Event declared, several things happen. First, the event declaration is replaced with a multicast delegate and a reference to that delegate is added. The names are then generated from the event's original name :

 Public Class A
   
    Public Delegate Sub NotifyEventHandler(msg As String)
    Private NotifyEvent As NotifyEventHandler 

Next, two methods are added to the class, which simply wrap calls to Delegate.Combine and Delegate.Remove . They are used by whatever class decides to consume the event.

  Public Sub add_Notify(obj As NotifyEventHandler)  NotifyEvent = NotifyEvent.Combine(NotifyEvent,obj)
    End Sub  Public Sub remove_Notify(obj As NotifyEventHandler)  NotifyEvent = NotifyEvent.Remove(NotifyEvent,obj)
    End Sub 

Finally, any instance of RaiseEvent is replaced with a small block of code that makes the call through the delegate.

 Public Sub Raise( )
        If Not NotifyEvent Is Nothing Then
            NotifyEvent.Invoke("Notifying")
        End If
    End Sub 

Example 3-14 contains the entire listing of class A . If you compile it, the IL produced will be identical to the IL produced from Example 3-13.

Example 3-14. Setting up an event from scratch
 Public Class A
   
  Public Delegate Sub NotifyEventHandler(ByVal msg As String)
  Private NotifyEvent As NotifyEventHandler
   
  Public Sub Raise( )
    If Not NotifyEvent Is Nothing Then
      NotifyEvent.Invoke("Notifying")
    End If
  End Sub
   
  Public Sub add_Notify(ByVal obj As NotifyEventHandler)
    NotifyEvent = NotifyEvent.Combine(NotifyEvent, obj)
  End Sub
   
  Public Sub remove_Notify(ByVal obj As NotifyEventHandler)
    NotifyEvent = NotifyEvent.Remove(NotifyEvent, obj)
  End Sub
   
End Class 

Like Event , when the compiler sees the WithEvents keyword, things start to happen. The instance variable declared with the WithEvents keyword is replaced by an ordinary definition of the class. The variable name is then prefixed with an underscore :

 Public Class B
   
    Private _myClassA As A 

Next, a property is added to the class that has the same name as the variable. This property is where the event target and event source are bound. A check is made to ensure that the event has not already been consumed. Then the event handler's address is passed to the add_Notify method in the event source:

 Public Sub NotifyMessage(msg As String)
    Console.WriteLine("Notified")
End Sub
   
Private Property MyClassA( ) As A
        Get
            Return _myClassC
        End Get
        Set
            If Not _myClassA Is Nothing Then
                _myClassA.remove_Notify(AddressOf NotifyMessage)
            End If
            _myClassA = Value        
            If Not _myClassA Is Nothing Then       
                _myClassA.add_Notify(AddressOf NotifyMessage)
            End If
        End Set
End Property 

The compiler then modifies the event target's constructor. The event source's private instance is attached to the event handler through the property that was added by the compiler:

 Public Class B
   
    Private _myClassA As ClassA
   
    Public Sub New( )
        MyClassA = New C( )
        MyClassA.Raise( )
    End Sub 

The entire listing for class B is shown in Example 3-15.

Example 3-15. Consuming an event from scratch
 Imports System
   
Public Class B
   
  Private _myClassA As A
   
  Public Sub New( )
    MyClassA = New A( )
    MyClassA.Raise( )
  End Sub
   
  Public Sub NotifyMessage(ByVal msg As String)
    Console.WriteLine("Notified")
  End Sub
   
  Private Property MyClassA( ) As A
    Get
      Return _myClassA
    End Get
    Set(ByVal Value As A)
      If Not _myClassA Is Nothing Then
        _myClassA.remove_Notify(AddressOf NotifyMessage)
      End If
      _myClassA = Value
      If Not _myClassA Is Nothing Then
        _myClassA.add_Notify(AddressOf NotifyMessage)
      End If
    End Set
  End Property
   
End Class 

3.8.5 Event Arguments

Forget everything you have seen about event arguments. Although you can define an event with any number of arguments, the .NET framework uses the following convention (and you should, too):

 Public Event   EventName   (ByVal sender As Object, ByVal e As System.EventArgs) 

The first parameter is the object that raised the event, and the second contains the arguments to the event. If the event does not send data, using System.EventArgs is acceptable; if it does, you should derive a class from EventArgs and provide additional members to describe the event. In the lottery example, for instance, it would be nice to know which numbers have matched. You can create a new event class LotteryEventArgs (all derived classes should end with " EventArgs ") that provides this information:

 Public Class LotteryEventArgs : Inherits EventArgs
   
    Public ReadOnly Matches( ) As Byte
    
    Public Sub New(ByVal matches( ) As Byte)
        Me.Matches = maches
    End Sub
    
End Class 
only for RuBoard