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

Picture1.png

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 the FilterControl.
  • 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:

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 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)

Picture2.png

Here the Custom Filter logic is implemented.

Picture3.png

Known Issue

  • FilterControl filter only work with ItemsControl which has ItemsSource is set. If the TargetControl Items 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 1, 2011 - More information about deriving from a control is explained.
- References link to the keywords/framework classes.
March 18, 2011 First Version published.
推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架
新浪微博粉丝精灵,刷粉丝、刷评论、刷转发、企业商家微博营销必备工具"