事件概述

事件是在代码中可以响应或“处理”的操作。 事件通常由用户作(如单击鼠标或按键)生成,但它们也可以由程序代码或系统生成。

事件驱动的应用程序运行代码以响应事件。 每个窗体和控件都会公开一组预定义的事件,你可以响应这些事件。 如果引发其中一个事件并且存在关联的事件处理程序,则调用处理程序并运行代码。

对象引发的事件类型会有所不同,但大多数控件通常有许多类型。 例如,大多数对象都有一个事件,该事件会在用户单击对象时触发。

注释

许多事件与其他事件一起发生。 例如,在发生 DoubleClick 事件的过程中,会发生 MouseDownMouseUpClick 事件。

有关如何引发和使用事件的常规信息,请参阅 .NET 中的处理和引发事件

代表及其角色

委托是 .NET 中通常用于生成事件处理机制的类。 委托大致等同于函数指针,通常用于 Visual C++ 和其他面向对象的语言。 但与函数指针不同的是,委托是面向对象的、类型安全的和保险的。 此外,如果函数指针仅包含对特定函数的引用,委托包含对对象的引用,以及对对象中的一个或多个方法的引用。

此事件模型使用 委托 将事件绑定到用于处理事件的方法。 委托允许其他类通过指定处理程序方法来注册事件通知。 当发生事件时,委托会调用绑定的方法。 有关如何定义委托的详细信息,请参阅 处理和引发事件

委托可绑定到单个方法或多个方法,后者又称为多路广播。 为事件创建委托时,通常会创建多播事件。 罕见的例外可能是导致特定过程(如显示对话框)的事件,这些过程通常不会为同一事件重复多次。 有关如何创建多播委托的信息,请参阅如何合并委托(多播委托)

多播委托维护绑定到它的方法的调用列表。 多播委托支持一个 Combine 方法用于向调用列表添加方法,以及一个 Remove 方法用于从中删除方法。

当应用程序记录事件时,控件通过调用该事件的委托来引发该事件。 委托依次调用绑定的方法。 在最常见的情况下(多播委托),委托依次调用绑定在调用列表中的每个方法,实现一对多通知。 此策略意味着控件不需要维护事件通知的目标对象列表,委托将处理所有注册和通知。

委托还允许将多个事件绑定到同一个方法上,从而允许多对一通知。 例如,按钮单击事件和菜单命令单击事件都可以调用相同的委托,然后调用单个方法以相同的方式处理这些单独的事件。

用于委托的绑定机制是动态的:委托可以在运行时与任何其签名与事件处理程序的签名匹配的方法绑定。 使用此功能,可以根据条件设置或更改绑定方法,并动态将事件处理程序附加到控件。

Windows 窗体中的事件

Windows 窗体中的事件使用用于处理程序的 EventHandler<TEventArgs> 委托来声明。 每个事件处理程序提供两个参数,使你能够正确处理事件。 以下示例演示 Button 控件 Click 事件的事件处理程序。

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)
{

}

第一个参数sender提供对引发事件的对象的引用。 第二个参数 e传递一个特定于正在处理的事件的对象。 通过引用对象的属性(有时,其方法),可以获取诸如鼠标事件的位置或拖放事件中传输的数据等信息。

通常,每个事件都会为第二个参数生成具有不同事件对象类型的事件处理程序。 某些事件处理程序(例如 MouseDownMouseUp 事件的事件处理程序)在其第二个参数中具有相同的对象类型。 对于这些类型的事件,可以使用同一事件处理程序来处理这两个事件。

还可以使用相同的事件处理程序来处理不同控件的相同事件。 例如,如果窗体上有一组 RadioButton 控件,则可以为每个 ClickRadioButton 事件创建一个单独的事件处理程序。 有关详细信息,请参阅 如何处理控件事件

异步事件处理程序

新式应用程序通常需要执行异步作来响应用户作,例如从 Web 服务下载数据或访问文件。 Windows 窗体事件处理程序可以声明为 async 支持这些方案的方法,但有一些重要注意事项可以避免常见的陷阱。

基本异步事件处理程序模式

可以使用 (Async在 Visual Basic) 修饰符中声明async事件处理程序,并使用 awaitAwait在 Visual Basic 中)进行异步作。 由于事件处理程序必须返回 void (或声明为 Visual Basic 中), Sub 因此它们是罕见的 async void 可接受用途之一(或在 Async Sub 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

重要

虽然 async void 不建议这样做,但对于事件处理程序(和类似事件处理程序的代码,如 Control.OnClick)是必需的,因为它们无法返回 Task。 始终将等待的作包装在块中 try-catch 以正确处理异常,如前面的示例所示。

常见陷阱和死锁

警告

切勿在事件处理程序或任何 UI 代码中使用阻塞调用,例如.Wait().Result.GetAwaiter().GetResult()事件处理程序或任何 UI 代码。 这些模式可能会导致死锁。

以下代码演示导致死锁的常见反模式:

// 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

这会导致死锁的原因如下:

  • UI 线程调用异步方法并阻止等待结果。
  • 异步方法捕获 UI 线程的 SynchronizationContext
  • 异步作完成后,它会尝试在捕获的 UI 线程上继续。
  • UI 线程被阻止,等待作完成。
  • 发生死锁,因为两个作都无法继续。

跨线程作

需要从异步作中的后台线程更新 UI 控件时,请使用适当的封送技术。 了解阻塞和非阻塞方法之间的差异对于响应式应用程序至关重要。

引入了 Control.InvokeAsync.NET 9,它为 UI 线程提供异步友好的封送处理。 与发送(阻止调用线程)不同Control.Invoke,将Control.InvokeAsync(非阻止)发布到 UI 线程的消息队列。 有关详细信息 Control.InvokeAsync,请参阅 如何对控件进行线程安全的调用

InvokeAsync 的主要优势:

  • 非阻塞:立即返回,允许调用线程继续。
  • 异步友好:返回可以等待的项 Task
  • 异常传播:正确将异常传播回调用代码。
  • 取消支持:支持 CancellationToken 作取消。
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

对于需要在 UI 线程上运行的真正异步作:

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

小窍门

.NET 9 包括分析器警告(WFO2001),以帮助检测异步方法何时错误地传递给同步重载 InvokeAsync。 这有助于防止“火灾和忘记”行为。

注释

如果使用 Visual Basic,则前面的代码片段使用扩展方法将 a ValueTask 转换为 a Task. GitHub 上提供了扩展方法代码

最佳做法

  • 一致地使用 async/await:不要将异步模式与阻止调用混合。
  • 处理异常:始终在事件处理程序中的 try-catch 块中 async void 包装异步作。
  • 提供用户反馈:更新 UI 以显示作进度或状态。
  • 在作期间禁用控制:防止用户启动多个作。
  • 使用 CancellationToken:支持长时间运行的任务的作取消。
  • 请考虑 ConfigureAwait(false):在库代码中使用以避免在不需要时捕获 UI 上下文。