是否适当扩展控制提供一贯安全的调用/ BeginInvokefunction?
在维护一个严重违反winform中的跨线程更新规则的旧应用程序的过程中,我创build了以下扩展方法,以便在发现它们时快速修复非法呼叫:
/// <summary> /// Execute a method on the control's owning thread. /// </summary> /// <param name="uiElement">The control that is being updated.</param> /// <param name="updater">The method that updates uiElement.</param> /// <param name="forceSynchronous">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.</param> 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时,使用这种方法更新UI软件是否是一个好的devise,或者如果新的Winforms代码通常包含一个特定的专用方法和适当的Invoke()
所有这些UI更新相关的pipe道? (我会尝试先使用其他适当的后台处理技术,当然,例如BackgroundWorker)。
有趣的是,这对ToolStripItems不起作用。 我刚刚发现他们直接从组件而不是从控制派生。 应该使用包含ToolStrip
的invoke。
跟进评论:
有些意见build议:
if (uiElement.InvokeRequired)
应该:
if (uiElement.InvokeRequired && uiElement.IsHandleCreated)
考虑下面的msdn文档 :
这意味着,如果Invoke不是必需的(调用发生在同一线程上),或者如果控件是在不同的线程上创build的,但控件的句柄尚未创build ,则InvokeRequired可以返回false 。
在控件的句柄尚未创build的情况下,不应该简单地调用控件上的属性,方法或事件。 这可能会导致在后台线程上创build控件的句柄,在没有消息泵的情况下将线程上的控件隔离,并使应用程序不稳定。
当InvokeRequired在后台线程上返回false时,可以通过检查IsHandleCreated的值来防止这种情况。
如果控件是在不同的线程上创build的,但控件的句柄尚未创build,则InvokeRequired
返回false。 这意味着如果InvokeRequired
返回true
, IsHandleCreated
将始终为真。 再次testing是多余的和不正确的。
我喜欢一般的想法,但我确实看到一个问题。 处理EndInvokes是很重要的,或者你可能有资源泄漏。 我知道很多人不相信这一点,但是确实如此。
这里有一个链接在谈论它 。 还有其他的。
但我的主要回应是:是的,我认为你在这里有一个好主意。
你也应该创buildBegin和End扩展方法。 如果你使用generics,你可以使电话看起来更好一点。
public static class ControlExtensions { public static void InvokeEx<T>(this T @this, Action<T> 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<T>(this T @this, Action<T> action) where T : Control { return @this.BeginInvoke((Action)delegate { @this.InvokeEx(action); }); } public static void EndInvokeEx<T>(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
,只需调用窗体或容器本身。
this.InvokeEx(f => f.toolStripItem1.Text = "Hello World");
这实际上不是一个答案,但回答了接受的答案的一些意见。
对于标准的IAsyncResult
模式, BeginXXX
方法包含了AsyncCallback
参数,所以如果你想说“我不在乎这个 – 只要在完成时调用EndInvoke并忽略结果”,你可以做这样的事情(这是为了Action
但应该能够调整为其他代表types):
... public static void BeginInvokeEx(this Action a){ a.BeginInvoke(a.EndInvoke, a); } ... // Don't worry about EndInvoke // it will be called when finish new Action(() => {}).BeginInvokeEx();
(不幸的是我没有一个解决scheme,没有每次使用这种模式时都没有声明variables的帮助函数)。
但是对于Control.BeginInvoke
我们没有AsyncCallBack
,所以没有简单的方法用Control.EndInvoke
保证被调用。 它的devise方式提示Control.EndInvoke
是可选的。