Click here to Skip to main content
15,868,127 members
Articles / Desktop Programming / WPF

WPF Custom Control - FilterControl for ListBox/ListView

Rate me:
Please Sign up or sign in to vote.
4.82/5 (35 votes)
26 Apr 2011CPOL4 min read 154.6K   3.1K   89   52
WPF Custom Control FilterControl - Can be used along with any ItemsControl

Table of Contents 

Introduction

ComputerCastle.Demo.Controls.FilterControl is a WPF-based Custom Control which can be used as a quick filter for any ItemsControl like ListBox, ListView, DataGrid, etc. This article tells you how easy it is to create a Custom Control in WPF using a combination of simple framework controls.

The article focuses on:

  • How to create a Custom Control in WPF briefly
  • Explanation about FilterControl
  • How to implement FilterControl

Picture1.png

Background

I have been working in WPF for the past two and half years. When I started developing in WPF, it was hard for me to adopt to the new technology. I searched everywhere especially in CodeProject to learn WPF. With the help of Josh Smith, Sacha Barber, Nishant and few more people here, now my WPF knowledge has grown better now. You might find a little resemblance of their style in the code.

How to Create a Custom Control in WPF

There are many articles in CodeProject discussing about Custom Control in WPF. So, I will explain in brief.

  • Open Visual Studio (Preferably Visual Studio 2010 as it is the latest)
  • Create a New WPF Project (FilterControlDemo)
  • Add a Class file and name it as FilterControl
  • Finish the FilterControl
  • Add a ResourceDictionary file where you write the Style for the FilterControl.
  • Make sure you merge all the resource dictionaries together and set the ThemeInfo in the AssemblyInfo.cs file.
C#
[assembly: ThemeInfo(
    ResourceDictionaryLocation.None, ResourceDictionaryLocation.SourceAssembly)]

Using the Code

The main class is FilterControl which is derived from <a href="http://msdn.microsoft.com/en-us/library/system.windows.controls.control.aspx" target="_blank">System.Windows.Controls.Control</a>. It is very important to determine the base class when you are developing a Custom Control. In some cases, you just require plain Control's feature but most of the cases you might require feature of on existing control like TextBox, Button, ListBox, etc. I could have used TextBox as base class. But TextBox properties and methods are too much for my Control. The XAML syntax is:

XML
<ctrl:FilterControl Height="25" Header="Type here to filter" DockPanel.Dock="Top"
                            TargetControl="{Binding ElementName=listBox}"
                            FilterTextBindingPath="Name"/>

And the class diagram is:

ClassDiagram1.png

 

FilterControl Members

Name Description
FilterFiringInterval Sets/Gets the interval of time to filter the filter action. The interval should be in milliseconds. The default value is 300 ms.
FilterOnEnter Filter action will be fired once you hit Enter key if it is set to True
FilterText Sets/Gets the filter text.
FilterTextBindingPath Use while TargetControl is bound.
Header Info to show the user if the Focus is not in.
TargetControl Used to bind the Target control to which filter has to be applied.

FilterControl Style

XML
<Style TargetType="{x:Type egl:FilterControl}">
        <Setter Property="Background" Value="{DynamicResource 
		{x:Static SystemColors.WindowBrushKey}}"/>
        <Setter Property="FilterOnEnter" Value="False"/>
        <Setter Property="Template">
            <Setter.Value>

                <ControlTemplate TargetType="{x:Type egl:FilterControl}">
                    <Border x:Name="border" CornerRadius="3,3,3,3"
                            Background="#ADADAD">
                		<Grid Margin="1">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="Auto"/>
                                <ColumnDefinition Width="*"/>
                                <ColumnDefinition Width="Auto"/>
                            </Grid.ColumnDefinitions>
                            <Border Background="{TemplateBinding Background}" 
				Grid.ColumnSpan="3" CornerRadius="3" 
				VerticalAlignment="Stretch" 
				HorizontalAlignment="Stretch" />
                            <Image Source="/ComputerCastle.Demo;
				component/Resources/Search.png"/>
                			<TextBox Style="{StaticResource FilterTextBoxStyle}"
                                     x:Name="PART_FilterBox"  AutoWordSelection="True"
                                     Grid.Column="1"
                                     Margin="0,1,0,1" VerticalAlignment="Center"
                                     Text="{Binding RelativeSource=
					{RelativeSource AncestorType=
					{x:Type egl:FilterControl}}, 
					Path=FilterText, Mode=TwoWay, 
					UpdateSourceTrigger=PropertyChanged}"/>
					<TextBlock x:Name="PART_Header" 
					Text="{TemplateBinding Header}" 
					VerticalAlignment="Center" 
					HorizontalAlignment="Left" Margin="2,0,0,0"
                                       Grid.Column="1"
                                       IsHitTestVisible="False" Foreground="#ADADAD"/>
                			<Button x:Name="PART_ClearButton" Grid.Column="2" 
					Margin="0,0,4,0" 
					Style="{StaticResource ClearButtonStyle}"
                                    Visibility="Collapsed"/>
                		</Grid>
                	</Border>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsFocused" Value="True" 
				SourceName="PART_FilterBox" >
                            <Setter Property="Background" 
				Value="{StaticResource FocusBackground}" 
				TargetName="border" />
                        </Trigger>
                        <Trigger Property="IsFocused" Value="True" 
				SourceName="PART_ClearButton" >
                            <Setter Property="Background" 
				Value="{StaticResource FocusBackground}" 
				TargetName="border" />
                        </Trigger>
                        <Trigger Property="IsFocused" Value="True" >
                            <Setter Property="Background" 
				Value="{StaticResource FocusBackground}" 
				TargetName="border" />
                        </Trigger>                       
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>               
    </Style>

