我知道在从任何非UI线程操作UI控件时,您必须封送对UI线程的调用以避免出现问题。一般的共识是你应该使用测试InvokeRequired,如果是,则使用.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测试,而不必在整个地方复制和粘贴此模式?
答案 0 :(得分:64)
那怎么样:
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 });
}
答案 1 :(得分:8)
从UI线程调用Invoke
效率有点低。
相反,您可以创建一个InvokeIfNeeded
扩展方法,该方法采用Action
参数。 (这也允许您从呼叫网站中删除new Action(...)
)
答案 2 :(得分:7)
我一直在阅读关于添加逻辑检查的来回的参数,以确定在不在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())是有利的,并且大大提高了效率。
答案 3 :(得分:5)
我意识到已经有one answer that's pretty much spot on,但我也希望发布它(我也发布了here)。
我的稍微不同,它可以稍微更安全地处理空控件,并在必要时返回结果。当我尝试在父窗体上显示一个可能为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));
答案 4 :(得分:3)
我不相信Control.Invoke
是更新用户界面的最佳选择。在你的情况下,我不能肯定地说,因为我不知道调用UpdateSummary
的情况。但是,如果您定期调用它作为显示进度信息的机制(这是我从代码片段中得到的印象),那么通常是一个更好的选择。该选项是让UI线程轮询状态,而不是让工作线程推送它。
在这种情况下应该考虑轮询方法的原因是:
Control.Invoke
强加的UI和工作线程之间的紧密耦合。因此,请考虑创建一个System.Windows.Forms.Timer
,定期检查要在Control
上显示的文本,而不是从工作线程启动推送。再一次,在不知道你的确切要求的情况下,我不愿意这说明你需要去的方向,但在大多数情况下很多情况下 比{{1}更好选项。
显然,这种方法完全取消了Control.Invoke
检查的必要性。没关系,事实上它简化了所有 UI /工作线程交互的其他方面。
答案 5 :(得分:2)
我对首选视图控件的首选方法是将所有控件状态封装在一个类中,该类可以在不经历任何不一致状态的情况下进行更新(一种简单的方法是将所有需要的东西放在一起)一起更新为不可变类,并在需要更新时创建类的新实例。然后有一个方法,它将Interlocked.Exchange一个updateNeeded标志,如果没有更新挂起但IsHandleCreated为true,则BeginInvoke更新过程。在进行任何更新之前,更新过程应该首先清除updateNeeded标志(如果有人在此时尝试更新控件,则另一个请求将是BeginInvoked)。请注意,如果控件在您准备更新时被处理掉,您必须准备捕获并吞下异常(我认为是IllegalOperation)。
顺便提一下,如果一个控件尚未加入一个线程(通过添加到一个可见窗口,或者它的窗口变得可见),直接更新它是合法的,但使用BeginInvoke或Invoke是不合法的在它上面。
答案 6 :(得分:1)
我无法发表评论,希望有人会看到这个并将其添加到已接受的答案中,否则该答案就会出现。
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 });
答案 7 :(得分:0)
如果可能,使用BackgroudWorker更容易使UI响应并使用ReportProgress更新UI,因为它在与UI相同的线程上运行,因此您不需要InvokeRequired。