3.8 Delegates and Events

only for RuBoard

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


Object-Oriented Programming with Visual Basic. Net
Object-Oriented Programming with Visual Basic .NET
ISBN: 0596001460
EAN: 2147483647
Year: 2001
Pages: 112
Authors: J.P. Hamilton

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net