Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

.NET

Improving .NET Events


September, 2004: Improving .NET Events

Strategies for generating events

Richard is the author of Programming with Managed Extensions for Microsoft Visual C++ .NET 2003 (Microsoft Press, 2003). He can be contacted at richardrichardgrimes.com.


More .NET on DDJ.com


Events are a useful paradigm: Objects can notify other objects when some event occurs—perhaps a change in state or maybe an error condition has occurred. Some other code—handler code—can register its interest to be notified that this condition has occurred. When the event is raised, the handler code is called. But while .NET provides mechanisms for event-based code, there are some shortcomings in this mechanism. Luckily, .NET provides the facilities for writing your own event mechanism, thereby making it possible to address these problems.

Delegates

As an example of generating and handling events, consider the user-interface events provided by the Control class and derived classes for handling Windows messages. In essence, the Control class contains a method equivalent to a Win32 windows procedure that accepts messages sent to the Win32 window attached to the Control object and handles each message by raising an appropriate event. A method written to handle a Windows message adds itself to the list of handler code. When the Windows message is sent to the window, the event is raised by calling all of the methods added to the handler list for the event. To enable this, .NET provides two facilities: a mechanism to invoke methods called "delegates," and a formalized mechanism for a class to identify the events it can raise.

Of all the .NET languages, only managed C++ lets you create C-style function pointers, but even C++ cannot create a function pointer to a .NET method. The reason is that .NET methods have a special calling convention called __clrcall that is not a supported calling convention. So managed C++, like all other .NET languages, must use a delegate to invoke a method. A delegate is a compiler-generated class that derives from the MulticastDelegate class. Listing One shows how to declare a delegate called OneParam that is used to call methods that take a single parameter, a 32-bit integer, and return no values. As you can see, the delegate instance is initialized with a pointer to a method that has the correct prototype. This is the only time that you can access a pointer to a managed method.

The compiler performs type checking when it uses this pointer to initialize the delegate through its constructor and so it ensures that a delegate will only be used to invoke a method with the correct parameters. This method can be an instance method, static method, and it can be a member of any class. You can initialize a delegate with a private method of one class, in another method in that class, and pass the delegate to another class where it can still be invoked. Furthermore, you can pass the delegate to unmanaged code and .NET provides the thunking code to allow the unmanaged function to invoke the managed method. Clearly, delegates are clever objects.

If you take a look at the IL generated for this, you see that the constructor of the delegate class looks like this:

.method public hidebysig specialname
rtspecialname instance void
.ctor(object 'object',
native int 'method')
runtime managed
{
} // end of method Del::.ctor

The runtime-managed modifier indicates that no IL exists for the constructor and that the runtime provides the code. The parameters are interesting: The first parameter indicates the object that contains the method that will be invoked, and C# conveniently infers this from the parameter that you pass. The second parameter is typeless and is effectively a .NET equivalent of a void* pointer. But again, the compiler performs type checks to ensure that the method used to initialize a delegate is of the right type. This information is stored in the delegate object and can be accessed through the Target and Method properties.

