Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
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.
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.
Related content
.NET Desktop feedback