Events


In the first half of this chapter, we've seen how to use delegates to invoke methods on objects. We've seen how to use single-cast delegates to invoke single methods, and how to use multicast delegates to store multiple methods in an invocation list and call them in sequence. We've also seen that we can use delegates to invoke methods asynchronously, if required.

In the second half of this chapter, we turn our attention to events. The .NET Framework event model relies on delegates to indicate which methods to call when a particular event is raised. Events in the .NET Framework are implemented using delegates to store the subscribed event handling methods and to call them when the event is raised.

Most programmers will be familiar with the use of events in GUI applications. Interaction between user interface elements and application code is one of the most important uses of events, but events can be used in many other scenarios as well. Events in the .NET Framework are an implementation of the Observer design pattern. In this pattern, the object that can be observed exposes methods for registering and unregistering interested observer objects for notifications. When we program objects in C#, we can have most of this functionality created for us automatically.

click to expand

We'll examine the following issues in this section:

  • What is the event architecture in the .NET Framework?

  • How do we publish events?

  • How do we subscribe to events?

  • How do we raise events?

Event Architecture

Here is a brief summary of the event architecture in the .NET Framework. We've also provided a formal definition for some of the key terms associated with event processing in .NET:

An object that can raise events is known as an event source. For example, a Button object is an event source because it raises events such as Click and MouseEnter. The event source publishes the events it can raise; in C#, we use the event keyword to publish an event in a class definition. The type of an event is always a delegate type; the signature of this delegate type defines the signature of methods that can handle the event.

Objects that define event handler methods are known as event receivers. Event receivers subscribe to the events they wish to handle on the event source. An event receiver must provide an event-handler method with the correct signature for the event it is subscribing to. The event source uses a multicast delegate to keep track of these event-handler methods.

The event source raises events when something important happens. For example, Button objects raise the Click event when the user clicks the button control on the screen. When an event is raised, the event source automatically uses its multicast delegate to call the specified event-handler method on each event receiver.

The following diagram illustrates the relationship between an event-source object and event-receiver object(s):

click to expand

In this simple example, the event-source object only raises a single kind of event. The event-source object has an invocation list for this event, to keep track of the event-receiver objects that have subscribed to this event. Each entry in the invocation list stores two pieces of information:

  • Target is a reference to an event-receiver object

  • Method is a reference to the event-handler method on the event-receiver object

When the event source object raises an event, it uses the information in the invocation list to call each of the registered event-handler methods.

Note

In the above figure, we've only shown the event-handler methods in the event-receiver objects. In reality, the event-receiver objects will have additional methods, properties, fields, and so on.

It should not come as a surprise that .NET uses a multicast delegate to provide the infrastructure for maintaining an invocation list of targets and methods.

So much for the theory, let's see how to use events in real C# code. We'll begin by seeing how to define events in a class.

Publishing and Subscribing to Events

To publish events in C#, we must declare event members in the event-source class. We must specify two pieces of information for each event:

  • The name of the event
    For example, the Button class publishes events named Click, MouseOver, and so on. We should declare event members for things that can happen to instances of our class, which might interest other objects.

  • The signature (parameter list) of the event
    When the event-source object raises the event, it will supply values for these parameters. These values will be passed to the event-handler method in the event-receiver object. This implies the event-handler methods must have the same signature as the event. The signature of an event is set by declaring the type of the event. This type must be a delegate.

For a simple example of a class exposing an event, have a look at the BankAccount class in the SimpleEvent project. You can find this project in the folder DelegatesEvents\SimpleEvent in the source code download.

Publishing our Event

First, we have a look at the structure of the code publishing the event in the BankAccount class:

    using System;    public class BankAccount    {      public BankAccount(string name)      {        mBalance = 0;        mAccountName = name;      }      public event EventHandler Overdrawn;      private double mBalance;      private string mAccountName;      public string Name{get{return mAccountName;}}      public double Balance{get{return mBalance;}}      public void Credit(double Amount)      {      }      public void Debit(double Amount)      {      }    } 

This class is fairly simple. It holds two private fields, mBalance and mAccountName. Both can be accessed through a read-only property. The account name can only be set through the constructor, while the balance can be changed by calling the methods Debit() and Credit(), but these have no implementation yet (we will see them implemented soon, when we deal with raising events). The only remarkable line is the one defining the event member.

