Edit

Share via


Events overview

An event is an action that you can respond to, or "handle," in code. Events are usually generated by a user action, such as clicking the mouse or pressing a key, but they can also be generated by program code or by the system.

Event-driven applications run code in response to an event. Each form and control exposes a predefined set of events that you can respond to. If one of these events is raised and there's an associated event handler, the handler is invoked and code is run.

The types of events raised by an object vary, but many types are common to most controls. For example, most objects have a Click event that's raised when a user clicks on it.

Note

Many events occur with other events. For example, in the course of the DoubleClick event occurring, the MouseDown, MouseUp, and Click events occur.

For general information about how to raise and consume an event, see Handling and raising events in .NET.

Delegates and their role

Delegates are classes commonly used within .NET to build event-handling mechanisms. Delegates roughly equate to function pointers, commonly used in Visual C++ and other object-oriented languages. Unlike function pointers however, delegates are object-oriented, type-safe, and secure. Also, where a function pointer contains only a reference to a particular function, a delegate consists of a reference to an object, and references to one or more methods within the object.

This event model uses delegates to bind events to the methods that are used to handle them. The delegate enables other classes to register for event notification by specifying a handler method. When the event occurs, the delegate calls the bound method. For more information about how to define delegates, see Handling and raising events.

Delegates can be bound to a single method or to multiple methods, referred to as multicasting. When creating a delegate for an event, you typically create a multicast event. A rare exception might be an event that results in a specific procedure (such as displaying a dialog box) that wouldn't logically repeat multiple times per event. For information about how to create a multicast delegate, see How to combine delegates (Multicast Delegates).

A multicast delegate maintains an invocation list of the methods bound to it. The multicast delegate supports a Combine method to add a method to the invocation list and a Remove method to remove it.

When an application records an event, the control raises the event by invoking the delegate for that event. The delegate in turn calls the bound method. In the most common case (a multicast delegate), the delegate calls each bound method in the invocation list in turn, which provides a one-to-many notification. This strategy means that the control doesn't need to maintain a list of target objects for event notification—the delegate handles all registration and notification.

Delegates also enable multiple events to be bound to the same method, allowing a many-to-one notification. For example, a button-click event and a menu-command–click event can both invoke the same delegate, which then calls a single method to handle these separate events the same way.

The binding mechanism used with delegates is dynamic: a delegate can be bound at run-time to any method whose signature matches that of the event handler. With this feature, you can set up or change the bound method depending on a condition and to dynamically attach an event handler to a control.

Events in Windows Forms

Events in Windows Forms are declared with the EventHandler<TEventArgs> delegate for handler methods. Each event handler provides two parameters that allow you to handle the event properly. The following example shows an event handler for a Button control's Click event.

Private Sub button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles button1.Click

End Sub
private void button1_Click(object sender, System.EventArgs e)
{

}

The first parameter,sender, provides a reference to the object that raised the event. The second parameter, e, passes an object specific to the event that's being handled. By referencing the object's properties (and, sometimes, its methods), you can obtain information such as the location of the mouse for mouse events or data being transferred in drag-and-drop events.

Typically each event produces an event handler with a different event-object type for the second parameter. Some event handlers, such as those for the MouseDown and MouseUp events, have the same object type for their second parameter. For these types of events, you can use the same event handler to handle both events.

You can also use the same event handler to handle the same event for different controls. For example, if you have a group of RadioButton controls on a form, you could create a single event handler for the Click event of every RadioButton. For more information, see How to handle a control event.

Async event handlers

Modern applications often need to perform asynchronous operations in response to user actions, such as downloading data from a web service or accessing files. Windows Forms event handlers can be declared as async methods to support these scenarios, but there are important considerations to avoid common pitfalls.

Basic async event handler pattern

Event handlers can be declared with the async (Async in Visual Basic) modifier and use await (Await in Visual Basic) for asynchronous operations. Since event handlers must return void (or be declared as a Sub in Visual Basic), they're one of the rare acceptable uses of async void (or Async Sub in Visual Basic):

