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 all EventArgs types. 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, use EventArgs.Empty.
  • Use EventHandler<TEventArgs>: Prefer the generic EventHandler<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 OnEventName method: 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 with EventHandler (e.g., ClickHandler, DataReceivedEventHandler).