Introduction
One functionality that is good to have for a data grid and quite common for Winform/ASP.NET is auto filtering. At the moment the Silverlight DataGrid doesn’t provide anything for this, so I have decided to address the problem. The idea is to have the option to choose from different filter criteria for each DataGrid column, depending on the bound data type.
In order to support filtering the DataGrid source collection should be able to accept various filtering algorithms and "present" back to the grid only those items matching the criteria. If you have a look at the System.ComponentModel
namespace you will find the ICollectionView
interface. Here is the MSDN Documentation remark on the interface.
"DataGrid control uses this interface to access the indicated functionality in the data source assigned to its ItemsSource
property. If the ItemsSource implements IList
, but does not implement ICollectionView
, the DataGrid wraps the ItemsSource
in an internal ICollectionView
implementation."
And another short note: "You can think of a collection view as a layer on top of a binding source collection that allows you to navigate and display the collection based on sort, filter, and group queries, all without having to manipulate the underlying source collection itself. If the source collection implements the INotifyCollectionChanged
interface, the changes that raise the CollectionChanged
event are propagated to the views."
Code Design
Based on this I have written a collection view on top of an IList
. The code should be self explanatory; the collection view manages internally an ordered set of indexes to the original collection for those elements matching the filters criteria and every time there is a change in the filtering context this set is being rebuilt. I made a restriction to my generic collection view, forcing the elements to inherit from INotifyPropertyChanged
. There is a small catch to this and I will discuss it at the end, later in the article. The reason behind this is to be able to handle any value change for an element property in order to be able to check if that collection element is matching the filtering criteria.
public interface IFilteredCollection : ICollectionView
{
void AddFilter(IFilter filter);
void RemoveFilter(IFilter filter);
…
}
public class FilteredCollectionView
<t> : IFilteredCollection, INotifyPropertyChanged
where T : INotifyPropertyChanged
As seen in the code snippet above the collection view allows to register/unregister an instance of IFilter
. All the classes inheriting this interface must implement:
bool IsMatch(object target)
: determines if the target object matches the criteria;
PropertyInfo PropertyInfo
: returns the PropertyInfo
object for the property (of a collection view element) that is targeted by the matching criteria;
EventHandler FilteringChanged
: Occurs when the filter has changed and the IsMatch
logic has been affected;
Have a look at the next class diagram for a full list of filtering logic implementations.
For each defined filter there is an equivalent UI control to manage, all these controls inherit the IFilterView
interface. Every filter UI view is bound to a presentation model represented by the IUIFilterPresentationModel
, the model being responsible for holding the actual IFilter
and managing its active/disabled state.
Depending on the data grid column type some of the filters apply while others don’t. The FilterViewInitializersManager
class manages a list of IFilterViewInitializer
; the initializer is responsible for creating the appropriate IFilter
and the corresponding UI view and presentation model:
public IFilterView GetFilterView(PropertyInfo propertyInfo, ICollectionView collection)
{
CheckArgument(propertyInfo);
IFilter filter = GetFilter(propertyInfo);
Type multiValuePresenterType = typeof(
MultiValuePresentationModel<>).MakeGenericType(propertyInfo.PropertyType);
IMultiValuePresentationModel model = (
IMultiValuePresentationModel)Activator.CreateInstance(
multiValuePresenterType, filter, FilterName, collection);
MultiValueFilterView view = new MultiValueFilterView();
view.Model = model;
return view;
}
protected virtual IFilter GetFilter(PropertyInfo propertyInfo)
{
return (IFilter)Activator.CreateInstance(
typeof(EqualFilter<>).MakeGenericType(propertyInfo.PropertyType),
propertyInfo);
}
By default the manager has a couple of initializers registered, but is up to you to add new ones or remove any existing ones (EqualFilterInitializer, GreterOrEqualFilterInitializer, LessOrEqualFilterInitializer, RangeFilterInitializer, StringFilterInitializer
).
The filters option is displayed in the column header. DataGridColumnFilter
is responsible for loading the FiltersView
and create the corresponding presentation model. In order to find the DataBoundColumn
for the corresponding column, it starts walking up the view tree searching for the DataGridColumnHeader
and then for the DataGridColumnHeadersPresenter
identifying the corresponding index in the grid column collections. Once the column is identified it will grab the binding path. If the binding path has a depth greater than 1 it will be ignored:
DataGridColumnHeader columnHeader = VisualHelper.GetParent
<datagridcolumnheader>(this);
if (columnHeader == null)
{
return null;
}
DataGridColumnHeadersPresenter presenter =
VisualHelper.GetParent<DataGridColumnHeadersPresenter>(columnHeader);
int columnIndex = presenter.Children.IndexOf(columnHeader);
if (!(columnIndex >= 0 && columnIndex < presenter.Children.Count))
{
return null;
}
DataGrid dataGrid = VisualHelper.GetParent<DataGrid>(columnHeader);
if (dataGrid == null)
{
return null;
}
if (columnIndex >= dataGrid.Columns.Count)
{
return null;
}
IFilteredCollection filteredCollection =
dataGrid.ItemsSource as IFilteredCollection;
if (!(filteredCollection != null &&
typeof(INotifyPropertyChanged).IsAssignableFrom(filteredCollection.Type)))
{
return null;
}
DataGridBoundColumn column =
dataGrid.Columns[columnIndex] as DataGridBoundColumn;
string bindingPath = null;
if (column == null || column.Binding == null)
{
DataGridTemplateColumn templateColumn =
dataGrid.Columns[columnIndex] as DataGridTemplateColumn;
if (templateColumn != null)
{
string header = templateColumn.Header as string;
if (header == null)
{
return null;
}
bindingPath = header;
}
else
{
return null;
}
}
else
{
bindingPath = column.Binding.Path.Path;
}
if (bindingPath.Contains(".") || string.IsNullOrEmpty(bindingPath))
{
return null;
}
…
Here are the steps to be done in order to get your DataGrid ready for filtering:
- Define the
FiltersViewInitializersManager
if you want to have control on the initializers list
- Define a column header template containing
DataGridColumnFilter
(bind the InitalizersManager
property to the initializers manager if it has been defined at step no 1)
- Set the grid
ColumnHeaderStyle
property to the be the style defined at step no 2
As I have mentioned there is a small catch to INotiftyPropertyChanged
. Say we have a class Data
that exposes a property called Value
. Every time this property changes the PropertyChanged
event is raised. Here is the scenario that interests us in particular. Say we apply a filter on this property. When the value falls outside the filter range the item is being removed from the grid; as it is being removed the grid will unregister the handlers for the event handlers while we are in the process of invoking the delegation list. The Silverlight code creates a WeakReferencePropertyListener
object for every handler registered (use Reflector to search for it); because the grid, upon item remove clears its handlers if we just call the event it will end up as an exception on the first line of WeakReferencePropertyListener.PropertyChangedCallback
. Because of this here is an approach on how to raise your event for the INotifyPropertyChanged
elements:
public double Value
{
get { return _value; }
set
{
if (_value != value)
{
_value = value;
OnPropertyChanged(args);
}
}
}
private void OnPropertyChanged(PropertyChangedEventArgs args)
{
Delegate[] delegates = _propertyChanged.GetInvocationList();
foreach (Delegate del in delegates)
{
if (_propertyChanged.GetInvocationList().Contains(del))
{
del.DynamicInvoke(this, args);
}
}
}
Using the Code
The first thing you have to do to get this working is defining the ContentTemplate
for the DataGridColumnHeader
. Within the source code you can find an example, here is how the template looks:
xmlns:primitives =
"clr-namespace:System.Windows.Controls.Primitives;assembly=System.Windows.Controls.Data"
xmlns:stepi="clr-namespace:Stepi.UIFilters;assembly=Stepi.UIFilters"
…
<Style TargetType="primitives:DataGridColumnHeader" x:Key="columnHeaderStyle" >
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Grid Height="{TemplateBinding Height}" Width="Auto">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="2"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<TextBlock Text="{Binding}" HorizontalAlignment="Center"
VerticalAlignment="Center" Margin="0.2"/>
<stepi:DataGridColumnFilter Grid.Row="1"
Background="Transparent"
Width="Auto" Height="Auto"
Margin="1"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
</StackPanel>
</Grid>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
If you want to control the list of IFilterViewInitializer
available you can define a local resource and then bind the InitalizersManager
property of the DataGridColumnFilter
to it.
<stepi:FilterViewInitializersManager x:Key="filtersViewInitializersManager"/>
The last step is to set up the DataGrid column header style to be the one defined above:
<data:DataGrid x:Name="dg" ColumnHeaderStyle="{StaticResource columnHeaderStyle}">
That is pretty much it, hopefully this might come handy. On a next entry I will get the same functionality ready for Microsoft WPF DataGrid.
History
- 15-Jan 2009 - Version 1.0.0