C# events — EventHandler, custom EventArgs, raise and subscribe to events
Events are the standard C# mechanism for the observer pattern — a publisher raises an event, and any number of subscribers handle it. Events are built on delegates, but the event keyword enforces encapsulation: only the declaring class can raise the event.
Basic Event with EventHandler
EventHandler is the built-in delegate type for events with no custom data.
public class Button
{
// Declare the event
public event EventHandler? Clicked;
// Raise the event
public void Click()
{
Console.WriteLine("Button clicked.");
Clicked?.Invoke(this, EventArgs.Empty);
}
}
var button = new Button();
// Subscribe with a lambda
button.Clicked += (sender, e) => Console.WriteLine("Handler 1: button was clicked!");
// Subscribe with a named method
void OnClicked(object? sender, EventArgs e)
=> Console.WriteLine("Handler 2: responding to click.");
button.Clicked += OnClicked;
button.Click();
// Button clicked.
// Handler 1: button was clicked!
// Handler 2: responding to click.
// Unsubscribe
button.Clicked -= OnClicked;
button.Click();
// Button clicked.
// Handler 1: button was clicked! ← only one handler nowCustom EventArgs
When you need to pass data with the event, inherit from EventArgs.
// Custom event data
public class OrderPlacedEventArgs : EventArgs
{
public int OrderId { get; }
public string Customer { get; }
public decimal Total { get; }
public OrderPlacedEventArgs(int orderId, string customer, decimal total)
{
OrderId = orderId;
Customer = customer;
Total = total;
}
}
public class OrderService
{
// EventHandler<T> carries the custom EventArgs
public event EventHandler<OrderPlacedEventArgs>? OrderPlaced;
public void PlaceOrder(int id, string customer, decimal total)
{
// ... save order logic ...
Console.WriteLine($"Order #{id} saved.");
// Raise the event
OrderPlaced?.Invoke(this, new OrderPlacedEventArgs(id, customer, total));
}
}
var service = new OrderService();
service.OrderPlaced += (sender, e) =>
Console.WriteLine($"Email sent to {e.Customer} for order #{e.OrderId} — {e.Total:C}");
service.OrderPlaced += (sender, e) =>
Console.WriteLine($"Inventory reserved for order #{e.OrderId}");
service.PlaceOrder(1001, "Alice", 149.99m);
// Order #1001 saved.
// Email sent to Alice for order #1001 — $149.99
// Inventory reserved for order #1001Safe Event Invocation
Always use the null-conditional operator ?.Invoke(...) — it captures a thread-safe copy of the delegate before invoking.
public class Sensor
{
public event EventHandler<double>? TemperatureChanged;
private double _temperature;
public double Temperature
{
get => _temperature;
set
{
if (value == _temperature) return;
_temperature = value;
// Safe invocation — thread-safe, no NullReferenceException
TemperatureChanged?.Invoke(this, value);
}
}
}
var sensor = new Sensor();
sensor.TemperatureChanged += (s, temp) => Console.WriteLine($"Temp changed: {temp}°C");
sensor.Temperature = 22.5; // Temp changed: 22.5°C
sensor.Temperature = 22.5; // No event — same value
sensor.Temperature = 37.0; // Temp changed: 37°CUnsubscribing to Avoid Memory Leaks
If a subscriber lives longer than the publisher, failing to unsubscribe keeps both objects in memory (the publisher holds a reference via the delegate).
public class EventSource
{
public event EventHandler? DataReceived;
public void Simulate() => DataReceived?.Invoke(this, EventArgs.Empty);
}
public class Listener : IDisposable
{
private readonly EventSource _source;
public Listener(EventSource source)
{
_source = source;
_source.DataReceived += OnDataReceived; // subscribe
}
private void OnDataReceived(object? sender, EventArgs e)
=> Console.WriteLine("Data received!");
public void Dispose()
{
_source.DataReceived -= OnDataReceived; // unsubscribe — prevents memory leak
}
}
var source = new EventSource();
var listener = new Listener(source);
source.Simulate(); // Data received!
listener.Dispose();
source.Simulate(); // (no output — unsubscribed)Event Accessors (add / remove)
For advanced scenarios (e.g., thread safety, logging subscriptions), implement custom add and remove accessors.
public class StockTicker
{
private EventHandler<decimal>? _priceChanged;
public event EventHandler<decimal> PriceChanged
{
add
{
Console.WriteLine($"Subscriber added: {value.Method.Name}");
_priceChanged += value;
}
remove
{
Console.WriteLine($"Subscriber removed: {value.Method.Name}");
_priceChanged -= value;
}
}
public void UpdatePrice(decimal price)
=> _priceChanged?.Invoke(this, price);
}Events vs Delegates
| Delegate | Event | |
|---|---|---|
| Who can invoke | Anyone with access | Only the declaring class |
Who can assign (=) | Anyone with access | Only the declaring class |
Who can subscribe (+=) | Anyone | Anyone |
| Use case | Callbacks, strategies | Observer pattern, notifications |
public class Demo
{
public Action? Callback; // any code can do: demo.Callback = null;
public event Action? Clicked; // only Demo can do: Clicked = null; (or Invoke)
}Interface with Events
Events can be part of an interface contract.
public interface INotifier
{
event EventHandler<string> MessageSent;
void Send(string message);
}
public class EmailNotifier : INotifier
{
public event EventHandler<string>? MessageSent;
public void Send(string message)
{
Console.WriteLine($"Sending email: {message}");
MessageSent?.Invoke(this, message);
}
}