it-swarm.com.de

Ausführen und Interagieren mit einer asynchronen Aufgabe über eine WPF-Benutzeroberfläche

Ich habe eine WPF-GUI, in der ich eine Taste drücken möchte, um eine lange Aufgabe zu starten, ohne das Fenster für die Dauer der Aufgabe einzufrieren. Während die Aufgabe ausgeführt wird, möchte ich Berichte über den Fortschritt erhalten und eine weitere Schaltfläche einfügen, mit der die Aufgabe jederzeit beendet werden kann.

Ich kann nicht herausfinden, wie ich async/await/task richtig verwende. Ich kann nicht alles einschließen, was ich versucht habe, aber das ist es, was ich im Moment habe.

Eine WPF-Fensterklasse:

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

Und eine Klasse für die Worker-Methode:

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

Dies wird ausgeführt, aber das WPF-Funktionsfenster wird weiterhin blockiert, sobald die Worker-Methode gestartet wird.

Ich muss wissen, wie ich die Async/Warten/Task-Deklarationen anordnen kann, um sie zuzulassen

A) Die Worker-Methode, um das GUI-Fenster nicht zu blockieren
B) Lassen Sie die Worker-Methode das GUI-Fenster aktualisieren
C) Erlauben Sie dem GUI-Fenster, den Interrupt zu stoppen und die Worker-Methode zu stoppen

Jede Hilfe oder Hinweise werden sehr geschätzt.

40
Kickaha

Um es kurz zu machen:

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"

Erläuterung:

  1. Sie können nur await in einer async - Methode verwenden.

  2. Sie können nur await ein awaitable - Objekt (d. H. Task oder Task<T>)

  3. Task.Run normalerweise stellt ein Task in den Thread-Pool] (dh es wird ein vorhandener Thread aus dem Thread-Pool verwendet) oder erstellt einen neuen Thread im Thread-Pool, um die Task auszuführen. Dies ist alles wahr, wenn die asynchrone Operation keine pure -Operation ist, andernfalls gibt es keinen Thread, sondern nur eine reine asynchrone Operation, die vom Betriebssystem und verarbeitet wird Gerätetreiber)

  4. Die Ausführung wartet um await auf die Beendigung der Aufgabe und gibt ihre Ergebnisse zurück, ohne den Hauptthread aufgrund der magischen Fähigkeit des Schlüsselworts async zu blockieren:

  5. Das Schlüsselwort magic von async ist, dass kein weiterer Thread erstellt wird. Der Compiler kann nur aufgeben und zurücknehmen die Kontrolle über diese Methode übernehmen. ( Verwechseln Sie die Methode nicht mit dem Schlüsselwort async mit der in Task eingeschlossenen Methode)

So

Ihr Hauptthread ruft die async - Methode (MyButton_Click) Wie eine normale Methode auf und bisher ohne Threading ... Jetzt können Sie eine Task in MyButton_Click Wie folgt:

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

oder einfach

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

oder wenn ExecuteLongProcedure einen Rückgabewert vom Typ string hat

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

oder einfach

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

Die Methode in der Task (oder ExecuteLongProcedure) läuft asynchron und sieht folgendermaßen aus:

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

Rückgabetypen:

Wenn task vom Typ Task<T> Ist, ist der von der await task - Anweisung zurückgegebene Wert ein Wert vom Typ T. Wenn task vom Typ Task ist, gibt await task Nichts zurück (oder gibt void zurück). An dieser Stelle können Sie den Compiler anweisen, die Aufgabe zu beenden await oder einfach in die nächste Zeile überzugehen.

Wenn Ihre async - Methode also nichts zurückgibt, können Sie async void MyMethod() oder async Task MyMethod() schreiben. Und wenn Ihre async - Methode etwas zurückgibt (z. B. eine Ganzzahl), können Sie async Task<int> MyMethod Schreiben. In diesem Fall könnte Ihr Code folgendermaßen aussehen:

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

Dies ist offensichtlich, weil wenn Sie nicht auf die Ergebnisse warten möchten, dann benötigen Sie wahrscheinlich kein Task als Rückgabetyp der async Methode. Aber wenn Sie auf ein Ergebnis warten möchten dann müssen Sie warten das Ergebnis der asynchronen Methode auf die gleiche Weise, wie Sie es getan haben innerhalb dieser Methode. z.B. var asyncResult = await MyMethod()

