Monday, January 15, 2018

Find out which winforms controls are accessed from a background thread

Leave a Comment

We have built a huge winforms project, already in progress for multiple years.

Sometimes, our users get an exception which looks like this one.

The resolution of this problem seems to be:

don't acces UI components from a background thread

.

But since our project is a very big project with a lot of different threads, we don't succeed in finding all these.

Is there a way to check (with some tool or debugging option) which components are called from a background thread?

To clarify:

I created a sample winforms project with a single Form, containing two 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         });     } } 

The background color of button2 is set to red when the button is clicked. This happens in a background thread (which is considered bad behavior). However, it doesn't (immediately) throw an exception. I would like a way to detect this as 'bad behavior'. Preferably by scanning my code, but if it's only possible by debugging, (so pausing as soon as a UI component is accessed from a background thread) it's also fine.

5 Answers

Answers 1

I've got 2 recommendations to use together, the first is a Visual Studio Plugin called DebugSingleThread.

You can freeze all the threads and work on one at a time (obviously the non-main-UI threads) and see each threads access to controls. Tedious I know but not so bad with the second method.


The second method is to get the steps in order to reproduce the problem. If you know the steps to reproduce it, it will be easier to see whats causing it. To do this I made this User Action Log project on Github.

It will record every action a user makes, you can read about it here on SO: User Activity Logging, Telemetry (and Variables in Global Exception Handlers).

I'd recommend you also log the Thread ID, then when you have been able to reproduce the problem, go to the end of the log and work out the exact steps. Its not as painful as it seems and its great for getting application telemetry.

You might be able to customise this project, eg trap a DataSource_Completed event or add a dummy DataSource property that sets the real Grids DataSource property and raises an INotifyPropertyChanged event - and if its a non-main thread ID then Debugger.Break();.


My gut feeling is you're changing a control's (eg a grid) data source in a background thread (for that non-freeze feel) and thats causing a problem with synchronisation. This is what happened to the other DevExpress customer who experienced this. Its discussed here in a different thread to the one you referenced.

Answers 2

Is your app set to ignore cross threading intentionally?

Cross-thread operations should be blowing up all the time in winforms. It checks for them like crazy in just about every method. for a starting point check out https://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/Control.cs.

Somewhere in your app, somebody might have put this line of code:

Control.CheckForIllegalCrossThreadCalls = False; 

Comment that out and run the app, then follow the exceptions.

(Usually you can fix the problem by wrapping the update in an invoke, e.g., in a worker thread if you see textbox1.text=SomeString; change it to `textbox.invoke(()=>{textbox1.text=SomeString;});.

You may also have to add checking for InvokeRequired, use BeginInvoke to avoid deadlocks, and return values from invoke, those are all separate topics.

this is assuming even a moderate refactor is out of the question which for even a medium sized enterprise app is almost always the case.

Note: it's not possible to guarantee successful discovery of this case thru static analysis (that is, without running the app). unless you can solve the halting problem ... https://cs.stackexchange.com/questions/63403/is-the-halting-problem-decidable-for-pure-programs-on-an-ideal-computer etc...

Answers 3

I did this to search for that specific situation but of course, need to adjust it to your needs, but the purpose of this is to give you at least a possibility.

I called this method SearchForThreads but since it's just an example, you can call it whatever you want.

The main idea here is perhaps adding this Method call to a base class and call it on the constructor, makes it somewhat more flexible.

Then use reflection to invoke this method on all classes deriving from this base, and throw an exception or something if it finds this situation in any class.

There's one pre req, that is the usage of Framework 4.5. This version of the framework added the CompilerServices attribute that gives us details about the Method's caller.

The documentation for this is here

With it we can open up the source file and dig into it.

What i did was just search for the situation you specified in your question, using rudimentary text search.

But it can give you an insight about how to do this on your solution, since i know very little about your solution, i can only work with the code you put on your post.

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);                 }             }         } 

I hope it helps you out.

You can get the Line number if you split the text so you can output it, but i didn't want to go through the trouble, since i don't know what would work for you.

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

Answers 4

Try that:

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

Then you can log the message and the call stack and that should give you enough information to fix the issue.

Answers 5

I recommend you update your GUI to handle this situation automatically for your convenience. You instead use a set of inherited controls. You don't need to go into the designer, you can instead do a find/replace on the designer files only.

Here is the textbox and button ones. You would add more of them as needed and add other properties as needed. Rather than putting code on individual forms.

So, in this case, the TextBox class instead you would use TextBoxBackgroundThread and the Button class instead you would use 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;             }         }     } } 
If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment