清理遍布InvokeRequired的代码
我知道,从任何非UI线程操纵UI控件时,您必须将您的调用编组到UI线程以避免问题。 一般的共识是你应该使用testingInvokeRequired,如果是true,则使用.Invoke来执行编组。
这导致了很多看起来像这样的代码:
private void UpdateSummary(string text) { if (this.InvokeRequired) { this.Invoke(new Action(() => UpdateSummary(text))); } else { summary.Text = text; } }
我的问题是:我可以省略InvokeRequiredtesting,只需调用Invoke,如下所示:
private void UpdateSummary(string text) { this.Invoke(new Action(() => summary.Text = text)); }
这样做有问题吗? 如果是这样,有没有更好的方法来保持InvokeRequiredtesting,而不必复制和粘贴这个模式到处?
那么这个怎么样:
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
的效率有点低。
相反,您可以创build一个InvokeIfNeeded
Action
参数的InvokeIfNeeded
扩展方法。 (这也可以让你从callsite中删除new Action(...)
)
我一直在阅读关于添加逻辑检查,以查明是否应该使用IFF,而不是在UI线程上,而不是在UI线程本身上来回参数。 我写了一个类来检查各种方法执行(通过秒表 )的时间,以粗略估计一种方法相对于另一种方法的效率。
结果可能会令你吃惊(这些testing是通过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"; } ) ); } } );
结果如下:
- TimedAction :: Go()+ 0 – debugging:[DEBUG]秒表[直接UI线程]: 300ms
- TimedAction :: Go()+ 0 – Debug:[DEBUG] Stopwatch [在UI线程上调用]: 299ms
- TimedAction :: Go()+ 0 – Debug:[DEBUG] Stopwatch [单独调用UI线程]: 649ms
我的结论是,无论您是在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
是更新用户界面的最佳select。 我不能确定你的情况,因为我不知道在哪个UpdateSummary
中调用的情况。 但是,如果您定期调用它作为显示进度信息的机制(这是我从代码片段中获得的印象),那么通常会有更好的select。 该选项是让UI线程轮询状态,而不是让工作线程推送它。
在这种情况下,应该考虑投票方式的原因是因为:
- 它打破了
Control.Invoke
强加的UI和工作线程之间的紧密耦合。 - 它把UI线程更新的责任放在它应该属于的UI线程上。
- UI线程可以决定更新应该发生的时间和频率。
- UI消息泵不会像工作者线程发起的封送处理技术那样溢出。
- 工作线程在进行下一步之前(即在UI线程和工作线程上获得更多的吞吐量),不必等待确认已执行更新。
因此,请考虑创build一个System.Windows.Forms.Timer
,定期检查要在Control
上显示的文本,而不是从工作线程启动推送。 再说一遍,如果不知道自己的具体要求,我不愿意说出这个绝对的方向,但是在绝大多数情况下,它比Control.Invoke
选项要好。
显然这种方法完全消除了InvokedRequired
检查的必要性。 没关系,它简化了UI /工作线程交互的所有其他方面。
我最喜欢的只能查看的控件的方法是将所有的控件状态封装在一个类中,这个类可以在不经历任何不一致的状态的情况下被更新(一个简单的方法是把所有需要更新的东西放在一起一个不可变的类,并且每当需要更新时创build一个类的新实例)。 然后有一个方法将Interlocked.Exchange一个updateNeeded标志,如果没有更新挂起,但IsHandleCreated为true,则BeginInvoke更新过程。 更新过程应该先清除updateNeeded标志,然后再做任何更新(如果有人试图在那一刻更新控件,另一个请求将被BeginInvoked)。 请注意,您必须准备好捕捉并吞下exception(我认为IllegalOperation),如果控制权被丢弃,就像准备更新它一样。
顺便提一句,如果一个控件还没有被join到一个线程中(通过被添加到一个可见的窗口,或者它的窗口变得可见),直接更新它是合法的,但是对它使用BeginInvoke或者Invoke是不合法的。
如果可能,使用BackgroudWorker更容易使UI响应,并使用ReportProgress更新UI,因为它与UI在同一个线程上运行,因此您不需要InvokeRequired。
我还不能评论,希望有人会看到这个,并将其添加到接受的答案,否则是现货。
control.Invoke(new Action(() => action(control)));
应该读
control.Invoke(new Action(() => action(control)), null);
由于写入接受的答案不会编译,因为ISynchronizeInvoke.Invoke()
没有像Control.Invoke()
那样只有一个参数的重载。
另一件事是,用法可能会更清楚
summary.InvokeIfRequired(c => { summary.Text = text; });
而不是像书面的summary.InvokeIfRequired(c => { textBox.Text = text });