I was recently asked to provide a field users could type in, that would filter a ListView with up to a couple hundred names in real-time.
If you'd like to follow along while you read, the code is available on
GitHub
.
In WinForms, filtering is easy – a few settings on a ComboBox control, set the data source, and off you go. Although WPF is a bit more complex, it provides for more flexibility out of the box like grouping and sorting, although we'll only look at filtering for now.
Data Binding in a ListView
One of the biggest strengths in WPF is in binding. As developers, we regularly bind to data – collections of numbers and strings, dates and classes. We present these to our users in grids, combo boxes, list views, etc.
When we bind a collection to a
ListView
in WPF, there's another layer between the control and the collection it's binding to, and that's the
CollectionView
.
The CollectionView Class
Much like a database table vs a view, or a
DataTable
vs a
DataView
,
the
CollectionView
allows us to manipulate the
presentation
of a collection of data without affecting the underlying data
. Per
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 against creating a
CollectionView
ourselves, so how do we take advantage of its capabilities? From the same source:
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 a
CollectionView
because WPF does it for us. That's convenient! Let's find out how to access and manipulate that default view.
The CollectionViewSource Class
Similar to how you can access the default view of a
DataTable
using the (appropriately named)
DataTable.DefaultView
, we can also access a collection's default view using
CollectionViewSource.GetDefaultView()
.
From
MSDN
again, the following restates some of what we just learned, but with an additional important note about how the default binding works for multiple controls
(emphasis mine)
.
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 with it?
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.
Let's take a look at filtering in action.
Filtering in a ListView
We'll start by creating a
Pirate
class, to eventually bind to a
ListView
:
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 we'll create a
ViewModel
that create a list of pirates:
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; }
}
Now we need the XAML, with a
ListView
and
TextBox
:
Here we're binding the
Pirates
collection to
PiratesListView
, and using the
TextBox
named
PiratesFilter
to help filter the contents of the list.
Finally, a few lines of code in the code-behind file help us wire up the filtering mechanism.
(If there's a way to define this in the XAML too, I'd like to hear about it, but 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));
In the
OnLoaded
event, we've attached a delegate to the
Filter
property, which runs the
UserFilter
method against each item in the collection to determine if it should be displayed in the
ListView
. That's it!
Manually Refreshing the View
Most of the time, WPF applies the filter automatically for us, without doing anything extra. However, if our code is performing some heavy calculations, we may want to have more control over the timing of when the filter is applied.
It's possible to manually
refresh
the view, even though most of the time it's unnecessary
:
Ultimately, we're in control of how often the view is refreshed. And now that's
really
it. 😄
One of the most enjoyable things about blogging is engaging with and learning from others.
Leave a comment below with your questions, comments, or ideas. Let's start a conversation!
I occasionally include
affiliate links
for products and services I find useful and want to share.
These links don't increase your cost at all, but using them helps pay for this blog and the time I put into it. Thanks!