C# Events
1. Why do we need events?
You already know delegates:
Action<string> printer = message => Console.WriteLine(message);
printer("Hello");A delegate variable can store one or more methods and call them later.
An event is based on delegates, but it is used for a special purpose:
An event allows one object to notify other objects that something happened.
Examples:
- A button was clicked.
- A timer has finished.
- A player lost health.
- A file download completed.
- A new message arrived.
- A value in a service changed and the UI should update.
The object that sends the notification is often called the publisher.
The objects that react to the notification are called subscribers.
2. Basic idea: publisher and subscriber
Imagine a DoorSensor.
When the door opens, the sensor should not directly decide what happens next.
Maybe:
- a lamp should turn on
- an alarm should start
- a log entry should be written
The sensor should only say:
“The door was opened.”
Other parts of the program can decide how to react.
This is exactly what events are for.
3. A first event example
public class DoorSensor
{
public event Action? DoorOpened;
public void OpenDoor()
{
Console.WriteLine("Door was opened.");
DoorOpened?.Invoke();
}
}Usage:
DoorSensor sensor = new DoorSensor();
sensor.DoorOpened += () =>
{
Console.WriteLine("Turning on the light.");
};
sensor.DoorOpened += () =>
{
Console.WriteLine("Writing log entry.");
};
sensor.OpenDoor();Output:
Door was opened.
Turning on the light.
Writing log entry.4. Event syntax
An event declaration usually looks like this:
public event Action? SomethingHappened;This means:
-
public
Other classes can subscribe to the event. -
event
This is not a normal delegate field. It is an event. -
Action?
The event uses the delegate typeAction.
This means the subscribed methods must returnvoidand take no parameters. -
SomethingHappened
The name of the event.
The ? means the event may currently be null, because nobody has subscribed yet.
5. Subscribing to an event
To subscribe to an event, use +=.
sensor.DoorOpened += ReactToDoorOpened;The method must match the delegate type.
static void ReactToDoorOpened()
{
Console.WriteLine("Someone opened the door!");
}You can also subscribe with a lambda:
sensor.DoorOpened += () =>
{
Console.WriteLine("Lambda reaction.");
};This works because the lambda matches Action.
6. Raising or firing an event
The class that owns the event can raise it by calling Invoke.
DoorOpened?.Invoke();The ?. operator is important.
It means:
Only call the event if it is not null.
Without it, this could crash if nobody subscribed:
DoorOpened.Invoke(); // dangerous if DoorOpened is nullSo this is the common safe version:
DoorOpened?.Invoke();7. Events with parameters
Often subscribers need more information.
Example:
- Which message was received?
- What is the new score?
- Which player lost health?
For that, use Action<T>.
public class ScoreCounter
{
public event Action<int>? ScoreChanged;
private int _score;
public void AddPoints(int points)
{
_score += points;
ScoreChanged?.Invoke(_score);
}
}Usage:
ScoreCounter counter = new ScoreCounter();
counter.ScoreChanged += newScore =>
{
Console.WriteLine($"New score: {newScore}");
};
counter.AddPoints(10);
counter.AddPoints(5);Output:
New score: 10
New score: 15Here, the event type is:
Action<int>So every subscriber must be compatible with:
void SomeMethod(int value)8. Events with multiple parameters
You can use Action<T1, T2>, Action<T1, T2, T3>, and so on.
public class TemperatureSensor
{
public event Action<double, string>? TemperatureMeasured;
public void Measure(double temperature, string room)
{
TemperatureMeasured?.Invoke(temperature, room);
}
}Usage:
TemperatureSensor sensor = new TemperatureSensor();
sensor.TemperatureMeasured += (temperature, room) =>
{
Console.WriteLine($"{room}: {temperature} °C");
};
sensor.Measure(22.5, "Classroom");9. Unsubscribing from an event
To unsubscribe, use -=.
sensor.DoorOpened -= ReactToDoorOpened;This is useful when an object should no longer react to an event.
Example:
DoorSensor sensor = new DoorSensor();
void Alarm()
{
Console.WriteLine("Alarm!");
}
sensor.DoorOpened += Alarm;
sensor.OpenDoor(); // Alarm!
sensor.DoorOpened -= Alarm;
sensor.OpenDoor(); // no alarmImportant:
If you subscribe with an anonymous lambda directly, unsubscribing is difficult:
sensor.DoorOpened += () => Console.WriteLine("Alarm!");This lambda has no name, so you cannot easily remove exactly the same handler later.
If you need to unsubscribe, use a named method or store the lambda in a variable:
Action handler = () => Console.WriteLine("Alarm!");
sensor.DoorOpened += handler;
sensor.DoorOpened -= handler;10. What is the difference between a delegate field and an event?
At first, this looks similar:
public Action? DoorOpened;and:
public event Action? DoorOpened;But there is an important difference.
With a public delegate field, outside code can do too much:
sensor.DoorOpened = null; // removes all subscribers
sensor.DoorOpened?.Invoke(); // outside code can fake the eventThat is dangerous.
With an event, outside code can only subscribe and unsubscribe:
sensor.DoorOpened += Handler;
sensor.DoorOpened -= Handler;Outside code cannot directly invoke the event.
sensor.DoorOpened?.Invoke(); // not allowed outside the classThis is the main reason why events exist.
They protect the publisher.
Another reason is, for simple Delegates, there can only be one Delegate assigned to it.
With Events, there can be multiple subscribers for one event. The event keyword basically manages a collection of delegates.
11. The standard .NET event pattern
Many .NET libraries use a standard event style:
public event EventHandler? SomethingHappened;or:
public event EventHandler<MyEventArgs>? SomethingHappened;EventHandler has this signature:
void Handler(object? sender, EventArgs e)The sender is the object that raised the event.
EventArgs contains additional event data.
Example:
public class MessageReceivedEventArgs : EventArgs
{
public string Message { get; }
public MessageReceivedEventArgs(string message)
{
Message = message;
}
}Publisher:
public class ChatRoom
{
public event EventHandler<MessageReceivedEventArgs>? MessageReceived;
public void SendMessage(string message)
{
MessageReceived?.Invoke(
this,
new MessageReceivedEventArgs(message)
);
}
}Subscriber:
ChatRoom room = new ChatRoom();
room.MessageReceived += (sender, e) =>
{
Console.WriteLine($"Message: {e.Message}");
};
room.SendMessage("Hello!");Output:
Message: Hello!12. Should we use Action or EventHandler?
For small examples, Action is simple and readable:
public event Action<int>? ScoreChanged;For larger applications and .NET-style APIs, EventHandler is more common:
public event EventHandler<ScoreChangedEventArgs>? ScoreChanged;A simple rule:
- Use
Actionwhen learning or for small internal code. - Use
EventHandler<TEventArgs>when writing reusable .NET-style classes.
13. Naming conventions
Events are usually named after what happened:
Started
Stopped
Clicked
ScoreChanged
MessageReceived
TemperatureChangedThe method that raises the event is often named with On....
protected virtual void OnScoreChanged(int newScore)
{
ScoreChanged?.Invoke(newScore);
}Then the class uses this method internally:
public void AddPoints(int points)
{
_score += points;
OnScoreChanged(_score);
}This keeps the code cleaner, especially when raising the event from multiple places.
14. Example: game health system
public class Player
{
public event Action<int>? HealthChanged;
public event Action? Died;
public int Health { get; private set; } = 100;
public void TakeDamage(int damage)
{
Health -= damage;
if (Health < 0)
Health = 0;
HealthChanged?.Invoke(Health);
if (Health == 0)
{
Died?.Invoke();
}
}
}Usage:
Player player = new Player();
player.HealthChanged += health =>
{
Console.WriteLine($"Health: {health}");
};
player.Died += () =>
{
Console.WriteLine("Game over!");
};
player.TakeDamage(30);
player.TakeDamage(80);Output:
Health: 70
Health: 0
Game over!The Player class does not need to know what should happen when the player dies.
Other classes can subscribe and react.
15. What are events useful for?
Events are useful when one part of a program should notify other parts without knowing them directly.
Typical use cases:
User interfaces
Buttons, text boxes, sliders and other UI elements use events.
Example:
button.Click += ButtonWasClicked;Games
Objects can react to actions:
- player died
- enemy spawned
- score changed
- item collected
Services
A service can inform other parts of the application:
- data changed
- connection lost
- message received
- download completed
Blazor Server
A shared service can raise an event when its state changes.
Multiple browser windows connected to the same server can subscribe to that event and update their UI.
16. Limits and disadvantages of events
Events are useful, but they also have disadvantages.
16.1 Program flow can become harder to understand
With a normal method call, it is clear what happens next:
SaveData();
PrintResult();With events, one line may call many unknown subscribers:
DataChanged?.Invoke();You may not immediately see which methods are executed.
This can make debugging harder.
16.2 Events can create hidden dependencies
The publisher does not know the subscribers.
That is often good.
But it also means the behavior of the program depends on who subscribed.
If an important subscriber is missing, something may silently not happen.
16.3 Forgetting to unsubscribe can cause problems
If a long-living object has an event and a short-living object subscribes to it, the subscriber may stay in memory longer than expected.
This can cause memory leaks.
Example idea:
service.Changed += ComponentWasChanged;If the component disappears but does not unsubscribe, the service may still keep a reference to it.
In Blazor components, you often unsubscribe in Dispose.
public void Dispose()
{
service.Changed -= OnServiceChanged;
}16.4 Exceptions in subscribers can interrupt other subscribers
If one event handler throws an exception, it can stop the event invocation.
SomethingHappened?.Invoke();If one subscriber crashes, later subscribers may not run.
For simple examples this is usually not a big issue, but in larger programs it matters.
16.5 Events are not automatically asynchronous
An event call is usually just a normal method call.
SomethingHappened?.Invoke();The subscribers run immediately and synchronously.
If a subscriber does slow work, the publisher has to wait.
Events are not automatically background tasks.
16.6 Too many events can make architecture messy
If everything communicates through events, the application can become difficult to understand.
Events should be used when they make the design clearer, not everywhere.
17. Summary
- An event is based on a delegate.
- An event lets an object notify other objects that something happened.
- The object raising the event is the publisher.
- The objects reacting to the event are subscribers.
- Subscribe with
+=. - Unsubscribe with
-=. - Raise an event with
?.Invoke(...). - Outside code can subscribe and unsubscribe, but cannot directly invoke the event.
- Events are useful for UI, games, services and state changes.
- Events can make code flexible, but they can also make program flow harder to follow.
- In long-living applications, remember to unsubscribe when necessary.