Click here to Skip to main content
15,867,308 members
Articles / Desktop Programming / WPF

Filtering the WPF DataGrid automatically via the header (inline filtering)

Rate me:
Please Sign up or sign in to vote.
4.81/5 (35 votes)
25 Aug 2009CPOL6 min read 227.3K   10.7K   73   52
This will help you create a grid that has inline filtering like you see in DevExpress / Telerik.

Problem

Before switching to WPF, we used WinForms + DevExpress. For reasons like performance, time, … I liked DevExpress. But now, with WPF, it feels strange to use commercial controls. I feel like the sky is the limit and I could do whatever I want…

…until I wanted to filter the DataGrid.

I want to achieve something similar that was used in the DevExpress XtraGrid. That filter was so nice!

Customizing the WPF DataGrid

OK, I always want the best for my customers, so let's do something nicer than the default thingies. I don’t know what UX article it was from, but I read that controls should not pollute the UI. They should only be visible when needed. And that is what we’re going to build.

Normal state

Filtering (mouse over)

Mouse out (after the filter is applied)

The grid will only show the filter textbox when hovering the header. Once the filter is applied (automatically), the header text itself will update automatically.

Getting started

We’ll need to do the following:

  • Create a lookless grid control that implements this filtering functionality (FilterDataGrid)
  • Create a nice style for the header of the grid
  • Create a converter that will format our header text

Creating the lookless control

Here is the code:

