Background Task sometimes able to update UI?
Asked Answered
G

1

6

I just answered a question about whether a Task can update the UI. As I played with my code, I realized I'm not clear myself on a few things.

If I have a windows form with one control txtHello on it, I'm able to update the UI from a Task, it seems, if I immediately do it on Task.Run:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        Task.Run(() =>
        {
            txtHello.Text = "Hello";
        });
    }
}

However if I Thread.Sleep for even 5 milliseconds, the expected CrossThread error is thrown:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        Task.Run(() =>
        {
            Thread.Sleep(5);
            txtHello.Text = "Hello"; //kaboom
        });
    }
}

I'm not sure why that happens. Is there some sort of optimization for an extremely short running Task?

Grazier answered 7/8, 2015 at 21:11 Comment(1)
This will come as a surprise to me as well if its true!Opprobrious
G
6

You didn't post the exception stack trace, but I expect that it looked something like this:

System.InvalidOperationException: Cross-thread operation not valid: Control 'textBox1' accessed from a thread other than the thread it was created on.
   at System.Windows.Forms.Control.get_Handle()
   at System.Windows.Forms.Control.set_WindowText(String value)
   at System.Windows.Forms.TextBoxBase.set_WindowText(String value)
   at System.Windows.Forms.Control.set_Text(String value)
   at System.Windows.Forms.TextBoxBase.set_Text(String value)
   at System.Windows.Forms.TextBox.set_Text(String value)
   at WindowsFormsApplicationcSharp2015.Form1.<.ctor>b__0_0() in D:\test\WindowsFormsApplicationcSharp2015\Form1.cs:line 27

We can see that the exception is thrown from the Control.Handle getter property. And in fact, if we look at the source code for that property, there it is, as expected:

public IntPtr Handle {
    get {
        if (checkForIllegalCrossThreadCalls &&
            !inCrossThreadSafeCall &&
            InvokeRequired) {
            throw new InvalidOperationException(SR.GetString(SR.IllegalCrossThreadCall,
                                                             Name));
        }

        if (!IsHandleCreated)
        {
            CreateHandle();
        }

        return HandleInternal;
    }
}

The interesting part is when we look at the code that calls Control.Handle. In this case, that's the Control.WindowText setter property:

set {
    if (value == null) value = "";
    if (!WindowText.Equals(value)) {
        if (IsHandleCreated) {
            UnsafeNativeMethods.SetWindowText(new HandleRef(window, Handle), value);
        }
        else {
            if (value.Length == 0) {
                text = null;
            }
            else {
                text = value;
            }
        }
    }
}

Notice that the Handle property is only invoked if IsHandleCreated is true.

And for completeness, if we look at the code for IsHandleCreated we see the following:

public bool IsHandleCreated {
    get { return window.Handle != IntPtr.Zero; }
}

So, the reason you don't get the exception, is because by the time the Task executes, the window handle hasn't been created yet, which is to be expected since the Task starts in the form's constructor, that is, before the form is even displayed.

Before the window handle is created, modifying a property doesn't yet require any work from the UI thread. So during this small time window at the start of your program, it would seem that it is possible to invoke the methods on control instances from a non-UI thread without getting the "cross thread" exception. But clearly, the existence of this special small time window doesn't change the fact that we should always make sure to invoke control methods from the UI thread to be safe.

To prove the point that the timing of the window handle creation is the determining factor in getting (or not) the "cross thread" exception, try modifying your example to force the creation of the window handle before you start the task, and notice how you will now consistently get the expected exception, even without a sleep:

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

        // Force creation of window handle
        var dummy = txtHello.Handle;

        Task.Run(() =>
        {
            txtHello.Text = "Hello"; // kaboom
        });
    }
}

Relevant documentation: Control.Handle

If the handle has not yet been created, referencing this property will force the handle to be created.

Gambit answered 7/8, 2015 at 21:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.