事件是您可以透過程式碼加以回應或「處理」的動作。 事件通常是由使用者動作產生,例如按下按鍵,但也可以由程式代碼或系統產生。
事件驅動應用程式會執行程式代碼以回應事件。 每個表單和控件都會公開一組您可以響應的預先定義事件。 如果引發其中一個事件,而且有相關聯的事件處理程式,則會叫用處理程式並執行程序代碼。
物件所引發的事件類型各有不同,但有許多類型為大多數控制項所共通。 例如,大部分物件都有 Click 當使用者按兩下時所引發的事件。
備註
許多事件與其他事件一起發生。 例如在發生 DoubleClick 的過程中,MouseDown、MouseUp 及 Click 事件會連帶發生。
如需如何引發和取用事件的一般資訊,請參閱 處理和引發 .NET 中的事件。
委派及其角色
委派是 .NET 中常用來建置事件處理機制的類別。 委派與函式指標大致相同,通常會在 Visual C++ 及其他物件導向語言中使用。 不同之處在於委派為物件導向、類型安全,安全性也較好。 此外,函式指標只包含特定函式的參考,而委派不僅包含了物件參考,還包含了物件中一或多個方法的參考。
此事件模型使用委派將事件繫結到要處理事件的方法。 委派允許透過指定處理常式方法的方式,為事件通知登錄其他類別。 當事件發生時,委派即會呼叫所繫結的方法。 如需有關如何定義委派的詳細資訊,請參閱處理和引發事件。
委派可以繫結到單一方法或多個方法,稱為「多點傳送」。 為事件建立委派時,您通常會建立多點傳送事件。 唯一例外是由邏輯上不會在每一事件中重複多次之特定程序 (例如顯示對話方塊) 所產生的事件,但此情況極為罕見。 如需如何建立多點傳送委派的相關資訊,請參閱如何合併委派 (多點傳送委派)。
多點傳送委派會維護繫結至它的方法的叫用清單。 多點傳送委派可以讓 Combine 方法新增方法到叫用清單,並支援 Remove 方法來移除此方法。
當應用程式記錄事件時,控制項會叫用該事件的委派來引發事件。 委派會接著呼叫所繫結的方法。 在最常見的情況 (多點傳送委派) 下,委派會輪流呼叫叫用清單中每一個繫結的方法,以提供一對多的通知。 此策略表示控制項無須針對事件通知而維護一份目標物件清單,因為委派會處理所有的登錄與通知事宜。
委派也可讓多個件繫結到同一方法,如此一來即可提供多對一通知。 例如按一下按鈕事件與按一下功能表命令事件都會叫用相同的委派,然後委派再呼叫一個方法,以相同方式處理這些個別的事件。
委派會隨機使用繫結機制,其可能在執行時繫結到任何簽章符合事件處理程式簽章的方法。 您可以利用此功能視條件設定或變更繫結方法,以及隨機將事件處理常式連結至控制項。
Windows Forms 的事件
Windows Forms 中的事件會使用 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會傳遞所處理之事件的特定物件。 藉由參考物件的屬性 (有時參考的是物件的方法),您就可以取得滑鼠事件的滑鼠位置或拖放事件中傳輸中資料等資訊。
一般而言,每個事件都會針對第二個參數產生具有不同事件物件類型的事件處理常式。 某些事件處理常式 (例如 MouseDown 和 MouseUp 事件的事件處理常式),其第二個參數的物件類型是相同的。 針對這些類型的事件,您可以使用相同的事件處理常式來處理這兩個事件。
您也可以使用相同的事件處理常式,來處理不同控制項的相同事件。 例如,如果您的表單體上有一組 RadioButton 控件,您可以為每個 Click 的事件 RadioButton建立單一事件處理程式。 如需詳細資訊,請參閱 如何處理控件事件。
非同步事件處理常式
現代應用程式通常需要執行非同步操作來回應使用者操作,例如從 Web 服務下載資料或存取檔案。 Windows Forms 事件處理常式可以宣告為 async 支援這些案例的方法,但有一些重要的考慮可以避免常見的陷阱。
基本非同步事件處理常式模式
事件處理常式可以使用 (Async在 Visual Basic 中) 修飾詞來async宣告,並使用 await (Await 在 Visual Basic 中) 進行非同步作業。 由於事件處理常式必須傳回 void (或在 Visual Basic 中宣告為 aSub),因此它們是 (或 Async Sub Visual Basic 中) 的async void罕見可接受用法之一:
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() 這些模式可能會導致死結。
下列程式碼示範導致死結的常見反模式:
// 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 控制項時,請使用適當的封送處理技術。 了解阻塞方法和非阻塞方法之間的差異對於響應式應用程式至關重要。
.NET 9 引進 Control.InvokeAsync,提供 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。 這有助於防止「一忘即走」行為。
最佳做法
- 一致地使用非同步/等待:不要將非同步模式與封鎖呼叫混合使用。
-
處理異常:一律將非同步作業包裝在事件處理常式的
async voidtry-catch 區塊中。 - 提供用戶反饋: 更新 UI 以顯示操作進度或狀態。
- 在操作期間停用控制:防止使用者啟動多個操作。
- 使用 CancellationToken:支援長時間執行任務的操作取消。
- 考慮 ConfigureAwait(false):在程式庫程式碼中使用,以避免在不需要時擷取 UI 內容。