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:
- Return Type: The return type is always
void. Events are notification mechanisms and should not return values directly to the publisher. - First Parameter (
sender): Represents the source that raised the event. It is typed asobject?to support polymorphism and decoupled architecture. Inside an instance method, this is usually passed asthis. - Second Parameter (
e): Represents the event payload. It must be of typeEventArgs(for events without custom data) or a class derived fromSystem.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}");
}