How to update UI from child tasks in WinForms

2020-02-10 15:12发布

问题:

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.

回答1:

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.";
}));


回答2:

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.