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?
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++!
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 .
|
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.
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.
|
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.
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.
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
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.
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 .
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.
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.
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.
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
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 |