1. Practical scenarios for using events
Events aren't just nice theory from textbooks. In practice they're needed in almost every second app, and if you're working with UI or network services — in most cases.
Let's walk through a few typical scenarios from real development. We'll also see how standards and best practices help avoid common mistakes.
Reacting to user actions (like in a UI)
Probably the most frequent scenario is when the user does something (clicks a button, selects an item) and the application must react. That's how Windows Forms, WPF, UWP, Avalonia, MAUI and other UI frameworks work.
Mini-example (no UI — we simulate a button):
public class Button
{
public event EventHandler? Click; // standard delegate
public void SimulateClick()
{
Click?.Invoke(this, EventArgs.Empty); // the "button" was "clicked"
}
}
class Program
{
static void Main()
{
var button = new Button();
button.Click += (sender, e) => Console.WriteLine("Button clicked!");
button.SimulateClick(); // > Button clicked!
}
}
In a real UI framework the Click event fires when the user presses the mouse button or taps the screen. Everything subscribed to that event will get notified — that's how app logic works.
Signaling progress, completion and errors
Imagine: file download, data processing, a long computation. You need to inform about progress or errors. Often there's an event for "progress step" and a separate event for "completed" or "failed".
File downloader example:
public class FileDownloader
{
public event EventHandler<int>? ProgressChanged;
public event EventHandler? DownloadCompleted;
public event EventHandler<string>? DownloadFailed;
public void Download()
{
for (int i = 1; i <= 100; i += 10)
{
Thread.Sleep(50); // simulate delay
ProgressChanged?.Invoke(this, i);
}
DownloadCompleted?.Invoke(this, EventArgs.Empty);
}
}
var downloader = new FileDownloader();
downloader.ProgressChanged += (s, progress) => Console.WriteLine($"Download: {progress}%");
downloader.DownloadCompleted += (s, e) => Console.WriteLine("Download completed.");
downloader.Download();
Here there are multiple events: you can subscribe to all or only the ones you need. This is similar to how many standard .NET classes for threading, file download, HTTP, etc. work.
Reacting to object lifecycle (e.g., "saved", "deleted")
Suppose you have a domain model. You want some processes to react when a user saves or deletes an object: update cache, log, send a message, etc.
public class UserRepository
{
public event EventHandler<UserEventArgs>? UserSaved;
public event EventHandler<UserEventArgs>? UserDeleted;
public void Save(User user)
{
//... save the user
UserSaved?.Invoke(this, new UserEventArgs(user));
}
public void Delete(User user)
{
//... delete the user
UserDeleted?.Invoke(this, new UserEventArgs(user));
}
}
public class UserEventArgs : EventArgs
{
public User User { get; }
public UserEventArgs(User user) => User = user;
}
Now different modules can subscribe and — without direct ties! — receive notifications about user changes. The repository itself doesn't know who's "listening".
Asynchronous events and multithreading
Sometimes events are raised not from the main (UI) thread — for example, from background tasks, timers or async operations. In such cases remember that handler code will run on a "foreign" thread. If a handler tries to update UI elements directly from another thread — you'll get exceptions.
What is marshaling? Marshaling is transferring code execution from one thread to another, usually from a background thread back to the UI thread to safely update the interface. In UI apps (WinForms, WPF) mechanisms like SynchronizationContext or Dispatcher are used to "route" the handler call to the proper thread.
Don't do this:
// The event is raised from a background thread, and the handler updates the UI directly — you'll get an exception!
Recommended: check the execution thread and marshal to the UI thread if needed (via SynchronizationContext or Dispatcher). In console and server apps such restrictions usually don't apply.
Event Aggregator / Messaging
In large apps it's common to use a centralized event aggregator. It reduces coupling: subscribers and publishers don't know each other and exchange messages via the hub.
public class EventAggregator
{
public event EventHandler<SomeEventArgs>? SomeEvent;
public void Publish(SomeEventArgs args)
{
SomeEvent?.Invoke(this, args);
}
}
2. Best patterns and practices
Use the [CallerMemberName] attribute for diagnostics
If you're writing infra code (e.g., loggers, tracers), log the name of the source method using the CallerMemberName attribute — it makes debugging easier.
Make events thread-safe when required
Events are multicast, and access from multiple threads needs care: copy the delegate to a local variable before invoking.
var handler = MyEvent;
if (handler != null) handler(this, args);
In C# 6+ use the safe call: MyEvent?.Invoke(this, args).
Design custom EventArgs as immutable
Define your EventArgs types with read-only properties — this prevents accidental mutation of event state by subscribers.
public class WorkCompletedEventArgs : EventArgs
{
public string Message { get; }
public WorkCompletedEventArgs(string message) => Message = message;
}
public or private event
Make events public event, and encapsulate raising them in a protected virtual method OnEventName. This ensures extensibility (a subclass can override behavior) and prevents external callers from firing the event directly.
Don't break lifecycle ordering
If you start a long chain of handlers, remember: someone might subscribe and try to alter internal logic. Document where and when events are raised, and whether an event can fire multiple times.
Multiple events and composition
When an object listens to multiple sources, group subscription and unsubscription in one place (e.g., Subscribe/Unsubscribe methods). If needed, keep private delegate fields for easier unsubscribing.
3. How NOT to use events: common mistakes
Mistake #1: ignoring the standard event pattern.
If every class declares custom delegates for each event it quickly becomes chaos. Try to use standard delegates EventHandler or EventHandler<T>, where T derives from EventArgs. This makes code clearer and familiar to .NET developers.
Mistake #2: invoking events incorrectly from external code.
Never invoke an event directly outside the publisher class. Events should be raised only inside the class, usually via a protected OnEventName method. Subscribers can only attach (+=) and detach (-=).
Bad:
foo.MyEvent(); // bad! You can't call an event directly from outside
Good:
// Only inside foo:
// protected virtual void OnMyEvent()
// {
// MyEvent?.Invoke(this, EventArgs.Empty);
// }
Mistake #3: calling an event without checking for subscribers (null).
If you try to invoke an event that has no subscribers you'll get a NullReferenceException. In C# 6.0+ use ?.Invoke(...) — the event is invoked only if there are subscribers.
Mistake #4: forgetting to unsubscribe from events (memory leak).
If an object subscribes to an event but doesn't unsubscribe before being collected, the publisher holds a reference to the subscriber. The GC can't free that memory, which is critical when publishers are long-lived and subscribers are short-lived (e.g., ViewModel, forms). The best solutions are explicit unsubscribe or using weak references (WeakReference) where appropriate.
Mistake #5: raising an event outside a protected virtual OnEvent method.
Raising an event directly deprives subclasses of the chance to override behavior. The correct pattern is to declare a protected virtual method that raises the event.
Standard example:
protected virtual void OnSomething(EventArgs e)
{
Something?.Invoke(this, e);
}
GO TO FULL VERSION