C#
/// <summary>
/// A grid that makes inline filtering possible.
/// </summary>
public class FilteringDataGrid : DataGrid
{
    /// <summary>
    /// This dictionary will have a list of all applied filters
    /// </summary>
    private Dictionary<string, string> columnFilters;
    /// <summary>
    /// Cache with properties for better performance
    /// </summary>
    private Dictionary<string, PropertyInfo> propertyCache;
    /// <summary>
    /// Case sensitive filtering
    /// </summary>
    public static DependencyProperty IsFilteringCaseSensitiveProperty =
         DependencyProperty.Register("IsFilteringCaseSensitive", 
         typeof(bool), typeof(FilteringDataGrid), new PropertyMetadata(true));
    /// <summary>
    /// Case sensitive filtering
    /// </summary>
    public bool IsFilteringCaseSensitive
    {
        get { return (bool)(GetValue(IsFilteringCaseSensitiveProperty)); }
        set { SetValue(IsFilteringCaseSensitiveProperty, value); }
    }
    /// <summary>
    /// Register for all text changed events
    /// </summary>
    public FilteringDataGrid()
    {
        // Initialize lists
        columnFilters = new Dictionary<string, string>();
        propertyCache = new Dictionary<string, PropertyInfo>();
        // Add a handler for all text changes
        AddHandler(TextBox.TextChangedEvent, 
          new TextChangedEventHandler(OnTextChanged), true);
        // Datacontext changed, so clear the cache
        DataContextChanged += new 
          DependencyPropertyChangedEventHandler(
          FilteringDataGrid_DataContextChanged);
    }
    /// <summary>
    /// Clear the property cache if the datacontext changes.
    /// This could indicate that an other type of object is bound.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void FilteringDataGrid_DataContextChanged(object sender, 
                 DependencyPropertyChangedEventArgs e)
    {
        propertyCache.Clear();
    }
    /// <summary>
    /// When a text changes, it might be required to filter
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void OnTextChanged(object sender, TextChangedEventArgs e)
    {
        // Get the textbox
        TextBox filterTextBox = e.OriginalSource as TextBox;
        // Get the header of the textbox
        DataGridColumnHeader header = 
          TryFindParent<DataGridColumnHeader>(filterTextBox);
        if (header != null)
        {
            UpdateFilter(filterTextBox, header);
            ApplyFilters();
        }
    }
    /// <summary>
    /// Update the internal filter
    /// </summary>
    /// <param name="textBox"></param>
    /// <param name="header"></param>
    private void UpdateFilter(TextBox textBox, DataGridColumnHeader header)
    {
        // Try to get the property bound to the column.
        // This should be stored as datacontext.
        string columnBinding = header.DataContext != null ? 
                                    header.DataContext.ToString() : "";
        // Set the filter 
        if (!String.IsNullOrEmpty(columnBinding))
            columnFilters[columnBinding] = textBox.Text;
    }
    /// <summary>
    /// Apply the filters
    /// </summary>
    /// <param name="border"></param>
    private void ApplyFilters()
    {
        // Get the view
        ICollectionView view = CollectionViewSource.GetDefaultView(ItemsSource);
        if (view != null)
        {
            // Create a filter
            view.Filter = delegate(object item)
            {
                // Show the current object
                bool show = true;
                // Loop filters
                foreach (KeyValuePair<string, string> filter in columnFilters)
                {
                    object property = GetPropertyValue(item, filter.Key);
                    if (property != null)
                    {
                        // Check if the current column contains a filter
                        bool containsFilter = false;
                        if (IsFilteringCaseSensitive)
                            containsFilter = property.ToString().Contains(filter.Value);
                        else
                            containsFilter = 
                              property.ToString().ToLower().Contains(filter.Value.ToLower());
                        // Do the necessary things if the filter is not correct
                        if (!containsFilter)
                        {
                            show = false;
                            break;
                        }
                    }
                }
                // Return if it's visible or not
                return show;
            };
        }
    }
    /// <summary>
    /// Get the value of a property
    /// </summary>
    /// <param name="item"></param>
    /// <param name="property"></param>
    /// <returns></returns>
    private object GetPropertyValue(object item, string property)
    {
        // No value
        object value = null;
        // Get property  from cache
        PropertyInfo pi = null;
        if (propertyCache.ContainsKey(property))
            pi = propertyCache[property];
        else
        {
            pi = item.GetType().GetProperty(property);
            propertyCache.Add(property, pi);
        }
        // If we have a valid property, get the value
        if (pi != null)
            value = pi.GetValue(item, null);
        // Done
        return value;
    }
    /// <summary>
    /// Finds a parent of a given item on the visual tree.
    /// </summary>
    /// <typeparam name="T">The type of the queried item.</typeparam>
    /// <param name="child">A direct or indirect
    /// child of the queried item.</param>
    /// <returns>The first parent item that matches the submitted
    /// type parameter. If not matching item can be found,
    /// a null reference is being returned.</returns>
    public static T TryFindParent<T>(DependencyObject child)
      where T : DependencyObject
    {
        //get parent item
        DependencyObject parentObject = GetParentObject(child);
        //we've reached the end of the tree
        if (parentObject == null) return null;
        //check if the parent matches the type we're looking for
        T parent = parentObject as T;
        if (parent != null)
        {
            return parent;
        }
        else
        {
            //use recursion to proceed with next level
            return TryFindParent<T>(parentObject);
        }
    }
    /// <summary>
    /// This method is an alternative to WPF's
    /// <see cref="VisualTreeHelper.GetParent"/> method, which also
    /// supports content elements. Do note, that for content element,
    /// this method falls back to the logical tree of the element.
    /// </summary>
    /// <param name="child">The item to be processed.</param>
    /// <returns>The submitted item's parent, if available. Otherwise null.</returns>
    public static DependencyObject GetParentObject(DependencyObject child)
    {
        if (child == null) return null;
        ContentElement contentElement = child as ContentElement;
        if (contentElement != null)
        {
            DependencyObject parent = ContentOperations.GetParent(contentElement);
            if (parent != null) return parent;
            FrameworkContentElement fce = contentElement as FrameworkContentElement;
            return fce != null ? fce.Parent : null;
        }
        // If it's not a ContentElement, rely on VisualTreeHelper
        return VisualTreeHelper.GetParent(child);
    } 
}

First, I want to thank Philipp Sumi for his snippet for finding ancestors of dependency objects. I have used his two methods: TryFindParent and GetParentObject that you can find at the end of the class.

