Section 6.1. Delegate-Based Events


6.1. Delegate-Based Events

Before 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 class
     public 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 publishing
     MyPublisher  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.

Delegates are widely used in .NET, not just as a consistent way of managing events but also for other tasks, such as asynchronous method invocation (described in Chapter 7) and creating of new threads (described in Chapter 8).


6.1.1. Delegate Inference

In 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 inference
     MyPublisher  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.

Delegate inference is supported only by C# 2.0. If your code is required to run on earlier versions of .NET, use explicit delegate instantiation, as in Example 6-2.


6.1.2. Generic Delegates

The 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 Keyword

Using 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.

When you type the += operator to add a target method to a delegate in Visual Studio, IntelliSense presents a tool tip offering to add a new delegate of the matching type when you press the Tab key. If you don't like the default target method name, simply type in a different name. If the target method doesn't exist, IntelliSense lets you generate a handling method by that name by pressing Tab once more. This IntelliSense support works only with delegates defined as events (not mere delegates). There is no IntelliSense support for removing a subscription.


6.1.4. Events in Visual Basic 2005

The 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 2005
     Public 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 

.NET Loosely Coupled Events

.NET event support eases the task of managing events, and it relieves you of the burden of providing the mundane code for managing the list of subscribers. However, .NET delegate-based events do suffer from several drawbacks:

  • The subscriber (or the client adding the subscription) must repeat the code for adding the subscription for every publisher object from which it wants to receive events. There is no way to subscribe to a type of event and have the event delivered to the subscriber, regardless of who the publisher is.

  • The subscriber has no way to filter events that are fired (e.g., to say "Notify me about the event only if a certain condition is met").

  • The subscriber must have a way to get hold of the publisher object in order to subscribe to it, which in turn introduces coupling between clients and objects and coupling between individual clients.

  • The publisher and the subscribers have coupled lifetimes; both publisher and subscriber have to be running at the same time. There is no way for a subscriber to say to .NET, "If any object fires this particular event, please create an instance of me and let me handle it."

  • There is no easy way for doing disconnected work, where the publisher object fires the event from an offline machine and the event is subsequently delivered to subscribing clients once the machine is brought online. The reversehaving a client running on an offline machine and later receiving events fired while the connection was downis also not possible.

  • Setting up connections has to be done programmatically. There is no administrative way to set up connections between publishers and subscribers.

To offset these drawbacks, .NET supports a separate kind of events, called loosely coupled events (LCE). LCE support is provided in the System.EnterpriseServices namespace. Even though LCE aren't based on delegates, their use is easy and straightforward and offers additional benefits, such as combining events with transactions and security. .NET Enterprise Services are beyond the scope of this book, but you can read about them in my book, COM and .NET Component Services (O'Reilly). Loosely coupled events are described in depth in Chapters 9 and 10 of that book.




Programming. NET Components
Programming .NET Components, 2nd Edition
ISBN: 0596102070
EAN: 2147483647
Year: 2003
Pages: 145
Authors: Juval Lowy

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