The illegal cross-thread call

Occurrence

This error is indicated by the following exception message: "Cross-thread operation not valid: Control 'AnyControl' accessed from a thread other than the thread it was created on."

It mostly occurs in early progress of legacy code migration from .Net 1.1 to 2.0 or higher, but those not familiar with the pervert conjunction between multi-threading and ActiveX objects can also easily encounter this error message.

The Problem

ActiveX objects only work in STA containers, and may only be used by the thread, that handles the containers message queue, because they dont have any thread sync methods. You can have multiple threads in an STA application context, but beauty of synchronizing them lies in your hands.

Solutions

Yes, as most problems, this also has multiple solutions. Let's start with the worst, and progress towards something I call satisfying.

The Brute Force of the Coder

Usually the legacy migrator sits by the code, and thinks: "This is not my code after all, let's find the fastest solution, and get rid of the problem..." Hell yeah, for them, Microsoft left a switch to do that, the following line of code at the entry point of the application will get rid of these messages:

Control.CheckForIllegalCrossThreadCalls = False

Ouch. A rookie knows, that a long property or class name cant mean any good in the .Net framework (like DataGridViewAutoSizeColumnsModeEventArgs) But this one works. Though it only tells the compiler to simply skip the check, but who cares? The code runs smooth, thats all...

We have to mention here, that though the code runs fine, it can cause nasty user interface glitches. By a text box and a single working background thread its no problem, but by two threads and a list... Well see it for yourself.

The answer of the "IT Pro"

Who does not care much of the given code, and looks the technical side: "Threading? Winforms? Why don't you use a Background Worker? Or Application.DoEvents()?" Well, thats a good answer, but in most cases we don't want to tinker with the logic and rewrite it. Using a background worker is the least elegant way of multi threading after all, and Application.DoEvents() is like GoTo... Every time you use it, god kills a puppy...

The "satisfying" solution

Invoking. Invoking is a WinForms implemented way to pass a method delegate cross-thread. If you are interrested in the backend, I'd say get yourself the Lutz Roeder's .Net Reflector (a useful tool for any coder who works with MSIL compiled languages), and take a look at the framework libraries. Lets take a simple example:

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        Dim workerThread As New Threading.Thread(AddressOf illegalCrossThreadCall)
        workerThread.IsBackground = True
        workerThread.Start()
    End Sub

    Private Sub illegalCrossThreadCall()
        Dim tempStorage As Integer
        tempStorage = 2 ^ 16 'This is the placeholder of a very time-consuming procedure
        TextBox1.Text = tempStorage.ToString()
    End Sub

Now this code will throw a  nasty exception, when it tries to set the text property of the control. We will make a large step now, and instantly overwrite this with a refactored version, something like this:

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
    Dim workerThread As New Threading.Thread(AddressOf LegalCrossThreadCall)
    workerThread.IsBackground = True
    workerThread.Start()
End Sub

Private Sub LegalCrossThreadCall()
    Dim tempStorage As Integer
    tempStorage = 2 ^ 16 'This is still the placeholder of a very time-consuming procedure
    SetControlText(TextBox1, tempStorage.ToString())
End Sub

''' <summary>
''' This is a delegate we'll use to pass the method call between threads
''' </summary>
''' <param name="control">The control we want to set the text property on</param>
''' <param name="text">The text supplied for the controls text property</param>
''' <remarks>The method parameters should be the same as the base method we pass</remarks>
Private Delegate Sub SetControlTextDelegate(ByVal control As Control, ByVal text As String)

''' <summary>
''' This is a general, thread safe function to set the text property
''' of any given control
''' </summary>
''' <param name="control">The control we want to set the text property on</param>
''' <param name="textToSet">The text supplied for the controls text property</param>
''' <remarks></remarks>
Private Sub SetControlText(ByVal control As Control, ByVal textToSet As String)
    'The InvokeRequired property shows if the calling thread is the same as the
    'one responsible for the message queue of the control
    If control.InvokeRequired Then
        'If not, we create a delegate from this method, 
        'and invoke it on the controls thread
        Dim d As New SetControlTextDelegate(AddressOf SetControlText)
        control.Invoke(d, New Object() {control, textToSet})
    Else
        'If we're on the right thread, set the text
        control.Text = textToSet
    End If
End Sub

We implemented a new method to set a controls property, now we can call from any thread or function, it will always act the same safe way to set the text. This can be a satisfying solution, a balance of modifying the given legacy code only that much, as it's needed to get safe results.

Comments

Great job! This is by far

Great job! This is by far the most clear tutorial on the web I found in years. And, worked like a charm.

Well written and very

Well written and very helpful!
thanks!

Cheers

Happy that you found use of it.

nice language and best

nice language and best explanation! god kills a puppy, lol