1. 程式人生 > >【轉】編寫高質量代碼改善C#程序的157個建議——建議87:區分WPF和WinForm的線程模型

【轉】編寫高質量代碼改善C#程序的157個建議——建議87:區分WPF和WinForm的線程模型

ons 拋出異常 ui線程 擴展方法 區分 cli inner 編寫 查看

建議87:區分WPF和WinForm的線程模型

WPF和WinForm窗體應用程序都有一個要求,那就是UI元素(如Button、TextBox等)必須由創建它的那個線程進行更新。WinForm在這方面的限制並不是很嚴格,所以像下面這樣的代碼,在WinForm中大部分情況下還能運行(本建議後面會詳細解釋為什麽會出現這種現象):

private void buttonStartAsync_Click(object sender, EventArgs e)  
{  
    Task t = new Task(() =>
        {  
            while (true
) { label1.Text = DateTime.Now.ToString(); Thread.Sleep(1000); } }); //如果有異常,就啟動一個新任務 t.ContinueWith((task) => { try { task.Wait(); } catch (AggregateException ex) {
foreach (Exception inner in ex.InnerExceptions) { MessageBox.Show(string.Format("異常類型:{0}{1}來自:{2}{3}異常內容:{4}", inner.GetType(),Environment.NewLine, inner.Source, Environment.NewLine, inner.Message)); } } }, TaskContinuationOptions.OnlyOnFaulted); t.Start(); }

但是,相同的一段代碼如果放到WPF環境中,就肯定會拋出System.InvalidOperationException異常。


理論上,WinForm和WPF的線程模型非常接近,它們最後都是調用API(GetMessage或PeekMessage)來處理其他線程發送過來的消息,這些消息存儲在系統的一個消息隊列中。在WinForm和WPF中,創建主界面的線程就是主線程,也就是UI線程,UI線程負責處理該消息隊列。只是兩者在處理消息隊列的上層機制上稍微有一些不同,這就造成了同樣的代碼得到不同的結果。

在WinForm框架中有一個ISynchronizeInvoke接口,所有的UI元素(表現為Control)都繼承了該接口。其中,接口中的InvokdRequired屬性表示了當前線程是否是創建它的線程。接口中的Invoke和BeginInvoke方法負責將消息發送到消息隊列中,這樣,UI線程就能夠正確處理它了。那麽,上面的這段代碼在WinForm上的改進版本為(僅列出While循環部分):

while (true)  
{  
    if (label1.InvokeRequired)  
        label1.BeginInvoke(new Action(() =>
            {  
                label1.Text = DateTime.Now.ToString();  
            }));  
    else  
        label1.Text = DateTime.Now.ToString();  
    Thread.Sleep(1000);  
} 

BeginInvoke方法接受的是一個Delegate類型的參數,在這裏我們用一個Action來實現。

WPF應用程序的線程模型則完全依賴於DispatcherObject類型。所有的WPF控件都繼承自一個抽象類Visual,而這個抽象類又最終繼承自DispatcherObject類型。在這個DispatcherObject類型中有一個屬性,兩個方法。屬性Dispatcher完成所有的工作線程和UI線程之間的調度任務。CheckAccess方法負責檢測工作線程是否可以訪問控件,如果是,則返回True;否則返回False。VerifyAccess方法則負責檢測工作線程是否具有控件的訪問權限,如果不能訪問則拋出異常InvalidOperationException。

WinForm應用程序用類似CheckAccess的方式進行訪問權限的判斷;WPF應用程序則進行了改進,所有的UI控件都采用VerifyAccess的方式進行工作線程訪問權限的判斷。這直接決定了本建議開頭處那個例子的輸出,WPF只要判斷出工作線程和UI線程不是同一個線程的,則直接拋出異常,而WinForm卻有成功執行的余地。但是,WinForm的這種機制直接造成了程序的不穩定,因為即使在大部分情況下代碼能很好的工作,可是在不確定的情況下,那樣的代碼中工作線程會直接操作UI元素,這樣還是會拋出異常的。

考慮到WinForm在這個問題上的局限性,再次對WinForm的線程模型處理進行改進:

//用於表示主線程,在本例中就是UI線程  
Thread mainThread;  
 
bool CheckAccess()  
{  
    return mainThread == Thread.CurrentThread;  
}  
 
void VerifyAccess()  
{  
    if (!CheckAccess())  
        throw new InvalidOperationException("調用線程無法訪問此對象,因為另一個線程擁有此對象");  
}  
 
private void buttonStartAsync_Click(object sender, EventArgs e)  
{  
    //當前線程就是主線程  
    mainThread = Thread.CurrentThread;  
    Task t = new Task(() =>
        {  
            while (true)  
            {  
                if (!CheckAccess())  
                    label1.BeginInvoke(new Action(() =>
                        {  
                            label1.Text = DateTime.Now.ToString();  
                        }));  
                else  
                    label1.Text = DateTime.Now.ToString();  
                Thread.Sleep(1000);  
            }  
        });  
    //如果有異常,就啟動一個新任務  
    t.ContinueWith((task) =>
    {  
        try  
        {  
            task.Wait();  
        }  
        catch (AggregateException ex)  
        {  
            foreach (Exception inner in ex.InnerExceptions)  
            {  
                MessageBox.Show(string.Format("異常類型:{0}{1}來自:{2}{3}異常內容:{4}", inner.GetType(), Environment.NewLine,  
                    inner.Source, Environment.NewLine, inner.Message));  
            }  
        }  
    }, TaskContinuationOptions.OnlyOnFaulted);  
    t.Start();  
} 

在這段代碼中,我們模擬WPF中DispatcherObject的兩個方法CheckAccess和VerifyAccess對線程模型進行了重新處理,增強了系統的穩定性。在實際工作中,我們也可以提取這兩個方法為擴展方法,以便項目中的所有UI類型都能使用到。

WPF支持這兩個方法,其全部代碼如下所示(註意查看While循環部分):

private void buttonStart_Click(object sender, RoutedEventArgs e)  
{  
    Task t = new Task(() =>
    {  
        while (true)  
        {  
            this.Dispatcher.BeginInvoke(new Action(() =>
                {  
                    textBlock1.Text = DateTime.Now.ToString();  
                }));  
            Thread.Sleep(1000);  
        }  
    });  
    //為了捕獲異常,啟動了一個新任務  
    t.ContinueWith((task) =>
    {  
        try  
        {  
            task.Wait();  
        }  
        catch (AggregateException ex)  
        {  
            foreach (Exception inner in ex.InnerExceptions)  
            {  
                MessageBox.Show(string.Format("異常類型:{0}{1}來自:{2}{3}異常內容:{4}", inner.GetType(), Environment.NewLine,  
                    inner.Source, Environment.NewLine, inner.Message));  
            }  
        }  
    }, TaskContinuationOptions.OnlyOnFaulted);  
    t.Start();  
} 


註意 為了演示方便,本建議中的異常沒有傳遞到主線程。在實際編碼中,應當始終考慮將異常包裝到主線程。

轉自:《編寫高質量代碼改善C#程序的157個建議》陸敏技

【轉】編寫高質量代碼改善C#程序的157個建議——建議87:區分WPF和WinForm的線程模型