We have used System.EventHandler as the type of our event. System.EventHandler is a built-in delegate, specially intended for use with simple events that do not need to pass any additional information to the event receiver. It is used by many events in the framework (for example the Click event in Button).

Events are members of the class, and can therefore have an access modifier. The default accessibility is public, because the nature of events is to broadcast information to other objects in the application. However, it is also possible to restrict event accessibility if appropriate:

  • We can declare events as private, which means only methods in our own class can register as event-handler methods. We can use this technique to trigger behavior in our own class. For example, we might have a background thread that triggers a Tick event every second, and define event-handler methods in our class to perform tasks on each timer tick. Such a pattern is rare, but can lead to quite elegant code.

  • We can declare events as internal, which means only methods in the same assembly can register as event-handler methods. We can use this technique in large applications that are spread over several assemblies. To reduce dependencies between assemblies, we can define class members with internal accessibility so that they cannot be accessed in other assemblies. This makes it easier to change the code inside one assembly, without adversely affecting the code in any other assembly. We investigate assemblies in detail in Chapter 8.

  • We can declare events as protected, which means only methods in this class, or classes that inherit from this class, can register as event-handler methods.

The name of the event in this example is Overdrawn. Later in the chapter, we'll see how to raise this event when the customer tries to withdraw too much money from the account.

Events cannot have a return type. Events constitute a one-way flow of information from the event-source object to the event-receiver objects. It is an error to specify a return type in an event definition. As such, while events make use of delegates, it's clear there are ways we can use delegates that are not exposed by events. Ignoring the other capabilities of delegates and just using the event infrastructure would be a mistake.

The code looks deceivingly simple, but actually accomplishes several things:

  • Publishes the event name and signature for event receivers

  • Creates registration methods for registering and unregistering event receivers

  • Keeps track of registered event receivers in an invocation list

By compiling the file as a .dll file and looking at the executable with ILDASM.exe, we can see these three pieces of the functionality appearing in our class.

    C:\Class Design\Ch 06\DelegatesEvents\SimpleEvent>csc /t:library    BankAccount.cs    Microsoft (R) Visual C# .NET Compiler version 7.00.9466    for Microsoft (R) .NET Framework version 1.0.3705    Copyright (C) Microsoft Corporation 2001. All rights reserved.    C:\Class Design\Ch 06\DelegatesEvents\SimpleEvent>ildasm    BankAccount.dll 

click to expand

In the compiled MSIL code, we find four members related to our event declaration:

  • A private field of type EventHandler called OverDrawn
    This field will hold an instance of System.EventHandler. It is used to store all of the registered event handlers as one multicast delegate. When the object is instantiated it is set to null. Note that this field is always private, even is the event itself is public.

  • A public method called add_OverDrawn()
    The compiler generated this method to add a new event handler for the Overdrawn event. This method is called whenever another event-receiver subscribes to this event. Notice the add_Overdrawn() method receives a parameter of type EventHandler. This parameter is an instance of the EventHandler delegate type, and specifies a new event-receiver object and event-handler method for the Overdrawn event.

    Open up the IL and take a look at statements IL_0002, IL_0007, and IL_0008. These statements combine the new delegate instance into the Overdrawn multicast delegate member in our BankAccount object. Notice the MSIL call to the method System.Delegate::Combine(), to combine the new delegate into the multicast delegate.

  • A public method called remove_OverDrawn()
    The compiler generated this method, to remove an existing event handler for the Overdrawn event. This method is called whenever an event receiver unsubscribes from this event. Looking at the MSIL, you'll find remove_Overdrawn() is similar to add_Overdrawn(), except that we now call the System.Delegate: :Remove() method to remove the specified delegate.

  • An event declaration called OverDrawn
    This member is only used during compilation of classes using this class, to identify the names of the methods for adding and removing event handlers for the Overdrawn event. It's similar in form to the property members we saw in Chapter 5, which pointed to the getter and setter methods. The MSIL code for this member is shown below:

click to expand

So apparently, the C# compiler creates registration methods by just prefixing the name of the event with add_ and remove_. This means that you cannot use these names for your own methods. The following code cannot be compiled:

    public event EventHandler Overdrawn;    public void add_Overdrawn(EventHandler e)    {    } 

