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
?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With