Each delegate is multicast, which means that you can combine two delegate objects to create a third object that contains the first two. To do this, the MulticastDelegate class has a linked list, and when you combine two delegates, their linked lists are chained. Delegates can be invoked synchronously (Invoke, which C# calls for you when you invoke the delegate as if it is a method, as in Listing One) or they can be invoked asynchronously. However, "asynchronously" means with respect to the code that invokes the delegate and not with respect to the individual delegates within the multicast delegate. There is no concept of invoking the delegates in the MulticastDelegate object in parallel. The default invocation mechanism will walk the linked list and invoke each delegate in turn, synchronously, waiting for an invocation to complete before invoking the next delegate in the list. If you invoke a multicast delegate asynchronously, all that happens is that this invocation is performed on another thread.

Also, note that if one delegate in a multicast throws an exception when it is invoked, then the entire invocation of the multicast delegate is aborted. This is a problem when you consider events (which I will do next) because an event invites anyone to add a delegate to provide a handler for the event. When the event delegate is invoked, rogue code provided by some third party could throw an exception. If this happens, no other handlers for the event will be called. If the exception is still left uncaught, then the object could be killed. This highlights the tight coupling between .NET classes that raise events and the classes that handle them.

Events

The support for events comes in two forms. First, there must be a delegate field for each event and these hold the delegates that will be invoked when the event is raised. Second, there must be a formalized mechanism to allow code outside of the class to add delegates initialized with handler methods to this event delegate. .NET compilers provide their own code to do this and, with C#, this is done with the event keyword as shown in Listing Two. In this code, I have used a framework-provided delegate called EventHandler, but events can be defined for any delegate type.

The event keyword adds a private delegate field and it adds public methods to add a delegate and remove a delegate from this delegate field. In addition, so that other code can tell that this class can generate events, the compiler adds some .NET metadata to indicate methods that are used to add and remove delegates from the event. The C# compiler allows you to call these event methods using the += and -= operators.

When the event is raised, the delegate is invoked, and the default mechanism in C#, and the other .NET languages, is to invoke the delegate synchronously on the thread where the event was raised. Again, it is important to point out that all of the delegates in the multicast delegate are invoked serially. For Windows Forms classes this is a useful side effect because GUI code should only be called on the process's GUI thread and the event handlers for controls usually access the control or other controls on the form. However, for nonGUI code, you may find this a restriction, particularly if the delegates are for methods on remote objects or methods that take a long time to perform.

The Control class defines 57 events, indicating that, by using the default event mechanism, a Control object would have 57 delegate fields even though you will use perhaps only a handful of these. This clearly represents a waste of memory, so the designers of the Control class override the default action of the event keyword and provide a more efficient mechanism to store event delegates. The Control class derives from the Component class, which has a property, Events. This property is a collection of delegates of type EventHandlerList. The entries in this collection are identified by "key" objects; each key identifies a delegate for a specific event, and each of these delegates can contain one or more event handler delegates. The keys are objects, not a simple integer index; indeed, the actual value of the object is immaterial because the collection uses the identity of the object as the key.

On first sight, this does not appear to solve the space issue because it implies that the Control class will need to maintain a key object for each event type. However, the key object can be the same for each event of the same type for each Control object, so the Control class provides these key objects as static (noninstance) objects. The space expense is paid by the class rather than the individual Control objects. C# lets you provide your own event methods and Listing Three shows how you can use these methods with an EventHandlerList collection.

Attempts to Overcome the Issues

Tightly coupled events are useful, particularly when events are handled within the application domain because they are lightweight. However, even though the event-raising code and handler code are tightly coupled, the providers of each code are likely to be completely unconnected. This means that even if your code is completely error free, if you invoke a delegate provided by someone else, their code can kill your object. There are a couple of ways to mitigate this risk. The first is the simplest: Use the [OneWay] attribute on the method that a delegate will invoke. The [OneWay] attribute adds metadata to the method, which is read by the .NET runtime when it invokes the method. This attribute indicates that the method does not return any values, so invocation code does not make any effort to retrieve return values. An exception is treated as an extra return parameter of the method, so [OneWay] tells the runtime to ignore the exception. However, this attribute is not suitable in all situations. The attribute only works when a call is made across a context boundary; hence, .NET remoting is used and can intercept method calls. In many cases, your code will run in the default context, so no interception occurs and the [OneWay] attribute will be ignored. Another problem is that the attribute is attached to the handler method by the author of that method; in other words, the person who writes the code that you regard as being a suspect has to mark his code to tell the runtime to ignore any exceptions it may throw. This is unlikely to happen.

Another way to protect your code from errant event handlers is to invoke the event delegate explicitly. To do this you need to get access to the linked list of delegates in a multicast delegate. The MultiCastDelegate class does this through the GetInvocationList method. This returns an array of Delegate references. Listing Four shows how to use this to invoke the delegates within a try block to catch any exceptions that are thrown. The example contains no code in the catch handler; hence, the exceptions are ignored.

Ignoring exceptions is not necessarily the best action—the client code may want to report that one of the delegates has generated an exception; however, to do this there must be some mechanism to collect all the exceptions that have been thrown and return them back to the client. To do this, I define a new exception class (see Listing Five) that maintains a list of ExceptionData objects that contain information about the exception and the delegate that generated the exception. This exception class has a method to add new exception information to the MultiException object, and to determine if exception information has been added to the object. The class also overrides the ToString method to add information about each exception in the collection. Finally, I have also provided a property that gives access to this collection of ExceptionData objects.

Listing Six shows how this exception class is used. When a delegate throws an exception, the exception and delegate are added to the MultiException object in the exception handler but the exception is not rethrown. I think that this action is reasonable because the delegates are not related to each other, so an exceptional condition in one handler does not necessarily reflect that all the handlers are invalid, and it certainly does not indicate that the code invoking the delegates has some invalid state.

When all delegates have been invoked, the code checks the MultiException object to see if any exceptions had been thrown and, if so, the MultiException object that contains those exceptions is thrown. Note that this exception class works fine if the exception is rethrown within the same context. In a future article, I'll explain why the exception cannot be thrown across a context boundary and how to fix this problem by providing serialization code. The client that called the event-generating method can catch the exception and use the information in it. For example, Listing Seven shows exception handler code that prints out information about the MultiException that was caught and the exceptions thrown by the delegates. This code then accesses the errant delegate via the Exceptions collection and removes this delegate from the event. In effect, this is self-healing code because once an individual delegate has thrown, action is taken to ensure that the delegate is never called again.

Finally, it is worth pointing out that you cannot use this technique with Control objects. The methods on the Control class have names beginning with "On" (for example, OnClick for the Click event). These methods are virtual so you can provide an override, which will be called by the Control object's windows procedure. However, the static key objects, used to access the event handlers in the Events property, are private and hence unavailable to your derived class. AppEvent.cs (available electronically; see "Resource Center," page 5) is an event mechanism that catches delegate-thrown events such as those described here.

DDJ



Listing One

public class Test
{
   // Tell the compiler to generate a delegate class
   delegate void OneParam(int x);
   void Proc(int x)
   {/* code */}
   public void InvokeCode()
   {
      // Create a delegate object and initialize with a method pointer
      OneParam d = new OneParam(Proc);
      // Invoke the delegate
      d(42);
   }
}
Back to article


Listing Two
public class Test
{
   // Indicate that the class can generate the event
   public event EventHandler SomethingHappened;
   public void DoSomething()
   {
      // Code here...
      // Now raise the event
      if (SomethingHappened != null)
         SomethingHappened(this, new EventArgs());
   }
}
public class UseCode
{
   void InformMe(object sender, EventArgs args)
   { /* handle event */ }
   public void UseTestObject()
   {
      Test t = new Test();
      t.SomethingHappened += new EventHandler(InformMe);
      t.DoSomething();
   }
}
Back to article


Listing Three
public class Test
{
   EventHandlerList events = new EventHandlerList();
   // This is the 'key' object for our event
   static object EventSomethingHappened;
   static Test()
   {
      EventSomethingHappened = new object();
   }
   // Declare the event and the custom event methods
   public event EventHandler SomethingHappened
   {
      add
      {
         events.AddHandler(
            EventSomethingHappened, value);
      }
      remove
      {
         events.RemoveHandler(
            EventSomethingHappened, value);
      }
   }
   // Helper method to get the event delegate and
   // invoke it
   protected void RaiseSomethingHappened(EventArgs args)
   {
      EventHandler d;
      d = (EventHandler)events[EventSomethingHappened];
      d(this, args);
   }
   public void DoSomething()
   {
      // Code...
      RaiseSomethingHappened(new EventArgs());
   }
}
Back to article


Listing Four
protected void RaiseSomethingHappened(EventArgs args)
{
   EventHandler d ;
   d = (EventHandler)events[EventSomethingHappened];
   Delegate[] dels = d.GetInvocationList();
   // Invoke each delegate individually
   foreach(Delegate del in dels)
   {
      EventHandler eh = del as EventHandler;
      try
      {
         eh(this, args);
      }
      catch(Exception){}
   }
}
Back to article


Listing Five
class MultiException : Exception
{
   // Helper class to hold info about the exception
   public struct ExceptionData
   {
      public Exception theException;
      public Delegate theDelegate;
      public ExceptionData(Exception e, Delegate d)
      {
         theException = e;
         theDelegate = d;
      }
   }
   // Field used to collate exception data
   ArrayList exceptions = new ArrayList();

   public MultiException(string str) : base(str) {}
   public void Add(Exception e, Delegate d)
   {
      exceptions.Add(new ExceptionData(e, d));
   }
   public bool HasExceptions
   {
      get {return (exceptions.Count != 0);}
   }
   public ExceptionData[] Exceptions
   {
      get
      {
         return (ExceptionData[])exceptions.ToArray(
            typeof(ExceptionData));
      }
   }
   // Return information about all the exceptions
   public override string ToString()
   {
      if (exceptions == null) return base.ToString();
      StringBuilder sb = new StringBuilder();
      sb.Append(base.ToString());
      sb.Append(Environment.NewLine);

      for (int idx = 0; idx < exceptions.Count; idx++)
      {
         ExceptionData ed;
         ed = (ExceptionData)exceptions[idx];
         sb.Append(String.Format(
            "{0} on {1} threw an exception:", 
            ed.theDelegate.Method.Name,
            ed.theDelegate.Target.GetType()));
         sb.Append(Environment.NewLine);
         sb.Append(ed.theException.ToString());
         sb.Append(Environment.NewLine);
      }
      return sb.ToString();
   }
}
Back to article


Listing Six
protected void RaiseSomethingHappened(EventArgs args)
{
   MultiException exceptions = new MultiException(
      "delegate(s) thrown exceptions");
   EventHandler d;
   d = (EventHandler)events[EventSomethingHappened];
   Delegate[] dels = d.GetInvocationList();
   foreach(Delegate del in dels)
   {
      EventHandler eh = del as EventHandler;
      try
      {
         eh(this, args);
      }
      catch(Exception e)
      {
         exceptions.Add(e, eh);
      }
   }
   if (exceptions.HasExceptions)
      throw exceptions;
}
Back to article


Listing Seven
try
{
   RaiseSomethingHappened(new EventArgs());
}
catch (MultiException me)
{
   Console.WriteLine("\n{0}", me.ToString());
   foreach(MultiException.ExceptionData ed 
              in me.Exceptions)
   {
      this.SomethingHappened -=
        (EventHandler)ed.theDelegate;
   }
}
Back to article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.