WPF Custom Control - FilterControl for ListBox/ListView
Introduction
ComputerCastle.Demo.Controls.FilterControl
is a WPF-based Custom Control
which can be used as quick filter for any ItemsControl
like ListBox
,
ListView
, DataGrid
, etc. This article is to tell you how
easy to create a Custom Control in WPF using combination of simple framework controls.
The article focus on:
- How to create a Custom Control in WPF briefly
- Explanation about
FilterControl
- How to implement
FilterControl
Background
I have been working in WPF for 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 grown better now. You might find few resemblence 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 (Preferrably 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 theFilterControl
. - Make sure you merge all the resource dictionaries together and set the ThemeInfo in the AssemblyInfo.cs file.
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, ResourceDictionaryLocation.SourceAssembly)]
Using the code
The main class is FilterControl
which is derived from System.Windows.Controls.Control
. It is very important
to determine the base class when you developing a Custom Control. Somecase 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:
<ctrl:FilterControl Height="25" Header="Type here to filter" DockPanel.Dock="Top"
TargetControl="{Binding ElementName=listBox}"
FilterTextBindingPath="Name"/>
And the class diagram is:
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 fire 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:
<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
<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 TemplatePartAttribute
. 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 define and assigned
the value on OnApplyTemplate()
overridden method. GetTemplateChild
(is a protected internal function
)
function is used in that case like below:
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
DependencyProperty
which would help in Binding or
normal property. A DependencyProperty
is defined as:
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 ItemsControl
to the control or you can write your
own Custom Filter logic inside the Filter event.
Filter
event triggers after the user stop 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.
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
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
ItemsSource
.
And finally, it would work like below:
Here the ListBox is directly bound to the FilterControl. (Sample Application attached)
Here the Custom Filter logic is implemented.
Known Issue
FilterControl
filter only work withItemsControl
which hasItemsSource
is set. If theTargetControl
Items
property is used, you need to handle theFilter
event and write your own filter logic in there and setIsFilterApplied = True
.
History
Date | Updated Notes |
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. |
发表评论
This is my article posted in code project. How come you using it without requesting permission from me???