Loading data in ViewModel asynchronously (with async and await) not working with databinding

user2137225 picture user2137225 · Mar 9, 2013 · Viewed 20.6k times · Source

I started a phone app with the default template which has a view model already defined. I modified the MainViewModel's LoadData() method to call an odata service asynchronously. But it is not working with the databinding. I have verified that the call returned successfully but no result is displayed.

The LongListSelector's items source is bound to the Items property in the view model.

<phone:LongListSelector ItemsSource="{Binding Items}" x:Name="MainLongListSelector" Margin="0,0,-12,0" SelectionChanged="MainLongListSelector_SelectionChanged">
                <phone:LongListSelector.ItemTemplate>
                    <DataTemplate>
                      <StackPanel Margin="0,0,0,17">
                            <TextBlock Text="{Binding UnReadCount}" TextWrapping="Wrap" Style="{StaticResource PhoneTextExtraLargeStyle}"/>
                            <TextBlock Text="{Binding description}" TextWrapping="Wrap" Margin="12,-6,12,0" Style="{StaticResource PhoneTextSubtleStyle}"/>
                      </StackPanel>
                    </DataTemplate>
                </phone:LongListSelector.ItemTemplate>
            </phone:LongListSelector>

Here's my modification to the view model (note the async and await usage):

public void LoadData()
    {
        FetchTileViewItems();        
    }

    private async void FetchTileViewItems()
    {
        var ret = await I2ADataServiceHelper.GetTileViewItemsAsync();
        this.Items = new ObservableCollection<TileViewItem>(ret);
        this.IsDataLoaded = true;
    }

And I'm calling the LoadData() method in the NavigatedTo event on the page just like before:

protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            if (!App.ViewModel.IsDataLoaded)
            {
                App.ViewModel.LoadData();
                pr1.IsVisible = false;
            }
        }

Hit run and nothing shows up...Am I missing anything? Any pointers are greatly appreciated.

Answer

Stephen Cleary picture Stephen Cleary · Mar 9, 2013

OK, the quick answer is that you're probably missing INotifyPropertyChanged notifications on your Items and/or IsDataLoaded setters.

The longer answer will take a bit. :)

First, you should avoid async void. I describe why in detail in my Best Practices in Asynchronous Programming article. In this case, consider your error handling. It's nice that your happy case is when "the call returned successfully" but the sad case will tear your program up.

So, let's rewrite everything as async Task as much as possible, and follow the *Async convention while we're at it:

public async Task LoadDataAsync()
{
    await FetchTileViewItemsAsync();
}

private async Task FetchTileViewItemsAsync()
{
    var ret = await I2ADataServiceHelper.GetTileViewItemsAsync();
    this.Items = new ObservableCollection<TileViewItem>(ret);
    this.IsDataLoaded = true;
}

protected override async void OnNavigatedTo(NavigationEventArgs e)
{
    if (!App.ViewModel.IsDataLoaded)
    {
        await App.ViewModel.LoadDataAsync();
    }
}

This is the more natural way to write async code.

Next, let's fix up that error situation. You can do a try/catch in OnNavigatedTo:

protected override async void OnNavigatedTo(NavigationEventArgs e)
{
    try
    {
        if (!App.ViewModel.IsDataLoaded)
        {
            await App.ViewModel.LoadDataAsync();
        }
    }
    catch (Exception ex)
    {
        ...
    }
}

But I actually lean more towards a ViewModel-centric, databinding-friendly system for error handling. That way, "disconnected" is a perfectly natural state for your application; even if all it does is display an error message, your application ends up being designed for a occasionally-connected system (i.e., a phone). Also, the resulting code is more testable.

I describe this approach in a couple of my blog posts: I cover the asynchronous initialization pattern in my post on async constructors, and the data-binding in particular in my post on async properties. I wrote a helper class called TaskCompletionNotifier which enables you to use Task with data binding.

Putting these designs in place, your ViewModel code ends up looking more like this:

public sealed class MyViewModel : INotifyPropertyChanged
{
    public ObservableCollection<TileViewItem> Items
    {
      get { return _items; }
      private set { _items = value; RaisePropertyChanged(); }
    }

    public ITaskCompletionNotifier Initialization { get; private set; }

    public MyViewModel()
    {
        Initialization = TaskCompletionNotifierFactory.Create(InitializeAsync());
    }

    private async Task InitializeAsync()
    {
        var ret = await I2ADataServiceHelper.GetTileViewItemsAsync();
        this.Items = new ObservableCollection<TileViewItem>(ret);
    }
}

(This is assuming you want to start loading data in the constructor.)

You can then bind to Items directly, and you can also bind to Initialization.IsSuccessfullyCompleted for the happy case, Initialization.IsFaulted and Initialization.ErrorMessage for the sad case, etc.