C#: Async/Await Fortschritts-Benachrichtigung und Abbruch-Möglichkeit

In dem Beitrag C#: FTP-Übertragung (Up- und Download) mit Standard .NET-Funktionalität (FtpWebRequest / FtpWebResponse) wurde beschrieben wie man mit async/await eine Datei via FTP Up- bzw. Downloaden kann. Gerade bei solchen Operationen (die potentiell länger dauern können) ist es sehr hilfreich etwas über den aktuellen Status zu erfahren. Besonders aus Usability-Sicht ist dies bei modernen Anwendungen unerlässlich (sei es bei Desktop-Anwendungen oder Smartphone Apps). Bei längeren andauernden Funktionen/Methoden soll die Anwendung ständig bedienbar bleiben und den Benutzer über die wichtigsten Parameter der aktuellen Operation informieren (z.B. Übertragene Dateigröße, Restdauer, …). In vielen Fällen soll der Anwender auch die Möglichkeit haben den Vorgang abzubrechen. In diesem Artikel werden nun einige Möglichkeiten aufgezeigt wie man solche Szenarien realisieren kann.

Benachrichtigung über den Fortschritt

Werden innerhalb einer Anwendung länger dauernde Funktionen/Methoden ausgeführt möchte man den Anwender oftmals über den aktuellen Fortschritt informieren. Solchen Operationen sind häufig

  • Netzwerkzugriffe (Upload/Download)
  • Zugriffe auf die Festplatte
  • Datenbankzugriffe

Im einfachsten Fall übergibt man der asynchronen Methode dafür einen Action-Delegaten, der immer dann aufgerufen wird wenn sich der Status ändert. Das könnte in etwa so aussehen:

Wie im Beispiel zu sehen wird der asynchronen Methode ein Action<int>-Delegate namens „onProgressChanged“ übergeben. Dieser Delegate wird dann bei Statusänderungen aufgerufen (Zeile 11). Um den Status jetzt über einen Progressbar zu visualisieren:

Hier wird ein einfacher Action<T>-Delegate mittels Lambda-Ausdruck initialisiert und an die asynchrone Methode übergeben. Diese Vorgehensweise eignet sich sehr gut für Konsoleanwendungen. Für WPF- oder WinForrms-Anwendungen ist dieses Konstrukt aber nur bedingt geeignet. Grund: Der Fortschritt wird im Workerthread ausgewertet und zurückgeliefert. In WPF- bzw. WinForms-Anwendungen muss das UI aber zwingend im UI-Thread aktualisiert werden (versucht man ein Control aus einem Workerthread heraus zu aktualisieren kommt es zu einer Exception). Innerhalb des .NET-Frameworks gibt es dafür aber eine Lösung: Das Interface IProgress<T> und die Klasse Progress<T>, welche dieses Interface implementiert.

IProgress<T> und Progress<T>

Alle asynchronen Methoden, die über den Fortschritt informieren sollen, erhalten einen Parameter vom Typ IProgress<T> (T kann hier ein beliebiger Typ sein). Dabei sollte dieser Parameter nach den „normalen“ Parametern stehen. Das IProgress<T>-Interface ist wie folgt definiert:

Wie zu erkennen enthält die Schnittstelle lediglich eine Report-Methode und glücklicherweise stellt uns das .NET-Framework auch gleich schon eine Implementierung dieser Schnittstelle bereit (in Form der Klasse Progress<T>):

Die Verwendung ist dann ganz einfach. In folgendem Beispiel handelt es sich um eine einfache WPF-Anwendung, die lediglich zwei Controls enthält: Einen Button, um eine Aktion zu starten und eine ProgressBar, um den Fortschritt zu visualisieren:

Diese Methode hat nur einen Parameter vom Typ IProgress<T>. In Zeile 10 wird die Report-Methode aufgerufen, um über den Fortschritt zu informieren. Damit auf die Fortschrittsmeldung reagiert werden kann benötigt man noch eine Instanz der Klasse Progress<T>. Um die ProgressBar zu aktualisieren könnte dies in etwa wie folgt aussehen:

Hier wird mit einem Lambda-Ausdruck ein Action<T>-Delegate erzeugt und dem Konstruktor der Klasse Progress<T> mitgegeben. Der Delgate übernimmt dabei die Aktualisierung der ProgressBar. Das schöne an der Klasse Progress<T> ist, dass direkt bei der Erzeugung ein Mapping auf den SynchronziationContext erfolgt. Ab diesem Zeitpunkt wird dann auch dieser SynchronziationContext verwendet und das Statusupdate erfolgt somit im richtigen Thread (UI-Thread um das UI zu aktualisieren). Aus diesem Grund kann das IProgress<T>-Interface aus jedem beliebigen Thread heraus aufgerufen werden und man braucht sich keine weiteren Gedanken darüber zu machen, ob man sich im richtigen Thread befindet. Das hier gezeigt Beispielprogramm steht am Ende Artikels als Download bereit.

