1. Style of declaring and naming events
Events are not just delegates. They are a distinct entity for communication between parts of the application, and their declaration should be clear.
Use the right delegate type
In 99% of cases use the standard delegates:
- EventHandler — for events without data.
- EventHandler<TEventArgs> — when you need to pass parameters.
Standardization makes code maintenance and integration with .NET libraries easier. Don't invent your own delegate if EventHandler fits.
public event EventHandler SomethingHappened; // No data
public event EventHandler<MyEventArgs> DataReceived; // Has additional data
If you need special customization — declare your own delegate, but that's rare.
Event naming
In .NET events are named in the past tense: Completed, Clicked, Changed, Received. That emphasizes that something has happened.
Examples:
public event EventHandler DataLoaded; // Data has been loaded
public event EventHandler<MessageEventArgs> MessageReceived; // Message received
public event EventHandler Saving; // The saving process has started
Sometimes the form Changing is used for "before" change events to give a chance to intervene.
2. Organizing the publisher class: the protected virtual OnEvent method
Always add a protected virtual method that raises the event: it gives a centralized call point, extensibility in inheritance, and predictable behavior.
public class FileLoader
{
public event EventHandler<FileLoadedEventArgs> FileLoaded;
protected virtual void OnFileLoaded(FileLoadedEventArgs e)
{
FileLoaded?.Invoke(this, e);
}
public void Load(string filename)
{
// ... file loading logic ...
OnFileLoaded(new FileLoadedEventArgs(filename));
}
}
public class FileLoadedEventArgs : EventArgs
{
public string FileName { get; }
public FileLoadedEventArgs(string fileName) => FileName = fileName;
}
Let only OnFileLoaded raise the event — it's easier to maintain and test that way.
3. Subscription and unsubscription rules: lifecycle, IDisposable
If the subscriber's lifetime is shorter than the publisher's, be sure to unsubscribe before the subscriber is destroyed. It's convenient to implement IDisposable and unsubscribe in Dispose().
public class TemporaryListener : IDisposable
{
private readonly Publisher _publisher;
public TemporaryListener(Publisher publisher)
{
_publisher = publisher;
_publisher.DataReceived += HandleData;
}
private void HandleData(object sender, EventArgs e)
{
// Working with data
}
public void Dispose()
{
_publisher.DataReceived -= HandleData;
}
}
// Usage with using:
using (var listener = new TemporaryListener(myPublisher))
{
// listener listens to events here
}
// After exiting using - Dispose is called, unsubscribe happened
If you forget to unsubscribe, the publisher will hold a reference to the subscriber's delegate — you'll get memory leaks and "zombie objects".
4. Thread-safe event invocation
In multithreaded code subscribers can be added/removed concurrently with event invocation. This can cause races and NullReferenceException. Use the thread-safe pattern: copy the delegate to a local variable.
protected virtual void OnSomethingHappened()
{
EventHandler handler = SomethingHappened;
handler?.Invoke(this, EventArgs.Empty);
}
With C# 6+ it's enough:
SomethingHappened?.Invoke(this, EventArgs.Empty);
5. Use EventArgs instead of object
Don't pass data via object or class fields. Use strong typing through EventArgs descendants.
public class DownloadCompletedEventArgs : EventArgs
{
public string FileName { get; }
public long Size { get; }
public DownloadCompletedEventArgs(string fileName, long size)
{
FileName = fileName;
Size = size;
}
}
public event EventHandler<DownloadCompletedEventArgs> DownloadCompleted;
6. Documenting events and subscribers
Document: when the event is raised, the meaning of EventArgs fields, whether unsubscription is needed and when.
/// <summary>
/// The event occurs after successful data load.
/// </summary>
public event EventHandler<DataLoadedEventArgs> DataLoaded;
7. Summary recommendations for event architecture
Separate responsibilities
The publisher only notifies about the fact. The subscriber decides when to subscribe and unsubscribe.
Avoid "bombarding" with events
Don't raise the same event dozens of times per second without need — that's excessive load.
Avoid using events for two-way communication
Events are for "one reports — many listen" schemes. For two-way communication consider interfaces, callbacks or other mechanisms.
Don't store subscribers in the class
Don't keep explicit references to subscribers — events and delegates will do that automatically.
8. Classic antipatterns
Typeless events
public event Action<object> SomethingHappened; // It's unclear what's inside
Bad: typing is broken, casts are required, maintainability is lost.
Forgetting to unsubscribe
public class ShortLivedListener
{
public ShortLivedListener(Publisher p) =>
p.DataReceived += DoWork;
private void DoWork(object sender, EventArgs e) { /* ... */ }
// No Dispose, no unsubscribe => zombie objects!
}
Violating SRP
A class that is simultaneously publisher, subscriber, and handler — mixing roles. Separate responsibilities.
9. Practical use in interviews and projects
In many publish-subscribe projects proper organization of events is a key to scalability and maintainability. In interviews you're often asked to:
- implement an event system with correct typing,
- show lifecycle management of subscribers,
- explain thread-safe event invocation.
Clean, documented, properly organized event code immediately sets you apart among candidates.
GO TO FULL VERSION