I am having difficulties with binding and INotifyPropertyChanged. I have a ListView with is bound to an ObservableCollection and there is no problem at startup: data is correctly added to the ListView. When I add a new item to the collection, however, it doesn't update the UI. I'm sure the collection contains the object because I added a button to show the whole content of the collection.
Here is my UI code:
<StackPanel>
<Button Content="Show title" Tapped="Button_Tapped"/>
<ListView ItemsSource="{Binding Subscriptions}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Image Grid.RowSpan="2" Margin="0 0 10 0"
Source="{Binding IconUri.AbsoluteUri}"/>
<TextBlock Grid.Column="1"
Text="{Binding Title.Text}" Style="{StaticResource BaseTextBlockStyle}"/>
<TextBlock Grid.Row="1" Grid.Column="1"
Text="{Binding LastUpdatedTime.DateTime}"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
And here is the data context class:
class RssReaderData : INotifyPropertyChanged
{
private string[] acceptContentTypes = {
"application/xml",
"text/xml"
};
private ObservableCollection<SyndicationFeed> _subscriptions;
public ObservableCollection<SyndicationFeed> Subscriptions
{
get { return _subscriptions; }
set { NotifyPropertyChanged(ref _subscriptions, value); }
}
public int SubscriptionsCount
{
get { return Subscriptions.Count; }
}
public RssReaderData()
{
Subscriptions = new ObservableCollection<SyndicationFeed>();
AddFeedAsync(new Uri("http://www.theverge.com/rss/index.xml"));
AddFeedAsync(new Uri("http://blogs.microsoft.com/feed/"));
}
public async Task<bool> AddFeedAsync(Uri uri)
{
// Download the feed at uri
HttpClient client = new HttpClient();
var response = await client.GetAsync(uri);
// Check that we retrieved the resource without error and that the resource has XML content
if (!response.IsSuccessStatusCode || !acceptContentTypes.Contains(response.Content.Headers.ContentType.MediaType))
return false;
var xmlFeed = await response.Content.ReadAsStringAsync();
// Create a new SyndicationFeed and load the XML to it
SyndicationFeed newFeed = new SyndicationFeed();
newFeed.Load(xmlFeed);
// If the title hasn't been set, the feed is invalid
if (String.IsNullOrEmpty(newFeed.Title.Text))
return false;
Subscriptions.Add(newFeed);
return true;
}
#region INotifyPropertyChanged management
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public bool NotifyPropertyChanged<T> (ref T variable, T value, [CallerMemberName] string propertyName = null)
{
if (object.Equals(variable, value)) return false;
variable = value;
NotifyPropertyChanged(propertyName);
return true;
}
#endregion
}
As you can see I implemented the INotifyPropertyChanged interface while I think I shouldn't even have to (the ObservableCollection does that for me). I don't care about notifying the changes in the items I add to my collection, what I need is to notify when a new item is added to it.
I would say that my code is OK as is, but it seems not and I don't see why :-/
Also, while I'm at it, I have two quick questions: what's are the differences between a ListView and a ListBox and between a Grid and a GridView ?
Thank you for you help :-)
EDIT : as requested, here's the code-behind of the page
RssReaderData context = new RssReaderData();
public FeedsPage()
{
this.InitializeComponent();
this.NavigationCacheMode = NavigationCacheMode.Required;
}
private async void Button_Tapped(object sender, TappedRoutedEventArgs e)
{
string feedsTitles = "\n";
foreach (var feed in context.Subscriptions)
{
feedsTitles += "\n " + feed.Title.Text;
}
MessageDialog d = new MessageDialog("There are " + context.SubscriptionsCount + " feeds:" + feedsTitles);
await d.ShowAsync();
}
private async void NewFeedSubscribeButton_Tapped(object sender, TappedRoutedEventArgs e)
{
string feedUri = NewFeedUriInput.Text;
if (String.IsNullOrEmpty(feedUri))
return;
if (!Uri.IsWellFormedUriString(feedUri, UriKind.Absolute))
{
MessageDialog d = new MessageDialog("The URL you entered is not valid. Please check it and try again.", "URL Error");
await d.ShowAsync();
return;
}
bool feedSubscribed = await context.AddFeedAsync(new Uri(feedUri));
if (feedSubscribed)
{
NewFeedUriInput.Text = String.Empty;
FeedsPivot.SelectedIndex = 0;
}
else
{
MessageDialog d = new MessageDialog("There was an error fetching the feed. Are you sure the URL is referring to a valid RSS feed?", "Subscription error");
await d.ShowAsync();
return;
}
}
private void FeedsList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (FeedsList.SelectedIndex > -1 && FeedsList.SelectedIndex < context.SubscriptionsCount)
{
Frame.Navigate(typeof(FeedDetailsPage), context.Subscriptions[FeedsList.SelectedIndex]);
}
}
It turned out that you have created two instances of RssReaderData - one in code behind and one in xaml with:
<Page.DataContext>
<DataModel:RssReaderData/>
</Page.DataContext
In this situation the collection to which your ListView is bound to is not the same you refer in the code behind - context
.
The simple solution may be to remove above lines from XAML and set DataContext in the code behind:
public FeedsPage()
{
this.InitializeComponent();
this.NavigationCacheMode = NavigationCacheMode.Required;
this.DataContext = context;
}
The other case is that it also probably may not update properly the UI, as KooKiz has pointed out - better would be first to create the item, then add it to a collection.
You may also take a look at this question and its answers which deals with problem when item changes inside ObservableCollection.