Back to Blog
C#

C# EventHandler: Events, Subscriptions, and Pitfalls

c# eventhandler: Learn how to declare, raise, and subscribe to events using C# EventHandler, including custom event args, thread safety, and avoiding memory leaks.

C# EventsEventHandlerDelegates.NETEvent Subscription
Illustration of a C# event handler connecting a publisher and subscriber with a delegate arrow.

c# eventhandler requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

The EventHandler delegate is the standard way to define events in C#. It has a simple signature: void EventHandler(object sender, EventArgs e). The generic variant, EventHandler<TEventArgs>, lets you pass a custom event data class. Understanding how to declare, raise, and subscribe to these events is a core skill for building decoupled, observable components in .NET.

The EventHandler Signature and Its Purpose

The non-generic EventHandler delegate is defined as public delegate void EventHandler(object? sender, EventArgs e). The sender parameter identifies the object that raised the event, and e contains any event-specific data. Because EventArgs is a base class with no members, you typically use EventArgs.Empty when no data is needed. The generic version, EventHandler<TEventArgs>, replaces EventArgs with a custom type derived from EventArgs, allowing you to pass structured information to subscribers.

This two-parameter pattern is a convention, not a language requirement. It exists to give subscribers context about the source of the event and to carry data without forcing a separate delegate definition for every event type. When you see public event EventHandler<OrderPlacedEventArgs> OrderPlaced;, you know exactly how to subscribe and what data you will receive.

Declaring and Raising Events

Declaring an event is straightforward. Inside a class, you define a public event member using an EventHandler or EventHandler<T> delegate. Raising the event is done by invoking the delegate, but you must always check for null because an event with no subscribers evaluates to null.

public class OrderService { public event EventHandler<OrderPlacedEventArgs>? OrderPlaced; public void PlaceOrder(Order order) { // ... business logic ... OnOrderPlaced(new OrderPlacedEventArgs(order.Id, order.Total)); } protected virtual void OnOrderPlaced(OrderPlacedEventArgs e) { OrderPlaced?.Invoke(this, e); } } public class OrderPlacedEventArgs : EventArgs { public int OrderId { get; } public decimal Total { get; } public OrderPlacedEventArgs(int orderId, decimal total) { OrderId = orderId; Total = total; } }

The ?.Invoke pattern is the recommended way to raise an event. It checks for null and then invokes the delegate on the same thread. The protected virtual method OnOrderPlaced is a common pattern that allows derived classes to override the invocation behavior, for example to add logging or to change the sender. This pattern also keeps the event invocation encapsulated.

Subscribing and Unsubscribing

Subscribers attach to an event using += and detach using -=. The handler method must match the delegate signature. With EventHandler<T>, the handler receives the custom event args.

var service = new OrderService(); service.OrderPlaced += OnOrderPlaced; void OnOrderPlaced(object? sender, OrderPlacedEventArgs e) { Console.WriteLine($"Order {e.OrderId} placed for {e.Total:C}"); } // Later, when no longer needed: service.OrderPlaced -= OnOrderPlaced;

Subscribing with a lambda is convenient but makes unsubscription harder because you need to keep a reference to the delegate. If you subscribe with service.OrderPlaced += (s, e) => ..., you cannot easily remove that exact handler later. For short-lived subscriptions, lambdas are fine; for long-lived ones, prefer a named method.

Custom Event Arguments and Best Practices

When your event needs to convey data, create a class that derives from EventArgs. The class should be immutable—expose properties with getters only, and set them in the constructor. This prevents subscribers from modifying the event data after it is raised. Name the class with the EventArgs suffix to follow .NET conventions.

public class TemperatureChangedEventArgs : EventArgs { public double NewTemperature { get; } public double OldTemperature { get; } public TemperatureChangedEventArgs(double newTemperature, double oldTemperature) { NewTemperature = newTemperature; OldTemperature = oldTemperature; } }

Using a custom event args class is preferable to passing a raw Dictionary or a primitive type because it gives compile-time type safety and self-documenting property names. It also allows you to add fields later without breaking existing subscribers.

Thread Safety and Invocation Context

Events are invoked on the thread that raises them. If a background thread raises an event, all subscribers run on that same background thread. This has implications for UI applications: updating UI controls from a non-UI thread throws an exception. The subscriber is responsible for marshaling to the UI thread if needed, typically via Dispatcher.Invoke in WPF or Control.Invoke in WinForms.

The null-check-and-invoke pattern is not atomic. In a multithreaded scenario, a subscriber could unsubscribe between the null check and the invocation, causing a NullReferenceException. To mitigate this, you can copy the delegate to a local variable before invoking:

var handler = OrderPlaced; handler?.Invoke(this, e);

This is the pattern recommended by the .NET documentation. It ensures that the delegate instance you invoke is the same one you checked for null, even if another thread modifies the event field concurrently. However, it does not guarantee that all subscribers receive the event if one unsubscribes during invocation—that is an inherent race condition.

Memory Leaks and the Weak Event Pattern

A common pitfall with events is memory leaks. If a subscriber holds a reference to an event source, and the subscriber does not unsubscribe, the source keeps the subscriber alive via the delegate reference. This prevents the subscriber from being garbage collected, even if it is no longer needed. For example, a long-lived service that raises events and a short-lived window that subscribes without unsubscribing will keep the window alive after it is closed.

The simplest fix is to unsubscribe when the subscriber is disposed or no longer needed. In UI frameworks, this often happens in the Closed or Dispose method. For cases where you cannot control the subscriber lifecycle, the .NET WeakEvent pattern exists. WPF provides WeakEventManager, and you can implement a custom weak event pattern using weak references, but it adds complexity and is rarely necessary if you follow the unsubscribe rule.

Exception Handling and Reentrancy

When a subscriber throws an exception, that exception propagates to the code that raised the event. If you raise an event inside a try-catch block, the exception will be caught there, but any subsequent subscribers will not be invoked because the delegate invocation stops at the first exception. This means one faulty subscriber can prevent others from receiving the event.

To isolate subscriber failures, you can invoke each subscriber individually inside its own try-catch. This requires accessing the delegate's invocation list and calling each method separately:

var handler = OrderPlaced; if (handler == null) return; foreach (EventHandler<OrderPlacedEventArgs> subscriber in handler.GetInvocationList()) { try { subscriber(this, e); } catch (Exception ex) { // Log and continue with next subscriber } }

This pattern ensures that one subscriber's exception does not break the event chain. However, it changes the semantics: normally exceptions propagate to the raiser, which may be desirable for transactional logic. Decide based on whether the event is a notification or a request for processing. Reentrancy is another concern: if a subscriber calls back into the event source and triggers the same event again, you can get infinite recursion. Guard against this with a flag or by designing the event source to be reentrant-safe.

When to Use EventHandler vs Custom Delegates

EventHandler<T> is the right choice for most events because it provides a consistent, familiar signature. Custom delegates are only needed when you require a different return type or more than two parameters. For example, an event that needs to return a cancellation flag could use Func<object, EventArgs, bool>, but that is unusual. Sticking with EventHandler<T> keeps your code consistent with the .NET ecosystem and makes it easier for other developers to understand. If you need to pass multiple pieces of data, bundle them into a custom EventArgs class rather than expanding the delegate signature.

The EventHandler delegate is a cornerstone of event-driven programming in C#. By following the standard declaration, raising, and subscription patterns, and by being mindful of thread safety, memory leaks, and exception isolation, you can build components that communicate cleanly without tight coupling.

c# eventhandler: Practical Usage and Code Examples | RYUSLOG DEV