Trigger Filter on CollectionViewSource

49,391

Solution 1

Don't create a CollectionViewSource in your view. Instead, create a property of type ICollectionView in your view model and bind ListView.ItemsSource to it.

Once you've done this, you can put logic in the FilterText property's setter that calls Refresh() on the ICollectionView whenever the user changes it.

You'll find that this also simplifies the problem of sorting: you can build the sorting logic into the view model and then expose commands that the view can use.

EDIT

Here's a pretty straightforward demo of dynamic sorting and filtering of a collection view using MVVM. This demo doesn't implement FilterText, but once you understand how it all works, you shouldn't have any difficulty implementing a FilterText property and a predicate that uses that property instead of the hard-coded filter that it's using now.

(Note also that the view model classes here don't implement property-change notification. That's just to keep the code simple: as nothing in this demo actually changes property values, it doesn't need property-change notification.)

First a class for your items:

public class ItemViewModel
{
    public string Name { get; set; }
    public int Age { get; set; }
}

Now, a view model for the application. There are three things going on here: first, it creates and populates its own ICollectionView; second, it exposes an ApplicationCommand (see below) that the view will use to execute sorting and filtering commands, and finally, it implements an Execute method that sorts or filters the view:

public class ApplicationViewModel
{
    public ApplicationViewModel()
    {
        Items.Add(new ItemViewModel { Name = "John", Age = 18} );
        Items.Add(new ItemViewModel { Name = "Mary", Age = 30} );
        Items.Add(new ItemViewModel { Name = "Richard", Age = 28 } );
        Items.Add(new ItemViewModel { Name = "Elizabeth", Age = 45 });
        Items.Add(new ItemViewModel { Name = "Patrick", Age = 6 });
        Items.Add(new ItemViewModel { Name = "Philip", Age = 11 });

        ItemsView = CollectionViewSource.GetDefaultView(Items);
    }

    public ApplicationCommand ApplicationCommand
    {
        get { return new ApplicationCommand(this); }
    }

    private ObservableCollection<ItemViewModel> Items = 
                                     new ObservableCollection<ItemViewModel>();

    public ICollectionView ItemsView { get; set; }

    public void ExecuteCommand(string command)
    {
        ListCollectionView list = (ListCollectionView) ItemsView;
        switch (command)
        {
            case "SortByName":
                list.CustomSort = new ItemSorter("Name") ;
                return;
            case "SortByAge":
                list.CustomSort = new ItemSorter("Age");
                return;
            case "ApplyFilter":
                list.Filter = new Predicate<object>(x => 
                                                  ((ItemViewModel)x).Age > 21);
                return;
            case "RemoveFilter":
                list.Filter = null;
                return;
            default:
                return;
        }
    }
}

Sorting kind of sucks; you need to implement an IComparer:

public class ItemSorter : IComparer
{
    private string PropertyName { get; set; }

    public ItemSorter(string propertyName)
    {
        PropertyName = propertyName;    
    }
    public int Compare(object x, object y)
    {
        ItemViewModel ix = (ItemViewModel) x;
        ItemViewModel iy = (ItemViewModel) y;

        switch(PropertyName)
        {
            case "Name":
                return string.Compare(ix.Name, iy.Name);
            case "Age":
                if (ix.Age > iy.Age) return 1;
                if (iy.Age > ix.Age) return -1;
                return 0;
            default:
                throw new InvalidOperationException("Cannot sort by " + 
                                                     PropertyName);
        }
    }
}

To trigger the Execute method in the view model, this uses an ApplicationCommand class, which is a simple implementation of ICommand that routes the CommandParameter on buttons in the view to the view model's Execute method. I implemented it this way because I didn't want to create a bunch of RelayCommand properties in the application view model, and I wanted to keep all the sorting/filtering in one method so that it was easy to see how it's done.

public class ApplicationCommand : ICommand
{
    private ApplicationViewModel _ApplicationViewModel;

    public ApplicationCommand(ApplicationViewModel avm)
    {
        _ApplicationViewModel = avm;
    }

    public void Execute(object parameter)
    {
        _ApplicationViewModel.ExecuteCommand(parameter.ToString());
    }

    public bool CanExecute(object parameter)
    {
        return true;
    }

    public event EventHandler CanExecuteChanged;
}

Finally, here's the MainWindow for the application:

<Window x:Class="CollectionViewDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:CollectionViewDemo="clr-namespace:CollectionViewDemo" 
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <CollectionViewDemo:ApplicationViewModel />
    </Window.DataContext>
    <DockPanel>
        <ListView ItemsSource="{Binding ItemsView}">
            <ListView.View>
                <GridView>
                    <GridViewColumn DisplayMemberBinding="{Binding Name}"
                                    Header="Name" />
                    <GridViewColumn DisplayMemberBinding="{Binding Age}" 
                                    Header="Age"/>
                </GridView>
            </ListView.View>
        </ListView>
        <StackPanel DockPanel.Dock="Right">
            <Button Command="{Binding ApplicationCommand}" 
                    CommandParameter="SortByName">Sort by name</Button>
            <Button Command="{Binding ApplicationCommand}" 
                    CommandParameter="SortByAge">Sort by age</Button>
            <Button Command="{Binding ApplicationCommand}"
                    CommandParameter="ApplyFilter">Apply filter</Button>
            <Button Command="{Binding ApplicationCommand}"
                    CommandParameter="RemoveFilter">Remove filter</Button>
        </StackPanel>
    </DockPanel>
</Window>

Solution 2

Nowadays, you often don't need to explicitly trigger refreshes. CollectionViewSource implements ICollectionViewLiveShaping which updates automatically if IsLiveFilteringRequested is true, based upon the fields in its LiveFilteringProperties collection.

An example in XAML:

  <CollectionViewSource
         Source="{Binding Items}"
         Filter="FilterPredicateFunction"
         IsLiveFilteringRequested="True">
    <CollectionViewSource.LiveFilteringProperties>
      <system:String>FilteredProperty1</system:String>
      <system:String>FilteredProperty2</system:String>
    </CollectionViewSource.LiveFilteringProperties>
  </CollectionViewSource>

Solution 3

CollectionViewSource.View.Refresh();

CollectionViewSource.Filter is reevaluated in this way!

Solution 4

Perhaps you've simplified your View in your question, but as written, you don't really need a CollectionViewSource - you can bind to a filtered list directly in your ViewModel (mItemsToFilter is the collection that is being filtered, probably "AllProjects" in your example):

public ReadOnlyObservableCollection<ItemsToFilter> AllFilteredItems
{
    get 
    { 
        if (String.IsNullOrEmpty(mFilterText))
            return new ReadOnlyObservableCollection<ItemsToFilter>(mItemsToFilter);

        var filtered = mItemsToFilter.Where(item => item.Text.Contains(mFilterText));
        return new ReadOnlyObservableCollection<ItemsToFilter>(
            new ObservableCollection<ItemsToFilter>(filtered));
    }
}

public string FilterText
{
    get { return mFilterText; }
    set 
    { 
        mFilterText = value;
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs("FilterText"));
            PropertyChanged(this, new PropertyChangedEventArgs("AllFilteredItems"));
        }
    }
}

