web-dev-qa-db-fra.com

Comment exécuter et interagir avec une tâche asynchrone à partir d'une interface graphique WPF

J'ai une interface graphique WPF, où je veux appuyer sur un bouton pour démarrer une tâche longue sans geler la fenêtre pour la durée de la tâche. Pendant que la tâche est en cours, je souhaite obtenir des rapports sur les progrès et incorporer un autre bouton qui arrêtera la tâche à tout moment de mon choix.

Je ne peux pas comprendre comment utiliser async/wait/task. Je ne peux pas inclure tout ce que j'ai essayé, mais c'est ce que j'ai pour le moment.

Une classe de fenêtre WPF:

public partial class MainWindow : Window
{
    readonly otherClass _burnBabyBurn = new OtherClass();
    internal bool StopWorking = false;

    //A button method to start the long running method
    private async void Button_Click_3(object sender, RoutedEventArgs e)
    {   
        Task burnTheBaby = _burnBabyBurn.ExecuteLongProcedureAsync(this, intParam1, intParam2, intParam3);

        await burnTheBaby;
    }

    //A button Method to interrupt and stop the long running method
    private void StopButton_Click(object sender, RoutedEventArgs e)
    {
        StopWorking = true;
    }

    //A method to allow the worker method to call back and update the gui
    internal void UpdateWindow(string message)
    {
        TextBox1.Text = message;
    }
}

Et une classe pour la méthode worker:

class OtherClass
{
    internal Task ExecuteLongProcedureAsync(MainWindow gui, int param1, int param2, int param3)
    {       
        var tcs = new TaskCompletionSource<int>();       

        //Start doing work
        gui.UpdateWindow("Work Started");        

        While(stillWorking)
        {
        //Mid procedure progress report
        gui.UpdateWindow("Bath water n% thrown out");        
        if (gui.StopTraining) return tcs.Task;
        }

        //Exit message
        gui.UpdateWindow("Done and Done");       
        return tcs.Task;        
    }
}

Cela s'exécute, mais la fenêtre de la fonction WPF est toujours bloquée une fois la méthode de travail démarrée.

J'ai besoin de savoir comment arranger les déclarations async/wait/task pour permettre

A) la méthode de travail pour ne pas bloquer la fenêtre graphique
B) laisser la méthode worker mettre à jour la fenêtre de l'interface graphique
C) permettre à la fenêtre d'interface graphique d'arrêter l'interruption et d'arrêter la méthode de travail

Toute aide ou pointeur est très apprécié.

40
Kickaha

Longue histoire courte:

private async void ButtonClick(object sender, RoutedEventArgs e)
{
    txt.Text = "started";// done in UI thread

    // wait for the task to finish, but don't block the UI thread
    await Task.Run(()=> HeavyMethod(txt));
    // The task is now completed.

    txt.Text = "done";// done in UI thread
}

// Running the Task causes this method to be executed in Thread Pool
internal void HeavyMethod(TextBox /*or any Control or Window*/ txt)
{
    while (stillWorking)
    {
        txt/*or a control or a window*/.Dispatcher.Invoke(() =>
        {
            // UI operations go inside of Invoke
            txt.Text += ".";
        });

        // CPU-bound or I/O-bound operations go outside of Invoke
        System.Threading.Thread.Sleep(51);
    }
}
Result:
txt.Text == "started....................done"

