找出从后台线程访问哪些winforms控件

时间:2018-01-05 12:41:09

标签: c# winforms devexpress

我们已经建立了一个巨大的winforms项目,已经进行了多年。

有时,我们的用户会收到类似this one的异常。

这个问题的解决方案似乎是:

  

不要从后台线程访问UI组件

但由于我们的项目是一个非常大的项目,有很多不同的主题,我们不能成功找到所有这些。

有没有办法检查(使用某些工具或调试选项)从后台线程调用哪些组件?

澄清:

我使用一个Form创建了一个示例winforms项目,其中包含两个Button

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
        button1.Text = "Clicked!";
    }

    private void button2_Click(object sender, EventArgs e)
    {

        Task.Run(() =>
        {
            button2.BackColor = Color.Red; //this does not throw an exception
            //button2.Text = "Clicked"; //this throws an exception when uncommented
        });
    }
}

单击按钮时,button2的背景颜色设置为红色。这发生在后台线程中(被认为是不良行为)。但是,它没有(立即)抛出异常。我想要一种方法来检测这种不良行为'。最好是通过扫描我的代码,但如果它只能通过调试来实现(一旦从后台线程访问UI组件就暂停),它也没问题。

5 个答案:

答案 0 :(得分:4)

我有2个建议可以一起使用,第一个是名为DebugSingleThread的Visual Studio插件。

您可以冻结所有线程并一次处理一个(显然是非主UI线程)并查看每个线程对控件的访问权限。单调乏味我知道但第二种方法并不是那么糟糕。

第二种方法是获取步骤以重现问题。如果您知道重现它的步骤,将会更容易看到导致它的原因。为此,我在Github上创建了这个User Action Log项目。

它会记录用户所做的每一个动作,你可以在SO:User Activity Logging, Telemetry (and Variables in Global Exception Handlers)上阅读。

我建议您也记录线程ID,然后当您能够重现问题时,请转到日志的末尾并确定准确的步骤。它不像看起来那么痛苦,也非常适合应用遥测。

您可以自定义此项目,例如捕获DataSource_Completed事件或添加一个虚拟DataSource属性,该属性设置真实的Grids DataSource属性并引发INotifyPropertyChanged事件 - 如果它是非主线程ID,那么Debugger.Break();

我的直觉是你在后台线程中更改控件的(例如网格)数据源(对于那种非冻结的感觉),这会导致同步问题。这是经历过这种情况的另一个DevExpress客户所发生的事情。它在与您引用的线程不同的线程中讨论了here

答案 1 :(得分:3)

您的应用是否设置为故意忽略交叉线程?

跨线程操作应该一直在winforms中爆炸。几乎每种方法都会疯狂地检查它们。作为起点,请查看https://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/Control.cs

在你的应用中的某个地方,有人可能已经放了这行代码:

Control.CheckForIllegalCrossThreadCalls = False;

注释并运行应用程序,然后按照例外情况进行操作。

(通常你可以通过在调用中包装更新来解决问题,例如,如果你看到textbox1.text=SomeString;将其更改为`textbox.invoke(()=> {textbox1.text = SomeString;});。

您可能还需要添加对InvokeRequired的检查,使用BeginInvoke来避免死锁,并从invoke返回值,这些都是单独的主题。

这是假设即使是一个温和的重构也是不可能的,即使是中型企业应用也几乎总是如此。

注意:通过静态分析(即不运行应用程序)无法保证成功发现此案例。除非你能解决停止问题... https://cs.stackexchange.com/questions/63403/is-the-halting-problem-decidable-for-pure-programs-on-an-ideal-computer等......

答案 2 :(得分:2)

我这样做是为了搜索特定情况,但当然需要根据您的需要进行调整,但这样做的目的是为您提供至少一种可能性。

我调用了这个方法SearchForThreads但是因为它只是一个例子,你可以随意调用它。

这里的主要思想是将这个Method调用添加到基类并在构造函数上调用它,使其更灵活。

然后使用反射在从此基础派生的所有类上调用此方法,并在任何类中发现此情况时抛出异常或其他内容。

有一个预先要求,即Framework 4.5的使用。 此版本的框架添加了CompilerServices属性,该属性为我们提供了有关Method的调用者的详细信息。

此文档为here

有了它,我们可以打开源文件并深入研究。

我所做的只是使用基本文本搜索来搜索您在问题中指定的情况。

但它可以让您深入了解如何在您的解决方案中执行此操作,因为我对您的解决方案知之甚少,我只能使用您在帖子上放置的代码。

public static void SearchForThreads(
        [System.Runtime.CompilerServices.CallerMemberName] string memberName = "",
        [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "",
        [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
        {
            var startKey = "this.Controls.Add(";
            var endKey = ")";

            List<string> components = new List<string>();

            var designerPath = sourceFilePath.Replace(".cs", ".Designer.cs");
            if (File.Exists(designerPath))
            {
                var designerText = File.ReadAllText(designerPath);
                var initSearchPos = designerText.IndexOf(startKey) + startKey.Length;

                do
                {
                    var endSearchPos = designerText.IndexOf(endKey, initSearchPos);
                    var componentName = designerText.Substring(initSearchPos, (endSearchPos - initSearchPos));
                    componentName = componentName.Replace("this.", "");
                    if (!components.Contains(componentName))
                        components.Add(componentName);

                } while ((initSearchPos = designerText.IndexOf(startKey, initSearchPos) + startKey.Length) > startKey.Length);
            }

            if (components.Any())
            {
                var classText = File.ReadAllText(sourceFilePath);
                var ThreadPos = classText.IndexOf("Task.Run");
                if (ThreadPos > -1)
                {
                    do
                    {
                        var endThreadPos = classText.IndexOf("}", ThreadPos);

                        if (endThreadPos > -1)
                        {
                            foreach (var component in components)
                            {
                                var search = classText.IndexOf(component, ThreadPos);
                                if (search > -1 && search < endThreadPos)
                                {
                                    Console.WriteLine($"Found a call to UI thread component at pos: {search}");
                                }
                            }
                        }
                    }
                    while ((ThreadPos = classText.IndexOf("Task.Run", ++ThreadPos)) < classText.Length && ThreadPos > 0);
                }
            }
        }

我希望它可以帮助你。

如果您拆分文本以便输出,您可以获取行号,但我不想解决问题,因为我不知道什么对您有用。

string[] lines = classText.Replace("\r","").Split('\n');

答案 3 :(得分:1)

试试:

public static void Main(string[] args)
{
    // Add the event handler for handling UI thread exceptions to the event.
    Application.ThreadException += new ThreadExceptionEventHandler(exception handler);

    // Set the unhandled exception mode to force all Windows Forms errors to go through the handler.
    Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);

    // Add the event handler for handling non-UI thread exceptions to the event. 
    AppDomain.CurrentDomain.UnhandledException += // add the handler here

    // Runs the application.
    Application.Run(new ......);
}

然后您可以记录消息和调用堆栈,这应该为您提供足够的信息来解决问题。

答案 4 :(得分:1)

我建议您更新GUI以自动处理这种情况,以方便您使用。您改为使用一组继承的控件。

这里的一般原则是覆盖属性Set方法,使其成为线程安全。因此,在每个重写的属性中,不是直接更新基本控件,而是检查是否需要调用(意味着我们在GUI的单独线程上)。然后,Invoke调用更新GUI线程上的属性,而不是辅助线程。

因此,如果使用了继承的控件,那么尝试从辅助线程更新GUI元素的表单代码可以保留原样。

这是文本框和按钮。您可以根据需要添加更多,并根据需要添加其他属性。而不是将代码放在单独的表单上。

您不需要进入设计器,您只能在设计器文件上进行查找/替换。例如,在所有designer.cs文件中,您将使用ThreadSafeControls.TextBoxBackgroundThread和System.Windows.Forms.Button将Thread.Windows.Forms.TextBox替换为ThreadSafeControls.ButtonBackgroundThread。

可以使用相同的原理创建其他控件,具体取决于哪种控件类型和控制类型。属性正在从后台线程更新。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace ThreadSafeControls
{
    class TextBoxBackgroundThread : System.Windows.Forms.TextBox
    {
        public override string Text
        {
            get
            {
                return base.Text;
            }

            set
            {
                if (this.InvokeRequired)
                    this.Invoke((MethodInvoker)delegate { base.Text = value; });
                else
                    base.Text = value;
            }
        }

        public override System.Drawing.Color ForeColor
        {
            get
            {
                return base.ForeColor;
            }

            set
            {
                if (this.InvokeRequired)
                    this.Invoke((MethodInvoker)delegate { base.ForeColor = value; });
                else
                    base.ForeColor = value;
            }
        }


        public override System.Drawing.Color BackColor
        {
            get
            {
                return base.BackColor;
            }

            set
            {
                if (this.InvokeRequired)
                    this.Invoke((MethodInvoker)delegate { base.BackColor = value; });
                else
                    base.BackColor = value;
            }
        }
    }

    class ButtonBackgroundThread : System.Windows.Forms.Button
    {
        public override string Text
        {
            get
            {
                return base.Text;
            }

            set
            {
                if (this.InvokeRequired)
                    this.Invoke((MethodInvoker)delegate { base.Text = value; });
                else
                    base.Text = value;
            }
        }

        public override System.Drawing.Color ForeColor
        {
            get
            {
                return base.ForeColor;
            }

            set
            {
                if (this.InvokeRequired)
                    this.Invoke((MethodInvoker)delegate { base.ForeColor = value; });
                else
                    base.ForeColor = value;
            }
        }


        public override System.Drawing.Color BackColor
        {
            get
            {
                return base.BackColor;
            }

            set
            {
                if (this.InvokeRequired)
                    this.Invoke((MethodInvoker)delegate { base.BackColor = value; });
                else
                    base.BackColor = value;
            }
        }
    }
}