Подтвердить что ты не робот

Доступ к элементам управления пользовательским интерфейсом в Task.Run с помощью async/wait на 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/wait в моем Task.Run, я могу установить свойство label1.Text. Однако при запуске этого кода я получаю сообщение об ошибке, что я не включен в поток пользовательского интерфейса, и я не могу получить доступ к элементу управления.

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

4b9b3361

Ответ 1

Когда вы используете 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";
}

Ответ 2

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

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.

Отвечает ли это на ваш вопрос?

Ответ 3

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

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

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

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

Ответ 4

Попробуйте следующее:

заменить

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 и назначение выполняется напрямую.

Ответ 6

Я дам вам мой последний ответ, который я дал для асинхронного понимания.

Решение такое, как вы знаете, когда вы вызываете метод async, вам нужно запустить его как задачу.

Вот быстрый код приложения консоли, который вы можете использовать для своей справки, это упростит вам понимание концепции.

using System;
using System.Threading;
using System.Threading.Tasks;

public class Program
{
    public static void Main()
    {
        Console.WriteLine("Starting Send Mail Async Task");
        Task task = new Task(SendMessage);
        task.Start();
        Console.WriteLine("Update Database");
        UpdateDatabase();

        while (true)
        {
            // dummy wait for background send mail.
            if (task.Status == TaskStatus.RanToCompletion)
            {
                break;
            }
        }

    }

    public static async void SendMessage()
    {
        // Calls to TaskOfTResult_MethodAsync
        Task<bool> returnedTaskTResult = MailSenderAsync();
        bool result = await returnedTaskTResult;

        if (result)
        {
            UpdateDatabase();
        }

        Console.WriteLine("Mail Sent!");
    }

    private static void UpdateDatabase()
    {
        for (var i = 1; i < 1000; i++) ;
        Console.WriteLine("Database Updated!");
    }

    private static async Task<bool> MailSenderAsync()
    {
        Console.WriteLine("Send Mail Start.");
        for (var i = 1; i < 1000000000; i++) ;
        return true;
    }
}

Здесь я пытаюсь инициировать задачу, называемую send mail. Временный Я хочу обновить базу данных, в то время как фон выполняет задачу отправки почты.

После обновления базы данных он ожидает завершения задачи отправки почты. Однако при таком подходе совершенно очевидно, что я могу запускать задачу на заднем плане и все равно продолжать оригинальный (основной) поток.