But let’s get to work now, this is how it works:

  • The FilteringDataGrid exposes a DependencyProperty: IsFilteringCaseSensitive. This is just a flag to that decides whether to do case sensitive checks or not on the filtering.
  • When the grid is initialized, we also initialize two dictionaries. More about this later. The most important thing happening in the constructor is binding to all TextChanged events.
  • Once some text changes in any TextBox in the grid, this event will happen. That's why in the method OnTextChanged, we only want to process TextBoxes that are present in the DataGridColumnHeader.
  • If this TextBox is in the DataGridColumnHeader, we can process. By using the DataContext on the header, we’ll find the name of the property that is bound to the current column. That is where our two lists come in handy.
  • First, we’ll use the columnsFilter. This is a dictionary that will keep track of all the current properties with all their current filters. This is important to know because one would want to filter on multiple columns at the same time.
  • After we update the columnsFilter dictionary, we’ll want to apply the whole filter to all the rows. This is done using the ICollectionView interface.
  • This is where the propertyCache comes in handy. We’re actually using Reflection to get the property of the object using the property name. After that, we’ll get the value of the current object for that property and we’ll check if it contains the value from our filter. But since Reflection is so heavy, I want to use some form of cache. If we know the object type, we could store the property that matches a certain name. Doing that, we won’t always need to do item.GetType().GetProperty().
  • And once the filtering is done for each object (including case sensitive checks or not), we’re done.
  • Just a last remark. If the DataContext changes (from List<Country> to List<City>, for example), we’ll want to clear the propertyCache. Because if we store the property “Name” of Country, and later on we want to use that property to get the value of a City object, we’ll get exceptions.

Well, well, we got us a nice filter control. But this control will not show any filter in the header. We still need to apply some styling.

You can style however you want, the only requirement is that you put a TextBox in the DataGridColumnHeader. You might ask yourself why I’m not using things like PART_filterControl or so, but the thing is we don’t have access to the actual DataGridColumnHeader. We only have access to its style from the DataGrid itself.

Styling the FilterDataGrid

First some XAML…

