2016-08-02 11 views
5

Ich habe versucht, einen MVVM-Bildschirm für eine WPF-Anwendung zu schreiben, mit den asynchronen & erwarten Schlüsselwörter zu schreiben asynchrone Methoden für 1. Anfängliches Laden von Daten, 2. Aktualisieren der Daten, 3. Speichern von Änderungen und dann erfrischend. Obwohl ich funktioniere, ist der Code sehr unordentlich und ich kann mir nicht helfen zu denken, dass es eine bessere Implementierung geben muss. Kann jemand eine einfachere Implementierung empfehlen?MVVM Async erwarten Muster

Dies ist eine abgespeckte Version meiner Ansichtsmodell:

public class ScenariosViewModel : BindableBase 
{ 
    public ScenariosViewModel() 
    { 
     SaveCommand = new DelegateCommand(async() => await SaveAsync()); 
     RefreshCommand = new DelegateCommand(async() => await LoadDataAsync()); 
    } 

    public async Task LoadDataAsync() 
    { 
     IsLoading = true; //synchronously set the busy indicator flag 
     await Task.Run(() => Scenarios = _service.AllScenarios()) 
      .ContinueWith(t => 
      { 
       IsLoading = false; 
       if (t.Exception != null) 
       { 
        throw t.Exception; //Allow exception to be caught on Application_UnhandledException 
       } 
      }); 
    } 

    public ICommand SaveCommand { get; set; } 
    private async Task SaveAsync() 
    { 
     IsLoading = true; //synchronously set the busy indicator flag 
     await Task.Run(() => 
     { 
      _service.Save(_selectedScenario); 
      LoadDataAsync(); // here we get compiler warnings because not called with await 
     }).ContinueWith(t => 
     { 
      if (t.Exception != null) 
      { 
       throw t.Exception; 
      } 
     }); 
    } 
} 

IsLoading zur Ansicht freigelegt wird, wo es an einer viel befahrenen Indikator gebunden ist.

LoadDataAsync wird vom Navigationsframework aufgerufen, wenn der Bildschirm zum ersten Mal angezeigt wird oder wenn eine Aktualisierungsschaltfläche gedrückt wird. Diese Methode sollte IsLoading synchron festlegen und anschließend die Steuerung an den UI-Thread zurückgeben, bis der Dienst die Daten zurückgegeben hat. Abschließend werden alle Ausnahmen ausgelöst, damit sie vom globalen Ausnahme-Handler abgefangen werden können (nicht zur Diskussion!).

SaveAync wird über eine Schaltfläche aufgerufen und übergibt aktualisierte Werte aus einem Formular an den Dienst. Es sollte synchron IsLoading festlegen, die Save-Methode für den Dienst asynchron aufrufen und dann eine Aktualisierung auslösen.

+1

Hast du das überprüft? https://msdn.microsoft.com/en-us/magazine/dn605875.aspx. – sam

+0

Ja, es ist ein toller Artikel. Ich bin mir nicht sicher, ob ich es mag, an Something.Result zu binden, aber ich habe das Gefühl, dass das ViewModel seinen Zustand deutlicher machen sollte. – waxingsatirical

+0

Nur eine Idee zu versuchen ... Machen Sie eine Standard-Getter-Eigenschaft und warten auf etwas warten. Binden mit IsAsync = true. – sam

Antwort

8

Es gibt ein paar Probleme in dem Code, den ich herausspringen:

  • Verwendung von ContinueWith. ContinueWith ist eine gefährliche API (es hat einen überraschenden Standardwert für seine TaskScheduler, so sollte es wirklich nur verwendet werden, wenn Sie eine TaskScheduler angeben). Es ist auch einfach peinlich im Vergleich zu dem entsprechenden Code await.
  • Einstellung Scenarios aus einem Thread-Pool-Thread. Ich folge immer der Richtlinie in meinem Code, dass datengebundene VM-Eigenschaften als Teil der Benutzeroberfläche behandelt werden und nur vom UI-Thread aus zugegriffen werden darf. Es gibt Ausnahmen zu dieser Regel (insbesondere bei WPF), aber sie sind nicht auf jeder MVVM-Plattform gleich (und sind von vornherein ein fragwürdiges Design, IMO). Daher behandle ich VMs nur als Teil der UI-Ebene.
  • Wo die Ausnahmen ausgelöst werden. Laut dem Kommentar, möchten Sie Ausnahmen auf Application.UnhandledException ausgelöst, aber ich glaube nicht, dass dieser Code das tun wird. Unter der Annahme, TaskScheduler.Current ist null zu Beginn der LoadDataAsync/SaveAsync, dann ist die Wiederbeschaffung Ausnahmecode tatsächlich die Ausnahme in einem Thread Thread-Pool erhöhen, nicht die UI Fäden, so dass es zu AppDomain.UnhandledException anstatt Application.UnhandledException senden.
  • Wie die Ausnahmen erneut ausgelöst werden. Sie werden Ihre Stack-Trace verlieren.
  • Aufruf LoadDataAsync ohne await. Mit diesem vereinfachten Code wird es wahrscheinlich funktionieren, aber es führt die Möglichkeit ein, nicht behandelte Ausnahmen zu ignorieren. Insbesondere, wenn einer der synchronen Teil von LoadDataAsync wirft, dann würde diese Ausnahme stillschweigend ignoriert werden.

