Click here to Skip to main content
15,860,972 members
Articles / Programming Languages / C#

Autofiltering Support for Silverlight DataGrid

Rate me:
Please Sign up or sign in to vote.
4.93/5 (29 votes)
16 Jan 2009Eclipse5 min read 162.6K   3.3K   66   53
Allows auto filtering functionality for the datagrid columns
Image 1

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.

C#
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:

C#
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:

C#
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:

C#
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:

XML
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.

XML
<stepi:FilterViewInitializersManager x:Key="filtersViewInitializersManager"/>

The last step is to set up the DataGrid column header style to be the one defined above:

XML
<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, along with any associated source code and files, is licensed under The Eclipse Public License 1.0


Written By
Software Developer (Senior) Lab49
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionAmazing code Pin
MarekMalczewski7-May-14 22:18
MarekMalczewski7-May-14 22:18 
QuestionWorking with PagedCollectionView Pin
Razputin25-Oct-11 22:04
Razputin25-Oct-11 22:04 
AnswerRe: Working with PagedCollectionView Pin
MrPC196914-Nov-11 23:39
MrPC196914-Nov-11 23:39 
GeneralThrowing Null ref exception for new row once sorted. Pin
rakeshchandra28-Apr-11 2:40
rakeshchandra28-Apr-11 2:40 
GeneralUnable to use this project in my own project. Pin
VIJAY JOSHI2-Mar-11 20:44
VIJAY JOSHI2-Mar-11 20:44 
GeneralNeed Help editing Filtered columns. Please dont tell me this artical is dead. Pin
Shady14u27-Jan-11 4:33
Shady14u27-Jan-11 4:33 
GeneralRe: Need Help editing Filtered columns. Please dont tell me this artical is dead. Pin
ACE_0125-May-11 4:08
ACE_0125-May-11 4:08 
GeneralFilters for Template Columns Pin
Shady14u7-Jan-11 15:00
Shady14u7-Jan-11 15:00 
GeneralRe: Filters for Template Columns Pin
ACE_0125-May-11 4:03
ACE_0125-May-11 4:03 
GeneralRe: Filters for Template Columns Pin
Razputin24-Oct-11 19:58
Razputin24-Oct-11 19:58 
GeneralHandling nullable types for range/lessthan/greaterthan Pin
TobyKraft2-Nov-10 8:45
TobyKraft2-Nov-10 8:45 
GeneralMethod to clear all filters Pin
TobyKraft2-Nov-10 7:35
TobyKraft2-Nov-10 7:35 
GeneralValues in MultiValueFilterView are not sorted - duh! Pin
TobyKraft18-Oct-10 11:05
TobyKraft18-Oct-10 11:05 
QuestionSupport for PagedCollectionView data source? [modified] Pin
TobyKraft5-Oct-10 10:50
TobyKraft5-Oct-10 10:50 
AnswerRe: Support for PagedCollectionView data source? Pin
kmkuntz6-Apr-11 2:31
kmkuntz6-Apr-11 2:31 
GeneralRe: Support for PagedCollectionView data source? Pin
MrPC196915-Nov-11 4:30
MrPC196915-Nov-11 4:30 
GeneralTheme Apply Pin
mannkaraja16-Jul-10 20:47
mannkaraja16-Jul-10 20:47 
QuestionIs this Grid support paging? Pin
rajesh_t2-Jul-10 4:01
rajesh_t2-Jul-10 4:01 
QuestionFilterCollectionView can only work with IList as parameter [modified] Pin
skliamis3-Jun-10 4:19
skliamis3-Jun-10 4:19 
GeneralProgrammatically add or remove filters Pin
Eric Maurer30-Apr-10 7:07
Eric Maurer30-Apr-10 7:07 
GeneralRe: Programmatically add or remove filters Pin
TobyKraft20-Oct-10 12:01
TobyKraft20-Oct-10 12:01 
GeneralRe: Programmatically add or remove filters Pin
Shady14u6-Jan-11 11:31
Shady14u6-Jan-11 11:31 
QuestionUnusual Behavior in Silverlight V3 DataGrid Pin
Richard Cook29-Apr-10 10:12
Richard Cook29-Apr-10 10:12 
AnswerRe: Unusual Behavior in Silverlight V3 DataGrid Pin
nakatayo9-Jun-10 15:33
nakatayo9-Jun-10 15:33 
QuestionQuery| Pin
AnonymousCoder11-Mar-10 2:24
AnonymousCoder11-Mar-10 2:24 

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.