Open suchja opened 3 years ago
BackgroundWorker
wird nicht mehr empfohlen von Microsoft. Dort hat sich in den letzten Jahren einiges geändert. In den aktuellen Versionen von WPF und C# werden async
/await
und Task.Run
verwendet um Dinge im Hintergrund zu erledigen. Das ist dann auch häufig etwas einfacher zu realisieren.View
(also XAML und Code-behind) wirklich nur die Dinge passieren, die für die Visualisierung unbedingt notwendig sind. Alles andere sollte davon in separate Klassen getrennt werden. Auch wenn es sehr üblich ist, dass ein Muster namens MVVM (Model View ViewModel) verwendet wird, ist es gerade für Einsteiger schwierig zu verstehen und auch nicht unbedingt sinnvoll es kompromisslos zu verfolgen.ProgressBarTaskOnWorkerThread
(siehe Beispielcode oben) eine Klasse ist. Diese Klasse bietet viel Funktionalität die sie durch Vererbung aus den WPF-Klassen bekommt, aber es ist letztlich eine "ganz normale" Klasse. Die Definition dieser Klasse im Code-Behind (public partial class ProgressBarTaskOnWorkerThread : Window
) sagt, dass diese Klasse in mehreren Dateien (deshalb das Schlüsselwort partial
) "lebt". Über : Window
wird angegeben, dass die Klasse von der WPF-Klasse Window
ableitet und somit deren Funktionalität erbt.Progress
im code-behindDie von Microsoft in WPF vorgeschlagene Lösung zum melden von Fortschritt ist in der Klasse Progress<T>
und dem Interface IProgress<T>
zu finden. Eine (leider nicht sehr umfangreiche) Beschreibung findet sich hier.
Die grundlegende Idee ist, dass im Kontext der Oberfläche (also üblicherweise im code-behind) eine Instanz von Progress<T>
angelegt wird. Da Progress<T>
ein generischer Typ ist (zu erkennen an <T>
) kann beim Anlegen entschieden werden welche Daten später als Fortschritt zur Oberfläche kommuniziert werden. In diesem Beispiel soll es ein ganzzahliger Wert zwischen 0 und 100 sein.
In dem (über diesem Issue) verlinkten Commit 9d4c976 habe ich beispielhaft gezeigt wie eine Instanz angelegt wird. Dabei ist der wichtigste Teil dieser:
IProgress<int> progress = new Progress<int>(value => { myProgress.Value = value; });
Wie du siehst erstelle ich eine Instanz von Progress<T>
mit dem Teil new Progress<int>
. Dabei definiere ich mit (value => { myProgress.Value = value; })
was passieren soll, wenn die Eigenschaft Value
einen neuen Wert zugewiesen bekommt. Sie soll diesen neuen Wert nämlich direkt dem ProgressBar
übergeben (also myProgress.Value = value;
).
Um zu sehen, dass das auch tatsächlich funktioniert, löse ich dann mit progress?.Report(_currentProgress);
eine Benachrichtigung aus.
Das tolle an Progress<T>
ist, dass es jegliche Synchronisation selbstständig erledigt. Wichtig zu verstehen ist nämlich, dass es bei der Verwendung von BackgroundWorker
und/oder Task
schnell zu einem Problem kommen kann. Die Aktualisierung der Steuerelemente auf der Oberfläche darf nur durch den Thread erfolgen, der die Oberfläche erstellt hat (auch UI-Thread genannt). Da Progress<T>
durch den UI-Thread erstellt wird, kann nun die Aktualisierung der Oberfläche darin erfolgen. Kommt der Aufruf von progress?.Report(_currentProgress);
aus einem anderen Thread, dann kümmert sich die Implementierung von Progress<T>
um die notwendige Synchronisation.
Somit sollte erstmal einigermaßen klar sein wie Progress<T>
funktioniert. Allerdings ist ja nun immer noch alles im code-behind und es läuft auch noch nichts asynchron (also ausser dem Benutzer, der jetzt den Button klicken muss ;).
Also schauen wir uns die nächste Änderung einmal an.
Task
aktualisierenOkay, wie du in 4705ce8 siehst, sind wir immer noch im code-behind, aber haben schon mal den Schritt gemacht, dass der Fortschritt aus einem Task
automatisch bis 100 aktualisiert wird.
Ein wichtiger Punkt für die Umsetzung ist dieser Teil im Quellcode:
IProgress<int> _progress;
public MainWindow()
{
InitializeComponent();
myProgress.Value = _currentProgress;
_progress = new Progress<int>(value => { myProgress.Value = value; });
}
Im Konstruktor public MainWindow()
wird nun das Progress<int>
Objekt angelegt und dem Feld _progress
zugewiesen. Damit kann jede Methode aus der Klasse darauf zugreifen. Fürs Verständnis wichtig ist, dass hier eigentlich noch nichts passiert. Es geht erstmal darum die Infrastruktur vorzubereiten. Wie du nämlich siehst, wird bisher _progress.Report()
noch gar nicht aufgerufen.
Das erfolgt erst im Event-Händler für den StartButton
:
private async void StartButton_Click(object sender, RoutedEventArgs e)
{
await Task.Run(async () => {
while (_currentProgress < 100)
{
_currentProgress += 10;
_progress?.Report(_currentProgress);
await Task.Delay(500);
}
});
}
Mithilfe von Task.Run
wird die innerhalb der gescheiten Klammer definierte Aufgabe (das Ganze inkl. dem =>
nennt man Lambda) nun von einem separaten Task
(und dieser wiederum von einem anderem Thread
) ausgeführt. Daher kannst du nun auf der Oberfläche beobachten, dass sich die ProgressBar
"füllt" bis sie am rechten Ende angekommen ist.
Die ursprüngliche Aufgabe war den Fortschritt aus der Geschäftslogik (also hauptsächlich einer anderen Klassen) zu erhöhen und dieses nicht im code-behind zu machen. Mit den Änderungen aus 119ee48 ist dieses nun möglich. Der Einsprungpunkt für die Geschäftslogik (hier "simuliert" durch die klasse BusinessLogic
) ist im StartButton_Click
-Handler:
private async void StartButton_Click(object sender, RoutedEventArgs e)
{
await Task.Run(async() => {
string result = await _logic.DoSomething(_progress);
});
}
Wie du sehen kannst wird die BusinessLogic.DoSomething
aufgerufen und dieser Methode wird eine Referenz auf IProgress<int>
übergeben. Damit kann die Geschäftslogik nun entscheiden wann sie soviel geschafft hat, dass ein weiterer Fortschritt zur Oberfläche geschickt werden kann.
Zwar habe ich nicht den gewünschten BackgroundWorker
genommen, aber mithilfe der für diesen Zweck gedachten Progress<T>
und IProgress<T>
Typen ist es relativ einfach (wenn man weiß wie es geht ;) den Fortschritt aus anderen Klassen als dem code-behind zu berichten.
Wichtig ist hier insbesondere das Verständnis von ObjektOrientierung. Letztlich müssen sich die Oberfläche und die Geschäftslogik in irgend einer Art und Weise kennen. Schließlich muss die Geschäftslogik irgendwie an eine Instanz von IProgress<int>
kommen, damit sie es nutzen kann um den Fortschritt zu berichten.
In diesem Beispiel habe ich es so gelöst, dass die Oberfläche die Geschäftslogik anlegt (BusinessLogic _logic = new BusinessLogic();
im MainWindow
). Wenn du MVVM einhalten möchtest oder sagst, dass im code-behind möglichst nichts stehen soll (wie das viele Entwickler fordern), dann ist der hier gezeigt Weg noch nicht optimal. Allerdings spielen beim Thema MVVM und gute Separierung von Oberfläche und Logik noch so viele Aspekte eine Rolle, dass es mehr ein ganzes Buch werden kann.
Zum Abschluss habe ich in 903f727 noch 2 Veränderungen vorgenommen. Aus meiner Sicht das wichtigste ist, dass IProgress<int> progress = new Progress<int>(value => { myProgress.Value = value; });
jetzt nur noch lokal im StartButton_Click
ausgeführt wird. D.h. dadurch, dass der Fortschritt nur an dieser einen Stelle benötigt wird, brauchen wir kein extra Feld mehr im MainWindow
. So ist es aus meiner Sicht noch etwas aufgeräumter.
Zum Schluss hast du vielleicht noch die Frage, warum Task.Run
im MainWindow
gemacht wird und nicht in BusinessLogic.DoSomething
(das würde nämlich auch gehen). Task.Run
ist der Aufruf der die ihm übergebene Methode (Aufgabe) im Hintergrund ausführt. Natürlich wäre es möglich diesen Aufruf auch in BusinessLogic
zu verlagern. Damit hätten wir noch weniger code-behind. Außerdem könnte man argumentieren, dass die BusinessLogic
vielleicht besser weiß wie lange ihre Aufgabe dauert und ob sie somit besser im Hintergrund ausgeführt wird.
Allerdings gibt es eine ganz klare Empfehlung von Microsoft. Eine Bibliothek (oder in diesem Fall auch eine einzelne Klasse) sollte möglichst selber nicht Task.Run
verwenden, sondern dem Aufrufer die Entscheidung überlassen, ob das notwendig ist, oder nicht. Dieses Vorgehen ermöglicht ein breiteres Einsatzgebiet der Bibliothek. Multithreading ist nämlich sehr anwendungsspezifisch und sollte somit von der Anwendung dort eingesetzt werden, wo es notwendig ist.
Hier gilt es folgende Frage die ich per Mail (am 21.06.21) bekommen habe zu beantworten.
Dazu gab es folgenden Code:
XAML
Code-Behind (xaml.cs)
Programmlogik (beliebige .cs Datei)