When using delegates, two emergent roles commonly appear:
broadcaster and subscriber. The
broadcaster is a type that contains a delegate field. The broadcaster
decides when to broadcast, by invoking the delegate. The
subscribers are the method target recipients. A
subscriber decides when to start and stop listening, by calling +=
and −=
on
the broadcaster’s delegate. A subscriber does not know about, or interfere
with, other subscribers.
Events are a language feature that formalizes this pattern. An
event
is a construct that exposes just
the subset of delegate features required for the broadcaster/subscriber
model. The main purpose of events is to prevent subscribers from
interfering with each other.
The easiest way to declare an event is to put the event
keyword in
front of a delegate member:
public class Broadcaster
{
public event
ProgressReporter Progress;
}
Code within the Broadcaster
type
has full access to Progress
and can
treat it as a delegate. Code outside of Broadcaster
can only perform +=
and −=
operations on the Progress
event.
In the following example, the Stock
class fires its PriceChanged
event every time the Price
of the Stock
changes:
public delegate void PriceChangedHandler
(decimal oldPrice, decimal newPrice);
public class Stock
{
string symbol; decimal price;
public Stock (string symbol) { this.symbol = symbol; }
public event PriceChangedHandler PriceChanged;
public decimal Price
{
get { return price; }
set
{
if (price == value) return;
// Fire event if invocation list isn't empty:
if (PriceChanged != null)
PriceChanged (price, value);
price = value;
}
}
}
If we remove the event
keyword
from our example so that PriceChanged
becomes an ordinary delegate field, our example would give the same
results. However, Stock
would be less
robust, in that subscribers could do the following things to interfere
with each other:
Replace other subscribers by reassigning PriceChanged
(instead of using the +=
operator).
Clear all subscribers (by setting PriceChanged
to null
).
Broadcast to other subscribers by invoking the delegate.
Events can be virtual, overridden, abstract, or sealed. They can also be static.
The .NET Framework defines a standard pattern for writing events. Its purpose is to provide consistency across both Framework and user code. Here is the preceding example refactored with this pattern:
public class PriceChangedEventArgs: EventArgs
{ public readonly decimal LastPrice, NewPrice; public PriceChangedEventArgs (decimal lastPrice, decimal newPrice) { LastPrice = lastPrice; NewPrice = newPrice; } } public class Stock { string symbol; decimal price; public Stock (string symbol) { this.symbol = symbol; } public eventEventHandler<PriceChangedEventArgs>
PriceChanged;protected virtual void OnPriceChanged
(PriceChangedEventArgs e)
{ if (PriceChanged != null) PriceChanged (this, e); } public decimal Price { get { return price; } set { if (price == value) return; OnPriceChanged (new PriceChangedEventArgs (price, value)); price = value; } } }
At the core of the standard event pattern is System.EventArgs
: a
predefined Framework class with no members (other than the static
Empty
property). EventArgs
is a base class for conveying
information for an event. In this example, we subclass Event
Args
to convey
the old and new prices when a PriceChanged
event is fired.
The generic System.EventHandler
delegate is also part of the .NET Framework and is defined as
follows:
public delegate void EventHandler<TEventArgs> (object source, TEventArgs e) where TEventArgs : EventArgs;
Before C# 2.0 (when generics were added to the language), the
solution was to instead write a custom event-handling delegate for
each EventArgs
type, as
follows:
delegate void PriceChangedHandler (object sender, PriceChangedEventArgs e);
For historical reasons, most events within the Framework use delegates defined in this way.
A protected virtual method, named On
-event-name, centralizes firing of the
event. This allows subclasses to fire the event (which is usually
desirable) and also allows subclasses to insert code before and after
the event is fired.
Here’s how we could use our Stock
class:
static void Main() { Stock stock = new Stock ("THPW"); stock.Price = 27.10M; stock.PriceChanged += stock_PriceChanged; stock.Price = 31.59M; } static void stock_PriceChanged (object sender, PriceChangedEventArgs e) { if ((e.NewPrice - e.LastPrice) / e.LastPrice > 0.1M) Console.WriteLine ("Alert, 10% price increase!"); }
For events that don’t carry additional information, the Framework
also provides a nongeneric EventHandler
delegate. We can demonstrate this
by rewriting our Stock
class such
that the PriceChanged
event fires
after the price changes. This means that no
additional information need be transmitted with the event:
public class Stock { string symbol; decimal price; public Stock (string symbol) {this.symbol = symbol;} public eventEventHandler
PriceChanged; protected virtual void OnPriceChanged (EventArgs e) { if (PriceChanged != null) PriceChanged (this, e); } public decimal Price { get { return price; } set { if (price == value) return; price = value; OnPriceChanged (EventArgs.Empty
); } } }
Note that we also used the EventArgs.Empty
property—this saves
instantiating an instance of EventArgs
.
An event’s accessors are the implementations of its +=
and −=
functions. By default, accessors are implemented implicitly by the
compiler. Consider this event declaration:
public event EventHandler PriceChanged;
The compiler converts this to the following:
A private delegate field.
A public pair of event accessor functions, whose
implementations forward the +=
and −=
operations to the private
delegate field.
You can take over this process by defining
explicit event accessors. Here’s a manual
implementation of the PriceChanged
event from our previous example:
EventHandler _priceChanged; // Private delegate public event EventHandler PriceChanged { add { _priceChanged += value; } remove { _priceChanged −= value; } }
This example is functionally identical to C#’s default accessor implementation (except that C# also ensures thread safety around updating the delegate). By defining event accessors ourselves, we instruct C# not to generate default field and accessor logic.
With explicit event accessors, you can apply more complex strategies to the storage and access of the underlying delegate. This is useful when the event accessors are merely relays for another class that is broadcasting the event, or when explicitly implementing an interface that declares an event:
public interface IFoo { event EventHandler Ev; } class Foo : IFoo { EventHandler ev; event EventHandler IFoo.Ev { add { ev += value; } remove { ev −= value; } } }