扩展 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 的调用。

对 cme​​ts 的跟进:

一些 cmets 建议:

if (uiElement.InvokeRequired)

应该是:

if (uiElement.InvokeRequired && uiElement.IsHandleCreated)

考虑以下msdn documentation:

这意味着 InvokeRequired 可以 返回 false 如果不需要调用 (调用发生在同一个线程上), 或 如果控件是在 不同的线程,但控件的 尚未创建句柄。

在控件的句柄的情况下 尚未创建,您应该 不仅仅是调用属性、方法, 或控件上的事件。这有可能 导致控件的句柄是 在后台线程上创建, 隔离线程上的控件 没有消息泵并制作 应用不稳定。

您可以通过以下方式防范这种情况 还检查的值 IsHandleCreated when InvokeRequired 在后台线程上返回 false。

如果控件是在不同的线程上创建的,但尚未创建控件的句柄,InvokeRequired 将返回 false。这意味着如果InvokeRequired 返回trueIsHandleCreated 将始终为真。再次测试它是多余且不正确的。

【问题讨论】:

+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);

关于Components,只需在表单或容器本身上调用即可。

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.BeginInvokeDelegate.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.BeginInvokeControl.EndInvoke被设计成不耦合,这就提示用户认为Control.EndInvoke是可选的。

以上是关于扩展 Control 以提供始终安全的 Invoke/BeginInvoke 功能是不是合适?的主要内容,如果未能解决你的问题,请参考以下文章

Google Play 政策更新 | 扩展目标 API 级别要求以增进用户安全

Access-Control-Allow-Origin

Access-Control-Allow-Origin: * 不安全吗?

可以以位置和关键字方式提供的参数?

CORS(Access-Control-Allow-Origin 标头)如何提高安全性?

reCaptcha 问题:即使我在 angularJS 中添加了标头,所请求的资源上也不存在“Access-Control-Allow-Origin”标头始终显示