Introduction to Event Handling
Event handling is a fundamental pattern in software development, especially in UI-driven applications and asynchronous programming. In .NET Core, it's built upon delegates and events, providing a powerful and flexible mechanism for components to communicate with each other without direct coupling. This guide delves into the core concepts of event handling in .NET Core, from understanding delegates to implementing sophisticated event-driven architectures.
Understanding Delegates
A delegate is a type that represents references to methods with a particular parameter list and return type. Think of it as a type-safe function pointer. Delegates are the foundation for event handling in .NET Core.
Key characteristics of delegates:
- Type Safety: Delegates ensure that the method assigned to them has a compatible signature.
- Flexibility: A delegate can reference static or instance methods.
- Multicast: A delegate can reference multiple methods.
Delegate Declaration
You declare a delegate using the delegate keyword:
public delegate void MyDelegate(string message);
This declares a delegate named MyDelegate that can point to any method that accepts a string argument and returns void.
Instantiating and Invoking a Delegate
// Define a method that matches the delegate signature
public void DisplayMessage(string msg)
{
Console.WriteLine($"Message: {msg}");
}
// In another part of your code:
MyDelegate del = new MyDelegate(DisplayMessage);
del("Hello, Delegates!"); // Invokes DisplayMessage
The new keyword is optional in modern C#:
MyDelegate del = DisplayMessage; // Shorthand
del("Another Message");
Events
An event is a mechanism that enables a class (the publisher) to notify other classes (the subscribers) when something happens. Events are built on delegates. A class declares an event of a specific delegate type. Other classes can then subscribe to this event, providing methods (event handlers) that will be executed when the event is raised.
Events provide a clear contract for communication and allow for loose coupling between components.
Convention for Event Arguments: By convention, event handler delegates have a signature that accepts two arguments: an object representing the sender (the publisher) and an EventArgs derived type (or a custom type) containing event data.
Declaring an Event
A class publishes an event using the event keyword:
// Define a custom EventArgs class (optional but good practice)
public class MyEventArgs : EventArgs
{
public string Status { get; }
public MyEventArgs(string status) { Status = status; }
}
// Define the delegate for the event
public delegate void StatusChangedEventHandler(object sender, MyEventArgs e);
public class StatusNotifier
{
// Declare the event
public event StatusChangedEventHandler StatusChanged;
public void ChangeStatus(string newStatus)
{
// ... logic to change status ...
// Raise the event if there are subscribers
OnStatusChanged(newStatus);
}
// Protected virtual method to raise the event
protected virtual void OnStatusChanged(string newStatus)
{
StatusChangedEventHandler handler = StatusChanged; // Copy to a local variable for thread safety
if (handler != null)
{
handler(this, new MyEventArgs(newStatus));
}
}
}
Event Handlers
An event handler is a method that is called when an event is raised. The signature of an event handler method must match the delegate type declared for the event.
Creating an Event Handler
In the subscriber class:
public class StatusDisplay
{
public void SubscribeToNotifications(StatusNotifier notifier)
{
// Subscribe to the StatusChanged event
notifier.StatusChanged += new StatusChangedEventHandler(HandleStatusChange);
}
// The event handler method
private void HandleStatusChange(object sender, MyEventArgs e)
{
Console.WriteLine($"Notification received from {sender.GetType().Name}: Status is now {e.Status}");
}
public void Unsubscribe(StatusNotifier notifier)
{
// Unsubscribe from the event
notifier.StatusChanged -= HandleStatusChange;
}
}
The += operator subscribes a method to an event, and -= unsubscribes it.
Publishing and Subscribing to Events
The relationship between an event publisher and its subscribers is key to the event-driven model.
Putting It Together
public class Program
{
public static void Main(string[] args)
{
StatusNotifier notifier = new StatusNotifier();
StatusDisplay display = new StatusDisplay();
// Subscriber subscribes to the publisher's event
display.SubscribeToNotifications(notifier);
// Publisher raises an event
notifier.ChangeStatus("Processing...");
notifier.ChangeStatus("Completed");
// Unsubscribe when no longer needed to prevent memory leaks
display.Unsubscribe(notifier);
}
}
When notifier.ChangeStatus() is called, it eventually calls OnStatusChanged(), which checks if StatusChanged (the event) has any subscribers. If so, it invokes them, passing the sender and event data.
Using Generic EventArgs and EventHandler
.NET provides generic types for simplifying event handling:
EventArgs: The base class for allEventArgstypes. Use it directly when no custom data needs to be passed.EventHandler: A generic delegate that simplifies declaring delegates for events where custom data is passed.
Using Generic Types
// No custom EventArgs needed if no specific data is passed
public class SimpleNotifier
{
// Use the built-in generic delegate
public event EventHandler SimpleEvent;
public void DoSomething()
{
// Raise the event with empty EventArgs
OnSimpleEvent(EventArgs.Empty);
}
protected virtual void OnSimpleEvent(EventArgs e)
{
SimpleEvent?.Invoke(this, e); // Null-conditional operator for concise null check
}
}
public class SimpleSubscriber
{
public void Subscribe(SimpleNotifier notifier)
{
notifier.SimpleEvent += HandleSimpleEvent;
}
// Handler signature matches EventHandler
private void HandleSimpleEvent(object sender, EventArgs e)
{
Console.WriteLine("Simple event occurred!");
}
}
For passing custom data, you still create a custom derived class from EventArgs, but then use EventHandler<YourCustomEventArgs>.
Best Practices for Event Handling
- Use
EventArgs.Empty: When an event does not need to pass any custom data, useEventArgs.Empty. - Use
EventHandler<TEventArgs>: Prefer the genericEventHandler<TEventArgs>delegate over custom delegate declarations when possible. - Thread Safety: When raising an event, copy the event delegate to a local variable to prevent race conditions if another thread unsubscribes from the event between the null check and the invocation. The null-conditional operator (
?.Invoke()) handles this more concisely and safely. - Provide an
OnEventNamemethod: Make the event-raising logic a protected virtual method (e.g.,OnStatusChanged) to allow derived classes to override and extend the behavior. - Unsubscribe: Always unsubscribe from events when the subscriber is no longer interested or is being disposed of, especially in long-lived objects, to prevent memory leaks.
- Naming Conventions: Event names typically use a verb (e.g.,
Clicked,DataReceived). Event handler delegate names often end withEventHandler(e.g.,ClickHandler,DataReceivedEventHandler).