Click here to Skip to main content
15,884,099 members
Articles / Web Development / CSS

A WPF ListView Custom Control with Search Filter Tutorial

Rate me:
Please Sign up or sign in to vote.
4.28/5 (6 votes)
30 Nov 2018MIT4 min read 29.3K   1.4K   10   7
A WPF tutorial on how to create FilteredListView: A ListView custom control with search filter that uses Throttling.

Introduction

A common challenge in WPF is to create a ListView with a Search widget that can filter through its items. This tutorial will show you how to create a Custom Control FilteredListView that derives from ListView and allows filtering.

The implementation explained in this article has several important advantages that are missing in other implementations:

  • The search widget is part of the ListView's template, which makes using it as simple as possible.
  • The filtering is done immediately as you type the text, without the need to click a button (although, it will be easy to change the code to filter on click instead)
  • The filtering uses <string>Reactive Extensions (Rx) Throttle to achieve the best user experience and performance. This means that the filtering occurs when the user stopped typing for half a second.

Using the Code

The most simple usage is:

XML
<FilteredListView ItemsSource={Binding Items}/>

As you can see, there's no extra TextBox needed for the Filter. The reason is that the TextBox is included in the custom control's template.

The default filter works by calling .ToString() on the item. We can customize it and build our own filter. For example, when our items are of type Person, we can define the Filter Predicate to check both the person's Name and Occupation:

XML
<FilteredListView ItemsSource={Binding Items} FilterPredicate="{Binding MyFilter}"/>

In the ViewModel:

C#
public Func<object, string, bool> MyFilter
{
	get
	{
		return (item, text) =>
		{
			var person = item as Person;
			return person.Name.Contains(text)
				 || person.Occupation.Contains(text);
		};
	}
}

Creating FilteredListView Tutorial

Custom controls should be used when you want to expand on existing functionality, but allow to keep using the existing functionality. Our scenario is a classic example for that. We want to add a Search Filter to a ListView, but retain all of the ListView's existing properties and methods.

The first order of business is to create a Custom Control. This is best done in Visual Studio in Project | Add New Item and search for Custom Control (WPF). This will create a class that derives from Control and a default template in Themes\Generic.xaml.

In the following tutorial, I assume you have basic knowledge in WPF and MVVM. If you're new to Custom Controls and Default Styles (or want to understand them better), I suggest reading Explicit, Implicit and Default styles in WPF.

Creating the Default Style

When creating custom controls, it's best to start with the existing default style in WPF and go from there. This is easiest to do with Blend. After getting the original style from Blend, copy-paste it to Generic.xaml and start editing. All I had to do in this case is to add a TextBox for the Filter on top of everything else.

Generic.xaml:

C#
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:FilteredListViewControl">

  <FontFamily x:Key="FontAwesome">pack://application:,,,
   /FilteredListViewControl;component/fonts/fontawesome-webfont.ttf#FontAwesome</FontFamily>

  <SolidColorBrush x:Key="ListBox.Static.Background" Color="Transparent"/>
  <SolidColorBrush x:Key="ListBox.Static.Border" Color="Transparent"/>
  <SolidColorBrush x:Key="ListBox.Disabled.Background" Color="#FFFFFFFF"/>
  <SolidColorBrush x:Key="ListBox.Disabled.Border" Color="#FFD9D9D9"/>
  <Style TargetType="{x:Type local:FilteredListView}" >
    <Setter Property="Background" Value="{StaticResource ListBox.Static.Background}"/>
    <Setter Property="BorderBrush" Value="{StaticResource ListBox.Static.Border}"/>
    <Setter Property="BorderThickness" Value="1"/>
    <Setter Property="Foreground" 
     Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
    <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
    <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
    <Setter Property="ScrollViewer.CanContentScroll" Value="true"/>
    <Setter Property="ScrollViewer.PanningMode" Value="Both"/>
    <Setter Property="Stylus.IsFlicksEnabled" Value="False"/>
    <Setter Property="VerticalContentAlignment" Value="Center"/>
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="{x:Type local:FilteredListView}">
          <Grid>
            <Grid.RowDefinitions>
              <RowDefinition Height="Auto"/>
              <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
            <Border Padding="0 5">
              <Grid>
                <TextBox Text="{Binding FilterText, UpdateSourceTrigger=PropertyChanged, 
                               RelativeSource={RelativeSource TemplatedParent}}"/>
                  <TextBlock  FontFamily="{StaticResource FontAwesome}" 
                              Text="&#xf002;" FontSize="14"
                              VerticalAlignment="Center"
                              HorizontalAlignment="Right"/>
              </Grid>
            </Border>
            <Border Grid.Row="1" x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}" 
              BorderThickness="{TemplateBinding BorderThickness}" 
              Background="{TemplateBinding Background}" Padding="1" SnapsToDevicePixels="true">
              <ScrollViewer Focusable="false" Padding="{TemplateBinding Padding}">
                <ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
              </ScrollViewer>
            </Border>
          </Grid>
          <ControlTemplate.Triggers>
            <Trigger Property="IsEnabled" Value="false">
              <Setter Property="Background" TargetName="Bd" 
               Value="{StaticResource ListBox.Disabled.Background}"/>
              <Setter Property="BorderBrush" TargetName="Bd" 
               Value="{StaticResource ListBox.Disabled.Border}"/>
            </Trigger>
            <MultiTrigger>
              <MultiTrigger.Conditions>
                <Condition Property="IsGrouping" Value="true"/>
                <Condition Property="VirtualizingPanel.IsVirtualizingWhenGrouping" Value="false"/>
              </MultiTrigger.Conditions>
              <Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
            </MultiTrigger>
          </ControlTemplate.Triggers>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>
</ResourceDictionary>

This might seem like much, but I actually just had to add a TextBox and a small magnifier-glass icon from Font Awesome:

XML
<Border Padding="0 5">
  <Grid>
	<TextBox Text="{Binding FilterText, UpdateSourceTrigger=PropertyChanged, 
                 RelativeSource={RelativeSource TemplatedParent}}"/>
	<TextBlock  FontFamily="{StaticResource FontAwesome}" 
                Text="&#xf002;" FontSize="14" Margin="0 0" 
				VerticalAlignment="Center"
				HorizontalAlignment="Right"/>
  </Grid>
</Border>

Some notes on this code:

  • I used FontAwesome here for the little magnifier glass icon on the right. This is available with the NuGet package FontAwesome.
  • The TextBox's Text is binded to FilterText and that binding is set to PropertyChanged trigger mode. This means the changed callback is called on each key stroke, as opposed to the default behavior where it's called on lost focus.

The Custom Control Code

The entire filtering code is placed in our custom control class, as follows:

C#
public class FilteredListView : ListView
{
	static FilteredListView()
	{
		DefaultStyleKeyProperty.OverrideMetadata(typeof(FilteredListView), 
                    new FrameworkPropertyMetadata(typeof(FilteredListView)));
	}

	public Func<object, string, bool> FilterPredicate
	{
		get { return (Func<object, string, bool>)GetValue(FilterPredicateProperty); }
		set { SetValue(FilterPredicateProperty, value); }
	}
	public static readonly DependencyProperty FilterPredicateProperty =
		DependencyProperty.Register("FilterPredicate", 
        typeof(Func<object, string, bool>), typeof(FilteredListView), new PropertyMetadata(null));

	public Subject<bool> FilterInputSubject = new Subject<bool>();

	public string FilterText
	{
		get { return (string)GetValue(FilterTextProperty); }
		set { SetValue(FilterTextProperty, value); }
	}
	public static readonly DependencyProperty FilterTextProperty =
		DependencyProperty.Register("FilterText",
			typeof(string),
			typeof(FilteredListView),
			new PropertyMetadata("",
				//This is the 'PropertyChanged' callback that's called 
                //whenever the Filter input text is changed
				(d, e) => (d as FilteredListView).FilterInputSubject.OnNext(true)));

	public FilteredListView()
	{
		SetDefaultFilterPredicate();
		InitThrottle();
	}

	private void SetDefaultFilterPredicate()
	{
		FilterPredicate = (obj, text) => obj.ToString().ToLower().Contains(text);
	}

	private void InitThrottle()
	{
		FilterInputSubject.Throttle(TimeSpan.FromMilliseconds(500))
			.ObserveOnDispatcher()
			.Subscribe(HandleFilterThrottle);
	}

	private void HandleFilterThrottle(bool b)
	{
		ICollectionView collectionView = CollectionViewSource.GetDefaultView(this.ItemsSource);
		if (collectionView == null) return;
		collectionView.Filter = (item) => FilterPredicate(item, FilterText);
	}
}

Let's explain what's written here.

  • The custom control class derives from ListView. This will inherit the full behavior of ListView and allows us to add to it, which is the point of Custom Controls.
  • The static constructor is boiler-plate code for any custom control that's telling WPF to use your Default Style.
  • FilterPredicate dependency property is the custom expression of our filter, which can be set from outside. The default implementation simply calls .ToString() on the item and checks if the text contains FilterText
  • FilterText is the property binded to the Text of our TextBox. On each input change in the TextBox, we call FilterInputSubject.OnNext(true) which triggers the Throttle mechanism. After half a second without calls, the Throttle is executed.
  • SetDefaultFilterPredicate sets the default FilterPredicate as written above.
  • InitThrottle initialize the Throttle to fire after 500 milliseconds without action, and then call HandleFilterThrottle.
    Using <string>Reactive Extensions requires the NuGet packages: System.Reactive.Linq and System.Reactive.
  • HandleFilterThrottle reapplies the Filter to our ListView. It's necessary to set the Filter property again, since the FilterText could have been changed.

Summary

This is it for the tutorial. I hope you understood everything and got some benefit from it.

It can be confusing to know when to use Custom Controls or User controls. You can think of User Controls as a reusable UI component that doesn't expand on anything prior. Custom controls, on the other hand, are adding abilities to existing controls. It makes less sense to have a custom control deriving from Control since it has no existing functionality. Deriving from ListBox, Button, or StackPanel does make sense.

Custom controls are a powerful tool in WPF. I find they require more work initially than user controls, but using them once ready in other Controls is much nicer. This makes them perfect for a dedicated Controls class library in your solution.

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Team Leader OzCode
Israel Israel
I'm a Senior Software Developer and a Blogger.
I dabble mostly in C# .NET, WPF, Vue.js, and Asp.NET.
You can find me at www.michaelscodingspot.com
I work as a team leader in OzCode, an extension for Visual Studio that makes C# debugging easier.

Comments and Discussions

 
Questionthank you Pin
Rob Smiley18-Feb-20 2:38
Rob Smiley18-Feb-20 2:38 
Questionthank you Pin
Member 145936531-Dec-19 22:16
Member 145936531-Dec-19 22:16 
QuestionLink to own blog Pin
Nelek21-Dec-18 4:48
protectorNelek21-Dec-18 4:48 
SuggestionFunctionality is tightly bound to one control Pin
Graeme_Grant30-Nov-18 14:09
mvaGraeme_Grant30-Nov-18 14:09 
GeneralRe: Functionality is tightly bound to one control Pin
Jeff Bowman3-Dec-18 9:11
professionalJeff Bowman3-Dec-18 9:11 
GeneralRe: Functionality is tightly bound to one control Pin
Michael Shpilt5-Dec-18 21:34
Michael Shpilt5-Dec-18 21:34 
GeneralMessage Closed Pin
29-Nov-18 3:05
Member 1407172029-Nov-18 3:05 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.