Click here to Skip to main content
15,885,366 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 163.3K   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

 
AnswerRe: Exception when click in a datagrid cell. Pin
AnonymousCoder27-Apr-10 0:26
AnonymousCoder27-Apr-10 0:26 
GeneralRe: Exception when click in a datagrid cell. Pin
aris1971-200826-Sep-10 11:41
aris1971-200826-Sep-10 11:41 
AnswerRe: Exception when click in a datagrid cell. Pin
TobyKraft20-Oct-10 10:33
TobyKraft20-Oct-10 10:33 
AnswerRe: Exception when click in a datagrid cell. Pin
TobyKraft20-Oct-10 10:45
TobyKraft20-Oct-10 10:45 
GeneralRudimentary "is filter enabled" indicator [modified] Pin
TobyKraft8-Feb-10 11:29
TobyKraft8-Feb-10 11:29 
QuestionExtending the grid for better usability ... Pin
CSharpian20-Jan-10 19:19
CSharpian20-Jan-10 19:19 
QuestionError with Ria services Pin
neozack10-Jan-10 9:20
neozack10-Jan-10 9:20 
AnswerRe: Error with Ria services Pin
Shady14u6-Jan-11 11:41
Shady14u6-Jan-11 11:41 
Was this issue ever resolved?
QuestionAny chances on using this in SL 4 & RIA Services? Pin
JCKodel12-Dec-09 10:52
JCKodel12-Dec-09 10:52 
GeneralNeed to show the filters... Pin
Venkatesan Jagadisan1-Dec-09 22:18
Venkatesan Jagadisan1-Dec-09 22:18 
GeneralRe: Need to show the filters... Pin
Member 37017893-Dec-09 21:15
Member 37017893-Dec-09 21:15 
QuestionHow did you make the drop downs? Pin
VertigoHopes8-Sep-09 12:54
VertigoHopes8-Sep-09 12:54 
GeneralRefresh Pin
Member 38631029-Jun-09 23:07
Member 38631029-Jun-09 23:07 
GeneralDataBinding Problem With Async Call Pin
Member 38631029-Jun-09 6:15
Member 38631029-Jun-09 6:15 
GeneralJust what I was looking for Pin
Stuart Campbell3-Mar-09 16:15
Stuart Campbell3-Mar-09 16:15 
GeneralVery usefult article Pin
Sathish Raja S19-Feb-09 23:32
Sathish Raja S19-Feb-09 23:32 
GeneralProblem with Stepi Filter and TreeView ( Dynamic Filter) Pin
cray12345616-Feb-09 5:05
cray12345616-Feb-09 5:05 
AnswerRe: Problem with Stepi Filter and TreeView ( Dynamic Filter) Pin
Stefan Bocutiu16-Feb-09 11:55
Stefan Bocutiu16-Feb-09 11:55 
GeneralVery nice article! (one notice!) Pin
ant0ine3-Feb-09 1:51
ant0ine3-Feb-09 1:51 
GeneralRe: Very nice article! (one notice!) Pin
Stefan Bocutiu4-Feb-09 1:43
Stefan Bocutiu4-Feb-09 1:43 

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.