[英]Issues with "void async" in Winform event handlers - and maybe a solution?
據我所見,何時調用“異步無效”方法(例如事件處理程序),調用者永遠無法知道它何時完成(因為它無法等待Task
完成)。 所以有效地它是一個火災和忘記電話。
使用此代碼演示了這一點(我已將 Button 和 TabControl 放在表單上並連接了 2 個事件)。 單擊按鈕時,它會更改選項卡,這會導致引發SelectedIndexChanged
事件,該事件是異步的。
private void button1_Click(object sender, EventArgs e)
{
Debug.WriteLine("Started button1_Click");
tabControl1.SelectedTab = tabControl1.SelectedIndex == 0 ? tabPage2 : tabPage1;
Debug.WriteLine("Ended button1_Click");
}
private async void tabControl1_SelectedIndexChanged(object sender, EventArgs e)
{
Debug.WriteLine("Started tabControl1_SelectedIndexChanged");
await Task.Delay(1000);
Debug.WriteLine("Ended tabControl1_SelectedIndexChanged");
}
結果 output 是
Started button1_Click
Started tabControl1_SelectedIndexChanged
Ended button1_Click
Ended tabControl1_SelectedIndexChanged
如您所見, SelectedIndexChanged
事件處理程序被觸發,但調用者沒有等待它完成(它不能等待,因為它沒有要等待的任務)。
我提出的解決方案
事件處理程序不使用async
,而是等待它使用的任何Async
方法,然后一切似乎都正常工作......它通過在調用DoEvents
時輪詢Task.IsCompleted
屬性來等待,以保持異步任務處於活動狀態並進行處理(在本例中為 Task 。延遲)。
private void button1_Click(object sender, EventArgs e)
{
Debug.WriteLine("Started button1_Click");
tabControl1.SelectedTab = tabControl1.SelectedIndex == 0 ? tabPage2 : tabPage1;
Debug.WriteLine("Ended button1_Click");
}
private void tabControl1_SelectedIndexChanged(object sender, EventArgs e)
{
Debug.WriteLine("Started tabControl1_SelectedIndexChanged");
Await(Task.Delay(1000));
Debug.WriteLine("Ended tabControl1_SelectedIndexChanged");
}
public static void Await(Task task)
{
while (task.IsCompleted == false)
{
System.Windows.Forms.Application.DoEvents();
}
if (task.IsFaulted && task.Exception != null)
throw task.Exception;
else
return;
}
這現在給出了預期的結果
Started button1_Click
Started tabControl1_SelectedIndexChanged
Ended tabControl1_SelectedIndexChanged
Ended button1_Click
任何人都可以看到采用這種方法的任何問題嗎???
它通過在調用 DoEvents 時輪詢 Task.IsCompleted 屬性來等待
這是一種阻塞異步代碼的形式,我稱之為嵌套消息循環黑客。
任何人都可以看到采用這種方法的任何問題嗎?
是的。 這種解決方案受到我所說的“意外重入”的影響。
從歷史上看,意外的重入一直是許多錯誤的原因,因為某些代碼最終不可避免地會從其他代碼(在同一堆棧上)中調用。 在您的示例中, tabControl1_SelectedIndexChanged
(或更具體地說, Await
)可以直接執行任何其他 UI 代碼。 包括tabControl1_SelectedIndexChanged
的另一個調用。 一旦你的代碼變得不平凡,這就會導致問題,更糟糕的是,它依賴於時間,所以你會得到“heisenbugs”。
有一句老話:“DoEvents is evil”。 這需要仔細考慮。
為什么不是所有與 DoEvents 相關的問題也與 await 相關?
這是個好問題; 許多開發人員最初對await
持懷疑態度,因為他們被DoEvents
嚴重燒毀了。 await
沒有落入這個陷阱的原因是因為只有一個消息循環。 await
返回到那個單一的消息循環。 沒有嵌套的消息循環,因此沒有意外的重新進入。
我永遠不會推薦DoEvents
作為解決方案。 總是有更好的解決方案。
不保留處理順序。 例如,我以編程方式更改選項卡,並期望事件處理程序加載選項卡數據,然后我想對加載的數據做一些事情,目前我設置選項卡,事件觸發 - 它開始加載選項卡數據並立即返回,事件處理程序調用返回,然后我嘗試與部分加載的選項卡進行交互。 此類問題發生在您無意中觸發異步事件處理程序的任何地方
所以,這是實際的問題:C# 事件(至少是絕大多數事件的返回void
事件)功能不足以充當通知以外的任何內容。 在設計方面,C# 事件允許您實現觀察者模式,但是是實現策略模式的錯誤選擇。
要應用於您的示例, tabControl1_SelectedIndexChanged
只是一個通知(觀察者模式),用於通知您的代碼選項卡索引已更改。 它不是提供數據加載(策略模式)的鈎子,並且試圖以這種方式使用它是導致這里實際問題的原因。
那么解決方案就是不依賴事件處理程序來驅動你的邏輯。 看看那里的一些模型-視圖-視圖模型 (MVVM) 概念以獲得一些靈感。 在這種情況下,使用 ViewModel 類型的方法可能會有所幫助。 這樣您的代碼根本不會更新選項卡控件; 相反,它會更新 ViewModel,並且您的 VM 可以異步完成工作(根本沒有async void
),然后您的代碼可以在工作完成后更新 UI。
一種簡單的方法(沒有數據綁定和顯式 VM)可能類似於:
private async void button1_Click(object sender, EventArgs e)
{
Debug.WriteLine("Started button1_Click");
var newTab = tabControl1.SelectedIndex == 0 ? tabPage2 : tabPage1;
ShowLoadingState(newTab);
tabControl1.SelectedTab = newTab;
await LoadDataForTab(newTab);
Debug.WriteLine("Ended button1_Click");
}
private async Task LoadDataForTab(TabPage tab)
{
await Task.Delay(1000);
// load data into tab
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.