Abbas
Abbas

Reputation: 14432

(Truly)ObservableCollection<T> not updating the UI

I have a list of objects of type Emblem that I show in a LongListMultiSelector. I only want to show the ones that are not achieved yet. I can select one or more items and change them to IsAchieved = true but the problem is that they don't disappear immediately, the UI is not updated automatically.

I thought this wouldn't be a problem since I used the ObservableCollection<T>. Then I found out that if the property of an item changes, the collection is not notified. As a result a implemented the INotifyPropertyChanged interface but this doesn't work either.

Here on SO I have found following questions (and more) that share this problem:

I also tried implementing the usage of the TrulyObservableCollection<T> but also no result. Here's what I have

XAML control:

<toolkit:LongListMultiSelector Name="EmblemsList"
                               ItemsSource="{Binding Emblems}"
                               Background="Transparent"
                               LayoutMode="List"
                               ItemTemplate="{StaticResource ItemTemplate}" />

Items are bound through the EmblemsViewModel:

public class EmblemsViewModel
{
    public EmblemsViewModel()
    {
        Emblems = new TrulyObservableCollection<Emblem>();
    }

    public TrulyObservableCollection<Emblem> Emblems { get; set; }
}

//Usage on the page
DataContext = new EmblemsViewModel { Emblems = DB.GetEmblems() }

The Emblem class is as follows:

public class Emblem : Achievement
{
    public int Level { get; set; }
}

public abstract class Achievement : INotifyPropertyChanged
{
    private bool _isAchieved;

    public string Description { get; set; }

    public bool IsAchieved
    {
        get { return _isAchieved; }
        set
        {
            if (_isAchieved != value)
            {
                _isAchieved = value;
                NotifyPropertyChanged("IsAchieved");
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    private void NotifyPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }

}

What am I missing/doing wrong that prevents this from working?

Update:

I've applied a CollectionViewSource to apply the filtering but now NO items are shown.

//Reference to the CollectionViewSource
_viewSource = (CollectionViewSource)Resources["EmblemsViewSource"];

//3 options in the ListBox: all, achieved & unachieved
private void FilterListBoxSelectionChanged(object sender, SelectionChangedEventArgs e)
{
    var selectedItem = ((ListBoxItem)FilterListBox.SelectedItem).Content.ToString();

    switch (selectedItem)
    {
        case "achieved": _filter = Filter.Achieved; _viewSource.Filter += new FilterEventHandler(CollectionViewSource_Filter); break;
        case "unachieved": _filter = Filter.Unachieved; _viewSource.Filter += new FilterEventHandler(CollectionViewSource_Filter); break;
        default: _filter = Filter.All; _viewSource.Filter -= new FilterEventHandler(CollectionViewSource_Filter); break;
    } 
}

private void CollectionViewSource_Filter(object sender, FilterEventArgs e)
{
    var item = e.Item as Emblem;

    switch (_filter)
    {
        case Filter.Achieved: e.Accepted = item.IsAchieved; break;
        case Filter.Unachieved: e.Accepted = !item.IsAchieved; break;
        case Filter.All: e.Accepted = true; break;
    }
}

XAML:

<CollectionViewSource x:Key="EmblemsViewSource" Source="{Binding Emblems}" />

<toolkit:LongListMultiSelector Name="EmblemsList"
                               ItemsSource="{Binding Source={StaticResource EmblemsViewSource}}"
                               Background="Transparent"
                               LayoutMode="List"
                               ItemTemplate="{StaticResource ItemTemplate}" />

Upvotes: 1

Views: 1674

Answers (3)

Rico Suter
Rico Suter

Reputation: 11858

I have implemented an ObservableCollectionView class on which you can set a Filter (a predicate) and which can track changes of the contained items and refilter if an item has changed...

Have a look at https://mytoolkit.codeplex.com/wikipage?title=ObservableCollectionView

Upvotes: 1

MarcE
MarcE

Reputation: 3731

You only need to set up a filter on a collection once, not every time the filter option changes. A single call to

_viewSource.Filter += new FilterEventHandler(CollectionViewSource_Filter);

should be all you need, then in your list box selection changed you can call _viewSource.Refresh() to force the filter predicate to re-evaluate the list items.

Another option may be to have the XAML data template that represents the emblem bind a visibility property direct to the IsAchieved property of the Emblem using a converter:

<DataTemplate>
  <Border Visibility="{Binding IsAchieved, Converter={StaticResource BoolVisibilityConverter}}">
...

Where BoolVisibilityConverter is a ValueConverter.

You'll have to try that to see if it scales for your scenario - running a lot of value converters can hurt with large data sets, but it has the advantage of being simple!

Upvotes: 1

t3chb0t
t3chb0t

Reputation: 18626

One solution could be that you create a new collection derived from the ObservableCollection and add a new property eg. FilteredItems.

MainWindow:

public partial class MainWindow : Window
{
    FilterableObservableCollection items;

    public MainWindow()
    {

        items = new FilterableObservableCollection()
        {
            new ListViewItem() { Name = "Hallo", IsArchived = false },
            new ListViewItem() { Name = "world", IsArchived = true },
            new ListViewItem() { Name = "!!!", IsArchived = false }
        };

        InitializeComponent();
    }

    public FilterableObservableCollection MyItems
    {
        get { return items; }
    }

    private void CheckBox_Checked(object sender, RoutedEventArgs e)
    {
        items.NotArchivedOnlyFilterEnabled = (sender as CheckBox).IsChecked.Value;
    }

    private void CheckBox_Unchecked(object sender, RoutedEventArgs e)
    {
        items.NotArchivedOnlyFilterEnabled = (sender as CheckBox).IsChecked.Value;
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        items.Add(new ListViewItem() { Name = "Item" + (items.Count + 1), IsArchived = items.Count % 2 == 0 });
    }
}

Custom observable collection:

public class FilterableObservableCollection : ObservableCollection<ListViewItem>
{
    private bool notArchivedOnlyFilterEnabled;

    public IEnumerable<ListViewItem> FilteredItems
    {
        get
        {
            if (notArchivedOnlyFilterEnabled)
            {
                return this.Where(x => x.IsArchived == false);

            }
            else
            {
                return this;
            }
        }
    }

    public bool NotArchivedOnlyFilterEnabled
    {
        get { return notArchivedOnlyFilterEnabled; }
        set
        {
            notArchivedOnlyFilterEnabled = value;
            OnPropertyChanged(new PropertyChangedEventArgs("FilteredItems"));
        }
    }
}

Data item:

public class ListViewItem
{
    public string Name { get; set; }

    public bool IsArchived { get; set; }
}

XAML:

<Window x:Class="ObservableCollectionDemo1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        xmlns:c="clr-namespace:ObservableCollectionDemo1">
    <Grid>
        <ListView HorizontalAlignment="Left" Height="142" Margin="81,47,0,0" VerticalAlignment="Top" Width="302" x:Name="listView" DataContext="{Binding MyItems}" ItemsSource="{Binding FilteredItems}">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}"  Width="100"/>
                    <GridViewColumn Header="Is Archived" DisplayMemberBinding="{Binding IsArchived}" Width="100"/>
                </GridView>
            </ListView.View>
        </ListView>
        <CheckBox Content="Is Not Archived" HorizontalAlignment="Left" Margin="278,194,0,0" VerticalAlignment="Top" Checked="CheckBox_Checked" Unchecked="CheckBox_Unchecked"/>
        <Button Content="New Item" HorizontalAlignment="Left" Margin="278,214,0,0" VerticalAlignment="Top" Width="105" Click="Button_Click"/>

    </Grid>
</Window>

Upvotes: 1

Related Questions