GOTCHA 7 Uninitialized event handlers aren t treated gracefully


GOTCHA #7 Uninitialized event handlers aren't treated gracefully

Delegates are very effective for implementing callbacks in .NET. A delegate encapsulates a pointer to a method, and an instance of an object on which that method needs to be executed. A delegate can also encapsulate a pointer to a static/Shared method. The syntax provided to use a delegate is intuitive. You do not have to deal with messy pointers to functions as in C++.

Delegates are used to specify the handlers that will be called when an event occurs. If you want to register multiple methods of a class as event handlers, you can do so very easily without having to resort to something as complicated as anonymous inner classes, as you do in Java.

In order to call the handler that a delegate represents, you can either use the DynamicInvoke() method, or you can just call the delegate as if it were itself a method:

 MyDelegate.DynamicInvoke(...) 

Or:

 MyDelegate(...) 

Their ease of use sometimes obscures the fact that delegates are just classes, created when the compiler sees the delegate keyword. When you use a delegate, you are using an object through a special syntax. Of course, you know not to invoke methods on an object reference that you haven't initialized. However, it may not be readily apparent when a delegate is uninitialized.

When raising an event, you should consider the possibility that no handlers have been added or registered. Consider the code in Example 1-13.

Example 1-13. Accessing an uninitialized delegate

C# (Delegate)

  // AComponent.cs using System; namespace UnInitializedDelegate {     public delegate void DummyDelegate();     public class AComponent     {         public event DummyDelegate myEvent;         protected virtual void OnMyEvent()         {             myEvent();         }         public void Fire()         {             Console.WriteLine("Raising event");             OnMyEvent(); // Raising the event             Console.WriteLine("Done raising event");         }     } } //Test.cs using System; namespace UnInitializedDelegate {     public class Test     {         private void callback1()         {             Console.WriteLine("callback1 called");         }         private void callback2()         {             Console.WriteLine("callback2 called");         }         private void Work()         {             AComponent obj = new AComponent();             Console.WriteLine("Registering 2 callbacks");             obj.myEvent += new DummyDelegate(callback1);             obj.myEvent += new DummyDelegate(callback2);             obj.Fire();             Console.WriteLine("Removing 1 callback");             obj.myEvent -= new DummyDelegate(callback2);             obj.Fire();                       Console.WriteLine("Removing the other callback");             obj.myEvent -= new DummyDelegate(callback1);             obj.Fire();         }         [STAThread]         static void Main(string[] args)         {             Test testObj = new Test();             testObj.Work();         }     } } 

VB.NET (Delegate)

  'AComponent.vb Public Delegate Sub DummyDelegate() Public Class AComponent     Public Event myEvent As DummyDelegate     Protected Overridable Sub OnMyEvent()         RaiseEvent myEvent()     End Sub     Public Sub Fire()         Console.WriteLine("Raising event")         OnMyEvent() ' Raising the event         Console.WriteLine("Done raising event")     End Sub End Class 'Test.vb Public Class Test     Private Sub callback1()         Console.WriteLine("callback1 called")     End Sub     Private Sub callback2()         Console.WriteLine("callback2 called")     End Sub     Private Sub Work()         Dim obj As New AComponent         Console.WriteLine("Registering 2 callbacks")         AddHandler obj.myEvent, New DummyDelegate(AddressOf callback1)         AddHandler obj.myEvent, New DummyDelegate(AddressOf callback2)         obj.Fire()         Console.WriteLine("Removing 1 callback")         RemoveHandler obj.myEvent, New DummyDelegate(AddressOf callback2)         obj.Fire()         Console.WriteLine("Removing the other callback")         RemoveHandler obj.myEvent, New DummyDelegate(AddressOf callback1)         obj.Fire()     End Sub     Shared Sub Main(ByVal args As String())         Dim testObj As New Test         testObj.Work()     End Sub End Class 

When executed, the C# version of the program produces the result shown in Figure 1-10.

As Figure 1-10 shows, a NullReferenceException is thrown when the third call to the Fire() method tries to raise the event. The reason for this is that no event handler delegates are registered at that moment.

Figure 1-10. Output from the C# version of Example 1-13


The VB.NET version of the program, however, does not throw an exception. It works just fine.[*] Why? In the MSIL generated for RaiseEvent() (shown in Example 1-14), a check for the reference being Nothing is made.

[*] Actually, there is a problem we are not seeing--RaiseEvent() is not thread-safe. See Gotcha #64.

Example 1-14. MSIL translation of a RaiseEvent() statement
   IL_0000:  nop   IL_0001:  ldarg.0   IL_0002:  ldfld      class UnInitializedDelegate.DummyDelegate                        UnInitializedDelegate.AComponent::myEventEvent   IL_0007:  brfalse.s  IL_0015   IL_0009:  ldarg.0   IL_000a:  ldfld      class UnInitializedDelegate.DummyDelegate                        UnInitializedDelegate.AComponent::myEventEvent   IL_000f:  callvirt   instance void UnInitializedDelegate.DummyDelegate::Invoke()   IL_0014:  nop   IL_0015:  nop 

You can view the MSIL generated for your code using the tool ildasm.exe that comes with the .NET Framework. Simply run the tool and open the assembly you are interested in. You can view the MSIL generated for methods, properties, etc.


The correct way to implement this code in C# is to program defensively by checking for a null reference before raising the event, as shown in Example 1-15.

Example 1-15. Checking for an uninitialized delegate

C# (Delegate)

         protected virtual void OnMyEvent()         {             if(myEvent != null)             {                 myEvent();             }         } 

Checking to see if the delegate is not null prevents the NullReferenceException. The delegate will be null if no one has asked to be notified when the event triggers.

Note that there is still a problem. It is possible that the last registered event handler has been removed between the line where you check if myEvent is null and the line where you raise the event, and the code may still fail. You need to consider this possibility and raise the event in a thread-safe way. See Gotcha #64, "Raising events lacks thread-safety" for details on this.

IN A NUTSHELL

Use caution when raising an event. If no event handler has been registered, an exception is thrown in C# when you raise an event. Check to make sure that the delegate is not null before raising the event. In both C# and VB.NET, you need to worry about thread-safety when raising events.

SEE ALSO

Gotcha #64, "Raising events lacks thread-safety."



    .NET Gotachas
    .NET Gotachas
    ISBN: N/A
    EAN: N/A
    Year: 2005
    Pages: 126

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