C# Event Accessors
When working with events in C#, you might need finer control over how clients subscribe to or unsubscribe from your events. This is where event accessors come into play. They allow you to customize the behavior of event subscription and unsubscription, similar to how property accessors enable you to control access to fields.
Understanding Event Accessors
By default, when you declare an event in C#, the compiler automatically generates code to manage the list of subscribers. However, sometimes you might want to implement custom logic when clients subscribe or unsubscribe from an event.
Event accessors consist of two parts:
add
- executed when a client subscribes to the event using the+=
operatorremove
- executed when a client unsubscribes from the event using the-=
operator
Basic Syntax of Event Accessors
Here's the basic syntax for declaring event accessors:
public event EventHandler MyEvent
{
add
{
// Code that executes when clients subscribe to the event
}
remove
{
// Code that executes when clients unsubscribe from the event
}
}
Automatic vs. Custom Event Accessors
Automatic Event Accessors
When you declare an event without explicit accessors, C# generates them for you:
// The compiler generates the add and remove accessors automatically
public event EventHandler AutomaticEvent;
Custom Event Accessors
Custom event accessors give you control over subscription and unsubscription:
private EventHandler _eventHandler;
public event EventHandler CustomEvent
{
add
{
Console.WriteLine("Someone has subscribed to CustomEvent");
_eventHandler += value;
}
remove
{
Console.WriteLine("Someone has unsubscribed from CustomEvent");
_eventHandler -= value;
}
}
Practical Example: Thread-Safe Event Implementation
One common use case for custom event accessors is implementing thread-safe events:
using System;
using System.Threading;
public class ThreadSafePublisher
{
private EventHandler _statusChanged;
private readonly object _lock = new object();
public event EventHandler StatusChanged
{
add
{
lock (_lock)
{
_statusChanged += value;
Console.WriteLine($"Subscriber added. Thread ID: {Thread.CurrentThread.ManagedThreadId}");
}
}
remove
{
lock (_lock)
{
_statusChanged -= value;
Console.WriteLine($"Subscriber removed. Thread ID: {Thread.CurrentThread.ManagedThreadId}");
}
}
}
public void UpdateStatus()
{
Console.WriteLine("Status is being updated...");
OnStatusChanged(EventArgs.Empty);
}
protected virtual void OnStatusChanged(EventArgs e)
{
lock (_lock)
{
_statusChanged?.Invoke(this, e);
}
}
}
Here's how to use this class:
class Program
{
static void Main()
{
var publisher = new ThreadSafePublisher();
// Subscribe to the event
EventHandler handler1 = (sender, e) => Console.WriteLine("Handler 1 called");
EventHandler handler2 = (sender, e) => Console.WriteLine("Handler 2 called");
publisher.StatusChanged += handler1;
publisher.StatusChanged += handler2;
// Trigger the event
publisher.UpdateStatus();
// Unsubscribe from the event
publisher.StatusChanged -= handler1;
// Trigger the event again
publisher.UpdateStatus();
}
}
Output:
Subscriber added. Thread ID: 1
Subscriber added. Thread ID: 1
Status is being updated...
Handler 1 called
Handler 2 called
Subscriber removed. Thread ID: 1
Status is being updated...
Handler 2 called
Custom Event Storage Example
You might want to use custom event accessors to store event handlers in a structure other than the default delegate invocation list:
using System;
using System.Collections.Generic;
public class CustomStorageEventPublisher
{
// Using a list instead of a delegate to store subscribers
private readonly List<EventHandler> _subscribers = new List<EventHandler>();
public event EventHandler CustomStoredEvent
{
add
{
_subscribers.Add(value);
Console.WriteLine($"Added subscriber. Total subscribers: {_subscribers.Count}");
}
remove
{
_subscribers.Remove(value);
Console.WriteLine($"Removed subscriber. Total subscribers: {_subscribers.Count}");
}
}
public void RaiseEvent()
{
Console.WriteLine("Raising event...");
// Create a copy of the list to avoid issues if handlers subscribe/unsubscribe during event raising
var subscribersCopy = new List<EventHandler>(_subscribers);
foreach (var subscriber in subscribersCopy)
{
try
{
subscriber(this, EventArgs.Empty);
}
catch (Exception ex)
{
Console.WriteLine($"Exception in event handler: {ex.Message}");
// Continue with other subscribers even if one fails
}
}
}
}
Usage example:
class Program
{
static void Main()
{
var publisher = new CustomStorageEventPublisher();
EventHandler handler1 = (sender, e) => Console.WriteLine("Handler 1 executed");
EventHandler handler2 = (sender, e) => Console.WriteLine("Handler 2 executed");
EventHandler handler3 = (sender, e) => throw new Exception("Simulated exception");
publisher.CustomStoredEvent += handler1;
publisher.CustomStoredEvent += handler2;
publisher.CustomStoredEvent += handler3;
publisher.CustomStoredEvent += handler1; // Adding the same handler again
publisher.RaiseEvent();
publisher.CustomStoredEvent -= handler1; // Will only remove one instance
publisher.CustomStoredEvent -= handler3;
publisher.RaiseEvent();
}
}
Output:
Added subscriber. Total subscribers: 1
Added subscriber. Total subscribers: 2
Added subscriber. Total subscribers: 3
Added subscriber. Total subscribers: 4
Raising event...
Handler 1 executed
Handler 2 executed
Exception in event handler: Simulated exception
Handler 1 executed
Removed subscriber. Total subscribers: 3
Removed subscriber. Total subscribers: 2
Raising event...
Handler 1 executed
Handler 2 executed
Event Accessor Restrictions
When implementing custom event accessors, keep these restrictions in mind:
- Both
add
andremove
accessors must be defined together - The
value
keyword represents the delegate instance being added or removed - You cannot directly invoke the event from outside the class (or derived classes if protected)
- Custom event accessors require you to manage delegate invocation yourself
Real-World Application: Logging Event Subscriptions
In large applications, tracking event subscriptions can help identify memory leaks and performance issues:
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
public class EventMonitor
{
private EventHandler _dataChanged;
public event EventHandler DataChanged
{
add
{
LogEventSubscription(value, true);
_dataChanged += value;
}
remove
{
LogEventSubscription(value, false);
_dataChanged -= value;
}
}
private void LogEventSubscription(Delegate handler, bool isSubscribing,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
var action = isSubscribing ? "Subscribed to" : "Unsubscribed from";
var handlerName = handler.Method.Name;
var targetType = handler.Target?.GetType().Name ?? "static method";
Debug.WriteLine($"{action} DataChanged event: {handlerName} in {targetType}");
Debug.WriteLine($"Called from {memberName} at {sourceFilePath}:{sourceLineNumber}");
// In real applications, you might log this to a file or monitoring system
}
public void OnDataChanged()
{
_dataChanged?.Invoke(this, EventArgs.Empty);
}
}
Summary
Event accessors in C# provide powerful control over the behavior of event subscription and unsubscription. With custom accessors, you can:
- Implement thread-safe events
- Log or monitor event subscriptions and unsubscriptions
- Use custom storage mechanisms for event handlers
- Add validation logic before allowing subscriptions
- Implement more complex event patterns
This level of control makes events in C# highly flexible and suitable for complex application scenarios while maintaining the simplicity of the standard event pattern for clients.
Additional Resources
Exercises
- Create a custom event implementation that limits the number of subscribers to five.
- Implement a custom event that automatically unsubscribes handlers after they've been called once.
- Create a thread-safe event implementation that can be paused and resumed (subscribers can still subscribe/unsubscribe, but events won't be raised while paused).
- Implement a custom event that maintains a timestamp of when each handler subscribed and provides a method to list all subscriptions.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)