How to Use Generic Event Handlers in C

In modern C# development, generic event handlers provide a strongly typed, reusable pattern for managing notifications between components without declaring custom delegates. By leveraging the built-in EventHandler<TEventArgs> delegate, developers can pass strongly typed data directly to subscribers while maintaining standard .NET design practices. This article breaks down how generic event handlers work in C# applications, how to declare custom event arguments, and how to publish and subscribe to these events cleanly.

The Standard Pattern: EventHandler<TEventArgs>

Prior to generic delegates, every event requiring custom data needed a dedicated delegate declaration. .NET simplifies this with the generic EventHandler<TEventArgs> delegate:

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

This delegate expects two parameters: the source of the event (sender) and a strongly typed object containing the event payload (e), which typically derives from System.EventArgs.

Defining Custom Event Data

To pass custom data with your event, define a class that inherits from EventArgs.

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

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

Publishing the Generic Event

The publishing class declares the event using EventHandler<TEventArgs> and raises it safely using the null-conditional operator (?.) along with Invoke.

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

    public void ProcessOrder(int orderId, decimal amount)
    {
        // Business logic to process order...

        // Raise the event
        OnOrderProcessed(new OrderProcessedEventArgs(orderId, amount));
    }

    protected virtual void OnOrderProcessed(OrderProcessedEventArgs e)
    {
        OrderProcessed?.Invoke(this, e);
    }
}

Subscribing to the Generic Event

Subscribers attach a handler method to the event using the += operator. The handler method's signature must match the generic delegate.

public class NotificationService
{
    public void Subscribe(OrderService orderService)
    {
        orderService.OrderProcessed += HandleOrderProcessed;
    }

    public void Unsubscribe(OrderService orderService)
    {
        orderService.OrderProcessed -= HandleOrderProcessed;
    }

    private void HandleOrderProcessed(object? sender, OrderProcessedEventArgs e)
    {
        Console.WriteLine($"Order #{e.OrderId} processed for a total of ${e.TotalAmount:F2}");
    }
}

Subscribers can also use inline lambda expressions when simple logic is needed:

orderService.OrderProcessed += (sender, e) =>
{
    Console.WriteLine($"Quick log: Order {e.OrderId} finished.");
};

Key Advantages

  • Compile-Time Type Safety: Event data is strongly typed, eliminating the need to cast object parameters inside event handlers.
  • Reduced Boilerplate: Eliminates the need to write custom delegate declarations for every unique event.
  • Consistent API: Adheres to standard .NET framework guidelines, making the code predictable and easier for other developers to consume.