Explication:

  1. Vous ne pouvez que await dans une méthode async.

  2. Vous ne pouvez que await un objet awaitable (c'est-à-dire Task ou Task<T>)

  3. Task.Run généralement place un Task dans le pool de threads (c’est-à-dire qu’il utilise un thread existant du pool de threads ou crée un nouveau thread dans le pool de threads pour exécuter la tâche. C’est vrai si l’opération asynchrone n’est pas une opération pure , sinon il y aura pas de thread, juste une opération async pure gérée par le système d'exploitation et les pilotes de périphérique)

  4. L'exécution attend à await que la tâche se termine et renvoie ses résultats, sans bloquer le thread principal à cause du mot clé async capacité magique:

  5. Le mot clé magique de async signifie qu'il ne crée pas un autre thread. Cela permet seulement au compilateur d'abandonner et de reprendre le contrôle sur cette méthode. ( ne confondez pas la méthode avec le mot clé async avec la méthode intégrée à un Task)

Alors

Votre thread principal appelle la méthode async (MyButton_Click) Comme une méthode normale et aucun thread jusqu'à présent ... Maintenant, vous pouvez exécuter une tâche dans le MyButton_Click Comme ceci:

private async void MyButton_Click(object sender, RoutedEventArgs e)
{
    //queue a task to run on threadpool
    Task task = Task.Run(()=>
        ExecuteLongProcedure(this, intParam1, intParam2, intParam3));
    //wait for it to end without blocking the main thread
    await task;
}

ou simplement

private async void MyButton_Click(object sender, RoutedEventArgs e)
{
    await Task.Run(()=>
        ExecuteLongProcedure(this, intParam1, intParam2, intParam3));
}

ou si ExecuteLongProcedure a une valeur de retour de type string

private async void MyButton_Click(object sender, RoutedEventArgs e)
{
    Task<string> task = Task.Run(()=>
        ExecuteLongProcedure(this, intParam1, intParam2, intParam3));
    string returnValue = await task;
}

ou simplement

private async void MyButton_Click(object sender, RoutedEventArgs e)
{
    string returnValue = await Task.Run(()=>
        ExecuteLongProcedure(this, intParam1, intParam2, intParam3));

    //or in cases where you already have a "Task returning" method:
    //  var httpResponseInfo = await httpRequestInfo.GetResponseAsync();
}

La méthode à l'intérieur de la tâche (ou ExecuteLongProcedure) s'exécute de manière asynchrone et se présente comme suit:

//change the value for the following flag to terminate the loop
bool stillWorking = true;

//calling this method blocks the calling thread
//you must run a task for it
internal void ExecuteLongProcedure(MainWindow gui, int param1, int param2, int param3)
{
    //Start doing work
    gui.UpdateWindow("Work Started");

    while (stillWorking)
    {
        //put a dot in the window showing the progress
        gui.UpdateWindow(".");
        //the following line will block main thread unless
        //ExecuteLongProcedure is called with await keyword
        System.Threading.Thread.Sleep(51);
    }

    gui.UpdateWindow("Done and Done");
} 

Types de retour:

Si task est de type Task<T>, La valeur renvoyée par l'instruction await task Est une valeur de type T. Si task est de type Task alors await task Ne renvoie rien (ou renvoie void). À ce stade, vous pouvez indiquer au compilateur de await la tâche à terminer ou simplement de passer à la ligne suivante.

Par conséquent, si votre méthode async ne renvoie rien, vous pouvez écrire async void MyMethod() ou async Task MyMethod(). Et si votre méthode async renvoie quelque chose (par exemple, un entier), vous pouvez écrire async Task<int> MyMethod. Dans ce cas, votre code peut ressembler à ceci:

private async Task<int> MyMethod()
{
    int number = await Task.Run(todo);
    return number;
}

Ceci est évident car si vous ne voulez pas attendre les résultats , vous n’avez probablement pas besoin de Task comme type de retour de la méthode asynchrone. Mais si vous voulez attendre un résultat , alors vous devez attendre le résultat de l'async méthode de la même manière que dans cette méthode. par exemple. var asyncResult = await MyMethod()

Encore confus? Lire les types de retour async sur on MSDN .

Remarque:

Task.Run Est une version plus récente (.NetFX4.5) et plus simple de Task.Factory.StartNew

await est pas Task.Wait()

Blocage:

Les opérations liées à la CPU ou à l'IO telles que Sleep vont bloquer le thread principal même si elles sont appelées dans une méthode avec async mot clé. ( encore une fois, ne confondez pas la méthode async avec la méthode dans un Task. Évidemment, cela n’est pas vrai si la méthode async elle-même est exécutée en tant que tâche: await MyAsyncMethod)

await empêche une tâche de bloquer le thread principal car le compilateur abandonnera son contrôle sur la méthode async.

private async void Button_Click(object sender, RoutedEventArgs e)
{
        Thread.Sleep(1000);//blocks
        await Task.Run(() => Thread.Sleep(1000));//does not block
}

WPF GUI:

Si vous devez accéder à l'interface graphique de manière asynchrone (à l'intérieur de la méthode ExecuteLongProcedure), invoquez toute opération impliquant la modification d'un objet non thread-safe. . Par exemple, tout objet d'interface graphique WPF doit être appelé à l'aide d'un objet Dispatcher associé au thread d'interface graphique:

void UpdateWindow(string text)
{
    //safe call
    Dispatcher.Invoke(() =>
    {
        txt.Text += text;
    });
}

Cependant, si une tâche est démarrée à la suite d'une modification de rappel de propriété à partir du ViewModel, il n'est pas nécessaire d'utiliser Dispatcher.Invoke Car le rappel est en réalité exécuté à partir du thread d'interface utilisateur.

Accès aux collections sur des threads non-UI

WPF vous permet d'accéder aux collections de données sur des threads autres que celui qui a créé la collection et de les modifier. Cela vous permet d'utiliser un thread d'arrière-plan pour recevoir des données d'une source externe, telle qu'une base de données, et d'afficher les données sur le thread d'interface utilisateur. En utilisant un autre thread pour modifier la collection, votre interface utilisateur reste sensible aux interactions de l'utilisateur.

Les changements de valeur déclenchés par INotifyPropertyChanged sont automatiquement redirigés vers le répartiteur.

Comment activer l'accès cross-thread

Rappelez-vous, la méthode async s'exécute elle-même sur le thread principal. Donc ceci est valide:

private async void MyButton_Click(object sender, RoutedEventArgs e)
{
    txt.Text = "starting"; // UI Thread
    await Task.Run(()=> ExecuteLongProcedure1());
    txt.Text = "waiting"; // UI Thread
    await Task.Run(()=> ExecuteLongProcedure2());
    txt.Text = "finished"; // UI Thread
}

Convention de nommage

Il suffit de postfixer le nom de la méthode avec le type de retour Task ou Task<T> Avec Async. par exemple:

Task WriteToFileAsync(string fileName)
{
    return Task.Run(()=>WriteToFile(fileName));
}
async void DoJob()
{
    await WriteToFileAsync("a.txt");
}
void Main()
{
    DoJob();
}

N'utilisez pas Async postfix pour une méthode qui sera transmise à Task.Run().

Personnellement, je pense que le Async postfixe ne devrait pas être utilisé pour une méthode qui ne renvoie pas un Task ou Task<T>. Mais la plupart des gens utilisent ce préfixe sur n’importe quelle méthode async.

Est-ce tout pour ça?

Il y a encore beaucoup à apprendre sur async, son contexte et sa suite .

La tâche utilise le fil? Êtes-vous sûr?

Pas nécessairement. Lire cette réponse pour en savoir plus sur le vrai visage de async.

Stephen Cleary a parfaitement expliqué async-await. Il explique également dans son autre article de blog quand aucun fil n'est impliqué.

Lire la suite

ValueTask et Task

MSDN explique Task

MSDN explique async

comment-appeler-méthode-asynchrone-depuis-méthode-synchrone

async await - Dans les coulisses

async await - FAQ

Assurez-vous de connaître la différence entre Asynchrone, Parallèle et Concurrent.

Vous pouvez également lire n simple rédacteur de fichier asynchrone pour savoir où vous devez être simultané.

Enquêter espace de noms simultané

En fin de compte, lisez ce livre électronique: Patterns_of_Parallel_Programming_CSharp

73
Bizhan

Votre utilisation de TaskCompletionSource<T> est incorrect. TaskCompletionSource<T> est un moyen de créer wrappers compatibles avec TAP pour les opérations asynchrones. Dans votre méthode ExecuteLongProcedureAsync, l’exemple de code est lié à la CPU (c’est-à-dire intrinsèquement synchrone et non asynchrone).

Donc, il est beaucoup plus naturel d'écrire ExecuteLongProcedure en tant que méthode synchrone. C'est aussi une bonne idée d'utiliser des types standard pour les comportements standard, en particulier en utilisant IProgress<T> pour les mises à jour progressives et CancellationToken pour les annulations :

internal void ExecuteLongProcedure(int param1, int param2, int param3,
    CancellationToken cancellationToken, IProgress<string> progress)
{       
  //Start doing work
  if (progress != null)
    progress.Report("Work Started");

  while (true)
  {
    //Mid procedure progress report
    if (progress != null)
      progress.Report("Bath water n% thrown out");
    cancellationToken.ThrowIfCancellationRequested();
  }

  //Exit message
  if (progress != null)
    progress.Report("Done and Done");
}

Vous avez maintenant un type plus réutilisable (pas de dépendance d'interface graphique) qui utilise les conventions appropriées. Il peut être utilisé comme tel:

public partial class MainWindow : Window
{
  readonly otherClass _burnBabyBurn = new OtherClass();
  CancellationTokenSource _stopWorkingCts = new CancellationTokenSource();

  //A button method to start the long running method
  private async void Button_Click_3(object sender, RoutedEventArgs e)
  {
    var progress = new Progress<string>(data => UpdateWindow(data));
    try
    {
      await Task.Run(() => _burnBabyBurn.ExecuteLongProcedure(intParam1, intParam2, intParam3,
          _stopWorkingCts.Token, progress));
    }
    catch (OperationCanceledException)
    {
      // TODO: update the GUI to indicate the method was canceled.
    }
  }

  //A button Method to interrupt and stop the long running method
  private void StopButton_Click(object sender, RoutedEventArgs e)
  {
    _stopWorkingCts.Cancel();
  }

  //A method to allow the worker method to call back and update the gui
  void UpdateWindow(string message)
  {
    TextBox1.Text = message;
  }
}
9
Stephen Cleary

Ceci est une version simplifiée de la réponse la plus populaire ici par Bijan. J'ai simplifié la réponse de Bijan pour m'aider à résoudre le problème en utilisant le formatage Nice fourni par Stack Overflow.

En lisant et en révisant attentivement le message de Bijan, j'ai finalement compris: Comment attendre que la méthode asynchrone se termine?

Dans mon cas, la réponse choisie pour cet autre poste est ce qui m'a finalement amené à résoudre mon problème:

"Éviter async void. Demandez à vos méthodes de retourner Task au lieu de void. Ensuite, vous pouvez await les ".

Voici ma version simplifiée de (excellente) réponse de Bijan:

1) Cela démarre une tâche en async et attend:

private async void Button_Click_3(object sender, RoutedEventArgs e)
{
    // if ExecuteLongProcedureAsync has a return value
    var returnValue = await Task.Run(()=>
        ExecuteLongProcedureAsync(this, intParam1, intParam2, intParam3));
}

2) Voici la méthode à exécuter de manière asynchrone:

bool stillWorking = true;
internal void ExecuteLongProcedureAsync(MainWindow gui, int param1, int param2, int param3)
{
    //Start doing work
    gui.UpdateWindow("Work Started");

    while (stillWorking)
    {
        //put a dot in the window showing the progress
        gui.UpdateWindow(".");

        //the following line blocks main thread unless
        //ExecuteLongProcedureAsync is called with await keyword
        System.Threading.Thread.Sleep(50);
    }

    gui.UpdateWindow("Done and Done");
} 

3) Invoquer l'opération qui implique une propriété de gui:

void UpdateWindow(string text)
{
    //safe call
    Dispatcher.Invoke(() =>
    {
        txt.Text += text;
    });
}

Ou,

void UpdateWindow(string text)
{
    //simply
    txt.Text += text;
}

Commentaires de clôture) Dans la plupart des cas, vous avez deux méthodes.

  • Première méthode (Button_Click_3) appelle la deuxième méthode et a le modificateur async qui indique au compilateur d'activer le threading pour cette méthode.

    • Thread.Sleep dans une méthode async bloque le thread principal. mais attendre une tâche ne le fait pas.
    • L'exécution s'arrête sur le thread actuel (deuxième thread) sur les instructions await jusqu'à la fin de la tâche.
    • Vous ne pouvez pas utiliser await en dehors d'une méthode async
  • La deuxième méthode (ExecuteLongProcedureAsync) est encapsulée dans une tâche et retourne un Task<original return type> objet qui peut être chargé de façon asynchrone en ajoutant await avant.

    • Tout dans cette méthode est exécuté de manière asynchrone

