代码之家  ›  专栏  ›  技术社区  ›  Greg D

扩展控制以提供一致的安全调用/开始调用功能是否合适?

  •  33
  • Greg D  · 技术社区  · 17 年前

    在维护严重违反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() -所有此类UI更新的相关管道?(当然,我会先尝试使用其他适当的背景处理技术,例如BackgroundWorker。)

    有趣的是,这对我来说是行不通的 ToolStripItems . 我最近才发现它们直接来源于 Component Control . 相反,包含 ToolStrip 应使用的调用。

    评论的后续行动:

    一些评论认为:

    if (uiElement.InvokeRequired)
    

    应该是:

    if (uiElement.InvokeRequired && uiElement.IsHandleCreated)
    

    考虑以下事项 msdn documentation

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

    尚未创建,您应该 不仅仅是调用属性、方法, 或控件上的事件。这可能 使控件的句柄 在后台线程上创建, 隔离线程上的控件 没有消息泵和使

    你可以通过 IsHandleCreated在需要时调用

    如果控件是在其他线程上创建的,但控件的句柄尚未创建, InvokeRequired 返回false。这意味着如果 调用所需 返回 true IsHandleCreated 永远都是真的。再次测试它是多余和不正确的。

    3 回复  |  直到 17 年前
        1
  •  11
  •   Samuel    17 年前

    您还应该创建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");
    
        2
  •  5
  •   Charlie Flowers    17 年前

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

    但我的主要回答是:是的,我认为你有一个好主意。

        3
  •  0
  •   Earth Engine    11 年前

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

    标准 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(); 
    

    (不幸的是,我没有一个解决方案,在每次使用这个模式时,如果没有声明一个变量,就没有一个helper函数)。

    要不是 Control.BeginInvoke 我们没有 AsyncCallBack ,因此没有简单的方法来表达这一点 Control.EndInvoke 保证会被叫来。它的设计方式提示了这样一个事实: Control.EndInvoke 是可选的。