Правильный способ вызова асинхронных методов из набора свойств, привязанного к данным?

Теперь я знаю, что свойства не поддерживают async/wait по уважительным причинам. Но иногда вам нужно запустить дополнительную обработку фона из средства настройки свойств - хорошим примером является привязка данных в сценарии MVVM.

В моем случае у меня есть свойство, связанное с SelectedItem ListView. Разумеется, я сразу же установил новое значение в поле поддержки и выполнил основную работу свойства. Но изменение выбранного элемента в пользовательском интерфейсе также должно инициировать вызов службы REST для получения новых данных на основе выбранного элемента.

Поэтому мне нужно вызвать метод async. Я не могу дождаться этого, очевидно, но я также не хочу запускать и забывать вызов, поскольку я мог бы пропустить исключения во время обработки асинхронизации.

Теперь мой прием следующий:

private Feed selectedFeed;
public Feed SelectedFeed
{
    get
    {
        return this.selectedFeed;
    }
    set
    {
        if (this.selectedFeed != value)
        {
            this.selectedFeed = value;
            RaisePropertyChanged();

            Task task = GetFeedArticles(value.Id);

            task.ContinueWith(t =>
                {
                    if (t.Status != TaskStatus.RanToCompletion)
                    {
                        MessengerInstance.Send<string>("Error description", "DisplayErrorNotification");
                    }
                });
        }
    }
}

Хорошо, так что помимо того, что я мог бы переместить обработку из сеттера синхронному методу, это правильный способ справиться с таким сценарием? Есть ли лучшее, менее захламленное решение, которое я не вижу?

Было бы очень интересно увидеть некоторые другие проблемы, связанные с этой проблемой. Мне немного любопытно, что мне не удалось найти других обсуждений по этой конкретной теме, поскольку мне кажется очень распространенным я в приложениях MVVM, которые сильно используют привязку данных.

Ответы

Ответ 1

У меня есть тип NotifyTaskCompletion в моей библиотеке AsyncEx, который по существу является оберткой INotifyPropertyChanged для Task/Task<T>. AFAIK в настоящее время доступно немного информации о async в сочетании с MVVM, поэтому дайте мне знать, если вы найдете какие-либо другие подходы.

В любом случае подход NotifyTaskCompletion работает лучше всего, если ваши задачи возвращают результаты. I.e., из вашего текущего примера кода выглядит, что GetFeedArticles устанавливает свойства, связанные с данными, как побочный эффект, а не возвращает статьи. Если вы сделаете этот возврат Task<T> вместо этого, вы можете получить код следующим образом:

private Feed selectedFeed;
public Feed SelectedFeed
{
  get
  {
    return this.selectedFeed;
  }
  set
  {
    if (this.selectedFeed == value)
      return;
    this.selectedFeed = value;
    RaisePropertyChanged();
    Articles = NotifyTaskCompletion.Create(GetFeedArticlesAsync(value.Id));
  }
}

private INotifyTaskCompletion<List<Article>> articles;
public INotifyTaskCompletion<List<Article>> Articles
{
  get { return this.articles; }
  set
  {
    if (this.articles == value)
      return;
    this.articles = value;
    RaisePropertyChanged();
  }
}

private async Task<List<Article>> GetFeedArticlesAsync(int id)
{
  ...
}

Затем ваша привязка данных может использовать Articles.Result для перехода к полученной коллекции (которая находится в null до завершения GetFeedArticlesAsync). Вы можете использовать NotifyTaskCompletion "из коробки" для привязки данных к ошибкам (например, Articles.ErrorMessage) и имеет несколько логических удобных свойств (IsSuccessfullyCompleted, IsFaulted) для обработки переключений видимости.

Обратите внимание, что это будет корректно обрабатывать операции, завершающиеся не по порядку. Поскольку Articles фактически представляет собой асинхронную операцию (вместо непосредственных результатов), она немедленно обновляется при запуске новой операции. Таким образом, вы никогда не увидите устаревшие результаты.

Вам не нужно использовать привязку данных для обработки ошибок. Вы можете сделать любую семантику, которая вам нужна, изменив GetFeedArticlesAsync; например, для обработки исключений, передав их в MessengerInstance:

private async Task<List<Article>> GetFeedArticlesAsync(int id)
{
  try
  {
    ...
  }
  catch (Exception ex)
  {
    MessengerInstance.Send<string>("Error description", "DisplayErrorNotification");
    return null;
  }
}

Аналогично, нет понятия автоматической отмены встроенного, но опять же легко добавить в GetFeedArticlesAsync:

private CancellationTokenSource getFeedArticlesCts;
private async Task<List<Article>> GetFeedArticlesAsync(int id)
{
  if (getFeedArticlesCts != null)
    getFeedArticlesCts.Cancel();
  using (getFeedArticlesCts = new CancellationTokenSource())
  {
    ...
  }
}

Это область текущей разработки, поэтому, пожалуйста, сделайте улучшения или предложения API!

Ответ 2

public class AsyncRunner
{
    public static void Run(Task task, Action<Task> onError = null)
    {
        if (onError == null)
        {
            task.ContinueWith((task1, o) => { }, TaskContinuationOptions.OnlyOnFaulted);
        }
        else
        {
            task.ContinueWith(onError, TaskContinuationOptions.OnlyOnFaulted);
        }
    }
}

Использование внутри свойства

private NavigationMenuItem _selectedMenuItem;
public NavigationMenuItem SelectedMenuItem
{
    get { return _selectedMenuItem; }
    set
    {
        _selectedMenuItem = val;
         AsyncRunner.Run(NavigateToMenuAsync(_selectedMenuItem));           
    }
}

private async Task NavigateToMenuAsync(NavigationMenuItem newNavigationMenu)
{
    //call async tasks...
}