Code Walkthrough

XML
<TextBox Style="{StaticResource FilterTextBoxStyle}"
                  x:Name="PART_FilterBox"  AutoWordSelection="True"
                  Grid.Column="1"
                  Margin="0,1,0,1" VerticalAlignment="Center"
                  Text="{Binding RelativeSource={RelativeSource AncestorType=
		{x:Type egl:FilterControl}}, Path=FilterText, Mode=TwoWay, 
		UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock x:Name="PART_Header" Text="{TemplateBinding Header}" 
		VerticalAlignment="Center" HorizontalAlignment="Left" Margin="2,0,0,0"
                            Grid.Column="1"
                            IsHitTestVisible="False" Foreground="#ADADAD"/>
<Button x:Name="PART_ClearButton" Grid.Column="2" Margin="0,0,4,0" 
			Style="{StaticResource ClearButtonStyle}"
                        Visibility="Collapsed"/>

In the above XAML, certain elements like TextBox, Button are defined as TemplateParts and these parts are referred in the FilterControl class's <a href="http://msdn.microsoft.com/en-us/library/system.windows.templatepartattribute.aspx" target="_blank">TemplatePartAttribute</a>. This is called as "named parts" of the template. When you are customizing this control's template and the name doesn't match, your control will not work properly (Reference for Custom Templates). To make it standard, I am using "PART_" as prefix.

The parts are accessed inside the control class and used further. Normally, if the parts are used throughout the control, a private variable is defined and assigned the value on OnApplyTemplate() overridden method. <a href="http://msdn.microsoft.com/en-us/library/system.windows.frameworkelement.gettemplatechild.aspx" target="_blank">GetTemplateChild</a> (is a protected internal function) function is used in that case like below:

C#
textBlock = GetTemplateChild(PART_Header) as TextBlock;

filterBox = GetTemplateChild(PART_FilterBox) as TextBox;

You can add your Properties to the class as you like. These properties can be a <a href="http://msdn.microsoft.com/en-us/library/system.windows.dependencyproperty.aspx" target="_blank">DependencyProperty</a> which would help in Binding or normal property. A <a href="http://msdn.microsoft.com/en-us/library/system.windows.dependencyproperty.aspx" target="_blank">DependencyProperty</a> is defined as:

C#
public string FilterText
{
    get
    {
        return (string)GetValue(FilterTextProperty);
    }
    set
    {
        SetValue(FilterTextProperty, value);
    }
}

public static readonly DependencyProperty FilterTextProperty =
	DependencyProperty.Register("FilterText", typeof(string),
	typeof(FilterControl), new PropertyMetadata(string.Empty));

Working

Once you placed the FilterControl in the Window, you can directly bind the <a href="http://msdn.microsoft.com/en-us/library/system.windows.controls.itemscontrol.aspx" target="_blank">ItemsControl</a> to the control or you can write your own Custom Filter logic inside the Filter event.

Filter event triggers after the user stops typing the text in the filter field. The     FilterFiringInterval is used at this time to avoid performance issue while typing. You can change the interval to any.

The ApplyFilterOnTarget is called after the Filter event fired, if the FilterEventArgs.IsFilterApplied is False. User can set IsFilterApplied to True if they wrote their own custom filter logic. In that case, TargetControl binding is not necessary.

C#
private void ApplyFilterOnTarget()
    {
        if (TargetControl == null || TargetControl.ItemsSource == null)
            return;

        ICollectionView collectionView = CollectionViewSource.GetDefaultView
					(TargetControl.ItemsSource);

        if (collectionView == null)
        {
            throw new InvalidOperationException
		("The TargetConrol should use ICollectionView as ItemSource.");
        }


        if (string.IsNullOrEmpty(this.FilterTextBindingPath))
        {
            throw new InvalidOperationException("FilterTextBindingPath is not set.");
        }

        collectionView.Filter = (m => (GetDataValue
	<string>(m, this.FilterTextBindingPath).IndexOf
	(filterBox.Text, StringComparison.InvariantCultureIgnoreCase) > -1));
        }

Here:

C#
collectionView.Filter = (m => (GetDataValue
	<string>(m, this.FilterTextBindingPath).IndexOf
	(filterBox.Text, StringComparison.InvariantCultureIgnoreCase) > -1));

is the code used to filter the collection bounded to TargetControl's <a href="http://msdn.microsoft.com/en-us/library/system.windows.controls.itemscontrol.itemssource.aspx" target="_blank">ItemsSource</a>.

And finally, it would work like below:

Here the ListBox is directly bound to the FilterControl. (Sample Application attached.)

Picture2.png

Here the Custom Filter logic is implemented.

Picture3.png

Known Issues

  • FilterControl filter only works with <a href="http://msdn.microsoft.com/en-us/library/system.windows.controls.itemscontrol.aspx" target="_blank">ItemsControl</a> which has <a href="http://msdn.microsoft.com/en-us/library/system.windows.controls.itemscontrol.itemssource.aspx" target="_blank">ItemsSource</a> set. If the TargetControlItems property is used, you need to handle the Filter event and write your own filter logic in there and set IsFilterApplied = True.

History

Date Updated Notes
April 27, 2011 Fixed the issue in Clear button clicked
April 25, 2011 Formatted the article with table of contents
April 1, 2011 More information about deriving from a control is explained
References link to the keywords/framework classes
March 18, 2011 First version published

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Team Leader
Australia Australia
He is from a small village in Tirunelveli, TamilNadu (India). He is fun loving guy. He like to travel a lot. Bikes/Cars/Mobiles are his fantasies.

He started his career as a BPO Data Entry Assistant and got chance to work as an ASP.NET Programmer in the same company after a year. And he move to the next company in 2006 as Software Engineer.

He worked as a Team Leader in a private software development company @Chennai, India for 8+years.

Now working as a Consultant @Sydney, Australia.

Comments and Discussions

 
QuestionCool Control - How can I make it sort the list so I get the best matches first? Pin
Erik Rude6-Jun-12 2:36
Erik Rude6-Jun-12 2:36 
AnswerRe: Cool Control - How can I make it sort the list so I get the best matches first? Pin
Erik Rude26-Jul-12 23:06
Erik Rude26-Jul-12 23:06 
The good Idea is this:
Cast the CollectionView to a ListCollectionView and use the CustomSort from that.
See below for listing:
C#
private void ApplyFilterOnTarget()
{
    if (TargetControl == null || TargetControl.ItemsSource == null) return;

    var collectionView = (ListCollectionView)CollectionViewSource.GetDefaultView(TargetControl.ItemsSource);

    if (collectionView == null)
    {
        throw new InvalidOperationException("The TargetControl should use ICollectionView as ItemSource.");
    }
    if (string.IsNullOrEmpty(this.FilterTextBindingPath))
    {
        throw new InvalidOperationException("FilterTextBindingPath is not set.");
    }
    collectionView.Filter =
        (m =>
         (GetDataValue<string>(m, this.FilterTextBindingPath).IndexOf(this.FilterText,
                                                                      StringComparison.
                                                                          InvariantCultureIgnoreCase) > -1));
    if (!collectionView.CanSort) return;
    collectionView.CustomSort = new SortTools(FilterText);}


And I implemented this SortTools class:
C#
public class SortTools : IComparer

 {
     private string _searchString = "";
     public SortTools()
     {

     }
     public SortTools(string searchString)
     {
         _searchString = searchString;
     }
     public int Compare(object x, object y)
     {
         var val = x.ToString().IndexOf(_searchString, StringComparison.InvariantCultureIgnoreCase) - y.ToString().IndexOf(_searchString, StringComparison.InvariantCultureIgnoreCase);
         if (val == 0)
             val = String.Compare(x.ToString(),y.ToString());
         return val ;
     }

 }

GeneralRe: Cool Control - How can I make it sort the list so I get the best matches first? Pin
Venkatesh Mookkan4-Sep-12 16:32
Venkatesh Mookkan4-Sep-12 16:32 

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.