XML
<local:HeaderFilterConverter x:Key="headerConverter"/>
    <Style TargetType="{x:Type my:DataGridColumnHeader}">
        <Setter Property="VerticalContentAlignment" Value="Center"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type my:DataGridColumnHeader}">
                    <ControlTemplate.Resources>
                        <Storyboard x:Key="ShowFilterControl">
                            <ObjectAnimationUsingKeyFrames BeginTime="00:00:00" 
                                     Storyboard.TargetName="filterTextBox" 
                                     Storyboard.TargetProperty="(UIElement.Visibility)">
                                <DiscreteObjectKeyFrame KeyTime="00:00:00" 
                                       Value="{x:Static Visibility.Visible}"/>
                                <DiscreteObjectKeyFrame 
                                  KeyTime="00:00:00.5000000" 
                                  Value="{x:Static Visibility.Visible}"/>
                            </ObjectAnimationUsingKeyFrames>
                            <ColorAnimationUsingKeyFrames BeginTime="00:00:00" 
                                  Storyboard.TargetName="filterTextBox" 
                                  Storyboard.TargetProperty=
                                    "(Panel.Background).(SolidColorBrush.Color)">
                                <SplineColorKeyFrame 
                                    KeyTime="00:00:00" 
                                    Value="Transparent"/>
                                <SplineColorKeyFrame 
                                    KeyTime="00:00:00.5000000" 
                                    Value="White"/>
                            </ColorAnimationUsingKeyFrames>
                        </Storyboard>
                        <Storyboard x:Key="HideFilterControl">
                            <ObjectAnimationUsingKeyFrames BeginTime="00:00:00" 
                                  Storyboard.TargetName="filterTextBox" 
                                  Storyboard.TargetProperty="(UIElement.Visibility)">
                                <DiscreteObjectKeyFrame KeyTime="00:00:00.4000000" 
                                      Value="{x:Static Visibility.Collapsed}"/>
                            </ObjectAnimationUsingKeyFrames>
                            <ColorAnimationUsingKeyFrames BeginTime="00:00:00" 
                                    Storyboard.TargetName="filterTextBox" 
                                    Storyboard.TargetProperty=
                                      "(UIElement.OpacityMask).(SolidColorBrush.Color)">
                                <SplineColorKeyFrame KeyTime="00:00:00" Value="Black"/>
                                <SplineColorKeyFrame 
                                  KeyTime="00:00:00.4000000" 
                                  Value="#00000000"/>
                            </ColorAnimationUsingKeyFrames>
                        </Storyboard>
                    </ControlTemplate.Resources>
                        <my:DataGridHeaderBorder x:Name="dataGridHeaderBorder" 
                                Margin="0" VerticalAlignment="Top" 
                                Height="31" 
                                IsClickable="{TemplateBinding CanUserSort}" 
                                IsHovered="{TemplateBinding IsMouseOver}" 
                                IsPressed="{TemplateBinding IsPressed}" 
                                SeparatorBrush="{TemplateBinding SeparatorBrush}" 
                                SeparatorVisibility="{TemplateBinding SeparatorVisibility}" 
                                SortDirection="{TemplateBinding SortDirection}" 
                                Background="{TemplateBinding Background}" 
                                BorderBrush="{TemplateBinding BorderBrush}" 
                                BorderThickness="{TemplateBinding BorderThickness}" 
                                Padding="{TemplateBinding Padding}" 
                                Grid.ColumnSpan="1">
                            <Grid x:Name="grid" Width="Auto" 
                                    Height="Auto" 
                                    RenderTransformOrigin="0.5,0.5">
                                <Grid.RenderTransform>
                                    <TransformGroup>
                                        <ScaleTransform/>
                                        <SkewTransform/>
                                        <RotateTransform/>
                                        <TranslateTransform/>
                                    </TransformGroup>
                                </Grid.RenderTransform>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="*"/>
                                </Grid.ColumnDefinitions>
                            <ContentPresenter x:Name="contentPresenter"
                                 HorizontalAlignment=
                                   "{TemplateBinding HorizontalContentAlignment}" 
                                 VerticalAlignment=
                                   "{TemplateBinding VerticalContentAlignment}" 
                                 SnapsToDevicePixels=
                                   "{TemplateBinding SnapsToDevicePixels}" 
                                 ContentStringFormat=
                                   "{TemplateBinding ContentStringFormat}" 
                                 ContentTemplate=
                                   "{TemplateBinding ContentTemplate}">
                              <ContentPresenter.Content>
                                 <MultiBinding 
                                       Converter="{StaticResource headerConverter}">
                                   <MultiBinding.Bindings>
                                     <Binding 
                                       ElementName="filterTextBox" 
                                       Path="Text" />
                                     <Binding 
                                       RelativeSource="{RelativeSource TemplatedParent}" 
                                       Path="Content" />
                                   </MultiBinding.Bindings>
                                 </MultiBinding>
                              </ContentPresenter.Content>
                            </ContentPresenter>
                            <TextBox x:Name="filterTextBox" 
                                HorizontalAlignment="Right" 
                                MinWidth="25" Height="Auto" 
                                OpacityMask="Black" 
                                Visibility="Collapsed" Text="" 
                                TextWrapping="Wrap" 
                                Grid.Column="0" 
                                Grid.ColumnSpan="1"/>
                            </Grid>
                      </my:DataGridHeaderBorder>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsMouseOver" Value="True">
                            <Trigger.EnterActions>
                                <BeginStoryboard 
                                   x:Name="ShowFilterControl_BeginStoryboard" 
                                   Storyboard="{StaticResource ShowFilterControl}"/>
                                <StopStoryboard 
                                  BeginStoryboardName=
                                    "HideFilterControl_BeginShowFilterControl"/>
                            </Trigger.EnterActions>
                            <Trigger.ExitActions>
                                <BeginStoryboard 
                                  x:Name="HideFilterControl_BeginShowFilterControl" 
                                  Storyboard="{StaticResource HideFilterControl}"/>
                                <StopStoryboard BeginSto
                                  ryboardName="ShowFilterControl_BeginStoryboard"/>
                            </Trigger.ExitActions>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

