[英]Cleaning up code littered with InvokeRequired
我知道當從任何非UI線程操作UI控件時,您必須封送對UI線程的調用以避免問題。 一般的共識是您應該使用測試InvokeRequired,如果為true,則使用.Invoke來執行封送處理。
這會導致很多代碼看起來像這樣:
private void UpdateSummary(string text)
{
if (this.InvokeRequired)
{
this.Invoke(new Action(() => UpdateSummary(text)));
}
else
{
summary.Text = text;
}
}
我的問題是:我可以省略InvokeRequired測試並只調用Invoke,如下所示:
private void UpdateSummary(string text)
{
this.Invoke(new Action(() => summary.Text = text));
}
這樣做有問題嗎? 如果是這樣,是否有更好的方法來保持InvokeRequired測試,而不必在整個地方復制和粘貼此模式?
那怎么樣:
public static class ControlHelpers
{
public static void InvokeIfRequired<T>(this T control, Action<T> action) where T : ISynchronizeInvoke
{
if (control.InvokeRequired)
{
control.Invoke(new Action(() => action(control)), null);
}
else
{
action(control);
}
}
}
像這樣使用它:
private void UpdateSummary(string text)
{
summary.InvokeIfRequired(s => { s.Text = text });
}
從UI線程調用Invoke
有點效率低下。
相反,您可以創建一個采用Action
參數的InvokeIfNeeded
擴展方法。 (這也允許您從調用點刪除new Action(...)
)
我一直在閱讀關於添加邏輯檢查來反復討論的問題,以確定在不在UI線程上而不在UI線程本身上時是否應該使用IFF調用。 我寫了一個類來檢查執行(通過秒表 )各種方法的時間,以粗略估計一種方法相對於另一種方法的效率。
對於你們中的一些人來說結果可能會令人驚訝(這些測試是通過Form.Shown事件運行的):
// notice that we are updating the form's title bar 10,000 times
// directly on the UI thread
TimedAction.Go
(
"Direct on UI Thread",
() =>
{
for (int i = 0; i < 10000; i++)
{
this.Text = "1234567890";
}
}
);
// notice that we are invoking the update of the title bar
// (UI thread -> [invoke] -> UI thread)
TimedAction.Go
(
"Invoke on UI Thread",
() =>
{
this.Invoke
(
new Action
(
() =>
{
for (int i = 0; i < 10000; i++)
{
this.Text = "1234567890";
}
}
)
);
}
);
// the following is invoking each UPDATE on the UI thread from the UI thread
// (10,000 invokes)
TimedAction.Go
(
"Separate Invoke on UI Thread",
() =>
{
for (int i = 0; i < 10000; i++)
{
this.Invoke
(
new Action
(
() =>
{
this.Text = "1234567890";
}
)
);
}
}
);
結果如下:
我的結論是,您可以隨時安全地調用,無論您是在UI線程還是工作線程,都沒有通過消息泵循環回來的顯着開銷。 但是,在UI線程上執行大部分工作而不是多次調用UI線程(通過Invoke() )是有利的並且大大提高了效率。
我意識到已經有一個答案 ,但是我也想發布它(我也在這里發布)。
我的稍微不同,它可以稍微更安全地處理空控件,並在必要時返回結果。 當嘗試調用在父窗體上顯示可能為null的MessageBox並返回顯示MessageBox的DialogResult時,這兩個對我來說都派上了用場。
using System;
using System.Windows.Forms;
/// <summary>
/// Extension methods acting on Control objects.
/// </summary>
internal static class ControlExtensionMethods
{
/// <summary>
/// Invokes the given action on the given control's UI thread, if invocation is needed.
/// </summary>
/// <param name="control">Control on whose UI thread to possibly invoke.</param>
/// <param name="action">Action to be invoked on the given control.</param>
public static void MaybeInvoke(this Control control, Action action)
{
if (control != null && control.InvokeRequired)
{
control.Invoke(action);
}
else
{
action();
}
}
/// <summary>
/// Maybe Invoke a Func that returns a value.
/// </summary>
/// <typeparam name="T">Return type of func.</typeparam>
/// <param name="control">Control on which to maybe invoke.</param>
/// <param name="func">Function returning a value, to invoke.</param>
/// <returns>The result of the call to func.</returns>
public static T MaybeInvoke<T>(this Control control, Func<T> func)
{
if (control != null && control.InvokeRequired)
{
return (T)(control.Invoke(func));
}
else
{
return func();
}
}
}
用法:
myForm.MaybeInvoke(() => this.Text = "Hello world");
// Sometimes the control might be null, but that's okay.
var dialogResult = this.Parent.MaybeInvoke(() => MessageBox.Show(this, "Yes or no?", "Choice", MessageBoxButtons.YesNo));
我不相信Control.Invoke
是更新UI的最佳選擇。 在你的情況下,我不能肯定地說,因為我不知道調用UpdateSummary
的情況。 但是,如果您定期調用它作為顯示進度信息的機制(這是我從代碼片段中得到的印象),那么通常有更好的選擇。 該選項是讓UI線程輪詢狀態,而不是讓工作線程推送它。
在這種情況下應該考慮輪詢方法的原因是因為:
Control.Invoke
強加的UI和工作線程之間的緊密耦合。 因此,請考慮創建一個System.Windows.Forms.Timer
,它定期檢查要在Control
上顯示的文本,而不是從工作線程啟動推送。 同樣,不知道您的具體要求,我不願意說這肯定你需要去的方向,但在
大多數
很多情況下, 是不是更好Control.Invoke
選項。
顯然,這種方法完全消除了InvokedRequired
檢查的必要性。 沒關系,事實上它簡化了UI /工作線程交互的
所有
其他方面。
我對視圖控件的首選方法是將所有控件狀態封裝在一個類中,該類可以在不經歷任何不一致狀態的情況下進行更新(一種簡單的方法是將所有需要更新的內容放在一起一個不可變的類,並在需要更新時創建該類的新實例)。 然后有一個方法,它將Interlocked.Exchange一個updateNeeded標志,如果沒有更新掛起但IsHandleCreated為true,則BeginInvoke更新過程。 在進行任何更新之前,更新過程應該首先清除updateNeeded標志(如果有人在此時嘗試更新控件,則另一個請求將是BeginInvoked)。 請注意,如果控件在您准備更新時被釋放,則必須准備捕獲並吞下異常(我認為是IllegalOperation)。
順便說一句,如果一個控件尚未加入一個線程(通過添加到一個可見的窗口,或者它的窗口變得可見),直接更新它是合法的,但在它上面使用BeginInvoke或Invoke是不合法的。
我還無法發表評論,希望有人會看到這個並將其添加到已接受的答案中,否則就是現場。
control.Invoke(new Action(() => action(control)));
應該讀
control.Invoke(new Action(() => action(control)), null);
如上所述,接受的答案將無法編譯,因為ISynchronizeInvoke.Invoke()
沒有像Control.Invoke()
那樣只有1個參數的重載。
另一件事是使用可能更清楚
summary.InvokeIfRequired(c => { summary.Text = text; });
而不是書面summary.InvokeIfRequired(c => { textBox.Text = text });
如果可能,使用BackgroudWorker更容易使UI響應並使用ReportProgress更新UI,因為它在與UI相同的線程上運行,因此您不需要InvokeRequired。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.