Question

Now I know properties do not support async/await for good reasons. But sometimes you need to kick off some additional background processing from a property setter - a good example is data binding in a MVVM scenario.

In my case, I have a property that is bound to the SelectedItem of a ListView. Of course I immediately set the new value to the backing field and the main work of the property is done. But the change of the selected item in the UI needs also to trigger a REST service call to get some new data based on the now selected item.

So I need to call an async method. I can't await it, obviously, but I also do not want to fire and forget the call as I could miss exceptions during the async processing.

Now my take is the following:

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

Ok so besides the fact I could move out the handling from the setter to a synchronous method, is this the correct way to handle such a scenario? Is there a better, less cluttered solution I do not see?

Would be very interested to see some other takes on this problem. I'm a bit curious that I was not able to find any other discussions on this concrete topic as it seems very common to me in MVVM apps that make heavy use of databinding.

Était-ce utile?

La solution

I have a NotifyTaskCompletion type in my AsyncEx library that is essentially an INotifyPropertyChanged wrapper for Task/Task<T>. AFAIK there is very little information currently available on async combined with MVVM, so let me know if you find any other approaches.

Anyway, the NotifyTaskCompletion approach works best if your tasks return their results. I.e., from your current code sample it looks like GetFeedArticles is setting data-bound properties as a side effect instead of returning the articles. If you make this return Task<T> instead, you can end up with code like this:

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)
{
  ...
}

Then your databinding can use Articles.Result to get to the resulting collection (which is null until GetFeedArticlesAsync completes). You can use NotifyTaskCompletion "out of the box" to data-bind to errors as well (e.g., Articles.ErrorMessage) and it has a few boolean convenience properties (IsSuccessfullyCompleted, IsFaulted) to handle visibility toggles.

Note that this will correctly handle operations completing out of order. Since Articles actually represents the asynchronous operation itself (instead of the results directly), it is updated immediately when a new operation is started. So you'll never see out-of-date results.

You don't have to use data binding for your error handling. You can make whatever semantics you want by modifying the GetFeedArticlesAsync; for example, to handle exceptions by passing them to your MessengerInstance:

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

Similarly, there's no notion of automatic cancellation built-in, but again it's easy to add to GetFeedArticlesAsync:

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

This is an area of current development, so please do make improvements or API suggestions!

Autres conseils

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

Usage within the property

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...
}
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top