自动化 InvokeRequired Code Pattern

Automating the InvokeRequired code pattern

提问人:Tom Corelis 提问时间:3/3/2010 最后编辑:CommunityTom Corelis 更新时间:2/1/2023 访问量:171987

问:

我痛苦地意识到,在事件驱动的 GUI 代码中,需要编写以下 Code Pattern 的频率,其中

private void DoGUISwitch() {
    // cruisin for a bruisin' through exception city
    object1.Visible = true;
    object2.Visible = false;
}

成为:

private void DoGUISwitch() {
    if (object1.InvokeRequired) {
        object1.Invoke(new MethodInvoker(() => { DoGUISwitch(); }));
    } else {
        object1.Visible = true;
        object2.Visible = false;
    }
}

这在 C# 中是一种尴尬的模式,无论是记忆还是键入。有没有人想出某种捷径或结构来在一定程度上自动化这一点?如果有一种方法可以将函数附加到执行此检查的对象,而不必经历所有这些额外的工作,例如类型快捷方式,那就太酷了。object1.InvokeIfNecessary.visible = true

之前的答案已经讨论了每次都调用 Invoke() 的不切实际,即便如此,Invoke() 语法既低效又难以处理。

那么,有没有人想出任何捷径呢?

C# 多线程处理 WinForms 线程安全 调用必需

评论

2赞 Taylor Leese 3/3/2010
我想知道同样的事情,但关于 WPF 的 Dispatcher.CheckAccess()。
0赞 Dan Tao 3/3/2010
我想出了一个相当疯狂的建议,灵感来自你的台词;看看我更新的答案,让我知道你的想法。object1.InvokeIfNecessary.Visible = true
1赞 Aaron Gage 2/15/2011
添加一个片段来帮助实现 Matt Davis 建议的方法:请参阅我的答案(晚了,但只是向后来的读者展示了如何;-))
3赞 Kamil 10/16/2012
我不明白为什么Microsoft没有在.NET中简化它。从线程为表单上的每个更改创建委托真的很烦人。
0赞 SteveCinq 2/21/2019
@Kamil我完全同意!鉴于其无处不在,这是一种疏忽。在框架内,如有必要,只需处理线程。似乎很明显。

答:

139赞 Lee 3/3/2010 #1

你可以写一个扩展方法:

public static void InvokeIfRequired(this Control c, Action<Control> action)
{
    if(c.InvokeRequired)
    {
        c.Invoke(new Action(() => action(c)));
    }
    else
    {
        action(c);
    }
}

并像这样使用它:

object1.InvokeIfRequired(c => { c.Visible = true; });

编辑:正如 Simpzon 在评论中指出的那样,您还可以将签名更改为:

public static void InvokeIfRequired<T>(this T c, Action<T> action) 
    where T : Control

评论

