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.
Event 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;
}
}
The event keyword provides encapsulation, preventing external code from directly invoking the delegate.
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.
public class ThreadSafePublisher
{
// Use EventHandler 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;
}
}
}
Memory Leaks: Event subscriptions create strong references. Failing to unsubscribe can lead to memory leaks. Implement IDisposable for cleanup.
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);
}
}
Best Practice: Use the built-in EventHandler<TEventArgs> pattern instead of defining custom delegates unless you need specific naming for clarity.
Why Event Handling is Important
- Open/Closed Principle (SOLID): Events allow extending object behavior without modifying existing code
- Loose Coupling: Publishers and subscribers remain independent, reducing dependencies
- Scalability: Event-driven architectures handle complex interaction patterns efficiently
Advanced Nuances
Async Event Handlers and Exception Handling
Async event handlers require special consideration:
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);
}
}
}
Event Aggregators for Complex Systems
// 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);
}
}
}
}
Roadmap Context
Event handling sits as the practical application layer in the “Delegates and Events” section. It builds upon delegate fundamentals and 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