[EDIT] Rephrased and Simplified whole post [/EDIT]
In this blog, the following (I simplified it a bit) is given as an example of using a SynchronizationContext object to run a Task on the UI thread:
Task.Factory.StartNew(() =>"Hello World").ContinueWith(
task => textBox1.Text = task.Result,
TaskScheduler.FromCurrentSynchronizationContext());
I can repeat these results in a fresh project, updating the UI safely, but for whatever reason in my current project (even though it's been working) I can't. I get the standard "You're not allowed to update the UI from the wrong thread" exception.
My code (in MainForm_Load(...)) is like this, which works in a fresh Project w/ a textBox1 added to the main form, but does not work in my current project:
var one = Task.Factory.StartNew(
() => "Hello, my name is Inigo Montoya");
var two = one.ContinueWith(
task => textBox1.Text = one.Result,
TaskScheduler.FromCurrentSynchronizationContext());
Anyone have any thoughts on what might be gong on.
[EDIT]
I've traced the error back to the instantiation of an object which uses a form to prompt the user for login information. The error only happens when the form has been shown. (If I return a hardcoded value before that Form's Show
happens the whole thing works fine).
New question: How can I get the SynchronizationContext for the form which I'm constructing if its own constructor displays another form before it has been shown? Here's how you can reproduce what's happening:
1) Create two forms: Form1 with a TextBox
, and Form2 with a Button
2) Create a class OwnedBy1Uses2
Form1
:
public partial class Form1 : Form
{
OwnedBy1Uses2 member;
public Form1()
{
InitializeComponent();
member = new OwnedBy1Uses2();
}
private void Form1_Load(object sender, EventArgs e)
{
var ui = TaskScheduler.FromCurrentSynchronizationContext();
Task<string> getData = Task.Factory.StartNew(
() => "My name is Inigo Montoya...");
Task displayData = getData.ContinueWith(
t => textBox1.Text = t.Result, ui);
}
}
Form2
:
public partial class Form2 : Form
{
public Form2()
{
InitializeComponent();
DialogResult = System.Windows.Forms.DialogResult.Cancel;
}
private void button1_Click(object sender, EventArgs e)
{
DialogResult = System.Windows.Forms.DialogResult.OK;
Hide();
}
}
OwnedBy1Uses2
:
class OwnedBy1Uses2
{
int x;
public OwnedBy1Uses2()
{
using (Form2 form = new Form2())
{
if (form.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
x = 1;
}
else
{
x = 2;
}
}
}
}
Just being on the main thread isn't sufficient. You need to have a valid
SynchronizationContext.Current
(set a breakpoint on theFromCurrentSynchronizationContext
line and examine the value ofSynchronizationContext.Current
; if it'snull
, then something's wrong).The cleanest fix is to execute your task code including
FromCurrentSynchronizationContext
from within the UI message loop - that is, from something likeForm.Load
for WinForms orWindow.Loaded
for WPF.Edit:
There was a bug in WinForms where putting it in
Form.Load
wasn't sufficient either - you actually had to force Win32 handle creation by reading theHandle
property. I was under the impression that this bug had been fixed, but I could be wrong.Edit 2 (copied from comment):
I suspect your problem is that you're calling
ShowDialog
outside ofApplication.Run
.ShowDialog
is a nested message loop, but in this case there's no parent message loop. If you set a watch onSynchronizationContext.Current
and step through theShowDialog
, you'll see that it's aWindowsFormsSynchronizationContext
before the dialog is shown but changes to a non-WinFormsSynchronizationContext
after the dialog is shown. Moving the member creation (including theShowDialog
) to theLoad
event fixes the problem.