Your View would then simply be:

<TextBox Text="{Binding Path=FilterText,UpdateSourceTrigger=PropertyChanged}" />
<ListView ItemsSource="{Binding AllFilteredItems}" />

Some quick notes:

  • This eliminates the event in the code behind

  • It also eliminates the "FilterOut" property, which is an artificial, GUI-only property and thus really breaks MVVM. Unless you plan to serialize this, I wouldn't want it in my ViewModel, and certainly not in my Model.

  • In my example, I use a "Filter In" rather than a "Filter Out". It seems more logical to me (in most cases) that the filter I am applying are things I do want to see. If you really want to filter things out, just negate the Contains clause (i.e. item => ! Item.Text.Contains(...)).

  • You may have a more centralized way of doing your Sets in your ViewModel. The important thing to remember is that when you change the FilterText, you also need to notify your AllFilteredItems collection. I did it inline here, but you could also handle the PropertyChanged event and call PropertyChanged when the e.PropertyName is FilterText.

Please let me know if you need any clarifications.

Solution 5

If I understood well what you are asking:

In the set part of your FilterText property just call Refresh() to your CollectionView.

Share:
49,391
Pieter Müller
Author by

Pieter Müller

Software development engineer.

Updated on July 09, 2022

Comments

  • Pieter Müller
    Pieter Müller almost 2 years

    I am working on a WPF desktop application using the MVVM pattern.

    I am trying to filter some items out of a ListView based on the text typed in a TextBox. I want the ListView items to be filtered as I change the text.

    I want to know how to trigger the filter when the filter text changes.

    The ListView binds to a CollectionViewSource, which binds to the ObservableCollection on my ViewModel. The TextBox for the filter text binds to a string on the ViewModel, with UpdateSourceTrigger=PropertyChanged, as it should be.

    <CollectionViewSource x:Key="ProjectsCollection"
                          Source="{Binding Path=AllProjects}"
                          Filter="CollectionViewSource_Filter" />
    
    <TextBox Text="{Binding Path=FilterText, UpdateSourceTrigger=PropertyChanged}" />
    
    <ListView DataContext="{StaticResource ProjectsCollection}"
              ItemsSource="{Binding}" />
    

    The Filter="CollectionViewSource_Filter" links to an event handler in the code behind, which simply calls a filter method on the ViewModel.

    Filtering is done when the value of FilterText changes - the setter for the FilterText property calls a FilterList method that iterates over the ObservableCollection in my ViewModel and sets a boolean FilteredOut property on each item ViewModel.

    I know the FilteredOut property is updated when the filter text changes, but the List does not refresh. The CollectionViewSource filter event is only fired when I reload the UserControl by switching away from it and back again.

    I've tried calling OnPropertyChanged("AllProjects") after updating the filter info, but it did not solve my problem. ("AllProjects" is the ObservableCollection property on my ViewModel to which the CollectionViewSource binds.)

    How can I get the CollectionViewSource to refilter itself when the value of the FilterText TextBox changes?

    Many thanks

  • Pieter Müller
    Pieter Müller almost 13 years
    Hi. That would work, yes, but it's not allowable in MVVM - the FilterText property is in the ViewModel, the CollectionView is in the View, and the ViewModel should not have any knowledge of the View.
  • Dummy01
    Dummy01 almost 13 years
    Late answer. I suggested that, because I put my collection views also in my ViewModel as properties.
  • Pieter Müller
    Pieter Müller almost 13 years
    All the answers given so far have been good. I'm marking this one as it solves the problem in the most MVVMish way and also helps me with the custom sorting problem I've been having. Thanks.
  • Pieter Müller
    Pieter Müller almost 13 years
    Robert, have you previously implemented this successfully? I can't seem to get it to work.
  • Robert Rossney
    Robert Rossney almost 13 years
    My implementations are all embedded in applications that are too involved to just yank out and post, so I whipped up a little demo app for you. See my edit.
  • Pieter Müller
    Pieter Müller almost 13 years
    Many, many thanks for this great example. I was trying to instantiate the CollectionView through its constructor, not through CollectionViewSource.GetDefaultView(), which solved my problem immediately. Seems to me like these things are not very well documented. You helped me out big time! :-)
  • Ahmed Fwela
    Ahmed Fwela over 8 years
    would like to say that this was added in wpf with .net 4.5
  • Andrew Truckle
    Andrew Truckle almost 8 years
    This is great! But there is a warning that the event handler CanExecuteChanged is never used. Can we stop this warning?
  • Trevor Elliott
    Trevor Elliott over 7 years
    This seems a little short-sighted. No custom bindings? Chances are if you have a collection of items in a view, you change some value on the parent viewmodel (eg. filter text or boolean filter flags). Not just a property on the items in the collection that's being filtered.
  • 15ee8f99-57ff-4f92-890c-b56153
    15ee8f99-57ff-4f92-890c-b56153 over 6 years
    In WPF with .NET 4.6.1, ICollectionViewLiveShaping is not implemented.
  • DonBoitnott
    DonBoitnott over 6 years
    It's worth noting that the example given here is quite complete, and that I only needed part of it to get my code to where it needed to be. Since I already had the sorted list in place and working, all I needed from this was the notion of the bound property and the filtering trigger that assigns the Predicate.
  • themightylc
    themightylc over 6 years
    @EdPlunkett what is the implication of your comment? I am trying to use this feature and nothing happens when any of the relevant properties are changed. Do I have to implement this manually now anyway?
  • 15ee8f99-57ff-4f92-890c-b56153
    15ee8f99-57ff-4f92-890c-b56153 over 6 years
    @themightylc I don't have a good answer to that question. CollectionViewSource has the same properties and methods as ICollectionViewLiveShaping but doesn't implement that interface, and it's unclear to me how, or if, you can get it to work.
  • themightylc
    themightylc over 6 years
    @EdPlunkett Didn't see your answer yet, sorry and thanks. I don't remember what my actual issue was but fwiw I've checked my implementation and I'm using my own class inherited from CollectionViewSource with DependencyProperties that triggers .Refresh() - this works fine but also - obviously - I couldn't get the LiveShaping to work in 4.6.1. And if it could be achieved so efficiently with this little effort I don't see why I should bother :)
  • dtoland
    dtoland about 6 years
    Great answer, 7 years after it was published (and 2 years after it was edited) it's still relevant. Thanks and nice work.
  • Devid
    Devid over 5 years
    This worked like a charm. My ItemSource is of type ObservableCollection. And the Property is of type bool which when change should update the CollectionViewSource.
  • James B
    James B about 4 years
    IMHO this is the way to go. The accepted answer will require a dependency on the presentation framework in your view models, and I'm trying to keep that away from my view models so I can more easily swap out the UI layer at a later date (e.g. to the Uno framework).