Das Threading Model von WPF und WindowsForms beschränkt den Zugriff auf Steuerelemente auf den Thread aus dem heraus Sie erstellt wurden. Dies führt in Multithreading Anwendungen zu einem erhöhten Aufwand bei parallelen Prozessen welche eine Aktualisierung der Benutzeroberfläche vornehmen sollen. In diesem Artikel möchte ich Ihnen aufzeigen wie Sie auch in Tasks auf Steuerelemente zugreifen können.
Beispielanwendung
Die Beispielanwendung ist eine WPF Anwendung deren Benutzeroberfläche eine Schaltfläche und ein Label enthält. Bei Klick auf die Schaltfläche soll eine Berechnung ausgeführt werden und das Resultat der Berechnung soll mittels des Labels ausgegeben werden. Der nachfolgende Quellcode zeigt die entsprechende Anwendung. Das Label wurde LabelResult genannt und ist über diesen Bezeichner zugreifbar. Bei Klick auf die Schaltfläche wird die Calculate Funktion aufgerufen und deren Ergebnis anschliessend an die UpdateUserInterface Funktion übergeben. Innerhalb dieser Funktion wird das Ergebnis als Inhalt des Labels gesetzt und somit an den Nutzer ausgegeben. Nachfolgend sehen Sie die erste Version der Anwendung bei welcher die entsprechenden Funktionen ohne Verwendung von Tasks aufgerufen werden.
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void Button_Click(object sender, RoutedEventArgs e) { int value; value = Calculate(); UpdateUserInterface(value); } private int Calculate() { return 42; } private void UpdateUserInterface(int value) { LabelResult.Content = value.ToString(); } }
Verwendung von Tasks
Die oben gezeigte Version der Anwendung ist voll funktional hat aber einen wesentlichen Nachteil: Wenn die Berechnungsfunktion sehr zeitaufwendig ist, dann wird die Benutzeroberfläche während dieser Zeit blockiert und friert ein. Solch lang laufende Funktionen sollten daher parallel ausgeführt werden. Wie ich Ihnen in vorhergehenden Artikeln dieser Artikelserie gezeigt habe, eignen sich Tasks sehr gut um diese parallele Implementierung durchzuführen. Daher habe ich die Anwendung in einem zweiten Schritt auf Tasks umgestellt. Bei Klick auf die Schaltfläche wird die Calculate Funktion nun in einem Task gestartet und die UpdateUserInterface Funktion wird als Continuation Task angegeben. Der nachfolgende Quellcode zeigt die angepasste Beispielanwendung.
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void Button_Click(object sender, RoutedEventArgs e) { Task.Factory.StartNew<int>(Calculate) .ContinueWith(task => UpdateUserInterface(task.Result)); } private int Calculate() { return 42; } private void UpdateUserInterface(int value) { LabelResult.Content = value.ToString(); } }
Zugriff auf UI Elemente innerhalb von Tasks
Die zuvor gezeigte auf Tasks erweiterte Beispielanwendung hat einen entscheidenden Nachteil: Sie stürzt mit einer Fehlermeldung ab. Der Aufruf zur Aktualisierung des Labels führt zu einer InvalidOperationException. Wie in der Einleitung beschrieben darf der Zugriff auf ein Steuerelement nur aus dem Thread heraus erfolgen in welchem das Steuerelement erzeugt wurde. Diesem Bedürfnis wird das .NET Framework aber gerecht und stellt bei der Verwendung von Tasks entsprechende Hilfsmittel bereit. Alle Tasks werden mit einem Task Scheduler verknüpft. Standardmässig ist dies der CLR Thread Pool. Zusätzlich existiert aber eine weitere Scheduler Implementierung, der Synchronization Context Scheduler. Dieser ist speziell zur Unterstützung des Threading Models von WPF und Windows Forms implementiert. Bei dem Aufruf von Tasks welche auf Steuerelemente zugreifen, können Sie daher den Synchronization Context Scheduler als Alternative zum Standard Scheduler verwenden. Die ContinueWith Funktion bietet für diesen Zweck eine überladene Variante an. Der nachfolgende Quellcode zeigt die entsprechend erweiterte Beispielanwendung. Der Synchronization Context Scheduler wird mittels der Funktion TaskScheduler.FromCurrentSynchronizationContext() ermittelt.
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void Button_Click(object sender, RoutedEventArgs e) { Task.Factory.StartNew<int>(Calculate) .ContinueWith( task => UpdateUserInterface(task.Result), TaskScheduler.FromCurrentSynchronizationContext()); } private int Calculate() { return 42; } private void UpdateUserInterface(int value) { LabelResult.Content = value.ToString(); } }
Fazit
Die Beispielanwendung zeigt, wie einfach der Zugriff auf Steuerelemente aus Tasks heraus erfolgen kann, welche in parallel laufenden Threads ausgeführt werden. Da das .NET Framework zu diesem Zweck bereits einen entsprechend implementierten Scheduler bereitstellt wird dem Entwickler hier viel Arbeit abgenommen und ein effizientes Arbeiten mit Tasks ermögli