33

在维护严重违反 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 代码通常应该包含一个特定的、专用的方法以及适当的Invoke()相关管道所有这些用户界面更新?(当然,我会先尝试使用其他合适的后台处理技术,例如 BackgroundWorker。)

有趣的是,这不适用于ToolStripItems。我最近才发现它们直接来自Component而不是来自Control。相反,ToolStrip应该使用包含的调用。

评论的跟进:

一些评论表明:

if (uiElement.InvokeRequired)

应该:

if (uiElement.InvokeRequired && uiElement.IsHandleCreated)

考虑以下msdn 文档

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

在尚未创建控件句柄的情况下,不应简单地调用控件上的属性、方法或事件。这可能会导致在后台线程上创建控件的句柄,从而在没有消息泵的线程上隔离控件并​​使应用程序不稳定。

当 InvokeRequired 在后台线程上返回 false 时,您还可以通过检查 IsHandleCreated 的值来防止这种情况。

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

4

3 回答 3

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

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

this.InvokeEx(f => f.toolStripItem1.Text = "Hello World");
于 2009-04-03T17:02:37.683 回答
5

我喜欢一般的想法,但我确实看到了一个问题。处理 EndInvokes 很重要,否则您可能会发生资源泄漏。我知道很多人不相信这一点,但这确实是真的。

这是一个谈论它的链接。还有其他的。

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

于 2009-04-03T16:25:47.070 回答
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是可选的。

于 2014-01-30T05:54:16.763 回答