Hinweis:

Der generische Parameter T kann ja ein beliebiger Typ sein. Der Typ int bietet sich z.B. für einfache Prozentangaben an. In anderen Szenarien werden eventuell mehr Informationen benötigt und man erstellt sich eine eigene Klasse für diese Informationen. Hier gilt es zu beachten, dass das Objekt potentiell in einem anderen Thread verwendet werden kann, als der Thread, in dem es erzeugt wurde. Für solche Szenarien bieten sich immutable-Typen an. immutable-Typen sind, wie der Name vermuten lässt, nach ihrer Erzeugung nicht mehr veränderbar (bei Gelegenheit werde ich mal einen separaten Artikel zu diesen Typen schreiben).

Möglichkeit die Operation abzubrechen

In manchen Situationen möchte man dem Anwender die Möglichkeit bieten eine laufende Operation abzubrechen. Auch dafür stellt das .NET-Framework entsprechende Klassen bereit. Der Abbruch einer asynchronen Methode kann mit einem sogenannten CancellationToken realisiert werden. Damit eine asynchrone Methode abgebrochen werden kann, wird diese um einen Parameter vom Typ CancellationToken erweitert. Dieser Parameter sollte nach den „normalen“ Parametern der Methode stehen. Um ein solches CancellationToken zu erzeugen kann die Klasse CancellationTokenSource genutzt werden (diese Klasse ist eine Art Hilfsklasse, um ein CancellationToken zu erzeugen). Wir erweitern einfach das obige Beispiel zum Starten einer Aktion um einen Cancel-Button und geben der asynchronen Methode einen zusätzlichen Parameter vom Typ CancellationToken:

In Zeile 1 wurde ein neuer Parameter vom CancellationToken eingefügt. In Zeile 13 wird die eigentliche asynchrone Operation gestartet, welcher das CancellationToken mitgegeben wird. Im EventHandler für den Cancel-Button (Zeile 37) wird die Cancel-Methode der CancellationTokenSource-Klasse aufgerufen. Durch den Aufruf der Cancel-Methode wird das erzeugte Token in den Canceled-Status versetzt und eine Exception vom Typ OperationCanceledException ausgelöst (mehr Informationen dazu unter [5] – MSDN – How to: Cancel a Task and Its Children). Die oben gezeigte Methode kann dann wie folgt aufgerufen werden:

Hier sieht man nun den EventHandler für den Start-Button. Hier wird einfach nur eine Instanz der Klasse CancellationTokenSource erzeugt, um ein entsprechendes CancellationToken zu generieren. Für die Statusrückmeldung wird in Zeile 6, via Lambda-Ausdruck, ein Action<T>-Delegate erzeugt. Diese Objekte werden dann der eigentlichen asynchronen Operation in Zeile 9 übergeben.

Beispiel FTP-Übertragung

Im Beitrag C#: FTP-Übertragung (Up- und Download) mit Standard .NET-Funktionalität (FtpWebRequest / FtpWebResponse) wurde eine Möglichkeit zum Up- bzw. Download von Dateien via FTP beschrieben. Dieses Beispiel soll nun um die folgenden Features erweitert werden:

  • Fortschrittsrückmeldung über die aktuell übertragenen Bytes
  • Möglichkeit die Übertragung abzubrechen

Hier mal die angepassten Methoden für den Up- bzw. Download. Ich spare mir an dieser Stelle einfach mal die Kommentare (mit den obigen Informationen sollten die gemachten Änderungen einfach nachvollziehbar sein). Bei weitere Fragen, Anregungen oder Kommentare stehe ich natürlich gerne zur Verfügung.

Upload

Download

Damit wären wir auch schon am Ende des Artikels. Gerade durch die im .NET-Framework bereitgestellten Klassen werden die asynchrone Programmierung und die damit verbundenen Tätigkeiten, wie z.B. Fortschrittsbenachrichtigungen und Abbruch-Möglichkeit, deutlich vereinfacht. Weitere Informationen können den unten stehenden Weblinks entnommen werden.

Literaturverzeichnis und Weblinks

Abk.Quelle
[1]MSDN - IProgress<T>
IProgress<T>-Schnittstelle
[2]MSDN - Progress<T>
Progress<T>-Klasse
[3]MSDN - CancellationTokenSource
CancellationTokenSource-Klasse
[4]MSDN - CancellationToken
CancellationToken-Struktur
[5]MSDN - How to: Cancel a Task and Its Children
How to: Cancel a Task and Its Children

Beispiel-Code

[wpdm_package id=’1444′]

Fork me on GitHub