When you try to compile it, it will raise an exception: 'Class ‘BankAccount’ already defines a member called ‘add_Overdrawn’ with the same parameter types'.

Registering to a Published Event

Once we have a class publishing an event, we will try to register a method of ours as an event handler. To see how this works, have a look at the code for the BankAccountForm class in the same project.

Apart from the usual forms code, we find the following methods:

    private BankAccount mBankAccount;    public BankAccountForm()    {      //      // Required for Windows Form Designer support      //      InitializeComponent();      mBankAccount = new BankAccount("Teun Duynstee");      RefreshTitle();      mBankAccount.Overdrawn += new        EventHandler(this.OverdrawnHandlerMethod);    }    private void RefreshTitle()    {      this.Text = String.Format("Account {0}, balance: {1}",                    mBankAccount.Name, mBankAccount.Balance );    }    private void btnCredit_Click(object sender, System.EventArgs e)    {      mBankAccount.Credit(Double.Parse(txtAmount.Text));      RefreshTitle();    }    private void btnDebit_Click(object sender, System.EventArgs e)    {      mBankAccount.Debit(Double.Parse(txtAmount.Text));      RefreshTitle();    }    private void OverdrawnHandlerMethod(object sender,                      System.EventArgs e)    {      MessageBox.Show("The bank account has been overdrawn. " +         "The current balance is " + mBankAccount.Balance,         "Overdrawn",         MessageBoxButtons.OK,         MessageBoxIcon.Information);    } 

Note the following points:

  • In the form's constructor, we create an instance of BankAccount and attach the method OverdrawnHandlerMethod() as a handler for the Overdrawn event. The syntax is rather verbose; we create a delegate of the type specified by the event. This delegate points at our handler method. Using the += operator, we add this delegate to the list of registered event receivers for this event. The += syntax is the only way in C# to connect a handler method to an event. In other .NET languages, the syntax may be quite different, but in the MSIL, the result should be identical. This guarantees us that programmers in other .NET languages can register for our event without problems or special considerations.

  • The OverdrawnHandlerMethod() method just shows a message in a message box. The two parameters that are passed are not used. The first parameter is a reference to the object that raised the event. In our case, this is the mBankAccount object. We already have a reference to this object, so in this case we don't bother casting the sender parameter to BankAccount. The System.EventArgs object we get passed in the second parameter is rather useless. In more complex events, this object contains any additional information about the event. We will have a look at these event arguments later in this chapter.

  • The RefreshTitle, btnCredit_Click, and btnDebit_Click methods don't do anything that would surprise you. They just use the available methods and properties on BankAccount to allow a user to perform operations on the BankAccount object.

Let's look at the part of the code marked Windows Form Designer generated code:

    // A lot of other controls and properties have been omitted here    this.btnCredit = new System.Windows.Forms.Button();    this.btnDebit = new System.Windows.Forms.Button();    // btnCredit    this.btnCredit.Text = "Credit";    this.btnCredit.Click += new                            System.EventHandler(this.btnCredit_Click);    // btnDebit    this.btnDebit.Text = "Debit";    this.btnDebit.Click += new System.EventHandler(this.btnDebit_Click); 

Directly after instantiating the buttons and setting the properties, an EventHandler delegate instance is created, pointing to the methods in the form. Then the delegate instance is added to the Click event of the button.

When using a form's editor in a graphical development environment, you will normally have only one event handler method for each event on the form and any event handler method will normally handle only one event.

Raising the Event

For our simple example to work, we need the event to be raised. The obvious place to raise the Overdrawn event would be in the Debit() method of the BankAccount class. We had temporarily left the implementation of the Debit() and Credit() methods empty, but here is what they look like in the BankAccount class:

    public void Credit(double Amount)    {      mBalance += Amount;    }    public void Debit(double Amount)    {      mBalance -= Amount;      if (mBalance < 0)      {        if (Overdrawn != null)        {          Overdrawn(this, new EventArgs());        }      }    } 

Both Credit() and Debit() make the appropriate changes to the mBalance field, but in Debit(), we also check if the result is lower than 0. If this is the case, we want to raise the Overdrawn event. As Overdrawn is implemented as a multicast delegate, raising the event comes down to calling the delegate. Two important points should be noted here:

  • Before anyone has registered for the event, the Overdrawn field equals null. If you try to call null, a runtime exception will be thrown. When raising an event in C#, you must always first check if the delegate you call is not null.

  • Like all predefined event handler classes, the signature of System.EventHandler returns void and has two parameters. In the first parameter, you must always pass a reference to the event-raising object (this). The second parameter must be an instance of System.EventArgs. System.EventArgs is like a dummy class. It can contain no information at all and is intended for use with events that pass no information anyway (like Overdrawn) and to serve as a base class for events that do.

    The next section (Creating Events that Pass Information) will go into this. In our code, we created an instance of System.EventArgs by calling new EventArgs(), but EventArgs() also exposes a static property Empty that returns a new instance of the class as well. Some people prefer EventArgs.Empty to the new syntax.

Note

The Debit() and Credit() methods should really also check for negative amounts and throw an exception whenever this happens, but that is beyond the scope of this example.

That completes our simple events example. We have created a BankAccount class exposing an Overdrawn event. On specific occasions (when the Debit() method is called and the resulting balance is lower than 0), the class raises the event. In our sample application, we have a main form using an instance of the class for performing bank transactions. On loading the form, it creates a new instance of BankAccount and registers for the event. When an event occurs, the form displays a message box informing us of the situation.

click to expand

Creating Events that Pass Information

Leaving the really simple events behind us, we now move on to the next sample application: DelegatesEvents\AdvancedEvents. We will use this application to show several of the more advanced event features. Eventually, the application will consist of a large bank manager form, which can be used to create many small bank account forms (like the one in SimpleEvent). The bank manager can select for which of the active accounts to have the events logged to the event log screen. Besides the Overdrawn event, the bank accounts also raise an event for a large deposit (when the deposit is larger than the threshold value). When the manager changes the large deposit threshold, all of the bank account forms are notified of this fact.

When it is running, the application looks like this:

click to expand

The first thing we will look into is defining events that pass information to their event destination. Most of the time, the notification alone is not enough. In our sample, we have an event called LargeDeposit. When some object is interested in the fact that a large deposit has just occurred on our BankAccount instance, they probably also want to know how large a deposit it was. The System.EventHandler type does not allow us to pass this amount to the event receiver, but we can of course define another delegate that does. In the first half of this chapter we have seen how this is done. Based on that experience, you might want to use the following code:

    // Bad practice, do not do this    public event MyDelegate LargeDeposit;    public delegate void MyDelegate(BankAccount b, double Amount); 

You could just declare a delegate that has a signature that includes not only the BankAccount that raised the event, but also a double containing the amount of the deposit. This would actually work just fine, but it does not follow the convention that Microsoft follows for both naming and signatures for events, delegates, and their arguments.

The name of the delegate type should follow the form XxxEventHandler. The signature is standardized; the return value should be void and there are two parameters:

  • First parameter should refer to the object raising the event. It is often declared as an object, to allow any kind of object to use the event, but if you are sure that the event will only ever be raised by BankAccount() (in this case), you can also type it more specifically and prevent many unnecessary castings.

  • The second parameter should hold an instance of a subclass of System.EventArgs. This class should be able to hold all of the information that must be passed to the event handler. The name of the class should be equal to the name of the delegate, but ending in EventArgs instead of EventHandler.

Following this convention, other programmers will understand your code more easily, but there are some direct advantages as well. Because the signatures of event handler methods follow the same form, you can use a method designed for handling events of type System.EventHandler for all other event types as well.

First let us have a look at the declaration as it should look following the event naming convention; we will explain the parts afterwards:

    public delegate void BankTransactionEventHandler                    (BankAccount sender, BankTransactionEventArgs args);    public class BankTransactionEventArgs : EventArgs    {      public BankTransactionEventArgs(double amount)      {        mAmount = amount;      }      private double mAmount;      public double Amount      {        get{return mAmount;}      }    }    public class BankAccount    {      public event BankTransactionEventHandler LargeDeposit;      // Many other members omitted    } 

At the top you see the declaration of a delegate type. It is named BankTransactionEventHandler.

Next comes the definition of the BankTransactionEventArgs class. It is a subclass of EventArgs and defines one extra property, Amount, which is read-only and can only be set through the constructor.

Last, we see how the LargeDeposit event in BankAccount is declared as type BankTransactionEventHandler.

Now when an actual large deposit occurs, we need to raise this event. Let's have a look at how that works.

    private static double mThreshold = 10000;    public void Credit(double Amount)    {      mBalance += Amount;      if (Amount >= mThreshold)      {        if (LargeDeposit != null)        {          LargeDeposit(this, new BankTransactionEventArgs(Amount));        }      }    } 

We have added two things:

  • A private static field holding the threshold amount above which we consider a deposit a large deposit. This is obviously quite arbitrary, but it must at least be an equal value for all instances of BankAccount. Later we will add functionality for changing this value and for being notified of this change.

  • We have added the code for calling the delegate holding the registered event handlers. Of course, we must check if the delegate is null. When we call it, we pass an instance of BankTransactionEventArgs. We instantiate it passing the amount of the deposit in the constructor. The event handlers will use this object to get at the size of the deposit. It is rather important to keep the EventArgs object immutable (keep the properties read-only), because the registered event handlers will all get the same instance passed sequentially. You never know who will register for the event and what they will try to do to the EventArgs instance.

By following this convention, you allow others to define event handlers that can handle any event. A method that can handle events of type System.EventHandler can effectively be used to handle all events (all events defined following the convention that is). The handling method expects two parameters, one of type object (you can pass any object there) and one of type System.EventArgs. As BankTransactionEventArgs is a subclass of EventArgs, the method accepts that type as well.

So following the convention for signatures of events makes them easier to use, because other programmers will understand from the naming what the classes are for, and more widely applicable, as they can be wired to standard event handlers.

Defining Static Events

We have seen events as instance members of objects, but it is also possible to define events as static (members of the type, not of the instance). Static events are much less common than instance events, for reasons that will become apparent when we look at how to subscribe to events in the next section.

First look into the BankAccount class of the AdvancedEvents sample. In the class you will find the following code:

    public static event EventHandler ThresholdChanged;    private static double mThreshold = 10000;    public static double Threshold    {      set      {        mThreshold = value;        if (ThresholdChanged != null)        {          ThresholdChanged(null, EventArgs.Empty);        }      }      get{return mThreshold;}    } 

A new event has been defined, but apart from the added static keyword, it looks quite normal. We also defined a static property Threshold for exposing the mThreshold field. When a new value is set, the ThresholdChanged delegate is called (after checking if it's null).

Now if we look into the BankAccountForm class, which registers to the ThresholdChanged event, we see that registration does look a bit different. The constructor of the form contains this code:

    mBankAccount = new BankAccount(accountName);    BankAccount.ThresholdChanged += new                   EventHandler(this.AlertThresholdChanged); 

Note that the event registered to is not a member of the mBankAccount instance, but of the BankAccount itself. In fact, to register to this event, you don't even need an instance of the class.

In the bankmanagerform, we expose the functionality to change the threshold value through the static Threshold property on BankAccount:

    private void btnSetThreshold_Click(object sender, System.EventArgs e)    {      BankAccount.Threshold = Double.Parse(txtThreshold.Text);    } 

As soon as the manager clicks the btnSetThreshold button, the static field in BankAccount changes value, changing the behavior of all instances of BankAccount. As soon as this has happened, all of the BankAccountForm instances will receive a notification of this event by way of a call to the AlertThresholdChanged() method. If you look at our executable in ildasm, you will see that a static event (just like a normal event) generates a private delegate field, two registration methods, and an event member. Only now all of these are marked static:

click to expand

Dynamically Registering and Unregistering

Event handlers are registered at the time of creation of objects and remain valid during the whole lifetime of the object. This is always the case for event handlers for controls on forms when you use a forms builder to wire the events. However, this is by no means necessary; you can register and unregister your interest in receiving certain notifications during the run time of the program. For example, in the sample application we allow the bank manager to select which bank accounts to monitor through the log window. Only for these selected accounts will events like Overdrawn and LargeDeposit result in a line in the log textbox.

To achieve this, we have two methods in place, one for logging the Overdrawn event and one for logging the LargeDeposit event. When we create a BankAccountForm (which in turn creates a BankAccount object), we add the created BankAccount to the listbox lstUnlogged. Only when the user clicks the button Log >>, we start wiring the events on the selected BankAccount to the appropriate methods.

    private void btnLog_Click(object sender, System.EventArgs e)    {      if (lstUnlogged.SelectedItem is BankAccount)      {        BankAccount b = (BankAccount)lstUnlogged.SelectedItem;        b.Overdrawn += new EventHandler(this.OverdrawnHandler);        b.LargeDeposit += new            BankTransactionEventHandler(this.LargeDepositHandler);        lstLogged.Items.Add(b);        lstUnlogged.Items.Remove(b);      }    } 

To prevent any runtime casting exceptions, we first test that the selected list item really is an instance of BankAccount (in this application, the only other thing that might be returned is null). If it is, we cast it to BankAccount and register the method OverdrawnHandler() to the Overdrawn event and the method LargeDepositHandler() to the event LargeDeposit. After that, we move the selected BankAccount from the list of items that are not logged to the list of items that are.

The reverse action is of course very much alike.

    private void btnUnlog_Click(object sender, System.EventArgs e)    {      if (lstLogged.SelectedItem is BankAccount)      {        BankAccount b = (BankAccount)lstLogged.SelectedItem ;        b.Overdrawn -= new EventHandler(this.OverdrawnHandler);        b.LargeDeposit -= new                BankTransactionEventHandler(this.LargeDepositHandler);        lstLogged.Items.Remove(b);        lstUnlogged.Items.Add(b);      }    } 

You can probably imagine what happens in the two event handler methods, so we will not show them here. In the screenshot in the Creating Events that Pass Information section, you can see how three accounts have been created, but we see events appearing in the bank manager form only from two of the three accounts. The account named Dave is still in the list of unlogged accounts.

The beauty of the concept of events is very clear in this example: it is the object receiving the events that decides which events are interesting (or when they are interesting). Methods are implemented only for these events. The object raising the events does not have to worry about who should receive which notification, it can just call the event delegate and this will make sure that all registered event handlers will be called. The part that does the administration of event handlers is hidden from us in the two methods add_eventname() and remove_eventname().

Defining your own Registration Methods

Since the administration of registrations is hidden from us, we can prevent administrative mistakes. But you may need to change the generated MSIL, to be able to do things differently. To achieve this, you can define your own custom add and remove methods for your events.

C# offers us a special syntax, very much like the syntax for defining accessor methods on properties. Keep in mind though it is all or nothing. The event declaration generates both methods and a private field of a delegate type. If you choose to define custom accessor methods, you must define all of these: the add() method, the remove() method, and the storage of the invocation list (you could choose another implementation than a multicast delegate).

In the sample application DelegatesEvents\CustomAddRemove we show how to modify the registration methods. The application has two buttons, one to create a new event destination and register it to the event source and a second button to force the raising of the event from the event source. All of the objects have access to a textbox for logging.

  • The event source logs a line just before it raises the event

  • The event receiver logs a line when it receives an event notification

  • The event source logs a line when it notices that a new event receiver has registered or unregistered. This is of course the functionality we want to demonstrate. We placed the logging code in the event registration methods.

The running application looks like this:

click to expand

We will only show the code for the event declaration. You should be familiar with all of the other functionality by now.

    private EventHandler mHappening;    public event EventHandler Happening    {      add      {        mHappening = (EventHandler)Delegate.Combine(mHappening, value);        mTextBox.Text += "A new handler has registered for                                                Happening\r\n";      }      remove      {        mHappening = (EventHandler)Delegate.Remove(mHappening, value);        mTextBox.Text += "A new handler has unregistered for                                                  Happening\r\n";      }    } 

Note how we declared two blocks of code within the event declaration. The code block after add will eventually be compiled into a method called add_Happening(), while the code behind remove will turn up in the remove_Happening() method in the MSIL. Apart from the lines adding some text to the logging textbox, the implementation as shown here is exactly identical to the default implementation. We declared a private field of type EventHandler to hold all of the event handler methods. The only thing we do for administering the methods is calling Delegate.Combine() in the add method and Delegate.Remove() in the remove methods. Both methods get an instance of EventHandler passed which can be accessed through the value keyword.

One other difference is in the naming. When we used the default implementation, the compiler generated a private field with a name identical to the name of the event (in this case, the private field would have been called Happening). This is fine in MSIL; an event and a private field can have the same name without problems. In C#, however, this is a naming conflict: the compiler would throw The class ‘CustomAddRemove.EventGenerator’ already contains a definition for ‘Happening’. So we have to choose a different name for the field (we chose mHappening) and we must use this name when we want to raise the event as well:

    public void SimulateEvent()    {      mTextBox.Text += "EventGenerator is raising the event.\r\n";      if (mHappening != null)      {        mHappening(this, EventArgs.Empty);      }    } 

Most developers will never need the ability to define custom registration methods and to ability to add some logging may not seem a very convincing application. Still, there are actually some real-world situations where this technique can make your code better and faster. In the framework library for example, most controls (both windows forms and ASP.NET) use custom registration methods.

The System.Windows.Forms.Control class is the base class for almost everything on a form: all of the controls, like buttons, labels checkboxes, and also the form itself. Control exposes 58 events. On an average form, there are many controls, most of which are very simple, and the large majority of the events are used only very seldom. Still, every instance of a control reserves some memory to hold the private delegate field (for keeping track of the event handlers). To keep controls lightweight, the implementers of the framework decided not to use a delegate instance per event, but to have one collection to store all of the event handlers of the control. When the control needs to raise a specific event, it knows how to retrieve the correct delegate from the collection and calls it. If no events are used at all, the control uses only the memory for the empty collection.

Note

Actually, this approach is not implemented in Control itself, but in its base class System.ComponentModel.Component. This class exposes the protected Events property, which all of its subclasses can use to store seldom used events in.

Role of Events in the Type Interfaces of the .NET Framework

Events in the .NET Framework are members just like properties and methods. A subclass inherits all of the events, including handler methods, from its base class. Even interfaces can contain events.

When we build a class that exposes an event, we tend to see the event and its implementation as one entity, one member. In MSIL though, the event, the private field to store the invocation list, and the registration methods are separate items. The event is just the published name and its references to the two registration methods. On the level of compiled .NET assemblies, the registration methods are methods like any other method. Also, the private field that holds the multicast delegate is a private field just like fields that have no relation with an event. The .NET runtime doesn't know it to be connected to the event. Keeping this in mind, we can explain the behavior of events in scenarios of inheritance and overriding.

If the event is declared virtual in the base class, you can override it. The declaration in the subclass would look like this (this code does not appear in the sample application):

    // Valid code, but will not work as you would expect    public override event EventHandler Happening; 

As you can see from the comment above it, this code will compile, but it will not work as you might expect. The override event will generate a new version of the event publication and a new version of the registration methods. These will all override the original implementation. It will also create a new private field for holding the multicast delegate. Now when some other class registers for an event, the event handler will be stored in the new private field in the subclass. However, if the base class tries to raise the event, it will call the delegate in the private field of the base class. This is because the default implementation of the event creates the field as private; the base class and subclass cannot refer to the same delegate. This makes this form of overriding pretty useless.

So to implement events that can be overridden will need custom implementation of the storage field. You can just keep the default implementation of the add() and remove() methods, but declare the delegate field as protected. Then in the subclass, you can effectively override the registration methods, but keep using the protected field for storage and for raising the event:

    class BaseClass    {      protected EventHandler mHappening;      public virtual event EventHandler Happening      {        add        {          mHappening = (EventHandler)Delegate.Combine(mHappening, value);        }        remove        {          mHappening = (EventHandler)Delegate.Remove(mHappening, value);        }      }    }    class SubClass : BaseClass    {      // No field declaration here      public override event EventHandler Happening      {        add        {          mHappening = (EventHandler)Delegate.Combine(mHappening, value);          mTextBox.Text += "A new handler has registered for                            Happening\r\n";        }        remove        {          mHappening = (EventHandler)Delegate.Remove(mHappening, value);          mTextBox.Text += "A new handler has unregistered for                            Happening\r\n";        }      }    } 

If the base class is using the default implementation, there is no way to implement a new registration method and still have access to the original field.




C# Class Design Handbook(c) Coding Effective Classes
C# Class Design Handbook: Coding Effective Classes
ISBN: 1590592573
EAN: 2147483647
Year: N/A
Pages: 90

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