4

[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;
            }
        }
    }
}
Chris Pfohl
  • 18,220
  • 9
  • 68
  • 111
  • I thought the whole point of a task was to run it asynch and per definition that should be in a different thread. You cannot access a control from another thread as you know. – Tony The Lion Dec 22 '10 at 19:50
  • More details: I'm loading objects from a database Asynch (very long running process). When that is done I have "displayItems" defined as a very fast running process that updates the UI. The *other* point of a Task is to govern the timing/ordering of Asynchronous processes/threads. That's what the displayItems task is doing here: "When previousTask is done display the loaded items" – Chris Pfohl Dec 22 '10 at 19:52
  • If you're using the System.Threading.Task namespace, your task uses a CLR thread pool thread, so this is no longer your UI thread. Check this: http://www.eggheadcafe.com/tutorials/aspnet/21013a52-fe11-4af8-bf8b-50cfd1a51577/task-parallelism-in-c-4.aspx – Tony The Lion Dec 22 '10 at 19:52
  • Except this code worked two hours ago. Perfectly. – Chris Pfohl Dec 22 '10 at 19:54
  • I don't see much wrong with your code, after reading those articles you linked to. Do you have any other similar code? If yes, could you post it? – Tony The Lion Dec 22 '10 at 21:03
  • @Cpfohl: This code does not look wrong, so it must be something else in your project that is causing your problem, if you can, please post more of you project's code in regards to that problem? – Tony The Lion Dec 22 '10 at 21:15

1 Answers1

5

Just being on the main thread isn't sufficient. You need to have a valid SynchronizationContext.Current (set a breakpoint on the FromCurrentSynchronizationContext line and examine the value of SynchronizationContext.Current; if it's null, 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 like Form.Load for WinForms or Window.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 the Handle 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 of Application.Run. ShowDialog is a nested message loop, but in this case there's no parent message loop. If you set a watch on SynchronizationContext.Current and step through the ShowDialog, you'll see that it's a WindowsFormsSynchronizationContext before the dialog is shown but changes to a non-WinForms SynchronizationContext after the dialog is shown. Moving the member creation (including the ShowDialog) to the Load event fixes the problem.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • My code *is* currently being executed from a UI message loop...It's in an automatically generated Handler for a TreeView SelectedChanged event. Furthermore, when I break at I get a Value, it's just not `null`. It's straight from Form.Load too. – Chris Pfohl Dec 22 '10 at 21:27
  • If you're seeing an instance of `WindowsFormsSynchronizationContext`, then please post your exact code. – Stephen Cleary Dec 22 '10 at 21:30
  • My exact code is several thousand lines long...I'll work on getting a copy that can be run separately in a second. I've narrowed the error down to the creation of an object that creates another form right before the main one gets created. Would that botch "SynchronizationContext.Current" some how? (It's a login form, and needs to be displayed before this one) – Chris Pfohl Dec 22 '10 at 21:38
  • Shoot, it'd definitely that! I had hardcoded my login-data for a while, but I recently turned it back on manually...now it doesn't work... – Chris Pfohl Dec 22 '10 at 21:41
  • There we go, I've updated with the problem perfectly demonstrated. – Chris Pfohl Dec 23 '10 at 14:10
  • I suspect your problem is that you're calling `ShowDialog` outside of `Application.Run`. `ShowDialog` is a *nested* message loop, but in this case there's no *parent* message loop. If you set a watch on `SynchronizationContext.Current` and step through the `ShowDialog`, you'll see that it's a `WindowsFormsSynchronizationContext` before the dialog is shown but changes to a non-WinForms `SynchronizationContext` after the dialog is shown. Moving the member creation (including the `ShowDialog`) to the `Load` event fixes the problem. – Stephen Cleary Dec 23 '10 at 16:49
  • Thanks so much! As soon as the storm lets up and a co-worker gets in to turn my computer on I'll remote desktop in and give it a try! Thanks! – Chris Pfohl Dec 27 '10 at 13:49
  • Thanks so much, that was a confusing mess for me. Appreciate the help. Do you want to add the above comment to the answer so I can accept it? – Chris Pfohl Dec 28 '10 at 18:10