2016-08-02 94 views
5

我一直在嘗試爲WPF應用程序編寫MVVM屏幕,使用異步&等待關鍵字爲1編寫異步方法。最初加載數據,2.刷新數據,3.保存更改並然後清爽。雖然我有這個工作,但代碼非常混亂,我不禁想到必須有更好的實現。任何人都可以建議一個更簡單的實現?MVVM異步等待模式

這是我的視圖模型的簡化版本:

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暴露到勢必繁忙指標的看法。

LoadDataAsync在第一次查看屏幕或按下刷新按鈕時由導航框架調用。此方法應同步設置IsLoading,然後將控制權返回給UI線程,直到服務返回數據。最後拋出任何異常,以便它們可以被全局異常處理程序捕獲(不需要討論!)。

SaveAync由按鈕調用,將更新後的值從表單傳遞到服務。它應該同步設置IsLoading,異步調用服務上的Save方法,然後觸發刷新。

+1

你檢查了嗎? https://msdn.microsoft.com/en-us/magazine/dn605875.aspx。 – sam

+0

是的,這是一篇很棒的文章。我不確定我喜歡綁定到Something.Result,儘管如此,感覺像ViewModel應該使它的狀態比這更明顯。 – waxingsatirical

+0

只是一個想法嘗試...做一個標準的只有getter屬性和在等待的東西。使用IsAsync = true綁定。 – sam

回答

8

裏有代碼,跳出來了我幾個問題:的ContinueWith

  • 用法。 ContinueWith是一個危險的API(它的默認值爲TaskScheduler,因此只有在指定TaskScheduler時才能使用)。與等價的代碼await相比,它也很笨拙。
  • 從線程池線程設置Scenarios。我始終遵循我的代碼中的指導原則,即將數據綁定的VM屬性視爲UI的一部分,並且只能從UI線程訪問。這條規則(特別是WPF)有例外,但是它們在每個MVVM平臺上都不相同(並且是一個有疑問的設計,IMO開始),所以我只是將VM視爲UI層的一部分。
  • 拋出異常的地方。根據評論,你想要異常提高到Application.UnhandledException,但我不認爲這個代碼會這樣做。假設TaskScheduler.CurrentnullLoadDataAsync/SaveAsync開始,然後再提高異常代碼實際上將提高在線程池線程,而不是UI線程異常,從而將其發送到AppDomain.UnhandledException而非Application.UnhandledException
  • 如何重新拋出異常。你會失去你的堆棧跟蹤。
  • 撥打LoadDataAsync不需要await。有了這個簡化的代碼,它可能會工作,但它確實會引入忽略未處理異常的可能性。特別是,如果LoadDataAsync的任何同步部分都拋出,那麼該異常將被默默忽略。

而是用手動異常重新拋出亂搞的,我建議只使用異常傳播的更自然的方法,通過await

  • 如果異步操作失敗,這項工作變得置於一個異常在上面。
  • await將檢查此異常,並以適當的方式重新提升它(保留原始堆棧跟蹤)。
  • async void方法沒有任何放置異常的任務,所以他們會直接在SynchronizationContext上重新提升它。在這種情況下,由於您的async void方法在UI線程上運行,異常將發送到Application.UnhandledException

(在async void方法,我指的是async代表傳遞給DelegateCommand)。

代碼現在變爲:

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

現在所有的問題都已經解決:

  • ContinueWith已經被替換爲更合適的await
  • Scenarios是從UI線程設置的。
  • 所有異常傳播到Application.UnhandledException而不是AppDomain.UnhandledException
  • 異常保持其原始堆棧跟蹤。
  • 沒有任何un-await -ed任務,所以所有異常都會以某種方式被觀察到。

而且代碼也比較乾淨。 IMO。 :)

+1

您好Stephen,謝謝您提供這樣一個完整的答案。這對我的代碼有很大的改進。 – waxingsatirical

+0

LoadDataAsync方法實際上位於我用於ViewModel的基類上,該類調用抽象方法loadData,該方法調用特定服務並設置特定屬性。有什麼辦法可以保留這個,並仍然在UI線程上設置屬性? protected abstract void loadData(); 受保護的虛擬異步任務loadDataAsync() { IsLoading = true; 正在等待Task.Run(()=> { loadData(); IsLoading = false; }); } – waxingsatirical

+0

@waxingsatirical:您希望將「IsLoading = false」移動到「Task.Run」之外,否則應該正常工作。請注意,如果'LoadData'是'async void',那麼這將導致問題 - 如果實現需要是'async',那麼抽象方法應該返回'Task'。 –