在我维护一个严重违反winforms中的跨线程更新规则的旧应用程序的过程中,我创建了以下扩展方法,以便在我发现它们时快速修复非法调用:
////// Execute a method on the control's owning thread. /// /// The control that is being updated. /// The method that updates uiElement. /// True to force synchronous execution of /// updater. False to allow asynchronous execution if the call is marshalled /// from a non-GUI thread. If the method is called on the GUI thread, /// execution is always synchronous. public static void SafeInvoke(this Control uiElement, Action updater, bool forceSynchronous) { if (uiElement == null) { throw new ArgumentNullException("uiElement"); } if (uiElement.InvokeRequired) { if (forceSynchronous) { uiElement.Invoke((Action)delegate { SafeInvoke(uiElement, updater, forceSynchronous); }); } else { uiElement.BeginInvoke((Action)delegate { SafeInvoke(uiElement, updater, forceSynchronous); }); } } else { if (!uiElement.IsHandleCreated) { // Do nothing if the handle isn't created already. The user's responsible // for ensuring that the handle they give us exists. return; } if (uiElement.IsDisposed) { throw new ObjectDisposedException("Control is already disposed."); } updater(); } }
样品用法:
this.lblTimeDisplay.SafeInvoke(() => this.lblTimeDisplay.Text = this.task.Duration.ToString(), false);
我喜欢如何利用闭包来读取,尽管在这种情况下forceSynchronous需要为true:
string taskName = string.Empty; this.txtTaskName.SafeInvoke(() => taskName = this.txtTaskName.Text, true);
我不怀疑这种方法在遗留代码中修复非法调用的用处,但是新代码呢?
如果您可能不知道哪个线程正在尝试更新ui,或者新的Winforms代码通常包含特定的专用方法以及相应的Invoke()
相关管道,那么使用此方法更新一个新软件中的UI是一种很好的设计吗?所有这些UI更新?(我将首先尝试使用其他适当的后台处理技术,例如BackgroundWorker.)
有趣的是,这对ToolStripItems不起作用.我刚刚发现它们直接来自Component而不是Control.相反,ToolStrip
应该使用包含的调用.
一些评论表明:
if (uiElement.InvokeRequired)
应该:
if (uiElement.InvokeRequired && uiElement.IsHandleCreated)
请考虑以下msdn文档:
这意味着如果不需要Invoke(调用发生在同一个线程上),或者控件是在另一个线程上创建但尚未创建控件的句柄,则InvokeRequired可以 返回false .
如果尚未创建控件的句柄,则不应简单地在控件上调用属性,方法或事件.这可能导致在后台线程上创建控件的句柄,在没有消息泵的情况下隔离线程上的控件并使应用程序不稳定.
当InvokeRequired在后台线程上返回false时,还可以通过检查IsHandleCreated的值来防止这种情况.
如果控件是在另一个线程上创建的,但尚未创建控件的句柄,则InvokeRequired
返回false.这意味着如果InvokeRequired
返回true
,IsHandleCreated
将始终为真.再次测试是多余和不正确的.
您还应该创建Begin和End扩展方法.如果你使用泛型,你可以让电话看起来更好一些.
public static class ControlExtensions { public static void InvokeEx(this T @this, Action action) where T : Control { if (@this.InvokeRequired) { @this.Invoke(action, new object[] { @this }); } else { if (!@this.IsHandleCreated) return; if (@this.IsDisposed) throw new ObjectDisposedException("@this is disposed."); action(@this); } } public static IAsyncResult BeginInvokeEx (this T @this, Action action) where T : Control { return @this.BeginInvoke((Action)delegate { @this.InvokeEx(action); }); } public static void EndInvokeEx (this T @this, IAsyncResult result) where T : Control { @this.EndInvoke(result); } }
现在你的通话变得更短更清洁:
this.lblTimeDisplay.InvokeEx(l => l.Text = this.task.Duration.ToString()); var result = this.BeginInvokeEx(f => f.Text = "Different Title"); // ... wait this.EndInvokeEx(result);
关于Component
s,只需在表单或容器本身上调用即可.
this.InvokeEx(f => f.toolStripItem1.Text = "Hello World");
我喜欢一般的想法,但我确实看到了一个问题.处理EndInvokes非常重要,否则可能会导致资源泄漏.我知道很多人不相信这一点,但确实如此.
这是一个谈论它的链接.还有其他人.
但我的主要反应是:是的,我认为你在这里有个好主意.