Saturday, November 21, 2009

Search highlighting in WPF DataGrid

Unfortunately the DataGrid of the WPF-Toolkit does not provide a text highlighting feature out of the box; but sometimes it is very usefull for the users to be able to search for text or numbers in the datagrid to find the interesting information more quickly.
Tomer Shamam did a very good work which highlights a search key in different colors depending on which column they were found.

The only things which I don't like on his solution are:
  1. It works only if the datasource of the datagrid are objects
  2. That you have to define a style for every property you want to be highlighted in the datagrid which may be a big work if you have 15 properties for example
  3. When a row with a match is selected you do not see anymore which cell was highlighted
For my pupose I don't need the feature of the different coloring because I want only to highlight all cells in the datagrid where the search term is present in the same color. To resolve all of the above issues I changed the code of Shamam slightly.
You still need this two helper classes written by Shamam:
public class SearchTermConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var stringValue = values[0] == null ? string.Empty : values[0].ToString();
var searchTerm = values[1] as string;

return !string.IsNullOrEmpty(searchTerm) &&
!string.IsNullOrEmpty(stringValue) &&
stringValue.ToLower().Contains(searchTerm.ToLower());
}

public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}

and
public static class SearchOperations
{
public static string GetSearchTerm(DependencyObject obj)
{
return (string)obj.GetValue(SearchTermProperty);
}

public static void SetSearchTerm(DependencyObject obj, string value)
{
obj.SetValue(SearchTermProperty, value);
}

public static readonly DependencyProperty SearchTermProperty =
DependencyProperty.RegisterAttached(
"SearchTerm",
typeof(string),
typeof(SearchOperations),
new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.Inherits));

public static bool GetIsMatch(DependencyObject obj)
{
return (bool)obj.GetValue(IsMatchProperty);
}

public static void SetIsMatch(DependencyObject obj, bool value)
{
obj.SetValue(IsMatchProperty, value);
}

/* Using a DependencyProperty as the backing store for IsMatch. This enables animation, styling, binding, etc...*/
public static readonly DependencyProperty IsMatchProperty =
DependencyProperty.RegisterAttached("IsMatch", typeof(bool), typeof(SearchOperations), new UIPropertyMetadata(false));
}

As far as I understood this code defines a new dependancy property on the datagrid where the search term is stored and another dependancy property on each cell which stores wheter it is a match or not. Through the style which is defined in the Resource section of the page the "IsMatch" property is used to format the cell accordingly.
This is my modified XAML code to search the text of the datagrid instead of the property. Here you can also change the different colors if you like.
<Grid.Resources>
<local:SearchTermConverter
x:Key="SearchTermConverter" />

<SolidColorBrush
x:Key="{x:Static SystemColors.HighlightBrushKey}"
Color="Blue" />

<SolidColorBrush
x:Key="HighlightColor"
Color="Yellow" />

<SolidColorBrush
x:Key="SelectedHighlightedColor"
Color="Red" />

<Style
x:Key="DefaultCell"
TargetType="{x:Type toolkit:DataGridCell}">
<Setter
Property="local:SearchOperations.IsMatch">
<Setter.Value>
<MultiBinding
Converter="{StaticResource SearchTermConverter}">
<Binding
RelativeSource="{RelativeSource Self}"
Path="Content.Text" />
<Binding
RelativeSource="{RelativeSource Self}"
Path="(local:SearchOperations.SearchTerm)" />
</MultiBinding>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger
Property="local:SearchOperations.IsMatch"
Value="True">
<Setter
Property="Background"
Value="{StaticResource HighlightColor}">
</Setter>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition
Property="IsSelected"
Value="True" />
<Condition
Property="local:SearchOperations.IsMatch"
Value="True" />
</MultiTrigger.Conditions>
<Setter
Property="Background"
Value="{StaticResource SelectedHighlightedColor}"></Setter>
</MultiTrigger>
</Style.Triggers>
</Style>
</Grid.Resources>

Then the style is assigned to the datagrid:
<toolkit:DataGrid
AutoGenerateColumns="True"
Name="dgvDataTable"
CellStyle="{StaticResource DefaultCell}"
Margin="0,256,0,0"
Background="White" />

And finally the textbox connected to the "SearchTerm" property of the datagrid;
either via code in the TextChanged event of the TextBox:
SearchOperations.SetSearchTerm(this.dgvObjects, this.textBox1.Text);


or via XAML:
<Window
x:Class="SearchHighlighting.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1"
Height="460.138"
Width="502"
xmlns:toolkit="http://schemas.microsoft.com/wpf/2008/toolkit"
xmlns:local="clr-namespace:SearchHighlighting">
...
<toolkit:DataGrid
AutoGenerateColumns="True"
Name="dgvDataTable"
CellStyle="{StaticResource DefaultCell}"
Margin="0,256,0,0"
Background="White"
local:SearchOperations.SearchTerm="{Binding ElementName=textBox1, Path=Text}" />


The result looks like this:



You can download the whole project from here.

4 comments:

Anonymous said...

Very nice code, but how can I get the datagrid jumping to the position of the first hit in the grid, when it's outside the display area.

Regards Peter

HFinley said...

This is nice for your average datagrid, however it does fail if the datagrid has virtualization. Scrolling one item at a time works, but the converter has issues when scrolling multiple items, an entire page or mouse wheel scrolling.

HFinley said...

Any thoughts on how to fix the virtualization issue?

Regards Hank

Anonymous said...

Thank you works very well.