Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Autofiltering Support for Silverlight DataGrid

0.00/5 (No votes)
16 Jan 2009 1  
Allows auto filtering functionality for the datagrid columns

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.

Filters UML

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.

Filters Presentation Model diagram

Filters View Diagram

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)
                {
                    //this might not always be the case
                    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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here