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

发布于 2024-07-16 11:52:53 字数 3566 浏览 5 评论 0原文

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

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

评论后续:

一些评论建议:

if (uiElement.InvokeRequired)

应该:

if (uiElement.InvokeRequired && uiElement.IsHandleCreated)

考虑以下 msdn 文档

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

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

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

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

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:

/// <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:

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

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.

Followup to comments:

Some comments suggest that:

if (uiElement.InvokeRequired)

should be:

if (uiElement.InvokeRequired && uiElement.IsHandleCreated)

Consider the following msdn documentation:

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.

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.

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

魔法少女 2024-07-23 11:52:53

您还应该创建 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 ([email protected])
        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);

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

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

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

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 ([email protected])
        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");
沫尐诺 2024-07-23 11:52:53

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

这里有一个讨论它的链接。 还有其他的。

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

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.

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.

春夜浅 2024-07-23 11:52:53

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

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

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

    ...
    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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文