Monday, April 7, 2014

WPF Custom Control - FilterControl for ListBox/ListView

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 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 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 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 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 with ItemsControl which has ItemsSource 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

Friday, October 14, 2011

DateTime format in .NET 4.0

Lets assume I have the DateTime pattern "dd-MMM-yyyy" in my locale.

DateTime.Now.ToString("yyyy/MM/dd") working different in 3.5 & 4.0 framework.

In 3.5, it uses the pattern given in the parameter to convert the date to string. So, the output would be "2011/11/10" and it the expected result.

But in 4.0, this would return as "2011-11-10" which is not correct. 4.0 is using the current culture's ("-") separator instead of using the pattern's separator ("/") given in the parameter. To fix it, we need to pass IFormatProvider along with the ToString function.


Example:
DateTime.Now.ToString("yyyy/MM/dd", DateTimeFormatInfo.InvariantInfo)

Friday, January 7, 2011

Different ways to bind a data in Codebind from Xaml

I see many questions in "Question & Answers" section regarding the data binding issue in WPF. And when I see the question they miss very simple thing.

This is a tip to help them out. And basically for the beginners who are trying to learn the data binding concepts.

The sample, I provide here is,
- WPF Application
- C# Language

Create a WPF Application from your Visual Studio. Place a ListBox.

XAML:

<Window x:Class="WpfApplication2.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:loc="clr-namespace:WpfApplication2"
        Title="Window1" Height="300" Width="300"
        x:Name="windowObject">
    <Grid>
        <ListBox  />
    </Grid>
</Window>

Add a simple Property in the codebehind of Window1 as CollectionOfString which is nothing but List<string>. Add few items into the CollectionOfString for our DataBinding example.

CS:

using System.Collections.Generic;using System.Windows;namespace WpfApplication2
{
    public partial class Window1 : Window
    {
        public Window1()
        {
            this.CollectionOfString = new List<string>();
            this.CollectionOfString.Add("Apple");
            this.CollectionOfString.Add("Banana");
            this.CollectionOfString.Add("Custard Apple");
            InitializeComponent();
        }
        public List<string> CollectionOfString
        {
            get;
            set;
        }
    }
}

Now go to the designer/Xaml window. You simple set binding of the CollectionOfString in two ways like below:

Way 1:

<ListBox ItemsSource="{Binding RelativeSource={RelativeSource AncestorType={x:Type loc:Window1}},  
Path=CollectionOfString}" />

Here "loc" is the namespace of Window1 class.

Way 2:

<ListBox ItemsSource="{Binding ElementName=windowObject,  
Path=CollectionOfString}"/>

Here "windowObject" is the name of the Window

Let me know if you have any question.

I hope it is helpful.