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:
public class FilteringDataGrid : DataGrid
{
private Dictionary<string, string> columnFilters;
private Dictionary<string, PropertyInfo> propertyCache;
public static DependencyProperty IsFilteringCaseSensitiveProperty =
DependencyProperty.Register("IsFilteringCaseSensitive",
typeof(bool), typeof(FilteringDataGrid), new PropertyMetadata(true));
public bool IsFilteringCaseSensitive
{
get { return (bool)(GetValue(IsFilteringCaseSensitiveProperty)); }
set { SetValue(IsFilteringCaseSensitiveProperty, value); }
}
public FilteringDataGrid()
{
columnFilters = new Dictionary<string, string>();
propertyCache = new Dictionary<string, PropertyInfo>();
AddHandler(TextBox.TextChangedEvent,
new TextChangedEventHandler(OnTextChanged), true);
DataContextChanged += new
DependencyPropertyChangedEventHandler(
FilteringDataGrid_DataContextChanged);
}
private void FilteringDataGrid_DataContextChanged(object sender,
DependencyPropertyChangedEventArgs e)
{
propertyCache.Clear();
}
private void OnTextChanged(object sender, TextChangedEventArgs e)
{
TextBox filterTextBox = e.OriginalSource as TextBox;
DataGridColumnHeader header =
TryFindParent<DataGridColumnHeader>(filterTextBox);
if (header != null)
{
UpdateFilter(filterTextBox, header);
ApplyFilters();
}
}
private void UpdateFilter(TextBox textBox, DataGridColumnHeader header)
{
string columnBinding = header.DataContext != null ?
header.DataContext.ToString() : "";
if (!String.IsNullOrEmpty(columnBinding))
columnFilters[columnBinding] = textBox.Text;
}
private void ApplyFilters()
{
ICollectionView view = CollectionViewSource.GetDefaultView(ItemsSource);
if (view != null)
{
view.Filter = delegate(object item)
{
bool show = true;
foreach (KeyValuePair<string, string> filter in columnFilters)
{
object property = GetPropertyValue(item, filter.Key);
if (property != null)
{
bool containsFilter = false;
if (IsFilteringCaseSensitive)
containsFilter = property.ToString().Contains(filter.Value);
else
containsFilter =
property.ToString().ToLower().Contains(filter.Value.ToLower());
if (!containsFilter)
{
show = false;
break;
}
}
}
return show;
};
}
}
private object GetPropertyValue(object item, string property)
{
object value = null;
PropertyInfo pi = null;
if (propertyCache.ContainsKey(property))
pi = propertyCache[property];
else
{
pi = item.GetType().GetProperty(property);
propertyCache.Add(property, pi);
}
if (pi != null)
value = pi.GetValue(item, null);
return value;
}
public static T TryFindParent<T>(DependencyObject child)
where T : DependencyObject
{
DependencyObject parentObject = GetParentObject(child);
if (parentObject == null) return null;
T parent = parentObject as T;
if (parent != null)
{
return parent;
}
else
{
return TryFindParent<T>(parentObject);
}
}
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;
}
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 TextBox
es 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…
<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
public class HeaderFilterConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
string filter = values[0] as string;
string headerText = values[1] as string;
string text = "{0}{3}" + headerText + " {4}";
if (!String.IsNullOrEmpty(filter))
text += "(Filter: {2}" + values[0] + "{4})";
text += "{1}";
text = new System.Xml.Linq.XText(text).ToString();
text = String.Format(text,
@"<TextBlock xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'>",
"</TextBlock>", "<Run FontWeight='bold' Text='",
"<Run Text='", @"'/>");
MemoryStream stream = new MemoryStream(ASCIIEncoding.UTF8.GetBytes(text));
TextBlock block = (TextBlock)System.Windows.Markup.XamlReader.Load(stream);
return block;
}
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!