Important:

Liero a soulevé un problème important. Lorsque vous liez un élément à une propriété ViewModel, la propriété modifiée callback est exécutée dans le thread d'interface utilisateur. Donc, il n'est pas nécessaire d'utiliser Dispatcher.Invoke. Les changements de valeur déclenchés par INotifyPropertyChanged sont automatiquement redirigés vers le répartiteur.

4
Eric D

Voici un exemple utilisant async/await, IProgress<T> et CancellationTokenSource. Ce sont les fonctionnalités modernes du langage C # et .Net Framework que vous devriez utiliser. Les autres solutions me font un peu saigner les yeux.

Caractéristiques du code

  • Compter jusqu'à 100 sur une période de 10 secondes
  • Afficher la progression sur une barre de progression
  • Travail de longue durée (une "période d'attente") effectué sans bloquer l'interface utilisateur
  • Annulation déclenchée par l'utilisateur
  • Mises à jour progressives
  • Rapport d'état post-opération

La vue

<Window x:Class="ProgressExample.MainWindow"
        xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.Microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="MainWindow" SizeToContent="WidthAndHeight" Height="93.258" Width="316.945">
    <StackPanel>
        <Button x:Name="Button_Start" Click="Button_Click">Start</Button>
        <ProgressBar x:Name="ProgressBar_Progress" Height="20"  Maximum="100"/>
        <Button x:Name="Button_Cancel" IsEnabled="False" Click="Button_Cancel_Click">Cancel</Button>
    </StackPanel>
