Basic Event Implementation
In the Observer pattern, a subject (publisher) defines an event, and observers (subscribers) register handlers for that event. When the subject triggers the event, all registered observers execute their handlers. A basic implementation follows this pattern:
public class Publisher
{
public event EventHandler<CustomEventArgs> DataChanged;
protected virtual void OnDataChanged(CustomEventArgs args)
{
DataChanged?.Invoke(this, args);
}
public void ProcessData()
{
// Business logic here
OnDataChanged(new CustomEventArgs());
}
}
public class CustomEventArgs : EventArgs
{
// Event data properties
}
External objects subscribe to events using the += operator:
var publisher = new Publisher();
publisher.DataChanged += HandleDataChanged;
void HandleDataChanged(object sender, CustomEventArgs e)
{
// Response logic
}
Custom Event Accessors
Similar to properties encapsulating fields, events can explicitly define add and remove accessors:
public class Publisher
{
private EventHandler<CustomEventArgs> _dataChangedHandler;
public event EventHandler<CustomEventArgs> DataChanged
{
add
{
_dataChangedHandler = (EventHandler<CustomEventArgs>)
Delegate.Combine(_dataChangedHandler, value);
}
remove
{
_dataChangedHandler = (EventHandler<CustomEventArgs>)
Delegate.Remove(_dataChangedHandler, value);
}
}
}
Explicit accessors allow adding custom logic during subscription and unsubscription, providing fine-grained control over the event registration process.
Thread Safety Considerations
When using the standard event declaration syntax, the compiler generates thread-safe accessor methods. The generated code resembles:
private EventHandler<CustomEventArgs> _dataChangedHandler;
[MethodImpl(MethodImplOptions.Synchronized)]
public void add_DataChanged(EventHandler<CustomEventArgs> handler)
{
_dataChangedHandler = (EventHandler<CustomEventArgs>)
Delegate.Combine(_dataChangedHandler, handler);
}
[MethodImpl(MethodImplOptions.Synchronized)]
public void remove_DataChanged(EventHandler<CustomEventArgs> handler)
{
_dataChangedHandler = (EventHandler<CustomEventArgs>)
Delegate.Remove(_dataChangedHandler, handler);
}
The MethodImplOptions.Synchronized attribute synchronizes access using lock(this). This approach has two significant drawbacks:
First, locking on this exposes the synchronization object to external code. If client code locks on the same instance, deadlocks can occur:
private void ProcessData(Publisher pub)
{
lock (pub) // External lock
{
pub.DataChanged += HandleDataChanged; // Attempts to lock again - deadlock
}
}
Second, when a class contains multiple events, all accessor methods lock on the same object. This unnecessary contention reduces performance since different events can safely be modified concurrently:
public class MultiEventPublisher
{
private EventHandler _event1Handler;
private EventHandler _event2Handler;
public void add_Event1(EventHandler handler)
{
lock (this) // Blocks Event2 operations unnecessarily
{
_event1Handler = (EventHandler)Delegate.Combine(_event1Handler, handler);
}
}
public void add_Event2(EventHandler handler)
{
lock (this) // Blocks Event1 operations unnecessarily
{
_event2Handler = (EventHandler)Delegate.Combine(_event2Handler, handler);
}
}
}
Improved Thread Safety Implementation
Using dedicated lock objects resolves both issues:
public class Publisher
{
private EventHandler<CustomEventArgs> _dataChangedHandler;
private readonly object _eventLock = new object();
public event EventHandler<CustomEventArgs> DataChanged
{
add
{
lock (_eventLock)
{
_dataChangedHandler = (EventHandler<CustomEventArgs>)
Delegate.Combine(_dataChangedHandler, value);
}
}
remove
{
lock (_eventLock)
{
_dataChangedHandler = (EventHandler<CustomEventArgs>)
Delegate.Remove(_dataChangedHandler, value);
}
}
}
}
For multiple events, each can have its own lock object:
public class MultiEventPublisher
{
private EventHandler _event1Handler;
private EventHandler _event2Handler;
private readonly object _event1Lock = new object();
private readonly object _event2Lock = new object();
public event EventHandler Event1
{
add
{
lock (_event1Lock)
{
_event1Handler = (EventHandler)Delegate.Combine(_event1Handler, value);
}
}
remove
{
lock (_event1Lock)
{
_event1Handler = (EventHandler)Delegate.Remove(_event1Handler, value);
}
}
}
public event EventHandler Event2
{
add
{
lock (_event2Lock)
{
_event2Handler = (EventHandler)Delegate.Combine(_event2Handler, value);
}
}
remove
{
lock (_event2Lock)
{
_event2Handler = (EventHandler)Delegate.Remove(_event2Handler, value);
}
}
}
}
Managing Multiple Events with Collections
When a class exposes many events, managing each with a separate delegate field becomes impractical. A collection-based approach centralizes event storage:
public class EventPublisher
{
private readonly Dictionary<object, Delegate> _handlers = new Dictionary<object, Delegate>();
private static readonly object Event1Key = new object();
private static readonly object Event2Key = new object();
public event EventHandler Event1
{
add
{
lock (_handlers)
{
if (_handlers.ContainsKey(Event1Key))
{
_handlers[Event1Key] = Delegate.Combine(_handlers[Event1Key], value);
}
else
{
_handlers[Event1Key] = value;
}
}
}
remove
{
lock (_handlers)
{
if (_handlers.ContainsKey(Event1Key))
{
_handlers[Event1Key] = Delegate.Remove(_handlers[Event1Key], value);
}
}
}
}
protected virtual void OnEvent1()
{
if (_handlers.TryGetValue(Event1Key, out Delegate handler))
{
((EventHandler)handler)?.Invoke(this, EventArgs.Empty);
}
}
}
This pattern mirrors the EventHandlerList class used by Windows Forms controls. Note that collection-based management introduces its own synchronization challenges, as all events share the same collection lock.
Memory Leaks in Event Programming
Event subscriptions create strong references from publishers to subscribers. When a subscriber registers an event handler, the publisher maintains a reference to the subscriber through the delegate's target object. This "implicit strong reference" prevents garbage collection even when the subscriber is no longer needed:
// Subscriber remains alive due to event subscription
public class Subscriber
{
public void Subscribe(Publisher publisher)
{
publisher.DataChanged += OnDataChanged;
}
private void OnDataChanged(object sender, CustomEventArgs e)
{
// Handler logic
}
}
// Even after subscriber goes out of scope, it cannot be collected
// if the publisher is still alive
The publisher's delegate chain holds a reference to each subscriber. If subscribers are not explicit unsubscribed, they remain in memory as "zombie objects" - no longer useful but still occupying heap space. In long-running applications with many short-lived subscribers, this accumulates into significant memory consumption.
Exceptions from Disposed Objects
Beyond memory leaks, unmanaged event subscriptions cause runtime exceptions. Consider this scenario:
public class MainForm : Form
{
private void MainForm_Load(object sender, EventArgs e)
{
var detailForm = new DetailForm();
detailForm.ActionTriggered += OnDetailAction;
detailForm.Show();
}
private void OnDetailAction(object sender, EventArgs e)
{
this.BringToFront(); // Exception if MainForm is disposed
}
}
After MainForm closes and disposes, the detailForm still holds a reference to it. When the event fires, attempting to acess the disposed form throws an ObjectDisposedException.
Preventing Event-Related Issues
The straightforward solution is unsubscribing from events when subscribers are no longer needed:
public class Subscriber : IDisposable
{
private readonly Publisher _publisher;
public Subscriber(Publisher publisher)
{
_publisher = publisher;
_publisher.DataChanged += OnDataChanged;
}
public void Dispose()
{
_publisher.DataChanged -= OnDataChanged;
}
private void OnDataChanged(object sender, CustomEventArgs e)
{
// Handler logic
}
}
However, this approach requires careful tracking of all subscriptions. For complex systems with numerous events, managing unsubscriptions becomes error-prone. Alternative approaches using weak references or weak event patterns can help decouple subscriber lifetimes from publisher references.