Back to Blog
C#

C# Custom EventArgs: Passing Data with Events

c# custom eventargs: Learn how to create custom EventArgs classes in C# to pass structured data with events, including design rules and memory behavior.

C#EventsEventArgsEvent Handling.NET
Illustration of a C# event with a custom EventArgs class carrying data between sender and handler.

Why EventArgs Alone Is Not Enough

The standard EventArgs class in C# is an empty marker type. It exists so that event handlers have a uniform signature, but it carries no information about what happened. When an event needs to communicate details—such as a file name, a progress percentage, or a user selection—you must create a custom EventArgs subclass. This is the core of the c# custom eventargs pattern: deriving a new type from EventArgs and adding properties that describe the event.

Consider a ProgressChanged event. With plain EventArgs, a subscriber knows only that progress changed, not by how much. A custom class like ProgressChangedEventArgs can expose a ProgressPercentage property, making the event genuinely useful.

Defining a Custom EventArgs Class

A custom EventArgs class is a simple class that inherits from System.EventArgs. It typically exposes read-only properties and a constructor that initializes them. The convention is to name the class with an EventArgs suffix.

public class ProgressChangedEventArgs : EventArgs { public int ProgressPercentage { get; } public ProgressChangedEventArgs(int progressPercentage) { ProgressPercentage = progressPercentage; } }

The properties are read-only because event data should be immutable once the event is raised. The subscriber receives a snapshot of the state at the moment the event occurred. If the sender needs to modify the data after raising the event, that would break the contract and lead to subtle bugs.

You can also include multiple properties, collections, or even complex objects. The class is just a container. There is no requirement to override any methods; the base class provides everything needed.

Raising an Event with Custom EventArgs

To raise an event that uses a custom EventArgs type, you declare the event with the EventHandler<T> delegate, where T is your custom type. This is the standard way to wire custom data into the event pipeline.

public class FileDownloader { public event EventHandler<ProgressChangedEventArgs>? ProgressChanged; public void Download(string url) { for (int i = 0; i <= 100; i += 10) { OnProgressChanged(new ProgressChangedEventArgs(i)); // Simulate work Thread.Sleep(50); } } protected virtual void OnProgressChanged(ProgressChangedEventArgs e) { ProgressChanged?.Invoke(this, e); } }

The protected OnProgressChanged method is a common pattern. It allows derived classes to override the invocation logic, for example to add logging or to suppress the event. It also centralizes the null check, so you don't have to repeat ?.Invoke everywhere.

When raising the event, you pass this as the sender and the custom EventArgs instance as the second argument. The EventHandler<T> delegate ensures that the subscriber's handler receives the correct type.

Subscribing and Reading Custom Event Data

On the subscriber side, you attach a handler that matches the EventHandler<T> signature. Inside the handler, you access the custom properties directly.

var downloader = new FileDownloader(); downloader.ProgressChanged += OnProgressChanged; void OnProgressChanged(object? sender, ProgressChangedEventArgs e) { Console.WriteLine($"Progress: {e.ProgressPercentage}%"); }

The handler receives the custom EventArgs instance as e. Because the type is known, you get compile-time access to ProgressPercentage without casting. This is the main benefit over using a generic object or a shared dictionary.

If you are using a lambda, the same principle applies:

downloader.ProgressChanged += (sender, e) => Console.WriteLine(e.ProgressPercentage);

The type inference works because the event is declared with EventHandler<ProgressChangedEventArgs>.

Design Rules for Custom EventArgs

Several conventions keep custom EventArgs classes maintainable and predictable.

First, keep the class immutable. Expose only getters and set the values in the constructor. This prevents subscribers from accidentally mutating shared state, which is especially important when multiple handlers run sequentially.

Second, use meaningful property names that describe the event's context. A property named Data is vague; FileName or ErrorCode communicates intent.

Third, consider whether the data should be optional. If some event occurrences lack a value, you can use nullable properties or provide a default constructor. For example, a ValidationFailedEventArgs might have an Errors collection that is empty when validation succeeds.

Fourth, avoid putting logic in the EventArgs class. It is a data transfer object, not a business object. Keep methods out of it unless they are simple helpers like ToString overrides for debugging.

Finally, follow the naming convention strictly. The suffix EventArgs is part of the .NET framework convention and helps other developers immediately understand the role of the class.

Memory and Allocation Behavior

Creating a custom EventArgs instance for every event occurrence introduces allocation overhead. For high-frequency events, such as progress updates or sensor readings, this can become measurable. The garbage collector will eventually reclaim these objects, but the allocation pressure can affect performance.

One mitigation is to reuse a single EventArgs instance when the data is immutable and the same value is raised repeatedly. For example, if you have a StateChangedEventArgs that only carries an enum, you can cache instances for each enum value.

public class StateChangedEventArgs : EventArgs { public State NewState { get; } private StateChangedEventArgs(State newState) => NewState = newState; public static StateChangedEventArgs FromState(State state) => state switch { State.Idle => Idle, State.Running => Running, State.Stopped => Stopped, _ => new StateChangedEventArgs(state) }; public static readonly StateChangedEventArgs Idle = new(State.Idle); public static readonly StateChangedEventArgs Running = new(State.Running); public static readonly StateChangedEventArgs Stopped = new(State.Stopped); }

This pattern works when the set of possible values is small and fixed. For data that varies, such as progress percentages, caching is impractical. In those cases, the allocation cost is usually acceptable unless the event fires thousands of times per second.

Another consideration is the event delegate itself. Each subscription adds a delegate to the invocation list. If you subscribe and unsubscribe frequently, you may create delegate instances that need collection. Using a custom EventArgs does not change this, but it is worth remembering when profiling event-heavy code.

Alternatives to Custom EventArgs

Custom EventArgs is not the only way to pass data with events. You can use the generic EventHandler<T> with a built-in type like string or int, but that loses semantic meaning. A ProgressChanged event that passes an int forces the subscriber to guess what the integer represents.

Another alternative is to use Action<T> or Func<T> delegates instead of events. These are more flexible but break the standard event pattern that many libraries and UI frameworks expect. If you are building a library that others will consume, sticking with EventHandler<T> and custom EventArgs is the conventional choice.

You can also skip EventArgs entirely and pass data as method parameters if you are not using events at all. But when you need the publish-subscribe pattern that events provide, custom EventArgs is the idiomatic C# solution.

The decision ultimately comes down to whether the event needs to carry structured data. If it does, a custom EventArgs class is the clearest and most maintainable approach. If the event is purely a notification, plain EventArgs is sufficient.

c# custom eventargs: Practical Usage and Code Examples | RYUSLOG DEV