Search code examples
c#listviewwindows-phonewindows-phone-8.1

Bound ObservableCollection doesn't update ListView


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

Solution

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