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.
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.