Skip to main content

C# Custom Events

Introduction

Events are a powerful feature in C# that enable a class or object to notify other classes or objects when something interesting happens. They follow the publisher-subscriber pattern, where a publisher raises an event, and subscribers respond to it. While C# provides standard event patterns, creating custom events gives you more control over how events behave in your applications.

In this tutorial, we'll explore how to create custom events in C#, giving you greater flexibility and control over event handling mechanisms.

Understanding Custom Events

Custom events in C# extend beyond the standard event pattern by providing explicit add and remove accessors. These accessors allow you to control what happens when clients subscribe to or unsubscribe from your events.

Basic Event Structure

First, let's recall how standard events work in C#:

csharp
public class Publisher
{
// Define a delegate type for the event
public delegate void MessageHandler(string message);

// Declare an event using the delegate
public event MessageHandler MessageReceived;

public void SendMessage(string message)
{
// Raise the event
MessageReceived?.Invoke(message);
}
}

Custom Event Implementation

A custom event implementation involves explicitly defining the add and remove accessors:

csharp
public class CustomPublisher
{
// Define a delegate type
public delegate void MessageHandler(string message);

// Private delegate field to store event handlers
private MessageHandler _messageHandlers;

// Custom event with explicit add/remove accessors
public event MessageHandler MessageReceived
{
add
{
// Custom logic when adding an event handler
Console.WriteLine("Event handler added");
_messageHandlers += value;
}
remove
{
// Custom logic when removing an event handler
Console.WriteLine("Event handler removed");
_messageHandlers -= value;
}
}

public void SendMessage(string message)
{
// Invoke the event handlers
_messageHandlers?.Invoke(message);
}
}

Benefits of Custom Events

Custom events offer several advantages:

  1. Controlled subscription: You can validate, log, or restrict subscriptions
  2. Thread safety: Implement thread-safe event subscription/unsubscription
  3. Additional behaviors: Execute additional code when handlers are added or removed
  4. Custom storage: Store event handlers in specialized data structures

Practical Examples

Example 1: Thread-Safe Custom Event

One common use of custom events is to make them thread-safe:

csharp
public class ThreadSafePublisher
{
// Define event delegate
public delegate void DataChangedEventHandler(object sender, EventArgs e);

// Private delegate field
private DataChangedEventHandler _dataChanged;

// Thread-safe custom event
public event DataChangedEventHandler DataChanged
{
add
{
lock (this) // Thread synchronization
{
_dataChanged += value;
}
}
remove
{
lock (this) // Thread synchronization
{
_dataChanged -= value;
}
}
}

public void ChangeData()
{
// Some data change logic

// Safely invoke event
DataChangedEventHandler handler;
lock (this)
{
handler = _dataChanged;
}

handler?.Invoke(this, EventArgs.Empty);
}
}

Example 2: Implementing Event with Weak References

To prevent memory leaks in long-lived publishers, we can use weak references:

csharp
using System;
using System.Collections.Generic;

public class WeakEventPublisher
{
// List of weak references to event handlers
private readonly List<WeakReference> _eventHandlers = new List<WeakReference>();

public event EventHandler StatusChanged
{
add
{
// Store a weak reference to the handler
_eventHandlers.Add(new WeakReference(value));
}
remove
{
// Find and remove the handler
for (int i = _eventHandlers.Count - 1; i >= 0; i--)
{
var weakReference = _eventHandlers[i];
var handler = weakReference.Target as EventHandler;

if (handler == null || handler == value)
{
_eventHandlers.RemoveAt(i);
}
}
}
}

public void UpdateStatus()
{
// Clean up and invoke handlers
for (int i = _eventHandlers.Count - 1; i >= 0; i--)
{
var weakReference = _eventHandlers[i];
var handler = weakReference.Target as EventHandler;

if (handler == null)
{
// Handler has been garbage collected, remove it
_eventHandlers.RemoveAt(i);
}
else
{
// Invoke the handler
handler(this, EventArgs.Empty);
}
}
}
}

Example 3: Real-World Application - Custom Event for Property Change Notifications

Here's a practical example of using custom events for property change notifications in a view model:

csharp
public class ViewModel
{
private string _name;

// Event delegate for property changes
public delegate void PropertyChangedEventHandler(string propertyName);

// Dictionary to store property-specific subscribers
private readonly Dictionary<string, PropertyChangedEventHandler> _handlers =
new Dictionary<string, PropertyChangedEventHandler>();

// Custom event with property filtering capabilities
public event PropertyChangedEventHandler PropertyChanged
{
add
{
lock (_handlers)
{
if (!_handlers.ContainsKey(""))
_handlers[""] = value;
else
_handlers[""] += value;
}
}
remove
{
lock (_handlers)
{
if (_handlers.ContainsKey(""))
_handlers[""] -= value;
}
}
}

// Subscribe to changes for a specific property
public void SubscribeToProperty(string propertyName, PropertyChangedEventHandler handler)
{
lock (_handlers)
{
if (!_handlers.ContainsKey(propertyName))
_handlers[propertyName] = handler;
else
_handlers[propertyName] += handler;
}
}

// Unsubscribe from changes for a specific property
public void UnsubscribeFromProperty(string propertyName, PropertyChangedEventHandler handler)
{
lock (_handlers)
{
if (_handlers.ContainsKey(propertyName))
_handlers[propertyName] -= handler;
}
}

public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged(nameof(Name));
}
}
}

