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.

C# Example Code
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 now

Custom EventArgs

When you need to pass data with the event, inherit from EventArgs.

C# Example Code
// 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 #1001

Safe Event Invocation

Always use the null-conditional operator ?.Invoke(...) — it captures a thread-safe copy of the delegate before invoking.

C# Example Code
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°C

Unsubscribing 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).

C# Example Code
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.

C# Example Code
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

DelegateEvent
Who can invokeAnyone with accessOnly the declaring class
Who can assign (=)Anyone with accessOnly the declaring class
Who can subscribe (+=)AnyoneAnyone
Use caseCallbacks, strategiesObserver pattern, notifications
C# Example Code
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.

C# Example Code
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);
    }
}