6.1. Delegate-Based EventsBefore I describe .NET event support, here are a few terms. The object publishing the event is called the source or the publisher, and any party interested in the event is called a sink or a subscriber. The event notifications are in the form of the publisher calling methods on the subscribers. Publishing an event is also called firing an event. .NET offers native support for events by providing dedicated CLR types and base-class implementations. .NET defines a standard mechanism for source and sink connection setup and tear-down, a standard and concise way of firing events, and a ready-made implementation of the sink list. .NET event support relies on delegates. Conceptually, a delegate is nothing more than a type-safe method referenceyou can think of it as a type-safe C function pointer or a function object in C++. As the name implies, a delegate allows you to delegate the act of calling a method to somebody else. The delegate can call static or instance methods. Consider, for example, the delegate NumberChangedEventHandler, defined as: public delegate void NumberChangedEventHandler(int number); This delegate can be used to call any method with a matching signature (a void return type and one int parameter). The name of the delegate, the names of the target methods, and the names of those methods' parameters are of no importance. The only requirement is that the methods being called have the exact signature (i.e., the same types) that the delegate expects. You typically define a delegate with a meaningful name, to convey its purpose to the readers of your code. For example, the name NumberChangedEventHandler indicates that this delegate is used to publish an event notifying subscribers that the value of a certain number they are monitoring has changed. In the case of the event just described, the event publisher has a public member variable of the delegate type: public delegate void NumberChangedEventHandler(int number); public class MyPublisher { public NumberChangedEventHandler NumberChanged; /* Other methods and members */ } The event subscriber has to implement a method with the required signature: public class MySubscriber { public void OnNumberChanged(int number) { string message = "New value is " + number; MessageBox.Show(message,"MySubscriber"); } } The compiler replaces the delegate type definition with a sophisticated class providing the implementation of the sink list. The generated delegate class derives from the abstract class Delegate, shown in Example 6-1. Example 6-1. Partial definition of the Delegate classpublic abstract class Delegate : //Interface list { public static Delegate Combine(Delegate a, Delegate b); public static Delegate Remove(Delegate source, Delegate value); public object DynamicInvoke(object[] args); public virtual Delegate[] GetInvocationList( ); //Other members } You can use the =, +=, and -= operators to manage the list of target methods. That list is actually a list of delegate objects, each referencing a single target method. The += operator adds a new subscriber (actually, just a new target method wrapped in a delegate) to the end of the list. To add a new target, you need to create a new delegate object wrapped around the target method. The -= operator removes a target method from the list (either by creating a new delegate object wrapped around the target method or using an existing delegate that targets that method). The = operator can initialize the list, typically with a delegate pointing at a single target. The compiler converts the use of the operators to matching calls to the static methods of the Delegate class, such as Combine( ) or Remove( ). When you want to fire the event, simply call the delegate, passing in the parameters. This causes the delegate to iterate over its internal list of targets, calling each target method with those parameters. Example 6-2 shows how to add two subscribers to the delegate list of sinks, how to fire the event, and how to remove a subscriber from the list. Example 6-2. Using delegates to manage event subscription and publishingMyPublisher publisher = new MyPublisher( ); MySubscriber subscriber1 = new MySubscriber( ); MySubscriber subscriber2 = new MySubscriber( ); //Adding subscriptions: publisher.NumberChanged += new NumberChangedEventHandler (subscriber1.OnNumberChanged); publisher.NumberChanged += new NumberChangedEventHandler (subscriber2.OnNumberChanged); //Firing the event: publisher.NumberChanged(3); //Removing a subscription: publisher.NumberChanged -= new NumberChangedEventHandler (subscriber2.OnNumberChanged); All the code in Example 6-2 does is delegate to the NumberChanged delegate the act of calling the subscribers' methods. Note that you can add the same subscriber's target method multiple times, as in: publisher.NumberChanged += new NumberChangedEventHandler (subscriber1.OnNumberChanged); publisher.NumberChanged += new NumberChangedEventHandler (subscriber1.OnNumberChanged); or: NumberChangedEventHandler del; del = new NumberChangedEventHandler(subscriber1.OnNumberChanged); publisher.NumberChanged += del; publisher.NumberChanged += del; The delegate then simply calls that subscriber a matching number of times. When you remove a target method from the list, if it has multiple occurrences, the first one found (i.e., the one closest to the list's head) is removed.
6.1.1. Delegate InferenceIn C# 2.0, the compiler can infer the type of delegate to instantiate when adding or removing a target method. Instead of instantiating a new delegate object explicitly, you can make a direct assignment of a method name into a delegate variable, without wrapping it first with a delegate object. I call this feature delegate inference. Example 6-3 shows the same code as in Example 6-2, this time using delegate inference. Example 6-3. Using delegate inferenceMyPublisher publisher = new MyPublisher( ); MySubscriber subscriber1 = new MySubscriber( ); MySubscriber subscriber2 = new MySubscriber( ); //Adding subscriptions: publisher.NumberChanged += subscriber1.OnNumberChanged; publisher.NumberChanged += subscriber2.OnNumberChanged; //Firing the event: publisher.NumberChanged(3); //Removing a subscription: publisher.NumberChanged -= subscriber2.OnNumberChanged; Using delegate inference, you can pass just the method name to any method that expects a delegate. For example: delegate void SomeDelegate( ); class SomeClass { public void SomeMethod( ) {...} public void InvokeDelegate(SomeDelegate del) { del( ); } } SomeClass obj = new SomeClass( ); obj.InvokeDelegate(obj.SomeMethod); When you assign a method name into a delegate, the compiler first infers the delegate's type, then the compiler verifies that there is a method by that name and that its signature matches that of the inferred delegate type. Finally, the compiler creates a new object of the inferred delegate type wrapping the method, and assigns that to the delegate. The compiler can only infer the delegate type if that type is a specific delegate typethat is, anything other than the abstract type Delegate. Delegate inference makes for readable and concise code, and it is the coding style used throughout this book.
6.1.2. Generic DelegatesThe introduction of generics in .NET 2.0 opens a new set of possibilities for event definition and management. As you will see later in this chapter, the ability to define generic delegates means that you will rarely have to define any new delegates to handle events. A delegate defined in a class can take advantage of the generic type parameter of that class. For example: public class MyClass<T> { public delegate void GenericEventHandler(T t); public void SomeMethod(T t) {...} } When you specify a type parameter for the containing class, it will affect the delegate as well: MyClass<int>.GenericEventHandler del; MyClass<int> obj = new MyClass<int>( ); del = obj.SomeMethod; del(3); Note that the compiler is capable of inferring the correct delegate type to instantiate, including the use of the correct generic type parameter. Therefore, this assignment: del = obj.SomeMethod; is actually converted at compile time to this assignment: del = new MyClass<int>.GenericEventHandler(obj.SomeMethod); Like classes, interfaces, structs, and methods, delegates too can define generic type parameters: public class MyClass<T> { public delegate void GenericEventHandler<X>(T t,X x); } Delegates defined outside the scope of a class can also use generic type parameters. In that case, you have to provide the specific types for the delegate when declaring and instantiating it: public delegate void GenericEventHandler<T>(T t); public class MyClass { public void SomeMethod(int number) {...} } GenericEventHandler<int> del; MyClass obj = new MyClass( ); del = obj.SomeMethod; del(3); And, naturally, a delegate can define constraints to accompany its generic type parameters: public delegate void GenericEventHandler<T>(T t) where T : IComparable<T>; The delegate-level constraints are enforced only on the using side, when declaring a delegate variable and instantiating a delegate object, similar to any other constraint at the scope of types or methods. 6.1.3. The event KeywordUsing raw delegates for event management is simple enough, but it has a flaw: the publisher class should expose the delegate member as a public member variable so that any party can add subscribers to that delegate list. Exposing the delegate as a public member allows anyone to access it and publish the event, however, even if no event takes place on the object side. To address this flaw, C# refines the type of delegates used for event subscription and notification, using the reserved word event. When you define a delegate member variable as an event, even if that member is public, only the publisher class (not even a subclass) can fire the event (although anyone can add target methods to the delegate list). It's then up to the discretion of the publisher class's developer whether to provide a public method to fire the event: public delegate void NumberChangedEventHandler(int number); public class MyPublisher { public event NumberChangedEventHandler NumberChanged; public void FireEvent(int number) { NumberChanged(number); } /* Other methods and members */ } The code that hooks up subscribers with the publisher remains the same as that shown in Example 6-2 or Example 6-3 (using the += and -= operators). Using events instead of raw delegates also promotes looser coupling between the publisher and the subscribers, because the business logic on the publisher side that triggers firing the event is hidden from the subscribers.
6.1.4. Events in Visual Basic 2005The semantics of the operations for event handling in Visual Basic 2005 are exactly the same as those described for C#. The syntax, however, is sufficiently different to merit a few words and some sample code. Visual Basic 2005 doesn't provide overloaded operators for adding and removing event-handling methods; instead, it uses the reserved words AddHandler and RemoveHandler. The AddressOf operator is used to obtain the address of the event-handling method. To fire the event in Visual Basic 2005, instead of using the delegate directly, as in C#, you need to use the RaiseEvent operator. Example 6-4 uses Visual Basic 2005 to implement the NumberChanged event. The code is comparable to that in Example 6-2, except it uses an event instead of a raw delegate. Example 6-4. .NET events using Visual Basic 2005Public Delegate Sub NumberChangedEventHandler(ByVal number As Integer) Public Class MyPublisher Public Event NumberChanged As NumberChangedEventHandler Public Sub FireEvent(ByVal number As Integer) RaiseEvent NumberChanged(number) End Sub End Class Public Class MySubscriber Public Sub OnNumberChanged(ByVal number As Integer) Dim message As String message = "New value is " + number.ToString( ) MessageBox.Show(message, "MySubscriber") End Sub End Class Dim publisher As MyPublisher = New MyPublisher( ) Dim subscriber1 As MySubscriber = New MySubscriber( ) Dim subscriber2 As MySubscriber = New MySubscriber( ) 'Adding subscriptions: AddHandler publisher.NumberChanged, AddressOf subscriber1.OnNumberChanged AddHandler publisher.NumberChanged, AddressOf subscriber2.OnNumberChanged publisher.FireEvent(3) 'Removing a subscription: RemoveHandler publisher.NumberChanged, AddressOf subscriber2.OnNumberChanged
|