是否适合扩展Control以提供始终如一的安全Invoke / BeginInvoke功能?

时间:2009-04-03 16:22:21

标签: c# winforms controls extension-methods invoke

在我维护一个严重违反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需要为true:

string taskName = string.Empty;
this.txtTaskName.SafeInvoke(() => taskName = this.txtTaskName.Text, true);

我不怀疑这种方法在遗留代码中修复非法调用的用处,但新代码呢?

当您可能不知道哪个线程正在尝试更新ui时,使用此方法更新一个新软件中的UI是不是很好的设计,或者新的Winforms代码通常是否包含具有相应{ {1}} - 所有此类UI更新的相关管道? (我将首先尝试使用其他适当的后台处理技术,例如BackgroundWorker。)

有趣的是,这对ToolStripItems无效。我最近发现它们直接来自Component,而不是来自Control。相反,应该使用包含Invoke()的调用。

对评论的跟进:

有些评论表明:

ToolStrip

应该是:

if (uiElement.InvokeRequired)

考虑以下msdn documentation

  

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

     

在控件的句柄中   尚未创建,你应该   不是简单地调用属性,方法,   或控件上的事件。这有可能   导致控件的句柄   在后台线程上创建,   隔离线程上的控件   没有消息泵和制作   应用不稳定。

     

您可以通过以下方式防止此情况   还要检查的价值   InvokeRequired时IsHandleCreated   在后台线程上返回false。

如果控件是在另一个线程上创建的,但尚未创建控件的句柄,则if (uiElement.InvokeRequired && uiElement.IsHandleCreated) 返回false。这意味着如果InvokeRequired返回InvokeRequired,则true将始终为真。再次测试是多余和不正确的。

3 个答案:

答案 0 :(得分:11)

您还应该创建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");

答案 1 :(得分:5)

我喜欢这个一般的想法,但我确实看到了一个问题。处理EndInvokes非常重要,否则可能会导致资源泄漏。我知道很多人不相信这一点,但确实如此。

Here's one link talking about it。还有其他人。

但我的主要反应是:是的,我认为你在这里有个好主意。

答案 2 :(得分:0)

这实际上不是一个答案,而是回答了对已接受答案的一些评论。

对于标准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是可选的。