Skip to main content

What is Event Handling?

Event handling is a mechanism that enables objects to notify other objects when something of interest occurs, following the publisher-subscriber pattern (also known as the observer pattern). Its core purpose is to establish loose coupling between components — the publisher doesn’t need to know about its subscribers, and subscribers can attach/detach dynamically. This solves the problem of tight coupling where objects would need direct references and method calls to communicate state changes.

How it works in C#

Declarations

Event declarations define the contract for notifications. In C#, events are built on the delegate foundation and use the event keyword to create a special type of member that can only be invoked by the containing class.
public class TemperatureSensor
{
    // 1. Define a delegate type that matches the event signature
    public delegate void TemperatureChangedHandler(object sender, TemperatureChangedEventArgs e);
    
    // 2. Declare the event using the delegate type
    public event TemperatureChangedHandler TemperatureChanged;
    
    private double _currentTemperature;
    
    public double CurrentTemperature
    {
        get => _currentTemperature;
        set
        {
            if (_currentTemperature != value)
            {
                _currentTemperature = value;
                
                // 3. Create event arguments
                var args = new TemperatureChangedEventArgs(_currentTemperature);
                
                // 4. Raise the event (null check required)
                TemperatureChanged?.Invoke(this, args);
            }
        }
    }
}

// Custom event arguments class
public class TemperatureChangedEventArgs : EventArgs
{
    public double NewTemperature { get; }
    
    public TemperatureChangedEventArgs(double newTemperature)
    {
        NewTemperature = newTemperature;
    }
}

Thread-safe events

Thread safety ensures that events can be safely used in multi-threaded scenarios where subscribers might be added/removed concurrently with event invocation. The main challenge is preventing NullReferenceException when subscribers are removed between the null check and invocation.
public class ThreadSafePublisher
{
    // UseEventHandler pattern for better thread safety
    public event EventHandler<TemperatureChangedEventArgs> TemperatureChanged;
    
    private readonly object _eventLock = new object();
    private double _temperature;
    
    public double Temperature
    {
        get => _temperature;
        set
        {
            _temperature = value;
            RaiseTemperatureChanged(value);
        }
    }
    
    private void RaiseTemperatureChanged(double newTemp)
    {
        // Thread-safe event invocation pattern
        var handlers = TemperatureChanged;
        if (handlers != null)
        {
            var args = new TemperatureChangedEventArgs(newTemp);
            handlers(this, args);
        }
        
        // Alternative: Using null-conditional operator (C# 6+)
        // TemperatureChanged?.Invoke(this, new TemperatureChangedEventArgs(newTemp));
    }
    
    // Thread-safe subscription management
    public void AddSubscriber(EventHandler<TemperatureChangedEventArgs> handler)
    {
        lock (_eventLock)
        {
            TemperatureChanged += handler;
        }
    }
    
    public void RemoveSubscriber(EventHandler<TemperatureChangedEventArgs> handler)
    {
        lock (_eventLock)
        {
            TemperatureChanged -= handler;
        }
    }
}

EventHandler patterns

C# provides standardized patterns for event handling that promote consistency and interoperability.
// Pattern 1: Standard EventHandler\<T\> pattern (Recommended)
public class StandardEventPattern
{
    // Uses the built-in EventHandler\<T\> delegate with EventArgs
    public event EventHandler<TemperatureChangedEventArgs> TemperatureChanged;
    
    protected virtual void OnTemperatureChanged(double newTemp)
    {
        TemperatureChanged?.Invoke(this, new TemperatureChangedEventArgs(newTemp));
    }
}

// Pattern 2: Custom delegate with event accessors
public class CustomAccessorPattern
{
    private EventHandler<TemperatureChangedEventArgs> _temperatureChanged;
    
    // Custom add/remove accessors for additional control
    public event EventHandler<TemperatureChangedEventArgs> TemperatureChanged
    {
        add
        {
            // Add validation, logging, or synchronization
            lock (this)
            {
                _temperatureChanged += value;
            }
        }
        remove
        {
            lock (this)
            {
                _temperatureChanged -= value;
            }
        }
    }
    
    protected virtual void OnTemperatureChanged(double newTemp)
    {
        _temperatureChanged?.Invoke(this, new TemperatureChangedEventArgs(newTemp));
    }
}

// Pattern 3: EventHandler (non-generic) for simple scenarios
public class SimpleEventPattern
{
    public event EventHandler TemperatureChanged;
    
