Event Fundamentals

Vaibhav • September 11, 2025

In the previous articles, we explored delegates, lambda expressions, and the built-in Action and Func types. These concepts laid the foundation for one of the most important features in C#: events. Events allow objects to communicate with each other in a loosely coupled way - a core principle in building scalable, maintainable applications.

In this article, we’ll introduce the fundamentals of events in C#. You’ll learn how events are declared, how they’re raised, how subscribers respond, and how events relate to delegates. We’ll also explore real-world analogies, best practices, and common pitfalls - all while building on the concepts you’ve already mastered.

What Are Events?

An event is a mechanism that allows a class to notify other classes when something happens. It’s built on top of delegates - meaning it uses the same method signature matching and invocation model. But unlike raw delegates, events add a layer of protection and intent: they’re designed specifically for broadcasting notifications.

Think of an event as a doorbell. The doorbell doesn’t care who’s listening - it just rings. Anyone interested (subscribers) can respond when it rings. The doorbell doesn’t need to know what happens next.

Events are based on delegates. When you declare an event, you’re actually declaring a delegate field with restricted access - only the class that declares the event can raise it.

Declaring an Event

To declare an event, you first define a delegate type that describes the signature of the event handler. Then you use the event keyword to declare an event based on that delegate.

public delegate void MessageHandler(string message);

public class Messenger
{
    public event MessageHandler OnMessage;
}

Here, we define a delegate MessageHandler that takes a string. Then we declare an event OnMessage using that delegate. Other classes can subscribe to this event and respond when it’s raised.

Subscribing to an Event

To respond to an event, you attach a method to it using the += operator. This method must match the delegate’s signature.

Messenger messenger = new Messenger();

messenger.OnMessage += msg => Console.WriteLine("Received: " + msg);

This lambda expression matches the MessageHandler delegate, so it can be attached directly. You can also use named methods:

void HandleMessage(string msg)
{
    Console.WriteLine("Handled: " + msg);
}

messenger.OnMessage += HandleMessage;

Now both handlers will be called when the event is raised. This is multicast behavior - just like with delegates.

Raising an Event

Only the class that declares the event can raise it. This is done by invoking the event like a delegate - typically inside a method that checks for null.

public void Send(string msg)
{
    OnMessage?.Invoke(msg);
}

The null-conditional operator ?. ensures that the event is only invoked if there are subscribers. This prevents a NullReferenceException if no one is listening.

Always use ?.Invoke() when raising events. It’s safe, concise, and avoids runtime errors.

Events vs Delegates

Events and delegates are closely related - but they serve different purposes. Delegates are general-purpose function pointers. Events are specialized for notifications. The key differences are:

  • Events can only be invoked from within the declaring class.
  • Events prevent external code from overwriting the invocation list.
  • Events express intent - they’re meant to be subscribed to, not called directly.

This makes events safer and more expressive than raw delegates - especially in large applications.

Using Built-in EventHandler

Instead of declaring your own delegate type, you can use the built-in EventHandler and EventHandler<TEventArgs> types. These follow a standard pattern and make your code more consistent.

public class Notifier
{
    public event EventHandler OnNotify;

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

The EventHandler delegate takes two parameters: the sender (usually this) and event data (usually EventArgs.Empty if no data is needed). This pattern is used throughout the .NET framework.

Custom Event Arguments

If you want to pass additional data with your event, you can define a class that inherits from EventArgs. Then use EventHandler<T> to declare the event.

public class MessageEventArgs : EventArgs
{
    public string Message { get; set; }
}

public event EventHandler<MessageEventArgs> OnMessage;

When raising the event, you pass an instance of MessageEventArgs:

OnMessage?.Invoke(this, new MessageEventArgs { Message = "Hello!" });

Subscribers can then access the message via e.Message in their handler.

Unsubscribing from Events

To remove a handler from an event, use the -= operator. This is important for avoiding memory leaks - especially in long-lived applications.

messenger.OnMessage -= HandleMessage;

If you don’t unsubscribe, the object may stay in memory longer than expected - because the event holds a reference to it.

Note: Always unsubscribe from events when the subscriber is no longer needed. This helps with garbage collection and prevents unexpected behavior.

Events in Real-World Applications

Events are used everywhere in C# applications. UI frameworks like WinForms and WPF use events for button clicks, mouse movements, and key presses. Game engines use events for collisions, scoring, and input. Business applications use events for notifications, logging, and workflow triggers.

For example, a button might expose a Click event. You subscribe to it with a handler that runs when the user clicks the button. The button doesn’t care what happens - it just raises the event.

Thread Safety and Events

Events are not thread-safe by default. If multiple threads subscribe or unsubscribe at the same time, you may get race conditions. To avoid this, use locking or thread-safe patterns.

Also, when raising an event, always copy the delegate to a local variable before invoking it. This prevents null reference issues if the event is unsubscribed between the null check and the invocation.

var handler = OnMessage;
if (handler != null)
{
    handler("Safe call");
}

This pattern ensures that the event is stable during invocation.

Common Mistakes with Events

Events are powerful - but they can be misused. Here are some common mistakes to avoid:

  • Invoking events from outside the declaring class
  • Forgetting to unsubscribe from events
  • Using events for tight coupling instead of loose notifications
  • Not checking for null before invoking

Be deliberate with your event design. Use them for broadcasting, not for direct control.

Best Practices for Events

Events are a core part of C# - and they work best when used with care. Here are some guidelines:

  • Use EventHandler and EventHandler<T> for consistency
  • Use ?.Invoke() to raise events safely
  • Unsubscribe when the subscriber is no longer needed
  • Use custom EventArgs classes for passing data
  • Keep event handlers short and focused

Design events for loose coupling. The publisher should not care who’s listening - and the subscriber should not depend on the publisher’s internal state.

Summary

Events are a powerful feature in C# that allow objects to communicate in a loosely coupled way. They’re built on delegates, but add safety, intent, and structure. You learned how to declare events, subscribe and unsubscribe, raise events safely, and use built-in patterns like EventHandler.

Events are essential for building responsive, scalable applications. They support reactive programming, UI interactions, and dynamic workflows. In the next article, we’ll explore Event Declaration and Handling - diving deeper into how events are structured, how to manage multiple subscribers, and how to design robust event-driven systems.