15

I've got a simple little winforms app that performs a long running process on another thread via a TPL Task. During this long running process I'd like to update the UI (the progress bar or something). Is there a way to do this without being required to .ContinueWith()?

public partial class Form1 : Form
{
    private Task _childTask;

    public Form1()
    {
        InitializeComponent();

        Task.Factory.StartNew(() =>
        {
            // Do some work
            Thread.Sleep(1000);

            // Update the UI
            _childTask.Start();

            // Do more work
            Thread.Sleep(1000);
        });

        _childTask = new Task((antecedent) =>
        {
            Thread.Sleep(2000);
            textBox1.Text = "From child task";
        }, TaskScheduler.FromCurrentSynchronizationContext());


    }
}

Executing this code I get the ubiquitous exception:

Cross-thread operation not valid: Control 'textBox1' accessed from a thread other than the thread it was created on.

devlife
  • 15,275
  • 27
  • 77
  • 131

2 Answers2

20

Yes, you can explicitly call BeginInvoke on the Window/Control that you want to communicate with. In your case this would look like this:

this.textBox.BeginInvoke(new Action(() =>
{
   this.textBox.Text = "From child task.";
}));
Drew Marsh
  • 33,111
  • 3
  • 82
  • 100
  • 1
    That definitely looks to be on the way but I get a compilation error: Cannot convert lambda expression to type 'System.Delegate' because it is not a delegate type – devlife Mar 12 '12 at 19:56
  • I created an Action and passed in the action instead of the anonymous method. Not sure why the above doesn't work but it got me 99% of the way there. Thanks Drew. – devlife Mar 12 '12 at 20:07
  • 1
    @devlife Oops, that's my fault for being so used to just working with newer APIs. Yes, you need to explicitly wrap with a new Action(...) because of the way the signature of the old WinForms BeginInvoke API was designed. I will update my answer. – Drew Marsh Mar 12 '12 at 21:00
  • That's exactly what I ended up doing. Thanks. – devlife Mar 13 '12 at 15:16
5

You're passing the TaskScheduler as state (or antecedent, as you called it). That doesn't make much sense.

I'm not sure what exactly do you want to do, but you need to specify the TaskScheduler when you're starting the Task, not when you're creating it. Also, it seems that childTask doesn't have to be a field:

var scheduler = TaskScheduler.FromCurrentSynchronizationContext();

Task childTask = null;

Task.Factory.StartNew(
    () =>
    {
        Thread.Sleep(1000);
        childTask.Start(scheduler);
        Thread.Sleep(1000);
    });

childTask = new Task(
    () =>
    {
        Thread.Sleep(2000);
        textBox1.Text = "From child task";
    });

Of course, all this is going to be much easier with C# 5 and await:

public Form1()
{
    InitializeComponent();

    StartAsync();
}

private async void StartAsync()
{
    // do some work
    await Task.Run(() => { Thread.Sleep(1000); });

    // start more work
    var moreWork = Task.Run(() => { Thread.Sleep(1000); });

    // update the UI, based on data from “some work”
    textBox1.Text = "From async method";

    // wait until “more work” finishes
    await moreWork;
}

You can't make async constructor, but you can run an async method from it. “Doing work” doesn't run on the UI thread, because it was started explicitly through Task.Run(). But since StartAsync() was called directly, it executes on the UI thread.

svick
  • 236,525
  • 50
  • 385
  • 514