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
andEventHandler<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.