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

时间:2023-01-23 04:25:53

In the course of my maintenance for an older application that badly violated the cross-thread update rules in winforms, I created the following extension method as a way to quickly fix illegal calls when I've discovered them:

在我维护一个严重违反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();
    }
}

Sample usage:

this.lblTimeDisplay.SafeInvoke(() => this.lblTimeDisplay.Text = this.task.Duration.ToString(), false);

I like how I can leverage closures to read, also, though forceSynchronous needs to be true in that case:

我喜欢如何利用闭包来读取,尽管在这种情况下forceSynchronous需要为true:

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

I don't question the usefulness of this method for fixing up illegal calls in legacy code, but what about new code?

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

Is it good design to use this method to update UI in a piece of new software when you may not know what thread is attempting to update the ui, or should new Winforms code generally contain a specific, dedicated method with the appropriate Invoke()-related plumbing for all such UI updates? (I'll try to use the other appropriate background processing techniques first, of course, e.g. BackgroundWorker.)

如果您可能不知道哪个线程正在尝试更新ui,或者新的Winforms代码通常包含具有相应Invoke()的特定专用方法,那么使用此方法更新一个新软件中的UI是不错的设计 - 所有这些UI更新的相关管道? (我将首先尝试使用其他适当的后台处理技术,例如BackgroundWorker。)

Interestingly this won't work for ToolStripItems. I just recently discovered that they derive directly from Component instead of from Control. Instead, the containing ToolStrip's invoke should be used.

有趣的是,这对ToolStripItems不起作用。我刚刚发现它们直接来自Component而不是Control。相反,应该使用包含ToolStrip的调用。

Followup to comments:

Some comments suggest that:

一些评论表明:

if (uiElement.InvokeRequired)

should be:

if (uiElement.InvokeRequired && uiElement.IsHandleCreated)

Consider the following msdn documentation:

请考虑以下msdn文档:

This means that InvokeRequired can return false if Invoke is not required (the call occurs on the same thread), or if the control was created on a different thread but the control's handle has not yet been created.

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

In the case where the control's handle has not yet been created, you should not simply call properties, methods, or events on the control. This might cause the control's handle to be created on the background thread, isolating the control on a thread without a message pump and making the application unstable.

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

You can protect against this case by also checking the value of IsHandleCreated when InvokeRequired returns false on a background thread.

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

If the control was created on a different thread but the control's handle has not yet been created, InvokeRequired returns false. This means that if InvokeRequired returns true, IsHandleCreated will always be true. Testing it again is redundant and incorrect.

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

3 个解决方案

#1


I like the general idea, but I do see one problem. It is important to process EndInvokes, or you can have resource leaks. I know a lot of people don't believe this, but it really is true.

我喜欢这个总体思路,但我确实看到了一个问题。处理EndInvokes非常重要,否则您可能会发生资源泄漏。我知道很多人不相信这一点,但确实如此。

Here's one link talking about it. There are others as well.

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

But the main response I have is: Yes, I think you've got a nice idea here.

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

#2


You should create Begin and End extension methods as well. And if you use generics, you can make the call look a little nicer.

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

Now your calls get a little shorter and cleaner:

现在你的通话变得更短更清洁:

this.lblTimeDisplay.InvokeEx(l => l.Text = this.task.Duration.ToString());

var result = this.BeginInvokeEx(f => f.Text = "Different Title");
// ... wait
this.EndInvokeEx(result);

And with regards to Components, just invoke on the form or container itself.

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

this.InvokeEx(f => f.toolStripItem1.Text = "Hello World");

#3


This is not actually an answer but answers some comments for the accepted answer.

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

For standard IAsyncResult patterns, the BeginXXX method contains AsyncCallback parameter, so if you want to say "I don't care about this--just call EndInvoke when it's done and ignore the result", you can do something like this (this is for Action but should be able to be adjusted for other delegate types):

对于标准的IAsyncResult模式,BeginXXX方法包含AsyncCallback参数,所以如果你想说“我不关心这个 - 只要在完成时调用EndInvoke并忽略结果”,你就可以这样做(这是为了操作但应该能够针对其他委托类型进行调整):

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

(Unfortunately I don't have a solution not to have a helper function without declaring a variable each time when use this pattern).

(不幸的是,我没有解决方案,如果没有在每次使用此模式时声明变量而没有辅助函数)。

But for Control.BeginInvoke we do not have AsyncCallBack, so there is no easy way to express this with Control.EndInvoke guaranteed to be called. The way it has been designed prompts the fact that Control.EndInvoke is optional.

但是对于Control.BeginInvoke,我们没有AsyncCallBack,所以没有简单的方法来表达这个,保证Control.EndInvoke被调用。它的设计方式提示Control.EndInvoke是可选的。

#1


I like the general idea, but I do see one problem. It is important to process EndInvokes, or you can have resource leaks. I know a lot of people don't believe this, but it really is true.

我喜欢这个总体思路,但我确实看到了一个问题。处理EndInvokes非常重要,否则您可能会发生资源泄漏。我知道很多人不相信这一点,但确实如此。

Here's one link talking about it. There are others as well.

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

But the main response I have is: Yes, I think you've got a nice idea here.

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

#2


You should create Begin and End extension methods as well. And if you use generics, you can make the call look a little nicer.

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

Now your calls get a little shorter and cleaner:

现在你的通话变得更短更清洁:

this.lblTimeDisplay.InvokeEx(l => l.Text = this.task.Duration.ToString());

var result = this.BeginInvokeEx(f => f.Text = "Different Title");
// ... wait
this.EndInvokeEx(result);

And with regards to Components, just invoke on the form or container itself.

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

this.InvokeEx(f => f.toolStripItem1.Text = "Hello World");

#3


This is not actually an answer but answers some comments for the accepted answer.

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

For standard IAsyncResult patterns, the BeginXXX method contains AsyncCallback parameter, so if you want to say "I don't care about this--just call EndInvoke when it's done and ignore the result", you can do something like this (this is for Action but should be able to be adjusted for other delegate types):

对于标准的IAsyncResult模式,BeginXXX方法包含AsyncCallback参数,所以如果你想说“我不关心这个 - 只要在完成时调用EndInvoke并忽略结果”,你就可以这样做(这是为了操作但应该能够针对其他委托类型进行调整):

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

(Unfortunately I don't have a solution not to have a helper function without declaring a variable each time when use this pattern).

(不幸的是,我没有解决方案,如果没有在每次使用此模式时声明变量而没有辅助函数)。

But for Control.BeginInvoke we do not have AsyncCallBack, so there is no easy way to express this with Control.EndInvoke guaranteed to be called. The way it has been designed prompts the fact that Control.EndInvoke is optional.

但是对于Control.BeginInvoke,我们没有AsyncCallBack,所以没有简单的方法来表达这个,保证Control.EndInvoke被调用。它的设计方式提示Control.EndInvoke是可选的。