Search code examples
c#wpflistviewinotifypropertychanged

WPF ListView to update automatically using INotifyPropertyChanged


I have a ListView called ChosenListView in my UserControl. In my UserControl's constructor I set the DataContext to my ViewModel, which includes an ObservableCollection (called Elements) of elements that have a bool IsChosen property.

I want to have my ChosenListView display all elements in Elements whose IsChosen property is true. I want it to be automatically updated every time I change one of the elements IsChosen property.

I tried to utilize INotifyPropertyChanged, but I can't seem to get it to work and would appreciate any help.

I define here a CollectionViewSource:

<UserControl.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary>
                <CollectionViewSource
                    x:Key="ChosenElementsViewSource"
                    x:Name="ChosenElementsViewSource"
                    Source="{Binding Elements}"
                    Filter="ChosenElementsViewSource_Filter"/>
            </ResourceDictionary>
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</UserControl.Resources>

This is the filter:

private void ChosenElementsViewSource_Filter(object sender, FilterEventArgs e)
{
    e.Accepted = e.Item is ChoosableElement choosableElement &&
        choosableElement.IsChosen;
}

This is my ViewModel:

private class ViewModel
{
    public ObservableCollection<ChoosableElement> Elements { get; set; }
}

In my UserControl's constructor I set DataContext to a new instance of my ViewModel, and initialize Elements based on my database, where all elements start with IsChosen=false.

My ListView defines an ItemsSource in the XAML:

ItemsSource="{Binding Source={StaticResource ChosenElementsViewSource}}"

Lastly, my ChoosableElement is defined as follows:

private class ChoosableElement : INotifyPropertyChanged
{
    public Element Element { get; set; }

    private bool isChosen = false;
    public bool IsChosen
    {
        get { return isChosen; }
        set
        {
            if (isChosen != value)
            {
                isChosen = value;
                OnPropertyChanged(nameof(IsChosen));
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

Solution

  • You need to refresh the CollectionViewSource to invoke the Filter event handler each time the IsChosen property is set.

    You can either do this programmatically yourself by handling the PropertyChanged event for all items in the code-behind of the UserControl. Something like this:

    public partial class UserControl1 : UserControl
    {
        public UserControl1()
        {
            InitializeComponent();
            var viewModel = new ViewModel();
            DataContext = viewModel;
    
            if (viewModel.Elements != null)
            {
                viewModel.Elements.CollectionChanged += OnCollectionChanged;
                foreach (object item in viewModel.Elements)
                    (item as INotifyPropertyChanged).PropertyChanged
                        += new PropertyChangedEventHandler(OnPropertyChanged);
            }
        }
    
        private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.NewItems != null)
                foreach (object item in e.NewItems)
                    (item as INotifyPropertyChanged).PropertyChanged
                            += new PropertyChangedEventHandler(OnPropertyChanged);
    
            if (e.OldItems != null)
                foreach (object item in e.OldItems)
                    (item as INotifyPropertyChanged).PropertyChanged
                            -= new PropertyChangedEventHandler(OnPropertyChanged);
        }
    
        private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == nameof(ChoosableElement.IsChosen))
            {
                (Resources["ChosenElementsViewSource"] as CollectionViewSource)?
                    .View.Refresh();
            }
        }
    
        private void ChosenElementsViewSource_Filter(object sender, FilterEventArgs e)
        {
            e.Accepted = e.Item is ChoosableElement choosableElement &&
                choosableElement.IsChosen;
        }
    
        ...
    }
    

    Or you could enabling the live filtering feature that was introduced in the .NET Framework 4.5 directly on the CollectionViewSource in the XAML markup:

    <CollectionViewSource
        x:Key="ChosenElementsViewSource"
        x:Name="ChosenElementsViewSource"
        Source="{Binding Elements}"
        Filter="ChosenElementsViewSource_Filter"
        IsLiveFilteringRequested="True"
        xmlns:system="clr-namespace:System;assembly=mscorlib">
        <CollectionViewSource.LiveFilteringProperties>
            <system:String>IsChosen</system:String>
        </CollectionViewSource.LiveFilteringProperties>
    </CollectionViewSource>