Events
An event lets one object announce that something happened while others subscribe with += and unsubscribe with -=.
public event EventHandler<TEventArgs>? Changed; // raise with Changed?.Invoke(this, e);An event is the language's publish-and-subscribe member. One object announces that something happened. Any number of other objects ask to be told. The announcer never learns who is listening.
An event is a delegate member with its access restricted. From outside the declaring type you may only add a handler with += and remove one with -=. You cannot read it, replace it with =, or raise it. Only code inside the declaring type can do those things. That restriction is the point: one subscriber cannot clear the others or fire the event itself.
The standard shape
EventHandler<TEventArgs> is the delegate type to use. In .NET 10 it is declared public delegate void EventHandler<in TEventArgs>(object? sender, TEventArgs e);. The sender is the object that raised the event and e carries the data. For an event with no data, use the non-generic EventHandler and pass EventArgs.Empty. TEventArgs no longer has to derive from EventArgs, though deriving from it remains the convention.
Raising it
An event with no subscribers is null, so declare it nullable and raise it with Changed?.Invoke(this, e);. Handlers run on the thread that raised the event, one after another in subscription order, and an exception thrown by one handler stops the remaining ones.
The lasting trap is lifetime. Subscribing makes the publisher hold a reference to the subscriber, so a long-lived publisher keeps a short-lived subscriber alive forever unless you unsubscribe.
Example
Thermostat thermostat = new();
void OnReading(object? sender, int temperature)
{
Console.WriteLine($"reading: {temperature}");
}
thermostat.ReadingTaken += OnReading; // subscribe
thermostat.Record(21);
thermostat.ReadingTaken -= OnReading; // unsubscribe
thermostat.Record(22); // nobody is listening, so nothing prints
class Thermostat
{
// Outside this class, only += and -= are allowed on this member.
public event EventHandler<int>? ReadingTaken;
public void Record(int temperature)
{
// ?.Invoke does nothing when there are no subscribers.
ReadingTaken?.Invoke(this, temperature);
}
}
// Output:
// reading: 21When to use it
- Tell interested code that a download finished without the downloader knowing who cares.
- Raise a changed notification from a settings object so a screen can refresh.
- Unsubscribe in Dispose so a long-lived publisher stops holding a short-lived subscriber alive.
More examples
Custom event data and the OnX method
The shape nearly all .NET events follow: a class derived from EventArgs carrying the data, and a protected virtual OnX method as the only place the event is raised. Follow it when the event belongs to a type others may inherit from.
public class OrderPlacedEventArgs : EventArgs
{
public OrderPlacedEventArgs(string orderId) => OrderId = orderId;
public string OrderId { get; }
}
public class OrderBook
{
public event EventHandler<OrderPlacedEventArgs>? OrderPlaced;
public void Place(string orderId)
{
OnOrderPlaced(new OrderPlacedEventArgs(orderId));
}
// The convention: a protected virtual method raises the event, so a
// derived class can observe it or decline to raise it.
protected virtual void OnOrderPlaced(OrderPlacedEventArgs e)
{
OrderPlaced?.Invoke(this, e);
}
}Unsubscribing a lambda
Subscribing with an inline lambda is convenient and permanent. Keep the delegate in a field whenever the subscriber's life is shorter than the publisher's, and remove it in Dispose.
OrderBook book = new();
// Store the handler. Without a variable there is nothing to pass to -=.
EventHandler<OrderPlacedEventArgs> handler = (sender, e) => Console.WriteLine(e.OrderId);
book.OrderPlaced += handler;
book.Place("A-1");
book.OrderPlaced -= handler;
book.Place("A-2"); // prints nothing
// Output:
// A-1An event with nothing to report
When the fact that something happened is the whole message, there is no data class to write: use EventHandler and pass the shared EventArgs.Empty instance rather than allocating a new EventArgs.
public class Door
{
// The non-generic EventHandler has the signature
// void (object? sender, EventArgs e). It is a separate, older type,
// not EventHandler<EventArgs>.
public event EventHandler? Closed;
public void Close()
{
Closed?.Invoke(this, EventArgs.Empty);
}
}
Discussion