在用.NET Framework架構的WinForm建構GUI程式界面時,如果要在控件的事件響應函數中改變控件的狀态,例如:某個按鈕上的文本原先叫“打開”,單擊之後按鈕上的文本顯示“關閉”,初學者往往會想當然地這麼寫:
void ButtonOnClick(object sender,EventArgs e)
{
button.Text="關閉";
}
這樣的寫法運作程式之後,可能會觸發異常,異常資訊大緻是“不能從不是建立該控件的線程調用它”。注意這裡是“可能”,并不一定會觸發該種異常。造成這種異常的原因在于,控件是在主線程中建立的(比如this.Controls.Add(...);),進入控件的事件響應函數時,是在控件所在的線程,并不是主線程。在控件的事件響應函數中改變控件的狀态,可能與主線程發生線程沖突。如果主線程正在重繪控件外觀,此時在别的線程改變控件外觀,就會造成畫面混亂。不過這樣的情況并不總會發生,如果主線程此時在重繪别的控件,就可能逃過一劫,這樣的寫法可以正常通過,沒有觸發異常。
正确的寫法是在控件響應函數中調用控件的Invoke方法(其實如果大家以前用過C++ Builder的話,也會找到類似Invoke那樣的激活到主線程的函數)。Invoke方法會順着控件樹向上搜尋,直到找到建立控件的那個線程(通常是主線程),然後進入那個線程改變控件的外觀,確定不發生線程沖突。正确寫法的示例如下:
button.Invoke(new EventHandler(delegate
{
button.Text="關閉";
}));
Invoke方法需要建立一個委托。你可以事先寫好函數和與之對應的委托。不過,若想直覺地在Invoke方法調用的時候就看到具體的函數,而不是到别處搜尋的話,上面的示例代碼是不錯的選擇。
這樣的寫法有一個煩人的地方:對不同的控件寫法不同。對于TextBox,要TextBoxObject.Invoke,對于Label,又要LabelObject.Invoke。有沒有統一一點的寫法呢?
主視窗類本身也有Invoke方法。如果你不想對不同的控件寫法不一樣,可以全部用this.Invoke:
this.Invoke(new EventHandler(delegate
在C# 3.0及以後的版本中有了Lamda表達式,像上面這種匿名委托有了更簡潔的寫法。.NET Framework 3.5及以後版本更能用Action封裝方法。例如以下寫法可以看上去非常簡潔:
this.Invoke(new Action(()=>
以上寫法往往充斥着WinForm建構的程式。
在微軟新一代的界面開發技術WPF中,由于界面呈現和業務邏輯原生态地分開在兩個線程中,是以控件的事件響應函數就不必Invoke了。但是,如果手動開辟一個新線程,那麼在這個新線程中改變控件的外觀,則還是要Invoke的。
當一個控件的InvokeRequired屬性值為真時,說明有一個建立它以外的線程想通路它,此時它将會在内部調用new MethodInvoker(LoadGlobalImage)來完成下面的步驟,這個做法保證了控件的安全,你可以這樣了解,有人想找你借錢,他可以直接在你的錢包中拿,這樣太不安全,是以必須讓别人先要告訴你,你再從自己的錢包把錢拿出來借給别人,這樣就安全
作者:嶽振威