CHAPTER 23

image

Events

Chapter 22 discussed how delegates can be used to pass a reference to a method so that it can be called in a general way. Being able to call a method in such a way is useful in graphical user interfaces, such as the one provided by the classes in System.Windows.Controls. It’s fairly easy to build such a framework by using delegates, but there are significant limitations to the delegate approach. Events remove those limitations.

A Simple Example Using Delegates

The following is a simple example using delegates:

using System;
public class Button
{
    public delegate void ClickHandler(object sender, EventArgs e);
    public ClickHandler Click;
    protected void OnClick()
    {
       if (Click != null)
       {
            Click(this, EventArgs.Empty);
       }
    }
    public void SimulateClick()
    {
       OnClick();
    }
}
class Test
{
    static public void ButtonHandler(object sender, EventArgs e)
    {
       Console.WriteLine("Button clicked");
    }
    public static void Main()
    {
       Button button = new Button();
       button.Click = ButtonHandler;
       button.SimulateClick();
    }
}

The Button class is supporting a click “event”1 by having the ClickHandler delegate tell what kind of method can be called to hook up, and a delegate instance can then be assigned to the event. The OnClick() method then calls this delegate, and everything works fine—at least in this simple case.

The situation gets more complicated in a real-world scenario. In real applications, a button such as this one would live in a form, and a button click might be of interest to more than one component of the application. Doing this isn’t a problem with delegates because more than one method can be called from a single delegate instance. In the previous example, if another class also wanted to be called when the button was clicked, the + = operator could be used, like this:

button.Click + = OtherMethodToCall;

Unfortunately, if the other class wasn’t careful, it might do the following:

button.Click = OtherMethodToCall;

This would be bad, because it would mean that the ButtonHandler would be unhooked, and only the new method would be called.

To unhook from the click, the right thing to do is use this code:2

button.Click - = OtherMethodToCall;

However, the following might be used instead:

button.Click = null;

This is also wrong.

What is needed is some way of protecting the delegate field so that it’s accessed only using the + = and - = operators.

Add and Remove Functions

One way to do this is to make the delegate field private and write a couple of methods that can be used to add or remove delegates.

using System;
public class Button
{
    public delegate void ClickHandler(object sender, EventArgs e);
    private ClickHandler click;
    public void AddClick(ClickHandler clickHandler)
    {
       click += clickHandler;
    }
    public void RemoveClick(ClickHandler clickHandler)
    {
       click -= clickHandler;
    }
    protected void OnClick()
    {
       if (click != null)
       {
            click(this, EventArgs.Empty);
       }
    }
    public void SimulateClick()
    {
       OnClick();
    }
}
class Test
{
    static public void ButtonHandler(object sender, EventArgs e)
    {
       Console.WriteLine("Button clicked");
    }
    public static void Main()
    {
       Button button = new Button();
       button.AddClick(ButtonHandler);
       button.SimulateClick();
       button.RemoveClick(ButtonHandler);
    }
}

In this example, the AddClick() and RemoveClick() methods have been added, and the delegate field is now private. It’s now impossible for users of the class to do the wrong thing when they hook or unhook.

This example is reminiscent of the example in Chapter 16. You had two accessor methods, and adding properties made those two methods look like a field. Let’s add a feature to the compiler so there’s a “property-like” delegate named Click. The compiler can write the AddClick() and RemoveClick() methods for you, and it can also change a use of + = or - = to the appropriate add or remove call. This gives you the advantage of having the Add and Remove methods without having to write them.

You need a keyword for this compiler enhancement, and event seems like a good choice.

using System;
public class Button
{
    public delegate void ClickHandler(object sender, EventArgs e);
    public event ClickHandler Click;
    protected void OnClick()
    {
       if (Click != null)
       {
            Click(this, EventArgs.Empty);
       }
    }
    public void SimulateClick()
    {
       OnClick();
    }
}
class Test
{
    static public void ButtonHandler(object sender, EventArgs e)
    {
       Console.WriteLine("Button clicked");
    }
    public static void Main()
    {
       Button button = new Button();
       button.Click + = ButtonHandler;
       button.SimulateClick();
       button.Click - = ButtonHandler;
    }
}

When the event keyword is added to a delegate, the compiler creates a private field to store the delegate and creates public add_Click() and remove_Click() methods. It also emits a bit of metadata that says there is an event named Click and associates the event with the add and remove methods so that object browsers can tell there’s an event on this class.

In Main(), the event is accessed as if it were a delegate, but since the add and remove methods are the only ways to access the private delegate, + = and - = are the only operations that can be performed on the event.

That’s the basic story for events. The arguments to the event handler, object sender and EventArgs e, are by convention and should be followed by other classes that expose events. The sender argument allows the user of the code to know which object fired the event, and the e argument contains the information associated with the event. In this case, there’s no additional information to pass, so EventArgs is used. If additional information needed to be passed, a class should be derived from EventArgs with the additional information. For example, the KeyEventArgs class in the .NET Framework looks like this:

