簡體   English   中英

Winform 事件處理程序中的“無效異步”問題 - 也許是一個解決方案?

[英]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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM