Section 6.2. Working with .NET Events


6.2. Working with .NET Events

This section discusses .NET event-design guidelines and development practices that promote loose coupling between publishers and subscribers, improve availability, conform to existing conventions, and generally take advantage of .NET's rich event-support infrastructure. Another event-related technique (publishing events asynchronously) is discussed in Chapter 7.

6.2.1. Defining Delegate Signatures

Although technically a delegate declaration can define any method signature, in practice, event delegates should conform to a few specific guidelines.

First, the target methods should have a void return type. For example, for an event dealing with a new value for a number, such a signature might be:

     public delegate void NumberChangedEventHandler(int number); 

The reason you should use a void return type is that it simply doesn't make sense to return a value to the event publisher. What should the event publisher do with those values? The publisher has no idea why an event subscriber wants to subscribe in the first place. In addition, the delegate class hides the actual publishing act from the publisher. The delegate is the one iterating over its internal list of sinks (subscribing objects), calling each corresponding method; the returned values aren't propagated to the publisher's code. The logic for using the void return type also suggests that you should avoid output parameters that use either the ref or out parameter modifiers, because the output parameters of the various subscribers don't propagate to the publisher.

Second, some subscribers will probably want to receive the same event from multiple event-publishing sources. Because there is no flexibility in providing as many methods as publishers, the subscriber will want to provide the same method to multiple publishers. To allow the subscriber to distinguish between events fired by different publishers, the signature should contain the publisher's identity. Without relying on generics (discussed later), the easiest way to do this is to add a parameter of type object, called the sender parameter:

     public delegate void NumberChangedEventHandler(object sender,int number); 

