C#中Invoke的用法
備註:該文章轉載地址來至https://www.cnblogs.com/vaevvaev/p/6909042.html
在用.NET Framework框架的WinForm構建GUI程式介面時,如果要在控制元件的事件響應函式中改變控制元件的狀態,例如:某個按鈕上的文字原先叫“開啟”,單擊之後按鈕上的文字顯示“關閉”,初學者往往會想當然地這麼寫:
void ButtonOnClick(object sender,EventArgs e)
{
button.Text="關閉";
}
這樣的寫法執行程式之後,可能會觸發異常,異常資訊大致是“不能從不是建立該控制元件的執行緒呼叫它”。注意這裡是“可能”,並不一定會觸發該種異常。造成這種異常的原因在於,控制元件是在主執行緒中建立的(比如this.Controls.Add(...);),進入控制元件的事件響應函式時,是在控制元件所在的執行緒,並不是主執行緒。在控制元件的事件響應函式中改變控制元件的狀態,可能與主執行緒發生執行緒衝突。如果主執行緒正在重繪控制元件外觀,此時在別的執行緒改變控制元件外觀,就會造成畫面混亂。不過這樣的情況並不總會發生,如果主執行緒此時在重繪別的控制元件,就可能逃過一劫,這樣的寫法可以正常通過,沒有觸發異常。
正確的寫法是在控制元件響應函式中呼叫控制元件的Invoke方法(其實如果大家以前用過C++ Builder的話,也會找到類似Invoke那樣的啟用到主執行緒的函式)。Invoke方法會順著控制元件樹向上搜尋,直到找到建立控制元件的那個執行緒(通常是主執行緒),然後進入那個執行緒改變控制元件的外觀,確保不發生執行緒衝突。正確寫法的示例如下:
void ButtonOnClick(object sender,EventArgs e)
{
button.Invoke(new EventHandler(delegate
{
button.Text="關閉";
}));
}
Invoke方法需要建立一個委託。你可以事先寫好函式和與之對應的委託。不過,若想直觀地在Invoke方法呼叫的時候就看到具體的函式,而不是到別處搜尋的話,上面的示例程式碼是不錯的選擇。
這樣的寫法有一個煩人的地方:對不同的控制元件寫法不同。對於TextBox,要TextBoxObject.Invoke,對於Label,又要LabelObject.Invoke。有沒有統一一點的寫法呢?
主視窗類本身也有Invoke方法。如果你不想對不同的控制元件寫法不一樣,可以全部用this.Invoke:
void ButtonOnClick(object sender,EventArgs e)
{
this.Invoke(new EventHandler(delegate
{
button.Text="關閉";
}));
}
在C# 3.0及以後的版本中有了Lamda表示式,像上面這種匿名委託有了更簡潔的寫法。.NET Framework 3.5及以後版本更能用Action封裝方法。例如以下寫法可以看上去非常簡潔:
void ButtonOnClick(object sender,EventArgs e)
{
this.Invoke(new Action(()=>
{
button.Text="關閉";
}));
}
以上寫法往往充斥著WinForm構建的程式。
在微軟新一代的介面開發技術WPF中,由於介面呈現和業務邏輯原生態地分開在兩個執行緒中,所以控制元件的事件響應函式就不必Invoke了。但是,如果手動開闢一個新執行緒,那麼在這個新執行緒中改變控制元件的外觀,則還是要Invoke的。
當一個控制元件的InvokeRequired屬性值為真時,說明有一個建立它以外的執行緒想訪問它,此時它將會在內部呼叫new MethodInvoker(LoadGlobalImage)來完成下面的步驟,這個做法保證了控制元件的安全,你可以這樣理解,有人想找你借錢,他可以直接在你的錢包中拿,這樣太不安全,因此必須讓別人先要告訴你,你再從自己的錢包把錢拿出來借給別人,這樣就安全了
another:
在設計中為了讓介面與邏輯分離,我的做法是使用事件,介面只要響應事件來處理介面的顯示就行了。而事件在邏輯處理中可能由不同的執行緒引發,這些事件的響應方法在修改介面中的控制元件內容時便會引發一個異常。
這時就用到了Control.InvokeRequired 屬性 與Invoke方法。
MSDN中說:
獲取一個值,該值指示呼叫方在對控制元件進行方法呼叫時是否必須呼叫 Invoke 方法,因為呼叫方位於建立控制元件所在的執行緒以外的執行緒中。
如果控制元件的 Handle 是在與呼叫執行緒不同的執行緒上建立的(說明您必須通過 Invoke 方法對控制元件進行呼叫),則為 true;否則為 false。
Windows 窗體中的控制元件被繫結到特定的執行緒,不具備執行緒安全性 。因此,如果從另一個執行緒呼叫控制元件的方法,那麼必須使用控制元件的一個 Invoke 方法來將呼叫封送到適當的執行緒。該屬性可用於確定是否必須呼叫 Invoke 方法,當不知道什麼執行緒擁有控制元件時這很有用。
下面來說下這個的用法(我的一般做法):
首先定義一個委託,與這個事件處理函式的簽名一樣委託,當然直接使用該事件的委託也是可以的,如:
private delegate void InvokeCallback( string msg);
然後就是判斷這個屬性的值來決定是否要呼叫Invoke函式:
void m_comm_MessageEvent( string msg)
{
if (txtMessage.InvokeRequired)
{
InvokeCallbackmsgCallback = new InvokeCallback(m_comm_MessageEvent);
txtMessage.Invoke(msgCallback, new object [] { msg } );
}
else
{
txtMessage.Text = msg;
}
}
說明:這個函式就是事件處理函式,txtMessage是一個文字框。
這樣就做到了窗體中控制元件的執行緒安全性。
------------------
InvokeRequired 當前執行緒不是建立控制元件的執行緒時為true
比如你可以自己開一個Thread,或使用Timer的事件來訪問窗體上的控制元件的時候,線上程中窗體的這個屬性就是True的。
簡單的說,如果有兩個執行緒,Thread A和Thread B,並且有一個Control c,是在Thread A裡面new的。
那麼在Thread A裡面執行的任何方法呼叫c.InvokeRequired都會返回false。
相反,如果在Thread B裡面執行的任何方法呼叫c.InvokeRequired都會返回true。
是否是UI執行緒與結果無關。(通常Control所在的執行緒是UI執行緒,但是可以有例外)
也可以認為,在new Control()的時候,control用一個變數記錄下了當前執行緒,在呼叫InvokeRequired時,返回當前執行緒是否不等於new的時候記錄下來的那個執行緒。
--------------------
我理解:如果InvokeRequired==true表示其它執行緒需要訪問控制元件,那麼呼叫invoke來轉給控制元件owner處理。