using System;
using System.Windows.Forms;
class KeyEventArgs: EventArgs
{
    Keys    keyData;
    KeyEventArgs(Keys keyData)
    {
       this.keyData = keyData;
    }
    public Keys KeyData
    {
       get
       {
            return(keyData);
       }
    }
    // other functions here...
}

The OnKey method will take a parameter of type Keys, encapsulate it into a KeyEventArgs class, and then call the delegate.

Safe Event Invocation

The previous examples use code that looks like this to call events:

if (Click != null)
{
    Click(this, EventArgs.Empty);
}

In a multithreaded environment, there is a small possibility that the thread might be interrupted after the null check but before the event was called and that the interrupting thread would unsubscribe from the event.

This can be prevented through the following construct:

ClickHandler clickHandler = Click;
if (clickHandler != null)
{
    clickHandler(this, EventArgs.Empty);
}

The clickHandler is now guaranteed to be non-null when the code calls it.

image Note  This has really only moved the race condition; there is now a case where the user may have unsubscribed to an event but could still be called after the unsubscription. There are some examples of using locking to prevent this, but it seems like a very corner case to me.

EventHandler <T>

The base class library provides the generic delegate EventHandler <T>. This delegate can be used instead of declaring an event-specific delegate. Following the first example, you would declare the Key event as follows:

public delegate void KeyHandler(object sender, KeyEventArgs e);
public event KeyHandler KeyDown;

Using EventHandler <T>, this can be simplified to the following:

public event EventHandler <KeyEventsArgs> KeyDown;

WHAT ABOUT EVENTARGS <T> ?

The EventHandler <T> delegate makes it easy to declare the event, but if you want to pass information along with the event, you still have to create a separate class derived from EventArgs. Many people have requested a generic EventArgs <T> class3 so that if all you wanted to pass was a string, you could write something like this:

public event EventHandler <EventArgs <string>> StringReceived;

Then you would be done with it. This is also very useful if you want to pass a class that is already defined (such as an Employee class) in the event args where the class isn’t derived from EventArgs.4

It’s useful enough that you can find an implementation of EventArgs <T> if you search for EventArgs <T> in your favorite search engine. I’ve written a basic implementation and found it to be quite useful in some scenarios.

One downside of this approach is that it is tempting to use a predefined type, such as an int, to pass data that would be more understandable if it were stored in a specific class. It also prevents you from adding more properties to your event args class without breaking the users of your class.

Custom Add and Remove

Because the compiler creates a private delegate fields for every event that is declared, a class that declares numerous events will use one field per event. The Control class in System.Windows.Forms declares more than 25 events, but there are usually just a couple of these events hooked up for a given control. Defining 25 fields and using only a few is wasteful. What’s needed is a way to avoid allocating the storage for the delegate unless it is needed.

The C# language supports this by allowing the add() and remove() methods that are provided by the compiler to be replaced with an implementation that stores the delegates in a more space-efficient manner. One typical way of doing this is to define a Dictionary to store the delegates.

public class Button
{
    ConcurrentDictionary <object, EventHandler> m_delegateStore =
       new ConcurrentDictionary <object, EventHandler> ();
    static object clickEventKey = new object();
    public event EventHandler Click
    {
       add
       {
            m_delegateStore.AddOrUpdate(
                clickEventKey,
                value,
                (key, oldValue) =>
                   (EventHandler)Delegate.Combine(oldValue, value));
       }
       remove
       {
            m_delegateStore.AddOrUpdate(
                clickEventKey,
                null,
                (key, oldValue) =>
                   (EventHandler)Delegate.Remove(oldValue, value));
       }
    }
    protected void OnClick()
    {
       EventHandler handler;
       if (m_delegateStore.TryGetValue(clickEventKey, out handler))
       {
            handler(this, EventArgs.Empty);
       }
    }
    public void SimulateClick()
    {
       OnClick();
    }
}
class Test
{
    static public void ButtonHandler(object sender, EventArgs e)
    {
       Console.WriteLine("Button clicked");
    }
    public static void Main()
    {
       Button button = new Button();
       button.Click + = ButtonHandler;
       button.SimulateClick();
       button.Click - = ButtonHandler;
    }
}

The add() and remove() methods are written using a syntax similar to the one used for properties, and they use the m_delegateStore concurrent dictionary to store the delegate. One problem with using a hash table is coming up with a key that can be used to store and fetch the delegates. There’s nothing associated with an event that can serve as a unique key, so clickEventKey is an object that’s created only so that you can use it as a key for the dictionary. It’s static because the same unique value can be used for all instances of the Button class.

1 This isn’t an “event” in the C# sense of the word but just the abstract concept of something happening.

2 This syntax may look weird since a new instance of the delegate is created just so it can be removed from the delegate. When Delegate.Remove() is called, it needs to find the delegate in the invocation list, so a delegate instance is required.

3 I don’t happen to know the base class library team’s opinion in this area, but if it were added now, it would conflict with the existing EventArgs <T> classes that people are already using, so calling it that would be a breaking change. Perhaps they could call it EventArgsEx <T>.

4 And more often, can’t be defined that way, since C# supports only single inheritance.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.222.204.247