A publisher then simply passes itself as the sender (using this in C# or Me in Visual Basic 2005).

Finally, defining actual event arguments (such as int number) couples publishers to subscribers, because the subscriber has to expect a particular set of arguments. If you want to change these arguments in the future, such a change affects all subscribers. To contain the impact of an argument change, .NET provides a canonical event arguments container, the EventArgs class, that you can use in place of a specific list of arguments. The EventArgs class definition is as follows:

     public class EventArgs     {        public static readonly EventArgs Empty;        static EventArgs(  )        {           Empty = new EventArgs(  );        }        public EventArgs(  )        {}     } 

Instead of specific event arguments, you can pass in an EventArgs object:

     public delegate void NumberChangedEventHandler(object sender,EventArgs eventArgs); 

If the publisher has no need for an argument, simply pass in EventArgs.Empty, taking advantage of the static constructor and the static read-only Empty class member.

If the event requires arguments, derive a class from EventArgs, such as NumberChangedEventArgs; add member variables, methods, or properties as required; and pass in the derived class. The subscriber should downcast EventArgs to the specific argument class associated with this event (NumberChangedEventArgs, in this example) and access the arguments. Example 6-5 demonstrates the use of an EventArgs-derived class.

Example 6-5. Events arguments using an EventArgs-derived class
     public delegate void NumberChangedEventHandler(object sender,EventArgs eventArgs);            public class NumberChangedEventArgs : EventArgs     {        public int Number;//This should really be a property     }     public class MyPublisher     {        public event NumberChangedEventHandler NumberChanged;        public void FireNewNumberEvent(EventArgs eventArgs)        {           //Always check delegate for null before invoking           if(NumberChanged != null)              NumberChanged(this,eventArgs);        }     }     public class MySubscriber     {        public void OnNumberChanged(object sender,EventArgs eventArgs)        {           NumberChangedEventArgs numberArg;           numberArg = eventArgs as NumberChangedEventArgs;           Debug.Assert(numberArg != null);           string message = numberArg.Number;           MessageBox.Show("The new number is "+ message);        }     }     //Client-side code     MyPublisher publisher = new MyPublisher(  );     MySubscriber subscriber = new MySubscriber(  );     publisher.NumberChanged += subscriber.OnNumberChanged;            NumberChangedEventArgs numberArg = new NumberChangedEventArgs(  );     numberArg.Number = 4;            //Note that the publisher can publish without knowing the argument type     publisher.FireNewNumberEvent(numberArg); 

Deriving a class from EventArgs to pass specific arguments allows you to add arguments, remove unused arguments, derive yet another class from EventArgs, and so on, without forcing changes on the subscribers that do not care about the new aspects.

Because the resulting delegate definition is now so adaptable, .NET provides the EventHandler delegate:

     public delegate void EventHandler(object sender,EventArgs eventArgs); 

EventHandler is used extensively by the .NET application frameworks, such as Windows Forms and ASP.NET. However, the flexibility of an amorphous base class comes at the expense of type safety. To address that problem, .NET provides a generic version of the EventHandler delegate:

     public delegate void EventHandler<E>(object sender,E e)                                                        where E : EventArgs; 

If you subscribe to the belief that all your events take the form of a sender object and an EventArgs-derived class, then this delegate can suffice for all your needs. In all other cases, you still have to define delegates for the specific signatures you have to deal with.

The convention for the subscriber's event-handling method name is On<EventName>, which makes the code standard and readable.


6.2.2. Defining Custom Event Arguments

As explained in the preceding section, you should provide arguments to an event handler in a class derived from EventArgs and have the arguments as class members. The delegate class simply iterates over its list of subscribers, passing the argument objects from one subscriber to the next. However, nothing prevents a particular subscriber from modifying the argument values and thus affecting all successive subscribers that handle the event. Usually, you should prevent subscribers from modifying these members as they are passed from one subscriber to the next. To preclude changing the argument, either provide access to the arguments as read-only properties or expose them as public members and apply the readonly access modifier. In both cases, you should initialize the argument values in the constructor. Example 6-6 shows both techniques.

Example 6-6. Preventing subscribers from modifying parameters in the argument class
     public class NumberEventArgs1 : EventArgs     {        public readonly int Number;        public NumberEventArgs1(int number)        {           Number = number;        }     }     public class NumberEventArgs2 : EventArgs     {        int m_Number;               public NumberEventArgs2(int number)        {           m_Number = number;        }        public int Number        {           get           {              return m_Number;           }        }     } 

6.2.3. The Generic Event Handler

Generic delegates are especially useful when it comes to events. Assuming that all delegates used for event management should return void and have no outgoing parameters, then the only thing that distinguishes one such delegate from another is the number of arguments and their type. This difference can easily be generalized using generics. Consider this set of delegate definitions:

     public delegate void GenericEventHandler(  );     public delegate void GenericEventHandler<T>(T t);     public delegate void GenericEventHandler<T,U>(T t,U u);     public delegate void GenericEventHandler<T,U,V>(T t,U u,V v);     public delegate void GenericEventHandler<T,U,V,W>(T t,U u,V v,W w);     public delegate void GenericEventHandler<T,U,V,W,X>(T t,U u,V v,W w,X x);     public delegate void GenericEventHandler<T,U,V,W,X,Y>(T t,U u,V v,W w,                                                                   X x,Y y);     public delegate void GenericEventHandler<T,U,V,W,X,Y,Z>(T t,U u,V v,W w,                                                               X x,Y y,Z z); 

The technique used in the definition of GenericEventHandler is called overloading by type parameter arity. The compiler actually assigns different names to the overloaded delegates, distinguishing them based on the number of arguments or the number of generic type parameters. For example, the following code:

         Type type = typeof(GenericEventHandler<,>);         Trace.WriteLine(type.ToString(  )); 

traces:

         GenericEventHandler`2[T,U]         because the compiler appends `2 to the delegate name (2 being the         number of generic type parameters used). 


The various GenericEventHandler versions can be used to invoke any event-handling method that accepts between zero and seven arguments (more than five or so arguments is a bad practice anyway, and you should use structures, or an EventArgs-derived class, to pass multiple arguments). You can define any combination of types using GenericEventHandler. For example:

     GenericEventHandler<int> del1;     GenericEventHandler<int,int> del2;     GenericEventHandler<int,string> del3;     GenericEventHandler<int,string,int> del4; 

or, for passing multiple arguments:

     struct MyStruct     {...}     public class MyArgs : EventArgs     {...}     GenericEventHandler<MyStruct> del5;     GenericEventHandler<MyArgs> del6; 

Example 6-7 shows the use of GenericEventHandler and a generic event-handling method.

Example 6-7. Generic event handling
     public class MyArgs : EventArgs     {...}     public class MyPublisher     {        public event GenericEventHandler<MyPublisher,MyArgs> MyEvent;        public void FireEvent(  )        {           MyArgs args = new MyArgs(...);           MyEvent(this,args);        }     }     public class MySubscriber<A> where A : EventArgs     {        public void OnEvent(MyPublisher sender,A args)        {...}     }     MyPublisher publisher = new MyPublisher(  );     MySubscriber<MyArgs> subscriber = new MySubscriber<MyArgs>(  );     publisher.MyEvent += subscriber.OnEvent; 

This example uses GenericEventHandler with two type parameters, the sender type and the argument container class, similar to generic and non-generic versions of EventHandler. However, unlike EventHandler, GenericEventHandler is type-safe, because it accepts only objects of the type MyPublisher (rather than merely object) as senders. Clearly, you can use GenericEventHandler with all event signatures, including those that do not comply with the sender object and the EventArgs derivation guideline.

For demonstration purposes, Example 6-7 also uses a generic subscriber, which accepts a generic type parameter for the event argument container. You could define the subscriber to use a specific type of argument container instead, without affecting the publisher code at all:

     public class MySubscriber     {        public void OnEvent(MyPublisher sender,MyArgs args)        {...}     } 

If you want to enforce the use of an EventArgs-derived class as an argument, you can put a constraint on GenericEventHandler:

     public delegate void GenericEventHandler<T,U>(T t,U u) where U : EventArgs; 

However, for the sake of generic use of GenericEventHandler, it is better to place the constraint on the subscribing type (or the subscribing method), as shown in Example 6-7.

Sometimes is it useful to alias a particular combination of specific types. You can do that via the using statement:

     using MyHandler = GenericEventHandler                                      <MyPublisher,MyArgs>;     public class MyPublisher     {        public event MyHandler MyEvent;        //Rest of MyPublisher     } 

Note that the scope of aliasing is the scope of the file, so you have to repeat aliasing across the project files, in the same way you would if you were using namespaces.


6.2.4. Publishing Events Defensively

In .NET, if a delegate has no targets in its internal list, its value will be set to null. A C# publisher should always check a delegate for a null value before attempting to invoke it. If no client has subscribed to the event, the delegate's target list will be empty and the delegate value set to null. When the publisher tries to access a nulled delegate, an exception is thrown. Visual Basic 2005 developers don't need to check the value of the delegate because the RaiseEvent statement can accept an empty delegate. Internally, RaiseEvent checks that the delegate is not null before accessing it.

A separate problem with delegate-based events is exceptions. Any unhandled exception raised by the subscriber will be propagated to the publisher. Some subscribers may encounter an exception in their handling of the event, not handle it, and cause the publisher to crash. For these reasons, you should always publish inside a try/catch block. Example 6-8 demonstrates these points.

Example 6-8. Defensive publishing
     public class MyPublisher     {        public event EventHandler MyEvent;        public void FireEvent(  )        {           try           {              if(MyEvent != null)                 MyEvent(this,EventArgs.Empty);           }           catch           {              //Handle exceptions           }        }     } 

However, the code in Example 6-8 aborts the event publishing in case a subscriber throws an exception. Sometimes you want to continue publishing even if a subscriber throws an exception. To do so, you need to manually iterate over the internal list maintained by the delegate and catch any exceptions thrown by the individual delegates in the list. You access the internal list by using a special method every delegate supports (see Example 6-1), called GetInvocationList( ). The method is defined as:

     public virtual Delegate[] GetInvocationList(  ); 

GetInvocationList( ) returns a collection of delegates you can iterate over, as shown in Example 6-9.

Example 6-9. Continuous publishing in the face of exceptions thrown by the subscribers
     public class MyPublisher     {        public event EventHandler MyEvent;        public void FireEvent(  )        {           if(MyEvent == null)           {              return;           }           Delegate[] delegates = MyEvent.GetInvocationList(  );           foreach(Delegate del in delegates)           {              EventHandler sink = (EventHandler)del;              try              {                 sink(this,EventArgs.Empty);              }              catch{}           }        }     } 

6.2.4.1 The EventsHelper class

The problem with the publishing code in Example 6-9 is that it isn't reusableyou have to duplicate it each time you want fault isolation between the publisher and the subscribers. It's possible, however, to write a helper class that can publish to any delegate, pass any argument collection, and catch potential exceptions. Example 6-10 shows the EventsHelper static class, which provides the static Fire( ) method. The Fire( ) method defensively fires any type of event.

Example 6-10. The EventsHelper class
     public static class EventsHelper     {        public static void Fire(Delegate del,params object[] args)        {           if(del == null)           {              return;           }           Delegate[] delegates = del.GetInvocationList(  );           foreach(Delegate sink in delegates)           {              try              {                 sink.DynamicInvoke(args);              }              catch{}           }        }     } 

There are two key elements to implementing EventsHelper. The first is its ability to invoke any delegate. This is possible using the DynamicInvoke( ) method, which every delegate provides (see Example 6-1). DynamicInvoke( ) invokes the delegate, passing it a collection of arguments. It is defined as:

     public object DynamicInvoke(object[] args); 

The second key in implementing EventsHelper is passing it an open-ended number of objects as arguments for the subscribers. This is done using the C# params parameter modifier (ParamArray in Visual Basic 2005), which allows inlining of objects as parameters. The compiler converts the inlined arguments into an array of objects and passes in that array.

Using EventsHelper is elegant and straightforward: simply pass it the delegate to invoke and the parameters. For example, for the delegate MyEventHandler, defined as:

     public delegate void MyEventHandler(int number,string str); 

the following can be the publishing code:

     public class MyPublisher     {        public event MyEventHandler MyEvent;        public void FireEvent(int number, string str)        {           EventsHelper.Fire(MyEvent,number,str);        }     } 

When using EventsHelper from Visual Basic 2005, you need to access the delegate directly. The Visual Basic 2005 compiler generates a hidden member variable corresponding to the event member. The name of that hidden member is the name of the event, suffixed with Event:

     Public Delegate Sub MyEventHandler (ByVal number As Integer, ByVal str As String)     Public Class MyPublisher        Public Event MyEvent As MyEventHandler                Public Sub FireEvent (ByVal number As Integer, ByVal str As String)           EventsHelper.Fire(MyEventEvent, number, str)        End Sub     End Class 

6.2.4.2 Making EventsHelper type-safe

The problem with EventsHelper as presented in Example 6-10 is that it is not type-safe. The Fire( ) method takes a collection of amorphous objects to allow any combination of parameters, including an incorrect combination. For example, given this delegate definition:

     public delegate void MyEventHandler(int number,string str); 

the following publishing code will compile fine but will fail to publish:

     public class MyPublisher     {        public event MyEventHandler MyEvent;        public void FireEvent(int number, string str)        {           EventsHelper.Fire(MyEvent,"Not","Type","Safe");        }     } 

Any mismatches in the number of arguments or their type will not be detected at compile time. In addition, EventsHelper will snuff out the exceptions, and you will not even be aware of the problem.

However, if you can commit to always using GenericEventHandler instead of defining your own delegates for event handling, there is a way to enforce compile-time type safety. Instead of defining the delegate like so:

     public delegate void MyEventHandler(int number,string str); 

use GenericEventHandler directly, or alias it:

     using MyEventHandler = GenericEventHandler<int,string>; 

Next, combine EventsHelper with GenericEventHandler, as shown in Example 6-11.

Example 6-11. The type-safe EventsHelper
     public static class EventsHelper     {        //Same as Fire(  ) in Example 6-10        public static void UnsafeFire(Delegate del,params object[] args)        {...}        public static void Fire(GenericEventHandler del)        {           UnsafeFire(del);        }        public static void Fire<T>(GenericEventHandler<T> del,T t)        {           UnsafeFire(del,t);        }        public static void Fire<T,U>(GenericEventHandler<T,U> del,T t,U u)        {           UnsafeFire(del,t,u);        }        public static void Fire<T,U,V>(GenericEventHandler<T,U,V> del,                                                                T t,U u,V v)        {           UnsafeFire(del,t,u,v);        }        public static void Fire<T,U,V,W>(GenericEventHandler<T,U,V,W> del,                                                            T t,U u,V v,W w)        {           UnsafeFire(del,t,u,v,w);        }        public static void Fire<T,U,V,W,X>(GenericEventHandler<T,U,V,W,X> del,                                                        T t,U u,V v,W w,X x)        {           UnsafeFire(del,t,u,v,w,x);        }        public static void Fire<T,U,V,W,X,Y>(GenericEventHandler<T,U,V,W,X,Y> del,                                                    T t,U u,V v,W w,X x,Y y)        {           UnsafeFire(del,t,u,v,w,x,y);        }        public static void Fire<T,U,V,W,X,Y,Z>(GenericEventHandler<T,U,V,W,X,Y,Z> del,                                                T t,U u,V v,W w,X x,Y y,Z z)        {           UnsafeFire(del,t,u,v,w,x,y,z);        }     } 

Because the number and type of arguments passed to GenericEventHandler are known to the compiler, you can enforce type safety at compile time. Thanks to the overloading by the number of generic type parameters, you need not even specify any of the generic type parameters passed to the Fire( ) methods. The publishing code remains the same as if you are using the definition of Example 6-10:

     using MyEventHandler = GenericEventHandler<int,string>;         public class MyPublisher     {        public event MyEventHandler MyEvent;        public void FireEvent(int number, string str)        {               //This is now type-safe           EventsHelper.Fire(MyEvent,number,str);        }     } 

The compiler infers the type and number of type parameters used and selects the correct overloaded version of Fire( ).

If you can only guarantee the use of EventHandler, rather than GenericEventHandler, you can add these overloaded methods to EventsHelper:

     public static void Fire(EventHandler del,object sender,EventArgs e)     {        UnsafeFire(del,sender,e);     }     public static void Fire<E>(EventHandler<E> del,object sender,E e)                                                         where E : EventArgs     {        UnsafeFire(del,sender,t);     } 

This will enable you to publish defensively, in a type-safe manner, any EventHandler-based event.

EventsHelper can still offer the non-type-safe UnsafeFire( ) method:

     public static void UnsafeFire(Delegate del,params object[] args)     {        if(args.Length > 7)        {           Trace.TraceWarning("Too many parameters. Consider a structure                               to enable the use of the type-safe versions");        }        //Rest same as in Example 6-11     } 

This is required in case you are dealing with delegates that are not based on GenericEventHandler or EventHandler, or when you have more than the number of parameters GenericEventHandler can deal with. Obviously, using more than even five parameters should be avoided for any method by definition, but at least now the developer using EventsHelper is aware of the type-safety pitfall. If you want to always enforce the use of the type-safe Fire( ) methods, simply define UnsafeFire( ) as private:

     static void UnsafeFire(Delegate del,params object[] args); 

6.2.5. Event Accessors

To hook up a subscriber to a publisher, you access the publisher's event member variable directly. Exposing class members in public is asking for trouble; it violates the core object-oriented design principle of encapsulation and information hiding, and it couples all subscribers to the exact member variable definition. To mitigate this problem, C# provides a property-like mechanism called an event accessor. Accessors provide a benefit similar to that of properties, hiding the actual class member while maintaining the original ease of use. C# uses add and removeperforming the functions of the += and -= operators, respectivelyto encapsulate the event member variable. Example 6-12 demonstrates the use of event accessors and the corresponding client code. Note that when you use event accessors, there is no advantage to marking the encapsulated delegate member as an event.

Example 6-12. Using event accessors
     using MyEventHandler = GenericEventHandler<string>;     public class MyPublisher     {        MyEventHandler m_MyEvent;        public event MyEventHandler MyEvent          {               add               {                     m_MyEvent += value;               }               remove               {                     m_MyEvent -= value;               }          }         public void FireEvent(  )        {           EventsHelper.Fire(m_MyEvent,"Hello");        }     }     public class MySubscriber     {        public void OnEvent(string message)        {           MessageBox.Show(message);        }     }     //Client code:     MyPublisher publisher = new MyPublisher(  );     MySubscriber subscriber = new MySubscriber(  );         //Set up connection:     publisher.MyEvent += subscriber.OnEvent;         publisher.FireEvent(  );         //Tear down connection:     publisher.MyEvent -= subscriber.OnEvent; 

6.2.6. Managing Large Numbers of Events

Imagine a class that publishes a very large number of events. This is common when developing frameworks; for example, the class Control in the System.Windows.Forms namespace has dozens of events corresponding to many Windows messages. The problem with handling numerous events is that it's simply impractical to allocate a class member for each event: the class definition, documentation, CASE tool diagrams, and even IntelliSense would be unmanageable. To address this predicament, .NET provides the EventHandlerList class (found in the System.ComponentModel namespace):

     public sealed class EventHandlerList : IDisposable     {         public EventHandlerList(  );         public Delegate this[object key]{get;set;}         public void AddHandler(object key, Delegate value);         public void AddHandlers(EventHandlerList listToAddFrom);         public void RemoveHandler(object key, Delegate value);         public virtual void Dispose(  );     } 

EventHandlerList is a linear list that stores key/value pairs. The key is an object that identifies the event, and the value is an instance of System.Delegate. Because the index is an object, it can be an integer index, a string, a particular button instance, and so on. You add and remove individual event-handling methods using the AddHandler and RemoveHandler methods, respectively. You can also add the content of an existing EventHandlerList, using the AddHandlers( ) method. To fire an event, you access the event list using the indexer with the key object, and you get back a System.Delegate object. You then downcast that delegate to the actual event delegate and fire the event.

Example 6-13 demonstrates using the EventHandlerList class when implementing a Windows Forms-like button class called MyButton. The button supports many events, such as mouse click and mouse move, all channeled to the same event list. Using event accessors, this is completely encapsulated from the clients.

Example 6-13. Using the EventHandlerList class to manage a large number of events
     using System.ComponentModel;            using ClickEventHandler = GenericEventHandler<MyButton,EventArgs>;     using MouseEventHandler = GenericEventHandler<MyButton,MouseEventArgs>;         public class MyButton     {        EventHandlerList m_EventList;        public MyButton(  )        {           m_EventList = new EventHandlerList(  );           /* Rest of the initialization */        }        public event ClickEventHandler Click          {               add               {                     m_EventList.AddHandler("Click",value);               }               remove               {                     m_EventList.RemoveHandler("Click",value);               }          }        public event MouseEventHandler MouseMove          {               add               {                     m_EventList.AddHandler("MouseMove",value);               }               remove               {                     m_EventList.RemoveHandler("MouseMove",value);               }          }        void FireClick(  )        {           ClickEventHandler handler = m_EventList["Click"] as ClickEventHandler;           EventsHelper.Fire(handler,this,EventArgs.Empty);        }        void FireMouseMove(MouseButtons button,int clicks,int x,int y,int delta)        {           MouseEventHandler handler = m_EventList["MouseMove"] as MouseEventHandler;           MouseEventArgs args = new MouseEventArgs(button,clicks,x,y,delta);           EventsHelper.Fire(handler,this,args);        }        /* Other methods and events definition */     } 

The problem with Example 6-13 is that events such as mouse move or even mouse click are raised frequently, and creating a new string as a key for each invocation increases the pressure on the managed heap. A better approach would be to use pre-allocated static keys, shared among all instances:

     public class MyButton     {        EventHandlerList m_EventList;        static object m_MouseMoveEventKey = new object(  );               public event MouseEventHandler MouseMove          {               add               {                     m_EventList.AddHandler(m_MouseMoveEventKey,value);               }               remove               {                     m_EventList.RemoveHandler(m_MouseMoveEventKey,value);               }          }        void FireMouseMove(MouseButtons button,int clicks,int x,int y,int delta)        {           MouseEventHandler handler;           handler = m_EventList[m_MouseMoveEventKey] as MouseEventHandler;           MouseEventArgs args = new MouseEventArgs(button,clicks,x,y,delta);           EventsHelper.Fire(handler,this,args);        }        /* Rest of the implementation  */     } 

6.2.7. Writing Sink Interfaces

By hiding the actual event members, event accessors provide barely enough encapsulation. However, you can improve on this model. To illustrate, consider the case where a subscriber wishes to subscribe to a set of events. Why should it make multiple potentially expensive calls to set up and tear down the connections? Why does the subscriber need to know about the event accessors in the first place? What if the subscriber wants to receive events on an entire interface, instead of individual methods? The next step is to provide a simple but generic way to manage the connections between the publisher and the subscribersone that will save the redundant calls, encapsulate the event accessors and members, and allow sinking interfaces. This section describes a technique I have developed to do just that. Consider an interface that defines a set of events, the IMySubscriber interface:

     public interface IMySubscriber     {        void OnEvent1(object sender,EventArgs eventArgs);        void OnEvent2(object sender,EventArgs eventArgs);        void OnEvent3(object sender,EventArgs eventArgs);     } 

Anybody can implement this interface, and the interface is really all the publisher should know about:

     public class MySubscriber : IMySubscriber     {        public void OnEvent1(object sender,EventArgs eventArgs)        {...}        public void OnEvent2(object sender,EventArgs eventArgs)        {...}        public void OnEvent3(object sender,EventArgs eventArgs)        {...}     } 

Next, define an enumeration of the events and mark the enum with the Flags attribute:

     [Flags]     public enum EventType     {        OnEvent1,        OnEvent2,        OnEvent3,        OnAllEvents = OnEvent1|OnEvent2|OnEvent3     } 

The Flags attribute indicates that the enum values could be used as a bit mask (see the EventType.OnAllEvents definition). This allows you to combine different enum values using the | (OR) bitwise operator or mask them using the & (AND) operator.

The publisher provides two methods, Subscribe( ) and Unsubscribe( ), each of which accepts two parameters: the interface and a bit-mask flag to indicate which events to subscribe the sink interface to. Internally, the publisher can have an event delegate member per method on the sink interface, or just one for all methods (it's an implementation detail, hidden from the subscribers). Example 6-14 uses an event member variable for each method on the sink interface. It shows Subscribe( ) and Un Subscribe( ), as well as the FireEvent( ) method, with error handling removed for clarity. Subscribe( ) checks the flag and subscribes the corresponding interface method:

     if((eventType & EventType.OnEvent1) == EventType.OnEvent1)     {        m_Event1 += subscriber.OnEvent1;     } 

UnSubscribe( ) removes the subscription in a similar fashion.

Example 6-14. Sinking interfaces
     using MyEventHandler = GenericEventHandler<object,EventArgs>;     public class MyPublisher     {         MyEventHandler m_Event1;         MyEventHandler m_Event2;         MyEventHandler m_Event3;                public void Subscribe(IMySubscriber subscriber,EventType eventType)         {             if((eventType & EventType.OnEvent1) == EventType.OnEvent1)             {                 m_Event1 += subscriber.OnEvent1;             }             if((eventType & EventType.OnEvent2) == EventType.OnEvent2)             {                 m_Event2 += subscriber.OnEvent2;             }             if((eventType & EventType.OnEvent3) == EventType.OnEvent3)             {                 m_Event3 += subscriber.OnEvent3;             }         }         public void Unsubscribe(IMySubscriber subscriber,EventType eventType)         {             if((eventType & EventType.OnEvent1) == EventType.OnEvent1)             {                 m_Event1 -= subscriber.OnEvent1;             }             if((eventType & EventType.OnEvent2) == EventType.OnEvent2)             {                 m_Event2 -= subscriber.OnEvent2;             }             if((eventType & EventType.OnEvent3) == EventType.OnEvent3)             {                 m_Event3 -= subscriber.OnEvent3;             }         }         public void FireEvent(EventType eventType)         {             if((eventType & EventType.OnEvent1) == EventType.OnEvent1)             {                 EventsHelper.Fire(m_Event1,this,EventArgs.Empty);             }             if((eventType & EventType.OnEvent2) == EventType.OnEvent2)             {                 EventsHelper.Fire(m_Event2,this,EventArgs.Empty);             }             if((eventType & EventType.OnEvent3) == EventType.OnEvent3)             {                 EventsHelper.Fire(m_Event3,this,EventArgs.Empty);             }         }     } 

The code required for subscribing or unsubscribing is equally straightforward:

     MyPublisher publisher = new MyPublisher(  );     IMySubscriber subscriber   = new MySubscriber(  );     //Subscribe to events 1 and 2     publisher.Subscribe(subscriber,EventType.OnEvent1|EventType.OnEvent2);     //Fire just event 1     publisher.FireEvent(EventType.OnEvent1); 

Still, it shows the elegance of this approach for sinking whole interfaces with one call, and shows how completely encapsulated the actual event class members are.



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