0赞 Oliver 11/18/2010
也许我只是太笨了,但这段代码不会编译。所以我修复了它,因为它是由我构建的(VS2008)。
5赞 Simon D. 11/18/2010
只是为了完整起见:在 WPF 中,有一种不同的调度机制,但它的工作方式非常相似。你可以在那里使用这个扩展方法: public static void InvokeIfRequired<T>(this T aTarget, Action<T> aActionToExecute) where T:DispatcherObject { if (aTarget.CheckAccess()) { aActionToExecute(aTarget); } else { aTarget.Dispatcher.Invoke(aActionToExecute);
1赞 Olivier Jacot-Descombes 8/29/2012
我添加了一个答案,稍微简化了 Lee 的解决方案。
0赞 Offler 6/5/2013
嗨,当我使用类似的东西时,这个通用实现可能会带来一个大问题。如果控件为 Disposing/Disposed,则将获得 ObjectDisposedException。
1赞 Lee 6/5/2013
@Offler - 好吧,如果它们被放置在不同的线程上,则您有同步问题,则此方法不是问题。
41赞 Matt Davis 3/3/2010 #2

这是我在所有代码中使用的表单。

private void DoGUISwitch()
{ 
    Invoke( ( MethodInvoker ) delegate {
        object1.Visible = true;
        object2.Visible = false;
    });
} 

我基于这里的博客文章。我没有让这种方法让我失望,所以我认为没有理由通过检查属性来使我的代码复杂化。InvokeRequired

希望这会有所帮助。

评论

0赞 Tom Bushell 4/21/2010
+1 - 我偶然发现了你所做的同一篇博客文章,并认为这是任何提议中最干净的方法
4赞 surfen 11/29/2011
使用这种方法会对性能造成很小的影响,在多次调用时可能会堆积起来。stackoverflow.com/a/747218/724944
8赞 56ka 1/17/2014
您必须使用是否可以在显示控件之前执行代码,否则将出现致命异常。InvokeRequired
10赞 Aaron Gage 2/15/2011 #3

创建一个 ThreadSafeInvoke.snippet 文件,然后只需选择更新语句,右键单击并选择“Surround With...”。或 Ctrl-K+S:

<?xml version="1.0" encoding="utf-8" ?>
<CodeSnippet Format="1.0.0" xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
  <Header>
    <Title>ThreadsafeInvoke</Title>
    <Shortcut></Shortcut>
    <Description>Wraps code in an anonymous method passed to Invoke for Thread safety.</Description>
    <SnippetTypes>
      <SnippetType>SurroundsWith</SnippetType>
    </SnippetTypes>
  </Header>
  <Snippet>
    <Code Language="CSharp">
      <![CDATA[
      Invoke( (MethodInvoker) delegate
      {
          $selected$
      });      
      ]]>
    </Code>
  </Snippet>
</CodeSnippet>
160赞 Olivier Jacot-Descombes 8/29/2012 #4

Lee的方法可以进一步简化

public static void InvokeIfRequired(this Control control, MethodInvoker action)
{
    // See Update 2 for edits Mike de Klerk suggests to insert here.

    if (control.InvokeRequired) {
        control.Invoke(action);
    } else {
        action();
    }
}

并且可以这样称呼

richEditControl1.InvokeIfRequired(() =>
{
    // Do anything you want with the control here
    richEditControl1.RtfText = value;
    RtfHelpers.AddMissingStyles(richEditControl1);
});

无需将控件作为参数传递给委托。C# 会自动创建闭包

如果必须返回值,可以使用以下实现:

private static T InvokeIfRequiredReturn<T>(this Control control, Func<T> function)
{
    if (control.InvokeRequired) {
        return (T)control.Invoke(function);
    } else {
        return function();
    }
}

更新

根据其他几张海报可以概括为:ControlISynchronizeInvoke

public static void InvokeIfRequired(this ISynchronizeInvoke obj,
                                         MethodInvoker action)
{
    if (obj.InvokeRequired) {
        var args = new object[0];
        obj.Invoke(action, args);
    } else {
        action();
    }
}

DonBoitnott 指出,与接口不同,该方法需要一个对象数组作为 .ControlISynchronizeInvokeInvokeaction


更新 2

Mike de Klerk 建议的编辑(请参阅插入点的第一个代码片段中的注释):

// When the form, thus the control, isn't visible yet, InvokeRequired  returns false,
// resulting still in a cross-thread exception.
while (!control.Visible)
{
    System.Threading.Thread.Sleep(50);
}

请参阅下面的 ToolmakerStevenawfal 的评论,了解对此建议的担忧。

评论

2赞 Odys 6/10/2013
不是更好吗?(向乔恩·斯基特致敬 stackoverflow.com/questions/711408/......ISynchronizeInvokeControl)
7赞 ToolmakerSteve 5/17/2016
@mike-de-clerk,我很担心你建议添加.对我来说,这有一种不好的代码味道,因为它是一个潜在的无限延迟(在某些情况下甚至可能是无限循环),在代码中可能有调用者不希望出现这样的延迟(甚至是死锁)。恕我直言,任何使用都应该是每个来电者的责任,或者应该在一个单独的包装中,清楚地标明其后果。恕我直言,通常最好是“严重失败”(异常,在测试期间捕获),或者如果控件没有准备好,则“什么都不做”。评论?while (!control.Visible) ..sleep..Sleep
3赞 rollsch 12/2/2016
而 (!可见)需要超时。可能导致难以调试的无限循环的不良做法。
2赞 Olivier Jacot-Descombes 10/23/2017
它没有,因为我们无论如何都在调用 winforms 控件。另请参阅 learn.microsoft.com/en-us/dotnet/api/...
2赞 nawfal 10/12/2021
@OlivierJacot-Descombes,我认为您对更新 2 的假设是不正确的(除了阻塞调用)。你说,但这有多合理?如果未为控件创建窗口句柄,则在不同的线程中更新控件是否仍然有效?此时,它只是将值设置为内存中对象。这就是为什么通用模式如此有效的原因。Thread.SleepWhen the form, thus the control, isn't visible yet, InvokeRequired returns false, resulting still in a cross-thread exception.if (c.InvokeRequired) c.Invoke(foo); else foo();
6赞 stephan Schmuck 6/21/2013 #5

我宁愿使用方法 Delegate 的单个实例,而不是每次都创建一个新实例。 就我而言,我曾经显示来自 Backroundworker 的进度和(信息/错误)消息,从 sql 实例复制和投射大数据。在大约 70000 次进度和消息调用之后,我的表单停止工作并显示新消息。 当我开始使用单个全局实例委托时,没有发生这种情况。

delegate void ShowMessageCallback(string message);

private void Form1_Load(object sender, EventArgs e)
{
    ShowMessageCallback showMessageDelegate = new ShowMessageCallback(ShowMessage);
}

private void ShowMessage(string message)
{
    if (this.InvokeRequired)
        this.Invoke(showMessageDelegate, message);
    else
        labelMessage.Text = message;           
}

void Message_OnMessage(object sender, Utilities.Message.MessageEventArgs e)
{
    ShowMessage(e.Message);
}
-4赞 Steve Wood 10/29/2014 #6

你永远不应该编写看起来像这样的代码:

private void DoGUISwitch() {
    if (object1.InvokeRequired) {
        object1.Invoke(new MethodInvoker(() => { DoGUISwitch(); }));
    } else {
        object1.Visible = true;
        object2.Visible = false;
    }
}

如果您确实有如下所示的代码,则您的应用程序不是线程安全的。这意味着您的代码已经从其他线程调用了 DoGUISwitch()。现在检查它是否在不同的线程中为时已晚。在调用 DoGUISwitch 之前,必须调用 InvokeRequire。不应从其他线程访问任何方法或属性。

参考:Control.InvokeRequired 属性,您可以在其中读取以下内容:

除了 InvokeRequired 属性之外,还有四个方法 线程安全调用的控件:Invoke、BeginInvoke、EndInvoke 和 CreateGraphics(如果控件的句柄已是 创建。

在单 CPU 架构中,没有问题,但在多 CPU 架构中,可能会导致部分 UI 线程分配给运行调用代码的处理器......如果该处理器与 UI 线程的运行位置不同,则当调用线程结束时,Windows 将认为 UI 线程已结束并将终止应用程序进程,即您的应用程序将退出而不会出错。

评论

0赞 Tom Corelis 11/5/2014
嘿,谢谢你的回答。自从我问这个问题以来已经有好几年了(自从我使用 C# 以来几乎一样长),但我想知道你是否可以进一步解释一下?您链接的文档指的是在为控件提供句柄之前调用等的特定危险,但恕我直言,您没有描述您所描述的内容。所有这些废话的全部意义在于以线程安全的方式更新 UI,我认为在阻塞上下文中放置更多指令会导致卡顿?(呃......很高兴我停止使用M$技术,太复杂了!invoke()invoke()
0赞 Tom Corelis 11/5/2014
我还想指出,尽管经常使用原始代码(很久以前),但我没有在我的双 CPU 桌面上观察到您描述的问题
3赞 public wireless 3/26/2015
我怀疑这个答案是否准确,因为 MSDN 显示了大量示例,就像 OP 给出的那样。
8赞 gxtaillon 4/8/2015 #7

以下是 Lee、Oliver 和 Stephan 答案的改进/组合版本。

public delegate void InvokeIfRequiredDelegate<T>(T obj)
    where T : ISynchronizeInvoke;

public static void InvokeIfRequired<T>(this T obj, InvokeIfRequiredDelegate<T> action)
    where T : ISynchronizeInvoke
{
    if (obj.InvokeRequired)
    {
        obj.Invoke(action, new object[] { obj });
    }
    else
    {
        action(obj);
    }
} 

该模板允许灵活且无强制转换的代码,其可读性更强,而专用委托则提供了效率。

progressBar1.InvokeIfRequired(o => 
{
    o.Style = ProgressBarStyle.Marquee;
    o.MarqueeAnimationSpeed = 40;
});
3赞 Walter Verhoeven 12/22/2016 #8

我有点喜欢做一点不同的事情,如果需要的话,我喜欢用“我自己”来称呼自己,

    private void AddRowToListView(ScannerRow row, bool suspend)
    {
        if (IsFormClosing)
            return;

        if (this.InvokeRequired)
        {
            var A = new Action(() => AddRowToListView(row, suspend));
            this.Invoke(A);
            return;
        }
         //as of here the Code is thread-safe

这是一个方便的模式,IsFormClosing 是我在关闭表单时设置为 True 的字段,因为可能有一些后台线程仍在运行......

7赞 Konstantin S. 10/2/2017 #9

用法:

control.InvokeIfRequired(c => c.Visible = false);

return control.InvokeIfRequired(c => {
    c.Visible = value

    return c.Visible;
});

法典:

using System;
using System.ComponentModel;

namespace Extensions
{
    public static class SynchronizeInvokeExtensions
    {
        public static void InvokeIfRequired<T>(this T obj, Action<T> action)
            where T : ISynchronizeInvoke
        {
            if (obj.InvokeRequired)
            {
                obj.Invoke(action, new object[] { obj });
            }
            else
            {
                action(obj);
            }
        }

        public static TOut InvokeIfRequired<TIn, TOut>(this TIn obj, Func<TIn, TOut> func) 
            where TIn : ISynchronizeInvoke
        {
            return obj.InvokeRequired
                ? (TOut)obj.Invoke(func, new object[] { obj })
                : func(obj);
        }
    }
}