Statt um mit dem manuellen-Ausnahme-rethrows von Messing, empfehle ich nur den natürlichen Ansatz Ausnahme Ausbreitung durch await:

  • Wenn ein asynchronen Vorgang fehlschlägt, ist die Aufgabe, eine Ausnahme wird gelegt darauf.
  • await wird diese Ausnahme untersuchen und es ordnungsgemäß (unter Beibehaltung der ursprünglichen Stack-Ablaufverfolgung) erneut auslösen.
  • async void Methoden haben keine Aufgabe, auf die eine Ausnahme zu platzieren, so dass sie es direkt auf ihre SynchronizationContext re-raise. Da Ihre async void-Methoden im UI-Thread ausgeführt werden, wird in diesem Fall die Ausnahme an Application.UnhandledException gesendet.

(die async void Methoden, die ich mich beziehe, sind die async Delegierten DelegateCommand bestanden).

Der Code jetzt wird:

public class ScenariosViewModel : BindableBase 
{ 
    public ScenariosViewModel() 
    { 
    SaveCommand = new DelegateCommand(async() => await SaveAsync()); 
    RefreshCommand = new DelegateCommand(async() => await LoadDataAsync()); 
    } 

    public async Task LoadDataAsync() 
    { 
    IsLoading = true; 
    try 
    { 
     Scenarios = await Task.Run(() => _service.AllScenarios()); 
    } 
    finally 
    { 
     IsLoading = false; 
    } 
    } 

    private async Task SaveAsync() 
    { 
    IsLoading = true; 
    await Task.Run(() => _service.Save(_selectedScenario)); 
    await LoadDataAsync(); 
    } 
} 

Nun sind alle Probleme gelöst wurden:

  • ContinueWith wurde mit dem geeignetere await ersetzt.
  • Scenarios wird über den UI-Thread festgelegt.
  • Alle Ausnahmen werden an Application.UnhandledException anstelle von AppDomain.UnhandledException weitergegeben.
  • Ausnahmen behalten ihren ursprünglichen Stack-Trace bei.
  • Es gibt keine Aufgaben, so dass alle Ausnahmen auf die eine oder andere Art beobachtet werden.

Und der Code ist auch sauberer. IMO. :)

+1

Hallo Stephen, danke für solch eine vollständige Antwort. Dies ist eine große Verbesserung für meinen Code. – waxingsatirical

+0

Die LoadDataAsync-Methode befindet sich tatsächlich auf einer Basisklasse, die ich für meine ViewModels verwende, die eine abstrakte Methode loadData aufruft, die einen bestimmten Dienst aufruft und eine bestimmte Eigenschaft festlegt. Gibt es eine Möglichkeit, dies zu behalten und trotzdem Eigenschaften für den UI-Thread festzulegen? geschützter Abstract void loadData(); geschützter virtueller Async-Task loadDataAsync() { IsLoading = true; warten Task.Run (() => { loadData(); IsLoading = false; }); } – waxingsatirical

+0

@waxingsatirical: Sie sollten das 'IsLoading = false 'außerhalb der' Task.Run 'verschieben, aber ansonsten sollte das gut funktionieren. Beachten Sie, dass, wenn 'LoadData'' async void' ist, dies zu Problemen führt - wenn Implementierungen 'async' sein müssen, sollte die abstrakte Methode' Task' zurückgeben. –