C#idiomatic C#
events-overviewcsharp

Language

Introduction to events

Learn about events in .NET and our language design goals for events in this overview.

  • page csharp/events-overview
  • updated 03/11/2025

Previous

Events are, like delegates, a late binding mechanism. In fact, events are built on the language support for delegates.

Events are a way for an object to broadcast (to all interested components in the system) that something happened. Any other component can subscribe to the event, and be notified when an event is raised.

You probably used events in some of your programming. Many graphical systems have an event model to report user interaction. These events would report mouse movement, button presses, and similar interactions. That's one of the most common, but not the only scenario where events are used.

You can define events that should be raised for your classes. One important consideration when working with events is that there might not be any object registered for a particular event. You must write your code so that it doesn't raise events when no listeners are configured.

Subscribing to an event also creates a coupling between two objects (the event source, and the event sink). You need to ensure that the event sink unsubscribes from the event source when no longer interested in events.

Design goals for event support

The language design for events targets these goals:

  • Enable minimal coupling between an event source and an event sink. These two components might be written by different organizations, and might even be updated on different schedules.
  • It should be simple to subscribe to an event, and to unsubscribe from that same event.
  • Event sources should support multiple event subscribers. It should also support having no event subscribers attached.

You can see that the goals for events are similar to the goals for delegates. That's why the event language support is built on the delegate language support.

Language support for events

The syntax for defining events, and subscribing or unsubscribing from events is an extension of the syntax for delegates.

You use the event keyword to define an event:

sample · csharpdocs/csharp/snippets/events/VersionOne.csDeclareEvent

csharp
namespace events;

// <EventArgsV1>
public class FileFoundArgs : EventArgs
{
    public string FoundFile { get; }

    public FileFoundArgs(string fileName) => FoundFile = fileName;
}
// </EventArgsV1>

// <FileSearcherV1>
public class FileSearcher
{
    // <DeclareEvent>
    public event EventHandler<FileFoundArgs>? FileFound;
    // </DeclareEvent>

    public void Search(string directory, string searchPattern)
    {
        foreach (var file in Directory.EnumerateFiles(directory, searchPattern))
        {
            // <InvokeEvent>
            FileFound?.Invoke(this, new FileFoundArgs(file));
            // </InvokeEvent>
        }
    }
}
// </FileSearcherV1>

The type of the event (EventHandler<FileListArgs> in this example) must be a delegate type. There are conventions that you should follow when declaring an event. Typically, the event delegate type has a void return. Event declarations should be a verb, or a verb phrase. Use past tense when the event reports something that happened. Use a present tense verb (for example, Closing) to report something that is about to happen. Often, using present tense indicates that your class supports some kind of customization behavior. One of the most common scenarios is to support cancellation. For example, a Closing event can include an argument that would indicate if the close operation should continue, or not. Other scenarios enable callers to modify behavior by updating properties of the event arguments. You can raise an event to indicate a proposed next action an algorithm will take. The event handler might mandate a different action by modifying properties of the event argument.

When you want to raise the event, you call the event handlers using the delegate invocation syntax:

sample · csharpdocs/csharp/snippets/events/VersionOne.csInvokeEvent

csharp
namespace events;

// <EventArgsV1>
public class FileFoundArgs : EventArgs
{
    public string FoundFile { get; }

    public FileFoundArgs(string fileName) => FoundFile = fileName;
}
// </EventArgsV1>

// <FileSearcherV1>
public class FileSearcher
{
    // <DeclareEvent>
    public event EventHandler<FileFoundArgs>? FileFound;
    // </DeclareEvent>

    public void Search(string directory, string searchPattern)
    {
        foreach (var file in Directory.EnumerateFiles(directory, searchPattern))
        {
            // <InvokeEvent>
            FileFound?.Invoke(this, new FileFoundArgs(file));
            // </InvokeEvent>
        }
    }
}
// </FileSearcherV1>

