2016-08-02 3 views
5

Я пытаюсь написать экран MVVM для приложения WPF, используя async & ждут ключевые слова для написания асинхронных методов для 1. Первоначально загружая данные, 2. Обновляя данные, 3. Сохраняя изменения и затем освежает. Хотя у меня это работает, код очень грязный, и я не могу не думать о том, что должна быть лучшая реализация. Может ли кто-нибудь посоветоваться о более простой реализации?MVVM async wait pattern

Это сокращенная версия моего 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 подвергается воздействию зрения, где он связан с оживленным индикатором.

LoadDataAsync вызывается каркасом навигации при первом просмотре экрана или при нажатии кнопки обновления. Этот метод должен синхронно устанавливать IsLoading, а затем возвращать управление в поток пользовательского интерфейса, пока служба не вернет данные. Наконец, бросая любые исключения, чтобы их можно было поймать глобальным обработчиком исключений (не обсуждать!).

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 рассматриваются как часть пользовательского интерфейса и должны быть доступны только из потока пользовательского интерфейса. Есть исключения из этого правила (в частности, WPF), но они не одинаковы на каждой платформе MVVM (и это вопрос, с которого нужно начинать с IMO), поэтому я просто рассматриваю виртуальные машины как часть слоя пользовательского интерфейса.
  • Куда исключены исключения. Согласно комментарию, вы хотите, чтобы исключения были подняты до Application.UnhandledException, но я не думаю, что этот код сделает это. Предполагая, что TaskScheduler.Current является null в начале LoadDataAsync/SaveAsync, то повторное повышение код исключения будет на самом деле поднять исключение на пул потоков нить, не UI нити, тем самым отправив его на AppDomain.UnhandledException, а не Application.UnhandledException.
  • Как исключаются повторные исключения. Вы потеряете трассировку стека.
  • Вызов LoadDataAsync без await. С помощью этого упрощенного кода он, вероятно, будет работать, но он вводит возможность игнорирования необработанных исключений. В частности, если какая-либо из синхронной части LoadDataAsync выбрасывает, то это исключение будет игнорироваться молча.

Вместо того, чтобы возиться с ручным исключениями rethrows, я рекомендую просто использовать более естественный подход распространения исключений через await:

  • Если асинхронная операция не выполняется, то задача получает исключение помещается в теме.
  • await рассмотрит это исключение и вернет его надлежащим образом (сохраняя исходную трассировку стека).
  • async void методы не имеют задачи для размещения исключения, поэтому они будут повторно поднимать его непосредственно на их SynchronizationContext. В этом случае, поскольку ваши методы async void запускаются в потоке пользовательского интерфейса, исключение будет отправлено на 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 установлен из нити пользовательского интерфейса.
  • Все исключения распространяются на Application.UnhandledException, а не AppDomain.UnhandledException.
  • Исключения сохраняют исходную трассировку стека.
  • Нет никаких заданий un-await, поэтому все исключения будут наблюдаться так или иначе.

И код также чище. ИМО. :)

+1

Привет, Стивен, спасибо за такой полный ответ. Это большое улучшение в моем коде. – waxingsatirical

+0

Метод LoadDataAsync на самом деле относится к базовому классу, который я использую для своих ViewModels, который вызывает абстрактный метод loadData, который вызывает определенную службу и задает конкретное свойство. Есть ли способ сохранить это и по-прежнему устанавливать свойства в потоке пользовательского интерфейса? защищенная абстрактная пустота loadData(); защищенная виртуальная асинхронная задача loadDataAsync() { IsLoading = true; wait Task.Run (() => { loadData(); IsLoading = false; }); } – waxingsatirical

+0

@waxingsatirical: вы хотите переместить 'IsLoading = false' вне' Task.Run', но в противном случае это должно работать нормально. Обратите внимание, что если 'LoadData' является' async void', то это вызовет проблемы - если реализации должны быть 'async', тогда абстрактный метод должен возвращать' Task'. –