Using a TextBox and CollectionViewSource to Filter a ListView in WPF

At one point, I was asked to code a text field to serve as a filter for a ListView that. WPF provides for more flexibility (including grouping and sorting, but I’m only going to cover filtering).

Using a TextBox and CollectionViewSource to Filter a ListView in WPF

I was recently asked to provide our customers with a field they could type in, in order to filter a ListView that could potentially hold a couple hundred names.

In WinForms, filtering used to be easier... a few settings on a ComboBox control, set the data source, and off you go.

  • DropDownStyle = Simple
  • AutoCompleteMode = SuggestAppend
  • AutoCompleteSource = ListItems

As with many things though, the slightly harder WPF way (but not very hard, as I'll show) provides for more flexibility like grouping and sorting (although I’m only going to cover filtering).

winforms combobox filter

Just the code...

Here's a simplified example, after getting it working in a more complex app. (You can also just grab the source code from GitHub.)

First, a class to use with the ComboBox:

public class Pirate
{
    public Pirate(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }
 
    public string FirstName { get; private set; }
    public string LastName { get; private set; }
 
    public string FullName
    {
        get { return string.Format("{0} {1}", FirstName, LastName); }
    }
}

Then, a ViewModel, to bind to the XAML:

public class MainWindowViewModel
{
    public MainWindowViewModel()
    {
        Pirates = new List<Pirate>
                  {
                      new Pirate("Anne", "Bonny"),
                      new Pirate("Black", "Bart"),
                      new Pirate("Hayreddin", "Barbarossa"),
                      new Pirate("Hector", "Barbossa"),
                      new Pirate("Henry", "Avery"),
                      new Pirate("Henry", "Morgan"),
                      new Pirate("Howell", "Davis"),
                      new Pirate("William", "Kidd"),
                      new Pirate("William", "Turner"),
                  };
    }
 
    public List<Pirate> Pirates { get; set; }
 
    public Pirate SelectedPirate { get; set; }
}

And the XAML, with the ComboBox, and a TextBox for filtering:

<Window x:Class="CollectionViewSourceSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:collectionViewSourceSample="clr-namespace:CollectionViewSourceSample"
        d:DataContext="{d:DesignInstance collectionViewSourceSample:MainWindowViewModel}"
        Title="Arrrr Matey" Width="250" Height="300" Background="WhiteSmoke"
        Loaded="MainWindow_OnLoaded">
    <StackPanel>
        <TextBox Name="PiratesFilter"
                 TextChanged="PiratesFilter_OnTextChanged"
                 Margin="5" FontSize="20" />
 
        <TextBox IsEnabled="False" Text="Pirates:"
                 FontSize="16" BorderThickness="0" />
 
        <ListView Name="PiratesListView"
                  ItemsSource="{Binding Path=Pirates}"
                  SelectedValue="{Binding Path=SelectedPirate}"
                  DisplayMemberPath="FullName"
                  BorderBrush="LightGray" Margin="5" />
    </StackPanel>
</Window>

And finally, a few lines of code in the code-behind file, in order to wire up the filtering mechanism. I might've stumbled across a way to define at least a portion of this in the XAML, but I can't find it now and anyway this works just fine too.

public partial class MainWindow
{
    public MainWindow()
    {
        InitializeComponent();
 
        DataContext = new MainWindowViewModel();
    }
 
    private void MainWindow_OnLoaded(object sender, RoutedEventArgs e)
    {
        CollectionViewSource.GetDefaultView(PiratesListView.ItemsSource).Filter = UserFilter;
    }
 
    private void PiratesFilter_OnTextChanged(object sender, TextChangedEventArgs e)
    {
        CollectionViewSource.GetDefaultView(PiratesListView.ItemsSource).Refresh();
    }
 
    private bool UserFilter(object item)
    {
        if (String.IsNullOrEmpty(PiratesFilter.Text))
            return true;
 
        var pirate = (Pirate)item;
 
        return (pirate.FirstName.StartsWith(PiratesFilter.Text, StringComparison.OrdinalIgnoreCase)
                || pirate.LastName.StartsWith(PiratesFilter.Text, StringComparison.OrdinalIgnoreCase));
    }
}

How it works...

One of the biggest strengths in WPF is in binding. As developers, we regularly bind to collections... collections of numbers and strings, dates and classes. We present these to our users in grids, combo boxes, list views, etc. But as it turns out, when we write <ListView ItemsSource="{Binding Path=Pirates}" /> there's another layer between the control and the collection it's binding to.

The CollectionView Class

Much like a database table vs a view, or a DataTable vs a DataView, the CollectionView allows you to manipulate the presentation of a collection of data (sorting, filtering, grouping) without affecting the actual underlying data. From MSDN:

You can think of a collection view as a layer on top of a binding source collection that allows you to navigate and display the collection based on sort, filter, and group queries, all without having to manipulate the underlying source collection itself.

But the documentation advises you not to create a CollectionView yourself, so how do we take advantage of its capabilities?

In WPF applications, all collections have an associated default collection view. Rather than working with the collection directly, the binding engine always accesses the collection through the associated view.

We don’t have to create it because WPF does it for us. That's convenient! Next we'll find out how to access and manipulate that default view.

The CollectionViewSource Class

Similar to how you can access a DataTable’s default view using the appropriately named DataTable.DefaultView, you can also access a collection's default view using CollectionViewSource.GetDefaultView().

From MSDN, the following sorta rehashes what we've already learned above, with an additional important note about multiple controls binding to the same collection (and default view).

All collections have a default CollectionView. WPF always binds to a view rather than a collection. If you bind directly to a collection, WPF actually binds to the default view for that collection. This default view is shared by all bindings to the collection, which causes all direct bindings to the collection to share the sort, filter, group, and current item characteristics of the one default view.

Once we have a reference to the default view, what can we do?

Views allow the same data collection to be viewed in different ways, depending on sorting, filtering, or grouping criteria. Every collection has one shared default view, which is used as the actual binding source when a binding specifies a collection as its source.

Filtering the View

Looking back at my code up above, you can see that I've attached a delegate to the Filter property, which "represents the method used to determine if an item is suitable for inclusion in the view."

CollectionViewSource.GetDefaultView(PiratesListView.ItemsSource).Filter = UserFilter;

The UserFilter method is run against each item in the collection, returning True if the item remains in the view, or False if the item should be dropped from the view. Only items returning True are displayed.

Refreshing the View

When a change occurs (such as typing in the "filter" TextBox) that potentially affects the Filter, you can manually refresh the view.

Re-creates the view. When you set the Filter, SortDescriptions, or GroupDescriptions property; a refresh occurs. You do not have to call the Refresh method immediately after you set one of those properties.

You're in control of how often the view is refreshed, and how often that is will depend on what kind of calculations you’re doing in the filter, etc.

arrmatey