As discussed in the section on delegates, the ?. operator makes it easy to ensure that you don't attempt to raise the event when there are no subscribers to that event.

You subscribe to an event by using the += operator:

sample · csharpdocs/csharp/snippets/events/Program.csAttachEventHandler

csharp
// <AttachEventHandler>
var fileLister = new FileSearcher();
int filesFound = 0;

EventHandler<FileFoundArgs> onFileFound = (sender, eventArgs) =>
{
    Console.WriteLine(eventArgs.FoundFile);
    filesFound++;
};

fileLister.FileFound += onFileFound;
// </AttachEventHandler>

// <Search>
fileLister.DirectoryChanged += (sender, eventArgs) =>
{
    Console.Write($"Entering '{eventArgs.CurrentSearchDirectory}'.");
    Console.WriteLine($" {eventArgs.CompletedDirs} of {eventArgs.TotalDirs} completed...");
};
// </Search>

fileLister.Search(".", "*.dll", true);

// <DetachHandler>
fileLister.FileFound -= onFileFound;
// </DetachHandler>

LocalUnusedCode();
void LocalUnusedCode()
{
    // <CancelSearch>
    EventHandler<FileFoundArgs> onFileFound = (sender, eventArgs) =>
    {
        Console.WriteLine(eventArgs.FoundFile);
        eventArgs.CancelRequested = true;
    };
    // </CancelSearch>
}

// <EventArgs>
public class FileFoundArgs : EventArgs
{
    public string FoundFile { get; }
    public bool CancelRequested { get; set; }

    public FileFoundArgs(string fileName) => FoundFile = fileName;
}
// </EventArgs>

// <SearchDirEventArgs>
internal class SearchDirectoryArgs : EventArgs
{
    internal string CurrentSearchDirectory { get; }
    internal int TotalDirs { get; }
    internal int CompletedDirs { get; }

    internal SearchDirectoryArgs(string dir, int totalDirs, int completedDirs)
    {
        CurrentSearchDirectory = dir;
        TotalDirs = totalDirs;
        CompletedDirs = completedDirs;
    }
}
// </SearchDirEventArgs>

public class FileSearcher
{
    // <DeclareEvent>
    public event EventHandler<FileFoundArgs>? FileFound;
    // </DeclareEvent>

    // <DeclareSearchEvent>
    internal event EventHandler<SearchDirectoryArgs> DirectoryChanged
    {
        add { _directoryChanged += value; }
        remove { _directoryChanged -= value; }
    }
    private EventHandler<SearchDirectoryArgs>? _directoryChanged;
    // </DeclareSearchEvent>

    // <FinalImplementation>
    public void Search(string directory, string searchPattern, bool searchSubDirs = false)
    {
        if (searchSubDirs)
        {
            var allDirectories = Directory.GetDirectories(directory, "*.*", SearchOption.AllDirectories);
            var completedDirs = 0;
            var totalDirs = allDirectories.Length + 1;
            foreach (var dir in allDirectories)
            {
                _directoryChanged?.Invoke(this, new (dir, totalDirs, completedDirs++));
                // Search 'dir' and its subdirectories for files that match the search pattern:
                SearchDirectory(dir, searchPattern);
            }
            // Include the Current Directory:
            _directoryChanged?.Invoke(this, new (directory, totalDirs, completedDirs++));
            SearchDirectory(directory, searchPattern);
        }
        else
        {
            SearchDirectory(directory, searchPattern);
        }
    }

    // <SearchWithCancel>
    private void SearchDirectory(string directory, string searchPattern)
    {
        foreach (var file in Directory.EnumerateFiles(directory, searchPattern))
        {
            var args = new FileFoundArgs(file);
            FileFound?.Invoke(this, args);
            if (args.CancelRequested)
                break;
        }
    }
    // </SearchWithCancel>
    // </FinalImplementation>
}

The handler method typically has the prefix 'On' followed by the event name, as shown in the preceding code.

You unsubscribe using the -= operator:

sample · csharpdocs/csharp/snippets/events/Program.csDetachHandler

csharp
// <AttachEventHandler>
var fileLister = new FileSearcher();
int filesFound = 0;

EventHandler<FileFoundArgs> onFileFound = (sender, eventArgs) =>
{
    Console.WriteLine(eventArgs.FoundFile);
    filesFound++;
};

fileLister.FileFound += onFileFound;
// </AttachEventHandler>

// <Search>
fileLister.DirectoryChanged += (sender, eventArgs) =>
{
    Console.Write($"Entering '{eventArgs.CurrentSearchDirectory}'.");
    Console.WriteLine($" {eventArgs.CompletedDirs} of {eventArgs.TotalDirs} completed...");
};
// </Search>

fileLister.Search(".", "*.dll", true);

// <DetachHandler>
fileLister.FileFound -= onFileFound;
// </DetachHandler>

LocalUnusedCode();
void LocalUnusedCode()
{
    // <CancelSearch>
    EventHandler<FileFoundArgs> onFileFound = (sender, eventArgs) =>
    {
        Console.WriteLine(eventArgs.FoundFile);
        eventArgs.CancelRequested = true;
    };
    // </CancelSearch>
}

// <EventArgs>
public class FileFoundArgs : EventArgs
{
    public string FoundFile { get; }
    public bool CancelRequested { get; set; }

    public FileFoundArgs(string fileName) => FoundFile = fileName;
}
// </EventArgs>

// <SearchDirEventArgs>
internal class SearchDirectoryArgs : EventArgs
{
    internal string CurrentSearchDirectory { get; }
    internal int TotalDirs { get; }
    internal int CompletedDirs { get; }

    internal SearchDirectoryArgs(string dir, int totalDirs, int completedDirs)
    {
        CurrentSearchDirectory = dir;
        TotalDirs = totalDirs;
        CompletedDirs = completedDirs;
    }
}
// </SearchDirEventArgs>

public class FileSearcher
{
    // <DeclareEvent>
    public event EventHandler<FileFoundArgs>? FileFound;
    // </DeclareEvent>

    // <DeclareSearchEvent>
    internal event EventHandler<SearchDirectoryArgs> DirectoryChanged
    {
        add { _directoryChanged += value; }
        remove { _directoryChanged -= value; }
    }
    private EventHandler<SearchDirectoryArgs>? _directoryChanged;
    // </DeclareSearchEvent>

    // <FinalImplementation>
    public void Search(string directory, string searchPattern, bool searchSubDirs = false)
    {
        if (searchSubDirs)
        {
            var allDirectories = Directory.GetDirectories(directory, "*.*", SearchOption.AllDirectories);
            var completedDirs = 0;
            var totalDirs = allDirectories.Length + 1;
            foreach (var dir in allDirectories)
            {
                _directoryChanged?.Invoke(this, new (dir, totalDirs, completedDirs++));
                // Search 'dir' and its subdirectories for files that match the search pattern:
                SearchDirectory(dir, searchPattern);
            }
            // Include the Current Directory:
            _directoryChanged?.Invoke(this, new (directory, totalDirs, completedDirs++));
            SearchDirectory(directory, searchPattern);
        }
        else
        {
            SearchDirectory(directory, searchPattern);
        }
    }

    // <SearchWithCancel>
    private void SearchDirectory(string directory, string searchPattern)
    {
        foreach (var file in Directory.EnumerateFiles(directory, searchPattern))
        {
            var args = new FileFoundArgs(file);
            FileFound?.Invoke(this, args);
            if (args.CancelRequested)
                break;
        }
    }
    // </SearchWithCancel>
    // </FinalImplementation>
}

It's important that you declare a local variable for the expression that represents the event handler. That ensures the unsubscribe removes the handler. If, instead, you used the body of the lambda expression, you're attempting to remove a handler that was never attached, which does nothing.

In the next article, you'll learn more about typical event patterns, and different variations on this example.

Next