Ok that's a big one. Let me explain in a few words what we're doing.

The thing is we have to modify the style of the header. We actually modify the DataGridHeaderBorder, this one contains the ContentPresenter.

Well, the ContentPresenter is wrapped in a grid with two rows. Row 0 contains ContentPresenter and row 1 contains the filterTextBox.

And, if you pay close attention, you’ll see that the content of the ContentPresenter contains a multi binding. This is set to the name of the column itself and the filter. Well see about this in the next section.

And finally, in the styling of our control, we have some triggers that will cause a nice fade-in/fade-out effect on the filter textbox. If you enter the DataGridColumnHeader with your mouse, the filterTextBox will appear after 0.5 sec. If you mouse out, it will disappear after 0.5 sec.

Styling the header even more

The final thing we want to do is show the status of the current filters in the headers. Because, when we move away with our mouse, the filterTextBox is gone and we want to know if the grid is filtered or not.

That’s why we want the header to show if a filter is enabled. Like this:

Things we’ll have to do:

  • Bind the filter text to the header content
  • Bind the name/header of the column to the header content
  • Apply some formatting to the displayed text (bold)

For this, I’ve created a HeaderFilterConverter. This class implements the IMultiValueConverter interface. This means you can input multiple values and return a value.

What we’ll do is, we’ll pass the header/name of the column (e.g.: Name) and the current filter (e.g.: San) and we’ll return a TextBlock. This TextBlock should look like this: “Name (Filter: San)”. If there is no text at all, it should only display the header/column text.

For this, we will use an interesting technique where we’ll create some XAML code and convert it to an actual UI element. An example of this dynamic XAML can also be found on: http://msdn.microsoft.com/en-us/library/dd894487(VS.95).aspx

C#
/// <summary>
/// This converter will:
///  - Take the header
///  - Take the filtered word (if any)
///  - Add '(Filter: (bold)x(/bold))' to the header
/// </summary>
public class HeaderFilterConverter : IMultiValueConverter
{
    /// <summary>
    /// Create a nice looking header
    /// </summary>
    /// <param name="values"></param>
    /// <param name="targetType"></param>
    /// <param name="parameter"></param>
    /// <param name="culture"></param>
    /// <returns></returns>
    public object Convert(object[] values, Type targetType, object parameter, 
                  System.Globalization.CultureInfo culture)
    {
        // Get values
        string filter = values[0] as string;
        string headerText = values[1] as string;

        // Generate header text
        string text = "{0}{3}" + headerText + " {4}";
        if (!String.IsNullOrEmpty(filter))
            text += "(Filter: {2}" + values[0] + "{4})";
        text += "{1}";

        // Escape special XML characters like <>&'
        text = new System.Xml.Linq.XText(text).ToString();

        // Format the text
        text = String.Format(text,
         @"<TextBlock xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'>",
         "</TextBlock>", "<Run FontWeight='bold' Text='", 
         "<Run Text='", @"'/>");

        // Convert to stream
        MemoryStream stream = new MemoryStream(ASCIIEncoding.UTF8.GetBytes(text));

        // Convert to object
        TextBlock block = (TextBlock)System.Windows.Markup.XamlReader.Load(stream);
        return block;
    }