// Notify subscribers about the property change
protected void OnPropertyChanged(string propertyName)
{
lock (_handlers)
{
// Notify property-specific subscribers
if (_handlers.TryGetValue(propertyName, out var specificHandlers))
specificHandlers?.Invoke(propertyName);

// Notify general subscribers
if (_handlers.TryGetValue("", out var generalHandlers))
generalHandlers?.Invoke(propertyName);
}
}
}

Demonstrating Custom Event Usage

Let's see how to use our custom events:

csharp
class Program
{
static void Main(string[] args)
{
// Using the CustomPublisher
Console.WriteLine("Custom Publisher Example:");
CustomPublisher publisher = new CustomPublisher();

// Add event handlers
publisher.MessageReceived += OnMessageReceived;
publisher.MessageReceived += OnAnotherMessageReceived;

// Trigger the event
publisher.SendMessage("Hello, Custom Events!");

// Remove an event handler
publisher.MessageReceived -= OnAnotherMessageReceived;

// Trigger the event again
publisher.SendMessage("Event after removal!");

Console.WriteLine("\nViewModel Example:");
// Using the ViewModel
ViewModel viewModel = new ViewModel();

// Subscribe to all property changes
viewModel.PropertyChanged += (propName) =>
Console.WriteLine($"General notification: {propName} changed");

// Subscribe to Name property changes specifically
viewModel.SubscribeToProperty("Name", (propName) =>
Console.WriteLine($"Specific notification: {propName} is now {viewModel.Name}"));

// Change the property
viewModel.Name = "John Doe";
}

static void OnMessageReceived(string message)
{
Console.WriteLine($"Message received: {message}");
}

static void OnAnotherMessageReceived(string message)
{
Console.WriteLine($"Another handler: {message}");
}
}

Output:

Custom Publisher Example:
Event handler added
Event handler added
Message received: Hello, Custom Events!
Another handler: Hello, Custom Events!
Event handler removed
Message received: Event after removal!

ViewModel Example:
General notification: Name changed
Specific notification: Name is now John Doe

Advanced Custom Event Patterns

Event Arguments with Custom Data

We can create custom event arguments to pass additional data:

csharp
// Custom event arguments
public class OrderEventArgs : EventArgs
{
public int OrderId { get; }
public decimal Amount { get; }
public DateTime OrderDate { get; }

public OrderEventArgs(int orderId, decimal amount, DateTime orderDate)
{
OrderId = orderId;
Amount = amount;
OrderDate = orderDate;
}
}

// Publisher with custom event args
public class OrderProcessor
{
// Private backing field
private EventHandler<OrderEventArgs> _orderProcessed;

// Custom event
public event EventHandler<OrderEventArgs> OrderProcessed
{
add
{
Console.WriteLine("Order processor: Adding subscriber");
_orderProcessed += value;
}
remove
{
Console.WriteLine("Order processor: Removing subscriber");
_orderProcessed -= value;
}
}

public void ProcessOrder(int orderId, decimal amount)
{
// Process the order...
Console.WriteLine($"Processing order {orderId} for ${amount}");

// Create event arguments with order details
var args = new OrderEventArgs(orderId, amount, DateTime.Now);

// Raise the event
_orderProcessed?.Invoke(this, args);
}
}

Usage:

csharp
// Create the order processor
var processor = new OrderProcessor();

// Subscribe to the event
processor.OrderProcessed += (sender, e) =>
{
Console.WriteLine($"Order #{e.OrderId} processed on {e.OrderDate.ToShortDateString()}");
Console.WriteLine($"Amount: ${e.Amount}");
};

// Process an order
processor.ProcessOrder(12345, 99.99m);

Output:

Order processor: Adding subscriber
Processing order 12345 for $99.99
Order #12345 processed on 4/15/2023
Amount: $99.99

Summary

Custom events in C# provide a powerful mechanism to control event handling behavior. By implementing custom add and remove accessors, you can:

  • Implement thread-safe event handling
  • Add validation or restrictions to event subscriptions
  • Implement specialized storage for event handlers
  • Create more efficient event handling patterns
  • Build more sophisticated publisher-subscriber systems

These capabilities make custom events particularly valuable in complex applications where standard event patterns might not provide sufficient control or flexibility.

Additional Resources

Exercises

  1. Create a custom event implementation that limits the number of subscribers to five.
  2. Implement a priority-based event system where subscribers can specify their priority level.
  3. Develop a custom event that maintains a log of all subscriptions and unsubscriptions.
  4. Create a custom event that allows selective event raising (only notifying specific subscribers based on criteria).
  5. Implement a custom event that supports subscription timeouts (subscribers automatically unsubscribe after a specified time).


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)