web-dev-qa-db-fra.com

Accès aux contrôles de l'utilisateur utilisateur dans Task.Run avec async/wait sur WinForms

J'ai le code suivant dans une application WinForms avec un bouton et une étiquette:

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

Ceci est une version simplifiée de l'application réelle sur laquelle je travaille. J'avais l'impression qu'en utilisant async/wait dans mon Task.Run, je pouvais définir la propriété label1.Text. Cependant, lors de l'exécution de ce code, le message d'erreur suivant s'affiche: Je ne suis pas sur le thread d'interface utilisateur et je ne peux pas accéder au contrôle.

Pourquoi ne puis-je pas accéder au contrôle des étiquettes?

19
Wouter de Kort

Lorsque vous utilisez Task.Run(), vous déclarez que vous ne souhaitez pas que le code s'exécute sur le contexte actuel, c'est exactement ce qui se produit.

Mais il n’est pas nécessaire d’utiliser Task.Run() dans votre code. Les méthodes async correctement écrites ne bloquent pas le thread actuel, vous pouvez donc les utiliser directement à partir du thread d'interface utilisateur. Si vous faites cela, await s'assurera que la méthode reprend sur le thread d'interface utilisateur.

Cela signifie que si vous écrivez votre code comme ceci, cela fonctionnera:

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

Essaye ça

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

Ou 

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

Ou

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/wait ne garantit pas qu'il s'exécutera dans le fil de l'interface utilisateur. await capturera la SynchronizationContext actuelle et poursuivra l'exécution avec le contexte capturé une fois la tâche terminée. 

Donc, dans votre cas, vous avez une await imbriquée qui est dans Task.Run et donc la seconde await va capturer le contexte qui ne va pas être UiSynchronizationContext car il est exécuté par WorkerThread depuis ThreadPool.

Est-ce que cela répond à votre question?

14
Sriram Sakthivel

Essaye ça:

remplacer

label1.Text = "test";

avec

SetLabel1Text("test");

et ajoutez ce qui suit à votre classe:

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

InvokeRequired renvoie true si vous n'êtes PAS sur le thread d'interface utilisateur. La méthode Invoke () prend le délégué et les paramètres, passe au thread d'interface utilisateur, puis appelle la méthode de manière récursive. Vous revenez après l'appel Invoke () car la méthode a déjà été appelée récursivement avant le retour de Invoke (). Si vous vous trouvez sur le thread d'interface utilisateur lorsque la méthode est appelée, InvokeRequired est false et l'affectation est effectuée directement.

8
Metro

Pourquoi utilisez-vous Task.Run? qui commencent un nouveau thread de travail (lié au cpu), et cela cause votre problème.

vous devriez probablement juste faire ça:

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

attendez que vous continuiez dans le même contexte sauf si vous utilisez .ConfigureAwait (false);

6
Persi

Parce que c'est sur un autre thread et que les appels inter-thread ne sont pas autorisés.

Vous devrez transmettre le "contexte" au fil que vous démarrez. Voir un exemple ici: http://reedcopsey.com/2009/11/17/synchronizing-net-4-tasks-with-the-ui-thread/

0
Gerrie Schenck