    /// <summary>
    /// Not required
    /// </summary>
    /// <param name="value"></param>
    /// <param name="targetTypes"></param>
    /// <param name="parameter"></param>
    /// <param name="culture"></param>
    /// <returns></returns>
    public object[] ConvertBack(object value, Type[] targetTypes, 
           object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

To-do

Take converters into account. Because it could be that the data in the grid does not match the data the user actually sees.

Downloadable sample

I’ve also prepared a fully working downloadable sample. In this sample, youl have a grid with 1000 random items where you can see the filter in action. It also includes all the code from this guide.

Comments

If you really enjoyed this article, don't hesitate to vote for the article of the month. Enjoy!

License

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


Written By
Technical Lead RealDolmen
Belgium Belgium
I'm a Technical Consultant at RealDolmen, one of the largest players on the Belgian IT market: http://www.realdolmen.com

All posts also appear on my blogs: http://blog.sandrinodimattia.net and http://blog.fabriccontroller.net

Comments and Discussions

 
Questionnew version available Pin
taze220711-May-20 11:26
taze220711-May-20 11:26 
AnswerRe: new version available Pin
revitarkitek29-Sep-20 5:46
revitarkitek29-Sep-20 5:46 
GeneralRe: new version available Pin
rjaszek24-Mar-21 6:42
rjaszek24-Mar-21 6:42 
QuestionClearing filtering shortcut Pin
Member 1338972218-Dec-19 12:00
Member 1338972218-Dec-19 12:00 
AnswerRe: Clearing filtering shortcut Pin
Member 1338972218-Dec-19 15:07
Member 1338972218-Dec-19 15:07 
QuestionRequired .sln file, missing in the sample projects. Pin
Member 1073838714-May-19 4:55
Member 1073838714-May-19 4:55 
Questionfilter textBox doesn't appear on column header Pin
Member 1279921011-Nov-16 12:29
Member 1279921011-Nov-16 12:29 
AnswerRe: filter textBox doesn't appear on column header Pin
Member 111510219-May-19 1:42
Member 111510219-May-19 1:42 
QuestionShrinking Column after Filter Removal Pin
arbiter3415-Sep-15 10:04
arbiter3415-Sep-15 10:04 
AnswerRe: Shrinking Column after Filter Removal Pin
revitarkitek29-Sep-20 5:49
revitarkitek29-Sep-20 5:49 
QuestionTableView and filtering Pin
Mohamed MANS11-Mar-15 22:22
professionalMohamed MANS11-Mar-15 22:22 
Questionhow to update the code if use DataTable use itemsource Pin
one_leaves15-Sep-14 19:51
one_leaves15-Sep-14 19:51 
GeneralMy vote of 3 Pin
califf2212-Mar-13 7:04
califf2212-Mar-13 7:04 
SuggestionFix for Column Alias Pin
smnabil8-Aug-12 0:48
smnabil8-Aug-12 0:48 
SuggestionRe: Fix for Column Alias Pin
lost.in.translation3-Sep-12 11:54
lost.in.translation3-Sep-12 11:54 
GeneralMy vote of 5 Pin
Kannan.V17-Jul-12 2:26
Kannan.V17-Jul-12 2:26 
SuggestionRemove Filter Pin
lothy@gmx.de7-Feb-12 23:56
lothy@gmx.de7-Feb-12 23:56 
GeneralRe: Remove Filter Pin
revitarkitek29-Sep-20 5:45
revitarkitek29-Sep-20 5:45 
QuestionNice job Pin
c4mel0t10-Nov-11 20:44
c4mel0t10-Nov-11 20:44 
QuestionHow can i hide a column? Pin
Ar.Gorgin16-Oct-11 20:02
Ar.Gorgin16-Oct-11 20:02 
QuestionHow I succeed compile this on VS2010 Pin
Nicolas Mathieu25-Sep-11 5:11
Nicolas Mathieu25-Sep-11 5:11 
AnswerRe: How I succeed compile this on VS2010 Pin
shlom2924-Apr-12 22:55
shlom2924-Apr-12 22:55 
You forgot:
1) Add reference to PresentationFramework.Aero
2) Change "my:DataGridHeaderBorder" to "dg:DataGridHeaderBorder"

Thanks, it was very helpful.
GeneralMy vote of 4 Pin
xam8re4-Aug-11 1:16
xam8re4-Aug-11 1:16 
SuggestionStatic Columns Pin
lothy@gmx.de3-Aug-11 6:20
lothy@gmx.de3-Aug-11 6:20 
GeneralRe: Static Columns Pin
Ldehoogh29-Nov-11 0:21
Ldehoogh29-Nov-11 0:21 

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.