24

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.

Bernhard Koenig
  • 1,342
  • 13
  • 23
  • Fun thing to be careful of here is handling when your property changes while a REST request is in progress. Especially because you aren't guaranteed that they will complete in the order they were invoked. – Robert Levy Jan 06 '14 at 14:21
  • Yes, that's true :) but this problem would arise no matter if I use a property setter to initiate the REST call or an event or anything else. My take is to cancel all still running requests whenever the selected item changes before submitting a new one. – Bernhard Koenig Jan 06 '14 at 14:42
  • You could instead attach a command to selection changed event instead of checking if the selected item was changed by the UI. – Paulo Morgado Jan 07 '14 at 06:27

2 Answers2

13

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!

chue x
  • 18,573
  • 7
  • 56
  • 70
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Thanks Stephen, that's an interesting approach. This also seems to be a good way to initiate async data loading out of the ViewModel's constructor. Currently I use an EventToCommand to call a method on the VM to fill the VM with data (which, of course, I also don't like very much). I think the code is cleaner than my approach. Will definitely try that out. – Bernhard Koenig Jan 06 '14 at 19:02
  • Looking at the source code for AsyncEx, I expected to see that somewhere the compiler warning CS4014 was being ignored. After playing around a bit, what actually seems to happen is that failing to `await` an `async` method call by assigning its return value to a variable (or in this case passing it to a function) causes that warning to go away. Do you know if that's intentional? – Adam Goodwin Oct 27 '16 at 03:55
  • 1
    Ok, yes it is intentional: https://msdn.microsoft.com/en-us/library/hh873131.aspx – Adam Goodwin Oct 27 '16 at 04:05
  • 1
    Yes, that approach was intentional. However, there have been some situations where keeping that task around is useful, so the [current version of that type](https://github.com/StephenCleary/Mvvm.Async/blob/master/src/Nito.Mvvm.Async/NotifyTask.cs) exposes it as a `TaskCompleted` property. – Stephen Cleary Oct 27 '16 at 13:17
  • 1
    In v5, INotifyTaskCompletion and NotifyTaskCompletion have been replaced with NotifyTask. INotifyTaskCompletion has been replaced with NotifyTask. Also in v5, if what you after is MVVM, the package you want to get is Nito.Mvvm.Async – tstojecki Apr 11 '18 at 14:48
1
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...
}
Alper Ebicoglu
  • 8,884
  • 1
  • 49
  • 55
  • Not sure if this is "hacky" but it seems to work (and is easy to understand in comparison to other XAML-MVVM "workarounds"). Have a "+". – Sed May 11 '22 at 09:37