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
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 theStyle
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 isFilterControl
which is derived from System.Windows.Controls.Control
. 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:<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 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
<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 defined 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 theFilterControl
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 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. 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 Issues
FilterControl
filter only works withItemsControl
which hasItemsSource
set. If theTargetControl
Items
property is used, you need to handle theFilter
event and write your own filter logic in there and setIsFilterApplied = True
.