2016-08-02 42 views
5

Próbowałem napisać ekran MVVM dla aplikacji WPF, używając asynchronicznego & czekać na słowa kluczowe, aby napisać metody asynchroniczne dla 1. Początkowo ładowanie danych, 2. Odświeżanie danych, 3. Zapisywanie zmian i następnie odświeżanie. Chociaż mam to działa, kod jest bardzo brudny i nie mogę przestać myśleć, że musi być lepsza implementacja. Czy ktoś może doradzić w sprawie prostszej implementacji?Async MVVM czeka na wzór

To jest wersja cut-dół mojego ViewModel:

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 narażona jest na widoku, w którym jest on związany ze wskaźnikiem zajętości.

LoadDataAsync jest wywoływana przez strukturę nawigacji po pierwszym wyświetleniu ekranu lub po naciśnięciu przycisku odświeżania. Ta metoda powinna synchronicznie ustawiać IsLoading, a następnie zwracać kontrolę do wątku interfejsu użytkownika, dopóki usługa nie zwróci danych. Wreszcie rzucając wszelkie wyjątki, aby mogły zostać przechwycone przez globalną obsługę wyjątków (nie do dyskusji!).

Funkcja SaveAync jest wywoływana za pomocą przycisku, przekazując zaktualizowane wartości z formularza do usługi. Powinien on synchronicznie ustawić IsLoading, asynchronicznie wywołać metodę Save w usłudze, a następnie uruchomić odświeżanie.

+1

Czy to sprawdziłeś? https://msdn.microsoft.com/en-us/magazine/dn605875.aspx. – sam

+0

Tak, to świetny artykuł. Nie jestem pewien, czy lubię wiązać się z Something.Result, ale czuję, że ViewModel powinien sprawić, że jego stan stanie się bardziej oczywisty. – waxingsatirical

+0

Po prostu pomysł, aby spróbować ... Zrób standardową właściwość gettera i poczekaj na Coś. Powiąż używając IsAsync = true. – sam

Odpowiedz

8

Istnieje kilka problemów w kodzie, które wyskakują mi:

  • wykorzystanie ContinueWith. ContinueWith jest niebezpiecznym API (ma zaskakującą wartość domyślną dla jego TaskScheduler, więc powinno być używane tylko wtedy, gdy podasz TaskScheduler). Jest to również po prostu niezręczne w porównaniu do równoważnego kodu await.
  • Ustawienie Scenarios z wątku puli wątków. Zawsze postępuję zgodnie z wytycznymi w moim kodzie, że związane z danymi właściwości maszyn wirtualnych są traktowane jako część interfejsu użytkownika i dostęp do nich można uzyskać wyłącznie z poziomu wątku interfejsu użytkownika. Istnieją wyjątki od tej reguły (w szczególności w przypadku WPF), ale nie są one takie same na każdej platformie MVVM (i na początku są to wątpliwe projekty, IMO), więc traktuję maszyny wirtualne jako część warstwy interfejsu użytkownika.
  • Gdzie są zgłaszane wyjątki. Zgodnie z komentarzem, chcesz wyjątków podniesiony do Application.UnhandledException, ale nie sądzę, że ten kod to zrobi. Zakładając TaskScheduler.Current jest null na początku LoadDataAsync/SaveAsync, a następnie ponowne podniesienie kod wyjątku rzeczywiście podnieść wyjątek na wątku basen nici, nie UI wątku, więc wysłanie go do AppDomain.UnhandledException zamiast Application.UnhandledException.
  • W jaki sposób wyjątki są ponownie zgłaszane. Stracisz ślad stosu.
  • Wywołanie LoadDataAsync bez numeru await. Dzięki temu uproszczonemu kodowi prawdopodobnie zadziała, ale wprowadzi możliwość ignorowania nieobsługiwanych wyjątków. W szczególności, jeśli którykolwiek z synchronicznych części LoadDataAsync rzuca, wtedy ten wyjątek byłby cicho ignorowany.

Zamiast bawić się z ręcznego wyjątku ponownie generuje, polecam tylko przy użyciu bardziej naturalne podejście propagacji wyjątku przez await:

  • Jeśli asynchronicznym operacja nie powiedzie się, zadanie staje się wyjątek umieszczonej na tym.
  • await zbada ten wyjątek i ponownie go uniesie we właściwy sposób (zachowując oryginalny ślad stosu).
  • async void metody nie mają zadania, na które można wstawić wyjątek, więc będą ponownie je podnosić bezpośrednio na swoim SynchronizationContext. W takim przypadku, ponieważ Twoje metody async void są uruchamiane w wątku interfejsu użytkownika, wyjątek zostanie wysłany pod numer Application.UnhandledException.

(na async void metody, o których piszę są async delegaci przeszli do DelegateCommand).

Kod staje się teraz:

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(); 
    } 
} 

Teraz wszystkie problemy zostały rozwiązane:

  • ContinueWith został zastąpiony bardziej odpowiednim await.
  • Scenarios jest ustawiany z wątku interfejsu użytkownika.
  • Wszystkie wyjątki są propagowane do Application.UnhandledException, a nie do AppDomain.UnhandledException.
  • Wyjątki zachowują swój oryginalny ślad stosu.
  • Nie ma żadnych zadań, więc wszystkie wyjątki będą przestrzegane w taki czy inny sposób.

Kod jest również bardziej przejrzysty. IMO. :)

+1

Witaj Stephen, dziękuję za tak kompletną odpowiedź. Jest to doskonała poprawa mojego kodu. – waxingsatirical

+0

Metoda LoadDataAsync jest faktycznie w klasie bazowej używam dla moich ViewModels, który wywołuje abstrakcyjną metodę loadData, która wywołuje określoną usługę i ustawia określoną właściwość. Czy jest jakiś sposób, aby zachować to i nadal ustawić właściwości na wątku interfejsu użytkownika? chronione abstrakcyjne void loadData(); chronione wirtualne asynchroniczne zadanie loadDataAsync() { IsLoading = true; czeka na Task.Run (() => { loadData(); IsLoading = false; }); } – waxingsatirical

+0

@waxingsatirical: Chciałbyś przenieść 'IsLoading = false' poza' Task.Run', ale poza tym to powinno działać dobrze. Zauważ, że jeśli 'LoadData' jest' async void', to spowoduje to problemy - jeśli implementacje muszą być "asynchroniczne", wówczas metoda abstrakcyjna powinna zwrócić 'Task'. –