it-roy-ru.com

Доступ к элементам управления пользовательского интерфейса в Task.Run с помощью async/await на WinForms

У меня есть следующий код в приложении WinForms с одной кнопкой и одной меткой:

using System;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private async void button1_Click(object sender, EventArgs e)
        {
            await Run();
        }

        private async Task Run()
        {
            await Task.Run(async () => {
                await File.AppendText("temp.dat").WriteAsync("a");
                label1.Text = "test";
            });    
        }
    }
}

Это упрощенная версия реального приложения, над которым я работаю. У меня сложилось впечатление, что с помощью async/await в моем Task.Run я могу установить свойство label1.Text. Тем не менее, при выполнении этого кода я получаю сообщение об ошибке, что я не в потоке пользовательского интерфейса, и я не могу получить доступ к элементу управления.

Почему я не могу получить доступ к элементу управления меткой?

19
Wouter de Kort

Когда вы используете Task.Run(), вы говорите, что вы не хотите, чтобы код выполнялся в текущем контексте, и это именно то, что происходит.

Но нет необходимости использовать Task.Run() в вашем коде. Правильно написанные методы async не блокируют текущий поток, поэтому вы можете использовать их непосредственно из потока пользовательского интерфейса. Если вы сделаете это, await обеспечит возобновление метода обратно в потоке пользовательского интерфейса.

Это означает, что если вы напишите свой код так, он будет работать:

private async void button1_Click(object sender, EventArgs e)
{
    await Run();
}

private async Task Run()
{
    await File.AppendText("temp.dat").WriteAsync("a");
    label1.Text = "test";
}
24
svick

Попробуй это

private async Task Run()
{
    await Task.Run(async () => {
       await File.AppendText("temp.dat").WriteAsync("a");
       });
    label1.Text = "test";
}

Или же 

private async Task Run()
{
    await File.AppendText("temp.dat").WriteAsync("a");        
    label1.Text = "test";
}

Или же

private async Task Run()
{
    var task = Task.Run(async () => {
       await File.AppendText("temp.dat").WriteAsync("a");
       });
    var continuation = task.ContinueWith(antecedent=> label1.Text = "test",TaskScheduler.FromCurrentSynchronizationContext());
    await task;//I think await here is redundant        
}

async/await не гарантирует, что он будет работать в потоке пользовательского интерфейса. await захватит текущую SynchronizationContext и продолжит выполнение с захваченным контекстом после завершения задачи. 

Таким образом, в вашем случае у вас есть вложенный await, который находится внутри Task.Run, поэтому вторая await будет захватывать контекст, который не будет UiSynchronizationContext, потому что он выполняется WorkerThread из ThreadPool.

Это отвечает на ваш вопрос?

14
Sriram Sakthivel

Попробуй это:

замещать

label1.Text = "test";

с

SetLabel1Text("test");

и добавьте в свой класс следующее:

private void SetLabel1Text(string text)
{
  if (InvokeRequired)
  {
    Invoke((Action<string>)SetLabel1Text, text);
    return;
  }
  label1.Text = text;
}

InvokeRequired возвращает true, если вы НЕ находитесь в потоке пользовательского интерфейса. Метод Invoke () принимает делегат и параметры, переключается на поток пользовательского интерфейса и затем рекурсивно вызывает метод. Вы возвращаетесь после вызова Invoke (), потому что метод уже вызывался рекурсивно до возврата Invoke (). Если вы оказались в потоке пользовательского интерфейса при вызове метода, InvokeRequired имеет значение false, и назначение выполняется напрямую.

8
Metro

Почему вы используете Task.Run? которые запускают новый рабочий поток (связанный с процессором), и это вызывает вашу проблему.

вы, вероятно, должны просто сделать это:

    private async Task Run()
    {
        await File.AppendText("temp.dat").WriteAsync("a");
        label1.Text = "test";    
    }

ожидайте, что вы продолжите в том же контексте, кроме случаев, когда вы используете .ConfigureAwait (false);

6
Persi

Потому что это в другом потоке, и вызовы между потоками не допускаются.

Вам нужно будет передать «контекст» в поток, который вы запускаете. Смотрите пример здесь: http://reedcopsey.com/2009/11/17/synchronizing-net-4-tasks-with-the-ui-thread/

0
Gerrie Schenck