Standard EventHandler Pattern in C

This article provides a concise overview of the standard EventHandler pattern in C#, breaking down its signature components, the roles of the sender and EventArgs parameters, and the recommended practices for defining, raising, and consuming events in modern .NET development.

The Standard Signature

In .NET, the standard signature for an event handler delegate is defined as:

public delegate void EventHandler(object? sender, EventArgs e);

For events that pass custom data, the generic version is used:

public delegate void EventHandler<TEventArgs>(object? sender, TEventArgs e);

In both variations, the pattern adheres to three core rules:

  1. Return Type: The return type is always void. Events are notification mechanisms and should not return values directly to the publisher.
  2. First Parameter (sender): Represents the source that raised the event. It is typed as object? to support polymorphism and decoupled architecture. Inside an instance method, this is usually passed as this.
  3. Second Parameter (e): Represents the event payload. It must be of type EventArgs (for events without custom data) or a class derived from System.EventArgs (when carrying custom state).

Custom Event Arguments

When an event requires passing data to subscribers, create a class that inherits from System.EventArgs:

public class OrderProcessedEventArgs : EventArgs
{
    public int OrderId { get; }
    public decimal TotalAmount { get; }

    public OrderProcessedEventArgs(int orderId, decimal totalAmount)
    {
        OrderId = orderId;
        TotalAmount = totalAmount;
    }
}

Declaring and Raising the Event

To implement the pattern in a publisher class:

public class OrderService
{
    // 1. Declare the event using the generic EventHandler<T>
    public event EventHandler<OrderProcessedEventArgs>? OrderProcessed;

    // 2. Raise the event using a protected virtual method
    protected virtual void OnOrderProcessed(OrderProcessedEventArgs e)
    {
        // Safe thread-safe invocation using null-conditional operator
        OrderProcessed?.Invoke(this, e);
    }

    public void ProcessOrder(int orderId, decimal amount)
    {
        // Business logic here...

        // Notify subscribers
        OnOrderProcessed(new OrderProcessedEventArgs(orderId, amount));
    }
}

If no data needs to be passed, declare the event as public event EventHandler? OrderCompleted; and trigger it using EventArgs.Empty:

OrderCompleted?.Invoke(this, EventArgs.Empty);

Subscribing to the Event

A subscriber attaches a method that matches the delegate's signature:

var service = new OrderService();
service.OrderProcessed += HandleOrderProcessed;

void HandleOrderProcessed(object? sender, OrderProcessedEventArgs e)
{
    Console.WriteLine($"Order {e.OrderId} processed for {e.TotalAmount:C}");
}