Event Declaration and Handling

Vaibhav • September 11, 2025

In the previous article, we introduced the fundamentals of events - how they allow objects to communicate through notifications, using delegates as their underlying mechanism. Now it’s time to go deeper and explore how events are declared, how they’re handled by subscribers, and how to design event-driven systems that are robust, readable, and maintainable.

This article builds on your understanding of delegates, lambda expressions, and the built-in EventHandler pattern. We’ll walk through the anatomy of an event declaration, how to raise events safely, how subscribers respond, and how to structure your code to make event handling predictable and clean.

Declaring Events with Custom Delegates

Events are declared using the event keyword followed by a delegate type. This delegate defines the signature that all event handlers must follow. Let’s start with a simple example:

public delegate void StatusChangedHandler(string status);

public class Server
{
    public event StatusChangedHandler StatusChanged;

    public void UpdateStatus(string newStatus)
    {
        StatusChanged?.Invoke(newStatus);
    }
}

Here, we define a delegate StatusChangedHandler that takes a string. The StatusChanged event uses this delegate. When UpdateStatus is called, the event is raised - notifying all subscribers.

The ?.Invoke() syntax ensures that the event is only raised if there are subscribers. This is the recommended way to raise events safely.

Using the Standard EventHandler Pattern

While custom delegates work fine, C# provides a standard pattern for events using EventHandler and EventHandler<TEventArgs>. This pattern improves consistency and makes your code easier to understand and maintain.

public class Timer
{
    public event EventHandler Tick;

    public void TriggerTick()
    {
        Tick?.Invoke(this, EventArgs.Empty);
    }
}

The EventHandler delegate takes two parameters: the sender (usually this) and an instance of EventArgs. This pattern is used throughout the .NET framework and should be preferred for general-purpose events.

Creating Custom EventArgs

If your event needs to pass additional data, you can define a class that inherits from EventArgs. This allows you to include properties that describe the event context.

public class ProgressEventArgs : EventArgs
{
    public int Percentage { get; }

    public ProgressEventArgs(int percentage)
    {
        Percentage = percentage;
    }
}

public class Downloader
{
    public event EventHandler<ProgressEventArgs> ProgressChanged;

    public void ReportProgress(int percent)
    {
        ProgressChanged?.Invoke(this, new ProgressEventArgs(percent));
    }
}

The ProgressChanged event uses EventHandler<ProgressEventArgs>. When raised, it passes an instance of ProgressEventArgs containing the current progress percentage. Subscribers can access this data in their handlers.

Subscribing to Events

To handle an event, you attach a method to it using the += operator. The method must match the delegate’s signature. For EventHandler, this means two parameters: sender and event args.

Downloader downloader = new Downloader();

downloader.ProgressChanged += (sender, e) =>
{
    Console.WriteLine("Progress: " + e.Percentage + "%");
};

This lambda expression matches the expected signature and prints the progress percentage. You can also use named methods:

void HandleProgress(object sender, ProgressEventArgs e)
{
    Console.WriteLine("Progress update: " + e.Percentage + "%");
}

downloader.ProgressChanged += HandleProgress;

Both approaches are valid. Use lambdas for short, inline logic and named methods for reusable or complex handlers.

Unsubscribing from Events

To remove a handler, use the -= operator. This is important for avoiding memory leaks - especially in long-running applications or when working with UI elements.

downloader.ProgressChanged -= HandleProgress;

If you don’t unsubscribe, the event may hold a reference to the handler - preventing garbage collection and causing unexpected behavior.

Note: Always unsubscribe from events when the subscriber is no longer needed. This helps with memory management and avoids dangling references.

Raising Events Internally

Events should only be raised from within the class that declares them. This encapsulation ensures that external code cannot trigger events directly - preserving the integrity of your object’s behavior.

If you need to expose event-like behavior to external code, provide public methods that raise the event internally. For example:

public class Alarm
{
    public event EventHandler AlarmTriggered;

    public void Trigger()
    {
        AlarmTriggered?.Invoke(this, EventArgs.Empty);
    }
}

External code calls Trigger(), which raises the event. This keeps the event logic centralized and controlled.

Event Invocation Best Practices

When raising events, follow these best practices:

  • Use ?.Invoke() to avoid null reference exceptions.
  • Pass this as the sender to help subscribers identify the source.
  • Use EventArgs.Empty when no data is needed.
  • Use custom EventArgs classes for meaningful data.

These patterns make your event code predictable, safe, and easy to integrate with other components.

Always raise events from within the declaring class, using ?.Invoke() and passing this as the sender. This ensures safe and consistent behavior.

Handling Multiple Subscribers

Events support multiple subscribers - all of which are called when the event is raised. The order of invocation matches the order of subscription. If one handler throws an exception, the remaining handlers are not called unless you handle exceptions manually.

foreach (Delegate d in AlarmTriggered?.GetInvocationList() ?? Array.Empty<Delegate>())
{
    try
    {
        d.DynamicInvoke(this, EventArgs.Empty);
    }
    catch (Exception ex)
    {
        Console.WriteLine("Handler error: " + ex.Message);
    }
}

This pattern ensures that all handlers are called - even if one fails. It’s useful in systems where reliability matters more than strict error propagation.

Events are multicast delegates under the hood. You can inspect their invocation list using GetInvocationList() - just like with regular delegates.

Designing Event-Driven Systems

Events are a key building block for reactive and event-driven systems. They allow components to respond to changes without tight coupling. When designing such systems:

  • Use events to signal state changes, user actions, or external triggers.
  • Keep event names descriptive and consistent (e.g., DataLoaded, ConnectionLost).
  • Use custom EventArgs to provide context.
  • Avoid exposing raw delegates - use events for encapsulation.

These principles help you build systems that are modular, testable, and easy to extend.

Summary

In this article, you learned how to declare and handle events in C#. We covered custom delegates, the standard EventHandler pattern, custom EventArgs, safe invocation, and best practices for managing subscribers. Events are a powerful tool for building loosely coupled systems - enabling components to respond to changes without knowing each other’s internals.

You now understand how to structure event declarations, raise events safely, and design handlers that respond predictably. In the next article, we’ll explore Custom Event Arguments - diving deeper into how to pass rich data with your events and how to design event payloads that are clear, extensible, and meaningful.