In C and C++ programs, callback functions and function pointers are often used for event notification. In C#, a specific type of delegate known as an event provides event notifications to clients. Event handling is a common idiom for developing applications with C#. For example, Windows Forms applications consume events to receive notifications about mouse movement and behavior, menu selection, button clicks, and similar occurrences.
Events simplify the task of event notification in C# applications. Because they’re based on delegates, events are type-safe and have a well-defined model for connecting a single producer with multiple consumers. Events also have specific programming guidelines that simplify their use as one-way notification methods. Some of the usage conventions for delegates are mandatory and are enforced by the compiler; other usage patterns are simply recommended and exist to make your events more usable.
The C# compiler enforces a key restriction for events: outside the class that declares an event, very little access is granted to an event field. A client of the enclosing class can add or remove event handling methods using the overloaded += and -= operators, as follows:
mainForm.MouseMove += new MouseEventHandler(OnMouseMove);
It’s reasonable for a client to use the += or -= operators with events, as this is the basic way for a client to manage event notifications. However, the client isn’t allowed any other access to the event. Other than the operations shown here, client classes have no access to the events in a class. Although a delegate can be invoked by any client with access, an event can be raised only by the class that contains the event.
Because events are used as one-way notifications to (potentially) many interested parties, the event delegate is typically declared as returning void. If you find that your event needs to pass information as a return value, you should use a delegate as described in the previous section instead of using the event pattern.
By convention, event handling delegates are declared with names ending with EventHandler and have two parameters, sender and e, as shown here:
public delegate void OverdraftEventHandler(object sender, EventArgs e);
The first parameter, sender, is a reference to the object that raised the event. This parameter is always declared as object—even when a more specialized type is known to be the sender. The second parameter, e, is an EventArgs object that describes the event. EventArgs is a base class for event argument classes and doesn’t carry any useful information to event subscribers.
If your event must pass additional information to event subscribers, subclass the EventArgs class and embed your additional information as properties in that class, as shown here:
public class DepositEventArgs: EventArgs { public DepositEventArgs(decimal aDeposit) { _deposit = aDeposit; } public decimal Deposit { get { return _deposit; } } protected decimal _deposit; }
This example follows the convention for naming EventArgs subclasses with names that end with EventArgs.
Events are used much like delegates, with a few differences. First, the event keyword is used to declare a field for the event, as follows:
public delegate void OverdraftEventHandler(object sender, OverdraftEventArgs e); public event OverdraftEventHandler OnOverdraftHandler;
The event keyword signals to the C# compiler that this delegate is used as an event, causing the compiler to place restrictions on its use, as described earlier.
If your event field is public, a client can subscribe to an event using the += operator to concatenate a new event handling delegate to the existing delegates. A useful design pattern is to provide methods that add and remove event handling delegates. The AddOnEventName method is used to add event handlers, like this:
myAccount.AddOnDeposit(new Account.DepositHandler(OnDeposit));
The RemoveOnEventName method is used to remove an event handler, like this:
myAccount.RemoveOnDeposit(new Account.DepositHandler(OnDeposit));
The implementation of the AddOnEventName and RemoveOnEventName methods simply uses the += and -= operators, like this:
public void AddOnOverdraft(OverdraftEventHandler handler) { OnOverdraftHandler += handler; } public void RemoveOnOverdraft(OverdraftEventHandler handler) { OnOverdraftHandler -= handler; }
As mentioned, an event can be raised only from within the class that declares the event. By convention, events have two parameters that must be properly initialized when the event is raised:
Sender The object that has raised the event
e An object that contains event arguments
Raising an event is much like invoking a delegate callback, as shown here:
EventArgs args = new EventArgs(); if(OnStrikeoutHandler != null) OnStrikeoutHandler(this, args);
In this code, a new instance of EventArgs is created and passed with the event. This example shows an event that has no event-specific event arguments and uses the EventArgs base class as a placeholder. In a case like this, it’s reasonable to create one EventArgs object, maintain a reference to it, and send the same EventArgs object each time the event is raised.
It’s a good idea to encapsulate your event-raising code in a method, as shown in the following example. This isolates the code in a single method and makes it possible for subclasses to raise events. By convention, this method is named OnEventName.
protected void OnStrikeout() { EventArgs args = new EventArgs(); if(OnStrikeoutHandler != null) OnStrikeoutHandler(this, args); }
When an event is raised, the client can invoke operations on your object. Keep in mind that an event handler can raise exceptions when handling an event, so you might want to use try, catch, and finally blocks around the code that raises the event.
To demonstrate using events with C#, let’s revisit the Bank example presented earlier, this time using an event instead of a callback delegate. The new version of the Bank example will raise an event when an overdraft occurs rather than invoke a delegate method.
The following code presents a new version of the Account class. In this version, the DebitPolicy delegate has been replaced by OverdraftEventHandler.
public class Account { public delegate void OverdraftEventHandler(object sender, OverdraftEventArgs e); public event OverdraftEventHandler OnOverdraftHandler; public Account(decimal anInitialBalance) { _balance = anInitialBalance; } public decimal Balance { get { return _balance; } } public void Deposit(decimal aDeposit) { if(aDeposit < 0) throw new ArgumentOutOfRangeException(); _balance += aDeposit; } public bool Withdrawal(decimal aDebit) { if(aDebit < 0) throw new ArgumentOutOfRangeException(); if(aDebit < _balance) { _balance -= aDebit; return true; } OverdraftEventArgs args = new OverdraftEventArgs(_balance, aDebit); OnOverdraft(args); return false; } public void AddOnOverdraft(OverdraftEventHandler handler) { OnOverdraftHandler += handler; } public void RemoveOnOverdraft(OverdraftEventHandler handler) { OnOverdraftHandler -= handler; } protected void OnOverdraft(OverdraftEventArgs e) { if(OnOverdraftHandler != null) OnOverdraftHandler(this, e); } protected decimal _balance; }
This new version of Account implements a new event named OnOverdraftHandler that uses the OverdraftEventHandler delegate type. Clients subscribe to the event by calling the AddOnOverdraft method and can unsubscribe by calling RemoveOnOverdraft. Internally, the overdraft event is raised by calling OnOverdraft.
The overdraft event includes an OverdraftEventArgs object that describes the nature of the overdraft. The declaration of the OverdraftEventArgs class is shown here:
public class OverdraftEventArgs: EventArgs { public OverdraftEventArgs(decimal balance, decimal withdrawal) { _balance = balance; _withdrawal = withdrawal; } public decimal Balance { get { return _balance; } } public decimal Withdrawal { get { return _withdrawal; } } protected decimal _balance; protected decimal _withdrawal; }
The OverdraftEventArgs class is derived from EventArgs and adds two fields: the current balance and the attempted withdrawal amount.
To use the new version of the Account class and subscribe to the overdraft event, a client can use code like this:
Account myAccount = new Account(100); Account.OverdraftEventHandler handler = null; handler = new Account.OverdraftEventHandler(OnOverdraft); myAccount.AddOnOverdraft(handler);
After creating an Account object, the client subscribes to the overdraft event by creating an event handler and calling AddOnOverdraft. The method used as a target for the event handler is shown here:
static void OnOverdraft(object sender, OverdraftEventArgs e) { Console.WriteLine("An overdraft occurred."); Console.WriteLine("The account balance is {0}.", e.Balance); Console.WriteLine("The withdrawal was {0}.", e.Withdrawal); }
The OnOverdraft method uses the OverdraftEventArgs class to obtain information about the overdraft.