    protected virtual void OnTemperatureChanged()
    {
        TemperatureChanged?.Invoke(this, EventArgs.Empty);
    }
}

Why is Event Handling important?

  1. Open/Closed Principle (SOLID): Events allow extending object behavior without modifying existing code—new subscribers can be added without changing the publisher.
  2. Loose Coupling: Publishers and subscribers remain independent, reducing dependencies and making systems more maintainable and testable.
  3. Scalability: Event-driven architectures handle complex interaction patterns efficiently, allowing many subscribers to react to single events without the publisher managing the relationships.

Advanced Nuances

1. Event Handlers and Garbage Collection

Event subscriptions create strong references that can prevent garbage collection. Failing to unsubscribe can lead to memory leaks:
public class MemoryLeakExample
{
    public event EventHandler SomeEvent;
    
    public void CreateLeak()
    {
        var subscriber = new SomeSubscriber();
        SomeEvent += subscriber.HandleEvent; // Strong reference created
        
        // Even if subscriber goes out of scope, it won't be GC'd
        // until unsubscribed or publisher is collected
    }
}

// Solution: Use weak events patterns or implement IDisposable
public class SafeSubscriber : IDisposable
{
    private readonly Publisher _publisher;
    
    public SafeSubscriber(Publisher publisher)
    {
        _publisher = publisher;
        _publisher.SomeEvent += HandleEvent;
    }
    
    public void Dispose()
    {
        _publisher.SomeEvent -= HandleEvent;
    }
    
    private void HandleEvent(object sender, EventArgs e) { }
}

2. Async Event Handlers and Exception Handling

Async event handlers require special consideration for error handling and completion tracking:
public class AsyncEventPattern
{
    public event Func<object, EventArgs, Task> AsyncEvent;
    
    public async Task RaiseAsyncEvent()
    {
        var handlers = AsyncEvent;
        if (handlers != null)
        {
            var tasks = handlers.GetInvocationList()
                .Cast<Func<object, EventArgs, Task>>()
                .Select(handler => handler(this, EventArgs.Empty));
            
            // Wait for all handlers to complete
            await Task.WhenAll(tasks);
        }
    }
}

// Exception handling in async events
public class RobustAsyncEvent
{
    public event Func<object, EventArgs, Task> AsyncEvent;
    
    public async Task RaiseSafely()
    {
        var handlers = AsyncEvent;
        if (handlers != null)
        {
            var tasks = handlers.GetInvocationList()
                .Cast<Func<object, EventArgs, Task>>()
                .Select(async handler =>
                {
                    try
                    {
                        await handler(this, EventArgs.Empty);
                    }
                    catch (Exception ex)
                    {
                        // Log error but don't break other handlers
                        Console.WriteLine($"Handler failed: {ex.Message}");
                    }
                });
            
            await Task.WhenAll(tasks);
        }
    }
}

3. Event Aggregators and Mediator Patterns

For complex systems, direct event coupling can become problematic. Advanced patterns provide more control:
// Event aggregator pattern for decoupled communication
public class EventAggregator
{
    private readonly Dictionary<Type, List<Delegate>> _handlers = new();
    
    public void Subscribe<TEvent>(Action<TEvent> handler)
    {
        var eventType = typeof(TEvent);
        if (!_handlers.ContainsKey(eventType))
            _handlers[eventType] = new List<Delegate>();
        
        _handlers[eventType].Add(handler);
    }
    
    public void Publish<TEvent>(TEvent eventData)
    {
        if (_handlers.TryGetValue(typeof(TEvent), out var handlers))
        {
            // Create copy to avoid modification during iteration
            foreach (var handler in handlers.ToArray())
            {
                ((Action<TEvent>)handler)(eventData);
            }
        }
    }
}

How this fits the Roadmap

Event handling sits as the practical application layer in the “Delegates and Events” section. It builds upon delegate fundamentals (prerequisite) and serves as the foundation for more advanced topics: Prerequisites: Understanding of delegates, lambda expressions, and anonymous methods. Unlocks:
  • Reactive Extensions (Rx.NET): Event streams and LINQ-like event processing
  • Async event patterns: Advanced asynchronous programming techniques
  • Dependency Injection integration: Event handling in IoC containers
  • Message buses and event sourcing: Distributed event-driven architectures
Mastering event handling is crucial for building responsive, maintainable applications that follow modern architectural patterns like MVVM, CQRS, and microservices communication.

Build docs developers (and LLMs) love