扩展 Control 以提供始终安全的 Invoke/BeginInvoke 功能是不是合适?
Posted
技术标签:
【中文标题】扩展 Control 以提供始终安全的 Invoke/BeginInvoke 功能是不是合适?【英文标题】:Is it appropriate to extend Control to provide consistently safe Invoke/BeginInvoke functionality?扩展 Control 以提供始终安全的 Invoke/BeginInvoke 功能是否合适? 【发布时间】:2010-10-17 09:53:24 【问题描述】:在维护严重违反 winforms 中的跨线程更新规则的旧应用程序的过程中,我创建了以下扩展方法,以便在发现非法调用时快速修复它们:
/// <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 需要为真:
string taskName = string.Empty;
this.txtTaskName.SafeInvoke(() => taskName = this.txtTaskName.Text, true);
我不怀疑这种方法对修复遗留代码中的非法调用的有用性,但是新代码呢?
当您可能不知道哪个线程正在尝试更新 ui 时,使用此方法更新新软件中的 UI 是否是一个好的设计,或者新的 Winforms 代码通常应该包含一个特定的、专用的方法和适当的 @所有此类 UI 更新的 987654328@ 相关管道? (当然,我会先尝试使用其他合适的后台处理技术,例如 BackgroundWorker。)
有趣的是,这不适用于ToolStripItems。我最近才发现它们直接来自Component,而不是来自Control。相反,应该使用包含 ToolStrip
的调用。
对 cmets 的跟进:
一些 cmets 建议:
if (uiElement.InvokeRequired)
应该是:
if (uiElement.InvokeRequired && uiElement.IsHandleCreated)
考虑以下msdn documentation:
这意味着 InvokeRequired 可以 返回 false 如果不需要调用 (调用发生在同一个线程上), 或 如果控件是在 不同的线程,但控件的 尚未创建句柄。
在控件的句柄的情况下 尚未创建,您应该 不仅仅是调用属性、方法, 或控件上的事件。这有可能 导致控件的句柄是 在后台线程上创建, 隔离线程上的控件 没有消息泵并制作 应用不稳定。
您可以通过以下方式防范这种情况 还检查的值 IsHandleCreated when InvokeRequired 在后台线程上返回 false。
如果控件是在不同的线程上创建的,但尚未创建控件的句柄,InvokeRequired
将返回 false。这意味着如果InvokeRequired
返回true
,IsHandleCreated
将始终为真。再次测试它是多余且不正确的。
【问题讨论】:
+1 问题 - 我发现自己一直在为主 UI 线程上的事物编写调用回调。如果扩展方法不包含太多缺点,这将是一个巨大的节省时间。 我会把它重命名为“SafeInvoke” @Joel:这个名字更好。更新问题以反映建议。 (我从来没有真正喜欢过“UpdateUI”这个名字。) 我总是将 ToolStripItem 设为静态以解决该特定问题。 @Lii: (1) 你可以做很多事情。如果你更喜欢抛出异常,那就去吧。 (2) 当我们到达BeginInvoke()
/Invoke()
时,我们还没有验证所有要求的先决条件。例如,我们尚未验证句柄是否存在。直接调用updater
会回避验证。
【参考方案1】:
您还应该创建 Begin 和 End 扩展方法。如果你使用泛型,你可以让调用看起来更好一点。
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
s,只需在表单或容器本身上调用即可。
this.InvokeEx(f => f.toolStripItem1.Text = "Hello World");
【讨论】:
有趣。如果我们这样做,我们需要插入对 EndInvoke() 的适当调用,我认为,b/c 我们将走出忽略 Control.EndInvoke() 的感知“安全区”。此外,在调用之前检查 IsHandleCreate 或 IsDisposed 也不安全。 我不明白 type 参数的好处。除非-它是否适用于不支持闭包的语言? BeginInvokeEx 将在调用 BeginInvoke 时抛出异常,如果尚未创建控制句柄。因此,InvokeEx 的重用可能是不可能的,我们需要复制代码或创建另一个私有方法供 InvokeEx 和 BeginInvokeEx 使用,该方法接受方法委托作为参数。【参考方案2】:我喜欢总体思路,但我确实看到了一个问题。处理 EndInvokes 很重要,否则您可能会发生资源泄漏。我知道很多人不相信这一点,但这确实是真的。
Here's one link talking about it。还有其他的。
但我的主要反应是:是的,我认为您在这里有一个好主意。
【讨论】:
FWIW,我记得在 MSDN 上读到,在这个单一的、特殊的、神奇的、特殊的情况下,它被记录为不需要。不过,我没有链接,在我有链接之前它不是真的。 :) Jon 链接的相关部分:>> “我刚刚从 WinForms 团队得到官方消息。没有必要调用 Control.EndInvoke。您可以以“即发即弃”的方式调用 BeginInvoke不受惩罚。” 您不必调用 Control.EndInvoke,因为 Control.BeginInvoke 调用解析为控件线程的消息循环上的 PostMessage。您无法轻易判断此消息是否被处理,并且没有返回值。 @LimitedAtonement:这真的是“规则”的“例外”吗,或者更公平地说,该规则要求对Delegate.BeginInvoke
的调用通过对@987654324 的调用来平衡@,但对也恰好命名为 BeginInvoke
的其他不相关函数没有任何要求?由于Control.BeginInvoke
与Delegate.BeginInvoke
无关,因此适用于前者的规则没有理由适用于后者。
@supercat 我认为所引用的规则是此处记录的开始和结束模式:msdn.microsoft.com/en-us/library/ms228963.aspx。【参考方案3】:
这实际上不是一个答案,而是为接受的答案回答了一些 cmets。
对于标准的IAsyncResult
模式,BeginXXX
方法包含AsyncCallback
参数,所以如果你想说“我不关心这个——完成后调用 EndInvoke 并忽略结果”,你可以做这样的事情(这是针对Action
,但应该能够针对其他委托类型进行调整):
...
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();
(不幸的是,在每次使用此模式时,如果不声明变量,我没有一个解决方案,即不使用辅助函数)。
但是对于Control.BeginInvoke
,我们没有AsyncCallBack
,所以没有简单的方法来表达这个Control.EndInvoke
保证被调用。它的设计方式提示Control.EndInvoke
是可选的。
【讨论】:
EndInvoke 在这种非常特殊的情况下是可选的,因为实现细节可能不会因 backcompat 问题而改变。我不认为这是因为方法签名中缺少参数。 我同意这个措辞有点误导。我想说的是,因为Control.BeginInvoke
和Control.EndInvoke
被设计成不耦合,这就提示用户认为Control.EndInvoke
是可选的。以上是关于扩展 Control 以提供始终安全的 Invoke/BeginInvoke 功能是不是合适?的主要内容,如果未能解决你的问题,请参考以下文章
Google Play 政策更新 | 扩展目标 API 级别要求以增进用户安全
Access-Control-Allow-Origin: * 不安全吗?
CORS(Access-Control-Allow-Origin 标头)如何提高安全性?
reCaptcha 问题:即使我在 angularJS 中添加了标头,所请求的资源上也不存在“Access-Control-Allow-Origin”标头始终显示