Как обновить пользовательский интерфейс из дочерних задач в WinForms

У меня есть небольшое приложение winforms, которое выполняет длительный процесс в другом потоке с помощью задачи TPL. Во время этого продолжительного процесса я хотел бы обновить пользовательский интерфейс (индикатор выполнения или что-то еще). Есть ли способ сделать это без необходимости .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());


    }
}

Выполняя этот код, я получаю вездесущее исключение:

Неверная операция поперечного потока: элемент управления 'textBox1' доступен из потока, отличного от потока, на котором он был создан.

Ответ 1

Да, вы можете явно вызвать BeginInvoke в окне/Элементе управления, с которым вы хотите установить связь. В вашем случае это будет выглядеть так:

this.textBox.BeginInvoke(new Action(() =>
{
   this.textBox.Text = "From child task.";
}));

Ответ 2

Вы передаете TaskScheduler как state (или antecedent, как вы его назвали). Это не имеет большого смысла.

Я не уверен, что именно вы хотите сделать, но вам нужно указать TaskScheduler, когда вы начинаете Task, а не когда вы его создаете. Кроме того, кажется, что childTask не обязательно должно быть полем:

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

Конечно, все это будет намного проще с С# 5 и 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;
}

Вы не можете создать конструктор async, но вы можете запустить из него метод async. "Выполнение работы" не запускается в потоке пользовательского интерфейса, потому что он был запущен явно через Task.Run(). Но так как StartAsync() вызывался напрямую, он выполнялся в потоке пользовательского интерфейса.