private async void downloadButton_Click(object sender, EventArgs e)
{
    downloadButton.Enabled = false;
    statusLabel.Text = "Downloading...";
    
    try
    {
        using var httpClient = new HttpClient();
        string content = await httpClient.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md");
        
        // Update UI with the result
        loggingTextBox.Text = content;
        statusLabel.Text = "Download complete";
    }
    catch (Exception ex)
    {
        statusLabel.Text = $"Error: {ex.Message}";
    }
    finally
    {
        downloadButton.Enabled = true;
    }
}
Private Async Sub downloadButton_Click(sender As Object, e As EventArgs) Handles downloadButton.Click
    downloadButton.Enabled = False
    statusLabel.Text = "Downloading..."

    Try
        Using httpClient As New HttpClient()
            Dim content As String = Await httpClient.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md")

            ' Update UI with the result
            loggingTextBox.Text = content
            statusLabel.Text = "Download complete"
        End Using
    Catch ex As Exception
        statusLabel.Text = $"Error: {ex.Message}"
    Finally
        downloadButton.Enabled = True
    End Try
End Sub

Important

While async void is discouraged, it's necessary for event handlers (and event handler-like code, such as Control.OnClick) since they can't return Task. Always wrap awaited operations in try-catch blocks to handle exceptions properly, as shown in the previous example.

Common pitfalls and deadlocks

Warning

Never use blocking calls like .Wait(), .Result, or .GetAwaiter().GetResult() in event handlers or any UI code. These patterns can cause deadlocks.

The following code demonstrates a common anti-pattern that causes deadlocks:

// DON'T DO THIS - causes deadlocks
private void badButton_Click(object sender, EventArgs e)
{
    try
    {
        // This blocks the UI thread and causes a deadlock
        string content = DownloadPageContentAsync().GetAwaiter().GetResult();
        loggingTextBox.Text = content;
    }
    catch (Exception ex)
    {
        MessageBox.Show($"Error: {ex.Message}");
    }
}

private async Task<string> DownloadPageContentAsync()
{
    using var httpClient = new HttpClient();
    await Task.Delay(2000); // Simulate delay
    return await httpClient.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md");
}
' DON'T DO THIS - causes deadlocks
Private Sub badButton_Click(sender As Object, e As EventArgs) Handles badButton.Click
    Try
        ' This blocks the UI thread and causes a deadlock
        Dim content As String = DownloadPageContentAsync().GetAwaiter().GetResult()
        loggingTextBox.Text = content
    Catch ex As Exception
        MessageBox.Show($"Error: {ex.Message}")
    End Try
End Sub

Private Async Function DownloadPageContentAsync() As Task(Of String)
    Using httpClient As New HttpClient()
        Return Await httpClient.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md")
    End Using
End Function

This causes a deadlock for the following reasons:

  • The UI thread calls the async method and blocks waiting for the result.
  • The async method captures the UI thread's SynchronizationContext.
  • When the async operation completes, it tries to continue on the captured UI thread.
  • The UI thread is blocked waiting for the operation to complete.
  • Deadlock occurs because neither operation can proceed.

Cross-thread operations

When you need to update UI controls from background threads within async operations, use the appropriate marshaling techniques. Understanding the difference between blocking and non-blocking approaches is crucial for responsive applications.

.NET 9 introduced Control.InvokeAsync, which provides async-friendly marshaling to the UI thread. Unlike Control.Invoke which sends (blocks the calling thread), Control.InvokeAsync posts (non-blocking) to the UI thread's message queue. For more information about Control.InvokeAsync, see How to make thread-safe calls to controls.

Key advantages of InvokeAsync:

  • Non-blocking: Returns immediately, allowing the calling thread to continue.
  • Async-friendly: Returns a Task that can be awaited.
  • Exception propagation: Properly propagates exceptions back to the calling code.
  • Cancellation support: Supports CancellationToken for operation cancellation.