Immer noch verwirrt? Lesen Sie asynchrone Rückgabetypen auf MSDN .

Hinweis:

Task.Run Ist eine neuere (.NetFX4.5) und einfachere Version von Task.Factory.StartNew

await ist nicht ​​Task.Wait()

Blockierung:

CPU-gebundene oder IO-gebundene Operationen wie Sleep werden block den Haupt-Thread, auch wenn sie in einer Methode mit dem Schlüsselwort async aufgerufen werden. ( Verwechseln Sie die Methode async nicht mit der Methode in einer Task. Dies ist offensichtlich nicht der Fall, wenn die asynchrone Methode selbst als Task ausgeführt wird: await MyAsyncMethod)

awaitverhindert, dass eine Task den Hauptthread blockiert, da der Compiler die Kontrolle über die Methode async aufgibt.

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

WPF GUI:

Wenn Sie asynchron auf die GUI zugreifen müssen (innerhalb der Methode ExecuteLongProcedure), invoke eine Operation, die Änderungen an einem nicht thread-sicheren Objekt beinhaltet. Beispielsweise muss jedes WPF-GUI-Objekt mit einem Dispatcher -Objekt aufgerufen werden, das dem GUI-Thread zugeordnet ist:

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

Wenn eine Aufgabe jedoch aufgrund eines Eigenschaft geänderter Rückruf vom ViewModel gestartet wird, muss Dispatcher.Invoke Nicht verwendet werden, da der Rückruf tatsächlich vom UI-Thread ausgeführt wird.

Zugreifen auf Sammlungen auf Nicht-UI-Threads

Mit WPF können Sie auf Datensammlungen in anderen Threads als denjenigen zugreifen und diese ändern, die die Sammlung erstellt haben. Auf diese Weise können Sie einen Hintergrundthread verwenden, um Daten von einer externen Quelle, z. B. einer Datenbank, zu empfangen und die Daten im UI-Thread anzuzeigen. Durch die Verwendung eines anderen Threads zum Ändern der Auflistung reagiert Ihre Benutzeroberfläche weiterhin auf Benutzerinteraktionen.

Wertänderungen, die von INotifyPropertyChanged ausgelöst werden, werden automatisch wieder auf den Dispatcher übertragen.

So aktivieren Sie den Cross-Thread-Zugriff

Denken Sie daran, dass die Methode async selbst im Hauptthread ausgeführt wird. Das ist also gültig:

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
}

Namenskonvention

Fixieren Sie einfach den Namen der Methode mit dem Rückgabetyp Task oder Task<T> Mit Async. z.B:

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

Verwenden Sie kein Postfix Async für eine Methode, die an Task.Run() übergeben wird.

Ich persönlich denke, dass das Postfix Async nicht für eine Methode verwendet werden sollte, die nicht ​​Task oder Task<T> Zurückgibt. Die meisten Leute verwenden dieses Präfix jedoch für jede Methode async.

Ist das alles dran?

Nein. Es gibt noch viel mehr über async, sein Kontext ​​und sein Fortsetzung zu lernen.

Task verwendet Thread? Bist du sicher?

Nicht unbedingt. Lesen Sie diese Antwort , um mehr über das wahre Gesicht von async zu erfahren.

Stephen Cleary hat async-await Perfekt erklärt. Er erklärt auch in seinem anderen Blog-Post , wenn es sich nicht um einen Thread handelt.

Weiterlesen

ValueTask und Task

MSDN erklärt Task

MSDN erklärt async

How-to-Call-asynchrone-Methode-from-synchrone-Methode

async await - Hinter den Kulissen

async await - FAQ

Stellen Sie sicher, dass Sie den Unterschied zwischen Asynchron, Parallel und Concurrent kennen.

Sie können auch ein einfacher asynchroner Dateischreiber lesen, um zu wissen, wo Sie gleichzeitig arbeiten müssen.

Untersuchen Sie gleichzeitiger Namespace