</Window>

Le code

    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private CancellationTokenSource currentCancellationSource;

        public MainWindow()
        {
            InitializeComponent();
        }

        private async void Button_Click(object sender, RoutedEventArgs e)
        {
            // Enable/disabled buttons so that only one counting task runs at a time.
            this.Button_Start.IsEnabled = false;
            this.Button_Cancel.IsEnabled = true;

            try
            {
                // Set up the progress event handler - this instance automatically invokes to the UI for UI updates
                // this.ProgressBar_Progress is the progress bar control
                IProgress<int> progress = new Progress<int>(count => this.ProgressBar_Progress.Value = count);

                currentCancellationSource = new CancellationTokenSource();
                await CountToOneHundredAsync(progress, this.currentCancellationSource.Token);

                // Operation was successful. Let the user know!
                MessageBox.Show("Done counting!");
            }
            catch (OperationCanceledException)
            {
                // Operation was cancelled. Let the user know!
                MessageBox.Show("Operation cancelled.");
            }
            finally
            {
                // Reset controls in a finally block so that they ALWAYS go 
                // back to the correct state once the counting ends, 
                // regardless of any exceptions
                this.Button_Start.IsEnabled = true;
                this.Button_Cancel.IsEnabled = false;
                this.ProgressBar_Progress.Value = 0;

                // Dispose of the cancellation source as it is no longer needed
                this.currentCancellationSource.Dispose();
                this.currentCancellationSource = null;
            }
        }

        private async Task CountToOneHundredAsync(IProgress<int> progress, CancellationToken cancellationToken)
        {
            for (int i = 1; i <= 100; i++)
            {
                // This is where the 'work' is performed. 
                // Feel free to swap out Task.Delay for your own Task-returning code! 
                // You can even await many tasks here

                // ConfigureAwait(false) tells the task that we dont need to come back to the UI after awaiting
                // This is a good read on the subject - https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html
                await Task.Delay(100, cancellationToken).ConfigureAwait(false);

                // If cancelled, an exception will be thrown by the call the task.Delay
                // and will bubble up to the calling method because we used await!

                // Report progress with the current number
                progress.Report(i);
            }
        }

        private void Button_Cancel_Click(object sender, RoutedEventArgs e)
        {
            // Cancel the cancellation token
            this.currentCancellationSource.Cancel();
        }
    }
3
Gusdor