类型安全的 Control.Invoke C#

Posted

技术标签:

【中文标题】类型安全的 Control.Invoke C#【英文标题】:type-safe Control.Invoke C# 【发布时间】:2020-04-29 18:43:00 【问题描述】:

我正在使用 C# 编写一个包含 2 个线程的软件

    控制窗体(Windows 窗体)并与用户交互的线程。 在后台检查在线数据的线程。

当在线数据不规则时,我需要第二个线程在表单上打印消息。

因为只有创建控件的线程才能更改它,所以我使用委托。 第二个线程通过 Control.Invoke 方法调用第一个线程执行委托。

例子:

    public partial class Form1 : Form

    public delegate void SafeCallDelegate(string text);
    public static SafeCallDelegate d;
    public Form1()
    
        InitializeComponent();
        d = new SafeCallDelegate(addText);
    
    private static void addText(string text)
    
        richTextBox1.Text += text + "\n";
    

static class Program

    static Thread secondThread;
    [STAThread]
    static void Main()
    
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        secondThread = new Thread(SecondThreadFunc);
        secondThread.Start();
        Application.Run(new Form1());
    
    static void SecondThreadFunc()
    
        int counter = 0;
        do
        
            if (Form.ActiveForm == null) continue;
            Form.ActiveForm.Invoke(Form1.d, new object[]  "SecondThreadFunc counter: " + counter.ToString() );
            counter++;
            Thread.Sleep(1000);
         while (true);
    

我可以接受这个不太干净的解决方案,但我的问题是这根本不是类型安全的。

Control.Invoke 函数接受一个对象数组,而不管委托需要什么,这可能导致运行时异常。

有没有一种更安全的方法可以解决我的问题?

谢谢。

【问题讨论】:

不是解决方案,但我通常会这样做。创建一个可以从任何线程调用的函数,但能够执行特定于 UI 线程的工作。使用Control.InvokeRequired 测试调用者是否不在 UI 线程上。如果是这样,调用相同的函数(有效地递归)。这样,非类型安全代码就在一个地方,就在调用要到达的地方。抱歉,我正在打电话,但这是一种很常见的模式,您应该可以找到示例 是的,我知道这个解决方案,这是我试图避免的解决方案。我只是发现 if(Control.InvokeRequired) 解决方案一点也不优雅。我觉得这么简单很奇怪(我认为很常见的情况)没有任何优雅的解决方案。 编写正确的异步代码并简单地从主线程await... 您可能会构建一个 lambda,在运行时使用表达式树调用您的委托? 【参考方案1】:

不要将参数传递给Invoke,而是将它们作为closured variable 在委托中传递。

所以,不要这样:

Form.ActiveForm.Invoke(Form1.d, new object[]  "SecondThreadFunc counter: " + counter.ToString() );

这样写:

Form.ActiveForm.Invoke
(
    new Action
    (
        () => Form1.d("SecondThreadFunc counter: " + counter.ToString())
    )
);

这避免了将参数传递给Invoke 的问题并消除了类型安全问题。

如果你觉得这有点罗嗦,你也可以定义一个简单的辅助扩展方法:

static class Helpers

    static public void MyInvoke(this Control control, Action action)
    
        if (control.InvokeRequired)
        
            control.Invoke(action);
        
        else
        
            action();
        
    

现在你只需要在你的主表单中写下这样的内容:

this.MyInvoke( () => addText("SecondThreadFunc counter: " + counter.ToString()));

您现在可以摆脱所有 SafeCallDelegate 的东西,只需在需要时传递您想要的任何 lambda。

【讨论】:

【参考方案2】:

据我了解,您希望构建 Action<...> 或 Func<...> 代表尊重 Control Invoke 要求。如果是这样,您可以像这样装饰现有的通用委托:

public static class InvokeFunc

    private class InvokeFuncImpl<TRusult>
    
        public Func<TRusult> Func  get; 

        public InvokeFuncImpl(Func<TRusult> f)
        
            Func = f;
        

        public static implicit operator Func<TRusult>(InvokeFuncImpl<TRusult> value)
        
            return () =>
            
                if(Form.ActiveForm == null)
                    return value.Func();
                if(!Form.ActiveForm.InvokeRequired)
                    return value.Func();
                return (TRusult)Form.ActiveForm.Invoke(value.Func);
            ;
        
    

    private class InvokeFuncImpl<TArg1, TRusult>
    
        public Func<TArg1, TRusult> Func  get; 

        public InvokeFuncImpl(Func<TArg1, TRusult> f)
        
            Func = f;
        

        public static implicit operator Func<TArg1, TRusult>(InvokeFuncImpl<TArg1, TRusult> value)
        
            return (arg) =>
            
                if(Form.ActiveForm == null)
                    return value.Func(arg);
                if(!Form.ActiveForm.InvokeRequired)
                    return value.Func(arg);
                return (TRusult)Form.ActiveForm.Invoke(value.Func, arg);
            ;
        
    

    public static Func<TResult> Bulid<TResult>(Func<TResult> f) => new InvokeFuncImpl<TResult>(f);
    public static Func<TArg1, TResult> Bulid<TArg1, TResult>(Func<TArg1, TResult> f) => new InvokeFuncImpl<TArg1, TResult>(f);

不幸的是,在 C# 中没有可变参数泛型参数,所以你应该隐式地使所有泛型重载。

所以你可以写这样的代码:

_f = InvokeFunc.Bulid((bool x) =>
        
            textBox1.Multiline = x;
            return textBox1.Text.Length;
        );

其中 _f 是 Func 类型的某个字段,并且可以在任何线程中安全调用。在实现中,我检查了调用要求,所以如果不需要,则直接调用。

为了行动<...>:

public static class InvokeAction

    private class InvokeActionImpl
    
        public Action Action  get; 

        public InvokeActionImpl(Action a)
        
            Action = a;
        

        public static implicit operator Action(InvokeActionImpl value)
        
            return () =>
            
                if(Form.ActiveForm == null)
                    value.Action();
                else if(!Form.ActiveForm.InvokeRequired)
                    value.Action();
                else
                    Form.ActiveForm.Invoke(value.Action);
            ;
        
    

    private class InvokeActionImpl<TArg1>
    
        public Action<TArg1> Action  get; 

        public InvokeActionImpl(Action<TArg1> a)
        
            Action = a;
        

        public static implicit operator Action<TArg1>(InvokeActionImpl<TArg1> value)
        
            return (arg) =>
            
                if(Form.ActiveForm == null)
                    value.Action(arg);
                else if(!Form.ActiveForm.InvokeRequired)
                    value.Action(arg);
                else
                    Form.ActiveForm.Invoke(value.Action, arg);
            ;
        
    

    public static Action Bulid(Action a) => new InvokeActionImpl(a);
    public static Action<TArg1> Bulid<TArg1>(Action<TArg1> a) => new InvokeActionImpl<TArg1>(a);

【讨论】:

以上是关于类型安全的 Control.Invoke C#的主要内容,如果未能解决你的问题,请参考以下文章

Control.Invoke() 与 Control.BeginInvoke() [重复]

带有 out 参数的 control.invoke

Control.Invoke() 与其委托的调用之间的延迟是多长时间?

Invoke调用中的匿名方法

System.Windows.Forms.Control.Invoke涓嶣eginInvoke

Invoke()/BeginInvoke()区别