private async void processButton_Click(object sender, EventArgs e)
{
    processButton.Enabled = false;
    
    // Start background work
    await Task.Run(async () =>
    {
        for (int i = 0; i <= 100; i += 10)
        {
            // Simulate work
            await Task.Delay(200);
            
            // Create local variable to avoid closure issues
            int currentProgress = i;
            
            // Update UI safely from background thread
            await progressBar.InvokeAsync(() =>
            {
                progressBar.Value = currentProgress;
                statusLabel.Text = $"Progress: {currentProgress}%";
            });
        }
    });
    
    processButton.Enabled = true;
}
Private Async Sub processButton_Click(sender As Object, e As EventArgs) Handles processButton.Click
    processButton.Enabled = False

    ' Start background work
    Await Task.Run(Async Function()
                       For i As Integer = 0 To 100 Step 10
                           ' Simulate work
                           Await Task.Delay(200)

                           ' Create local variable to avoid closure issues
                           Dim currentProgress As Integer = i

                           ' Update UI safely from background thread
                           Await progressBar.InvokeAsync(Sub()
                                                             progressBar.Value = currentProgress
                                                             statusLabel.Text = $"Progress: {currentProgress}%"
                                                         End Sub)
                       Next
                   End Function)

    processButton.Enabled = True
End Sub

For truly async operations that need to run on the UI thread:

private async void complexButton_Click(object sender, EventArgs e)
{
    // This runs on UI thread but doesn't block it
    statusLabel.Text = "Starting complex operation...";

    // Dispatch and run on a new thread
    await Task.WhenAll(Task.Run(SomeApiCallAsync),
                       Task.Run(SomeApiCallAsync),
                       Task.Run(SomeApiCallAsync));

    // Update UI directly since we're already on UI thread
    statusLabel.Text = "Operation completed";
}

private async Task SomeApiCallAsync()
{
    using var client = new HttpClient();

    // Simulate random network delay
    await Task.Delay(Random.Shared.Next(500, 2500));

    // Do I/O asynchronously
    string result = await client.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md");

    // Marshal back to UI thread
    await this.InvokeAsync(async (cancelToken) =>
    {
        loggingTextBox.Text += $"{Environment.NewLine}Operation finished at: {DateTime.Now:HH:mm:ss.fff}";
    });

    // Do more async I/O ...
}
Private Async Sub complexButton_Click(sender As Object, e As EventArgs) Handles complexButton.Click
    'Convert the method to enable the extension method on the type
    Dim method = DirectCast(AddressOf ComplexButtonClickLogic,
                            Func(Of CancellationToken, Task))

    'Invoke the method asynchronously on the UI thread
    Await Me.InvokeAsync(method.AsValueTask())
End Sub

Private Async Function ComplexButtonClickLogic(token As CancellationToken) As Task
    ' This runs on UI thread but doesn't block it
    statusLabel.Text = "Starting complex operation..."

    ' Dispatch and run on a new thread
    Await Task.WhenAll(Task.Run(AddressOf SomeApiCallAsync),
                       Task.Run(AddressOf SomeApiCallAsync),
                       Task.Run(AddressOf SomeApiCallAsync))

    ' Update UI directly since we're already on UI thread
    statusLabel.Text = "Operation completed"
End Function

Private Async Function SomeApiCallAsync() As Task
    Using client As New HttpClient()

        ' Simulate random network delay
        Await Task.Delay(Random.Shared.Next(500, 2500))

        ' Do I/O asynchronously
        Dim result As String = Await client.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md")

        ' Marshal back to UI thread
        ' Extra work here in VB to handle ValueTask conversion
        Await Me.InvokeAsync(DirectCast(
                Async Function(cancelToken As CancellationToken) As Task
                    loggingTextBox.Text &= $"{Environment.NewLine}Operation finished at: {DateTime.Now:HH:mm:ss.fff}"
                End Function,
            Func(Of CancellationToken, Task)).AsValueTask() 'Extension method to convert Task
        )

        ' Do more Async I/O ...
    End Using
End Function

Tip

.NET 9 includes analyzer warnings (WFO2001) to help detect when async methods are incorrectly passed to synchronous overloads of InvokeAsync. This helps prevent "fire-and-forget" behavior.

Note

If you're using Visual Basic, the previous code snippet used an extension method to convert a ValueTask to a Task. The extension method code is available on GitHub.

Best practices

  • Use async/await consistently: Don't mix async patterns with blocking calls.
  • Handle exceptions: Always wrap async operations in try-catch blocks in async void event handlers.
  • Provide user feedback: Update the UI to show operation progress or status.
  • Disable controls during operations: Prevent users from starting multiple operations.
  • Use CancellationToken: Support operation cancellation for long-running tasks.
  • Consider ConfigureAwait(false): Use in library code to avoid capturing the UI context when not needed.