Search code examples
c#wpfobservablecollectionicollectionview

LiveFiltering on ListCollectionView doesn't reevaluate the Filter when a property of an item in the list changes


I have a class Person, which implements INotifyPropertyChanged for the property Name:

public class Person : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    private void OnPropertyChanged(string property)
    {
        Console.WriteLine("PropertyChanged: " + Name + ": " + property);
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
    }

    private string _name;
    public string Name
    {
        get => _name;
        set
        {
            if (_name == value)
                return;
            _name = value;
            OnPropertyChanged("Name");
        }
    }
}

I am using a ListCollectionView with LiveFiltering to display a ObservableCollection, filtered to Match only a person whose name starts with "A":

var coll = new ObservableCollection<Person>();
ListCollectionView view = new ListCollectionView(coll)
{
    Filter = p => ((Person)p).Name[0]=='A',
    IsLiveFiltering = true,
    LiveFilteringProperties = { nameof(Person.Name) }
};

When items are added to the collection, they are filtered correctly.
Here is the Problem: When the Name of a Person is changed, the Filter isn't reevaluated, which is what I thought the IsLiveFiltering and LiveFilteringProperties are for in the first place. So, if i have change a Name from "Anna" to "Elsa" I would expect the view to update and not contain the item anymore. Likewise, changing "Eric" to "Arnold" should update the view so that the changed item is container in the view.

var p1 = new Person { Name = "Anna" };
var p2 = new Person { Name = "Eric" };

coll.Add(p1); // view is updated automatically and contains Anna now
coll.Add(p2); // view is updated, but Eric is filtered out

view.Dump(); // shows only "Anna" (LINQPad - Dump)

p1.Name = "Elsa"; // change Anna to Elsa -> the instance p1 should be removed from view (not from collection)
p2.Name = "Arnold"; // change Eric to Arnold -> the instance p2 should now be in the view

//view.Refresh(); // uncommenting this line leads to the behaviour I actually expected from LiveFiltering to be handled automatically.

view.Dump(); // shows "Elsa", but we are filtering for A*

Did I miss enabling something to get this behaviour? I do not really want to attach to PropertyChanged of each instance of Person manually - I still expect that this is what LiveFiltering does for me.

Edit: I have experienced the problem in my application with a bigger model and extracted the relevant parts to reproduce the problem in LinqPad. Here is the complete LinqPad-script. It will also require the using clauses:

using System.Collections.ObjectModel;
using System.Windows.Data;
using System.ComponentModel;

Here's the script

void Main()
{
    var coll = new ObservableCollection<Person>();
    ListCollectionView view = new ListCollectionView(coll)
    {
        IsLiveFiltering = true,
        LiveFilteringProperties = { nameof(Person.Name) },
        Filter = p => ((Person)p).Name[0] == 'A'
    };

    var p1 = new Person { Name = "Anna" };
    var p2 = new Person { Name = "Eric" };

    coll.Add(p1);
    coll.Add(p2);

    view.Dump();

    p1.Name = "Elsa";
    p2.Name = "Arnold";

    //view.Refresh();

    view.Dump();
    Debug.Assert(view.Cast<Person>().Single().Name == "Arnold", "Wrong item in view, expected Arnold");
}

public class Person : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    private void OnPropertyChanged(string property)
    {
        Console.WriteLine("PropertyChanged: " + Name + ": " + property);
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
    }

    private string _name;
    public string Name
    {
        get => _name;
        set
        {
            if (_name == value)
                return;
            _name = value;
            OnPropertyChanged("Name");
        }
    }
}

Solution

  • Where is your view and sample data...?

    The following code certainly works as expected.

    XAML:

    <ListBox x:Name="lb" DisplayMemberPath="Name" />
    <Button Content="Filter" Click="Button_Click" />
    

    Sample code:

    public partial class MainWindow : Window
    {
        Person _p = new Person() { Name = "Anna" };
    
        public MainWindow()
        {
            InitializeComponent();
    
            var coll = new ObservableCollection<Person>() { _p };
            ListCollectionView view = new ListCollectionView(coll)
            {
                Filter = p => ((Person)p).Name[0] == 'A',
                IsLiveFiltering = true,
                LiveFilteringProperties = { nameof(Person.Name) }
            };
    
            lb.ItemsSource = view;
        }
    
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            _p.Name = "Elsa";
        }
    }
    

    You should probably read this before you post another question.