Lesen Sie zum Schluss dieses E-Book: Patterns_of_Parallel_Programming_CSharp

73
Bizhan

Ihre Verwendung von TaskCompletionSource<T> ist falsch. TaskCompletionSource<T> ist eine Möglichkeit, TAP-kompatible Wrapper für asynchrone Vorgänge zu erstellen. In Ihrer ExecuteLongProcedureAsync -Methode ist der Beispielcode vollständig CPU-gebunden (d. H. Inhärent synchron, nicht asynchron).

Es ist also viel natürlicher, ExecuteLongProcedure als synchrone Methode zu schreiben. Es ist auch eine gute Idee, Standardtypen für Standardverhalten zu verwenden, insbesondere mit IProgress<T> für Fortschrittsaktualisierungen und CancellationToken für Stornierung :

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

Jetzt haben Sie einen wiederverwendbareren Typ (keine GUI-Abhängigkeiten), der die entsprechenden Konventionen verwendet. Es kann als solches verwendet werden:

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

Dies ist eine vereinfachte Version der beliebtesten Antwort von Bijan. Ich habe die Antwort von Bijan vereinfacht, um das Problem mithilfe der von Stack Overflow bereitgestellten Nice-Formatierung zu überdenken.

Durch sorgfältiges Lesen und Bearbeiten von Bijans Post habe ich endlich verstanden: Wie warte ich, bis die asynchrone Methode abgeschlossen ist?

In meinem Fall hat mich die gewählte Antwort auf diesen anderen Beitrag letztendlich dazu veranlasst, mein Problem zu lösen:

"Vermeiden Sie async void. Lassen Sie Ihre Methoden Task anstelle von void zurückgeben. Dann können Sie await."

Meine vereinfachte Version von Bijans (ausgezeichneter) Antwort lautet:

1) Dies startet eine Aufgabe mit Async und wartet:

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) Dies ist die asynchrone Methode:

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) Rufen Sie die Operation auf, die eine Eigenschaft von gui umfasst:

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

Oder,

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

Abschließende Kommentare) In den meisten Fällen haben Sie zwei Methoden.

  • Die erste Methode (Button_Click_3) Ruft die zweite Methode auf und hat den Modifikator async, der den Compiler anweist, das Threading für diese Methode zu aktivieren.

    • Thread.Sleep In einer async Methode blockiert den Hauptthread. aber warten auf eine aufgabe tut das nicht.
    • Die Ausführung wird für den aktuellen Thread (zweiten Thread) in await -Anweisungen angehalten, bis die Task beendet ist.
    • Sie können await nicht außerhalb einer async -Methode verwenden
  • Die zweite Methode (ExecuteLongProcedureAsync) wird in eine Task eingeschlossen und gibt ein generisches Task<original return type> - Objekt zurück, das durch Hinzufügen von await zur asynchronen Verarbeitung angewiesen werden kann.

    • Alles in dieser Methode wird asynchron ausgeführt

Wichtig:

Liero sprach ein wichtiges Thema an. Wenn Sie ein Element an eine ViewModel-Eigenschaft binden, wird die Eigenschaft geänderter Rückruf im UI-Thread ausgeführt. Es ist also nicht erforderlich, Dispatcher.Invoke Zu verwenden. Wertänderungen, die von INotifyPropertyChanged ausgelöst werden, werden automatisch wieder auf den Dispatcher übertragen.

4
Eric D

Hier ist ein Beispiel mit async/await, IProgress<T> und CancellationTokenSource. Dies sind die modernen C # - und .Net Framework-Sprachfunktionen, die Sie sollten verwenden. Die anderen Lösungen lassen meine Augen ein bisschen bluten.

Code-Funktionen

  • Zählen Sie über einen Zeitraum von 10 Sekunden bis 100
  • Zeigen Sie den Fortschritt in einem Fortschrittsbalken an
  • Lange laufende Arbeit (eine Wartezeit), ohne die Benutzeroberfläche zu blockieren
  • Vom Benutzer ausgelöste Kündigung
  • Inkrementelle Fortschrittsaktualisierungen
  • Statusbericht nach dem Betrieb

Die Aussicht

<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>

Der 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