NerdishByNature

WPF, MVVM, Dispatcher und AsyncOperation

Hier geht es um das Feedback für den User, wenn z.B. eine Liste grad gefiltert wird. Das Ganze mit Hilfe von MVVM, dem Dispatcher und einer AsyncOperation unter WPF.

Wird eine Liste gefiltert kann dies manchmal auch etwas länger dauern. Als Feedback für den Anwender benutze ich einen BusyIndicator als Ladeanzeige. Ich habe zwei ViewModels, FilterViewModel und MainViewModel. In dem MainViewModel gibt es ein Property “IsBusy” über welches ich den BusyIndicator steuer. In dem FilterViewModel gibt es eine Methode Filter, die das Filtern der Daten für die Liste übernimmt und am Anfang das “IsBusy” Property auf true setzt.
Bisher sieht meine Methode wie folgt aus:

private void Filter()
{
    [...]
    MainWinVM.IsBusy = true;
    DoFilter();
}

Leider reagiert die Oberfläche nicht sofort auf mein Property. Der Oberflächen-Thread kommt erst garnicht dazu die Änderung am Property an den BusyIndicator weiter zu geben. Um Änderungen vorzunehmen, die die Oberfläche betreffen, sollte der Dispatcher verwendet werden. Es gibt einen guten Artikel von Shawn Wildermuth zum Dispatcher und mögliche Herangehensweisen, um die Oberfläche zu aktualisieren.

Eine weitere Möglichkeit ist die Verwendung von AsyncOperation und sieht wie folgt aus:

private void Filter()
{
  [...]
  System.ComponentModel.AsyncOperation asyncOp =
     System.ComponentModel.AsyncOperationManager.CreateOperation(Guid.NewGuid());

  Action<System.ComponentModel.AsyncOperation> filter =
    (Action<System.ComponentModel.AsyncOperation>)delegate(System.ComponentModel.AsyncOperation a)
    {
      var dispOp = System.Windows.Threading.Dispatcher.CurrentDispatcher.BeginInvoke(
                     (Action)delegate
                     {
                         MainWinVM.IsBusy = true;
                     },
                     null);

        dispOp.Completed += (s, e) =>
        {
          // sleep to load and see the busyindicator
          // System.Threading.Thread.Sleep(750);
          a.PostOperationCompleted(new System.Threading.SendOrPostCallback(DoFilter), null);
        };

      dispOp.Wait();
    };

  filter.BeginInvoke(asyncOp, null, null);
}

Mit diesem Konstrukt setze ich im UI-Thread mit Hilfe des Dispatchers das “IsBusy” Property. Im Hintergrund wird dann das PropertyChanged Event gefeuert. Dies geschieht asynchron durch den Aufruf der Methode BeginInvoke der Dispatcher-Klasse. In dem Completed Event vom Dispatcher bzw. der DispatcherOperation wird der AsyncOperation gesagt, dass sie fertig ist und übergeben eine Callback-Methode. Diese Callback-Methode ist die Funktion zum Filtern. Mit dem Wait() der DispatcherOperation wird auf den Abschluss der Operation gewartet und führt quasi die Operation aus. Zuletzt beginnen wir die kapselnde AsyncOperation.

Des Pudels Kern liegt in den Cross-Thread-Zugriffen. Innerhalb eines neu erstellten Threads

new Thread(threadMethod).Start()

können keine Properties eines anderen Threads geändert werden. In diesem Beispiel wird indirekt durch die AsyncOperation ein Thread erzeugt. Das heißt, es wird ein nebenläufiger Thread erstellt, in dem über den Dispatcher ein Property gesetzt und die Oberfläche informiert wird und nach Abschluss dessen, der Thread abgeschlossen wird und beim zurückkommen einen Callback ausführen kann.

Auf diese Art und Weise kann man unter WPF, z.B. bei Datenbankzugriffen, ein gleiches Verhalten wie mit asynchron aufgerufenen WebService-Methoden erstellen.

Mit Microsoft Visual Studio Async CTP stehen nun auch APIs zur Verfügung, um solche Aufgaben einfacher zu bewerkstelligen.