Peter McGrattan’s Weblog

Silverlight, WCF, ASP.NET, AJAX, Graphics, RIA

Filtering Silverlight DataGrid Rows with ApplyMutateFilter<T>

Posted by petermcg on January 29, 2009

(Code download)

This post presents a solution to a question on Stack Overflow which I found interesting, this is a question I’ve seen crop up a few times now in one form or another:

"I have an ObservableCollection feeding a DataGrid that is updating nicely.  The point: I want to filter (collapse) the rows without removing them from the collection.  Is there a way to do this?"

You can remove rows from a DataGrid by removing items from the ObservableCollection<T> that the grid’s ItemsSource property is bound to, but the question asks is there a way to avoid this.  The way I see it a solution to this question will firstly offer a way to filter the rows displayed in a DataGrid without affecting the Count of the original ObservableCollection<T> and secondly provide a way to easily discern which rows are currently filtered and which are displayed after the filter has been applied.  The intent is also to provide a solution general enough to be reused with any type T in an ObservableCollection<T>.

The solution structure of the simple demo solution available for download above looks as follows:

Solution Structure

The demo application running looks as follows:

App Running Pre-Filter

This UI is produced from the contents of Page.xaml:

<UserControl x:Class="DataGridRowFiltering.Page"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"  
             xmlns:input="clr-namespace:Microsoft.Windows.Controls;assembly=Microsoft.Windows.Controls.Input">

    <StackPanel Margin="5,5,5,5" x:Name="LayoutRoot" Background="White">

        <StackPanel Orientation="Horizontal" Margin="0,15,0,15">
            <TextBlock Text="Age Filter" FontSize="16" VerticalAlignment="Center" TextAlignment="Center" Margin="0,0,10,0" />

            <input:NumericUpDown Width="40" x:Name="FilterValue" DecimalPlaces="0" Minimum="20.0" Maximum="25.0" Margin="0,0,10,0" />

            <Button x:Name="FilterButton" Content="Apply Filter" Click="FilterButton_Click" />
        </StackPanel>

        <StackPanel Margin="0,15,0,15">
            <TextBlock Text="Filtered People" FontSize="16" />
            <data:DataGrid x:Name="FilteredPeople"  AutoGenerateColumns="True" IsReadOnly="True" />
        </StackPanel>

        <StackPanel Margin="0,15,0,15">
            <TextBlock Text="All People" FontSize="16" />
            <data:DataGrid x:Name="AllPeople"  AutoGenerateColumns="True" IsReadOnly="True" />
        </StackPanel>

    </StackPanel>
</UserControl>

The Xaml contains two DataGrids, a NumericUpDown control from the Silverlight Toolkit and a standard Button control.  When the ‘Apply Filter’ button is clicked the people (or rows) shown in the FilteredPeople grid are filtered, the row count in the AllPeople grid remains unchanged.  The filter excludes people whose Age property does not equal the current value of the NumericUpDown control.

The events for the UI are in Page.xaml.cs:

using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;

namespace DataGridRowFiltering
{
    public partial class Page : UserControl
    {
        ObservableCollection<Person> people = new ObservableCollection<Person>()
        {
            new Person("Peter", 20),
            new Person("Bill", 21),
            new Person("Joe", 21),
            new Person("David", 23),
            new Person("Steve", 24),
            new Person("Jeff", 25),
        };

        public Page()
        {
            InitializeComponent();

            Loaded += Page_Loaded;
        }

        private void Page_Loaded(object sender, RoutedEventArgs e)
        {
            FilteredPeople.ItemsSource = people;
            AllPeople.ItemsSource = people;
        }

        private void FilterButton_Click(object sender, RoutedEventArgs e)
        {
            int selectedAge = Convert.ToInt32(FilterValue.Value);

            FilteredPeople.ItemsSource = people.ApplyMutateFilter(p => p.Age == selectedAge,
                                                                  p => p.IsVisible = true,
                                                                  p => p.IsVisible = false);
        }
    }
}

The Page class has an ObservableCollection<Person> called ‘people’ containing 6 instances of the Person class.  The Person class is a simple class that implements the INotifyPropertyChanged interface and has properties called FirstName, Age and IsVisible.

When an instance of the Page UserControl is loaded the ItemsSource property of both DataGrids is bound to the people collection resulting in the screenshot shown earlier.

When the ‘Apply Filter’ button is clicked we store the current value of the NumericUpDown control in the selectedAge variable and set the ItemsSource of the FilteredPeople grid to the result of calling the ApplyMutateFilter<T> extension method:

using System;
using System.Collections.Generic;
using System.Linq;

namespace DataGridRowFiltering
{
    public static class Extensions
    {
        /// <summary>
        /// Applies an action to each item in the sequence, which action depends on the evaluation of the predicate.
        /// </summary>
        /// <typeparam name="TSource">The type of the elements of source.</typeparam>
        /// <param name="source">A sequence to filter.</param>
        /// <param name="predicate">A function to test each element for a condition.</param>
        /// <param name="posAction">An action used to mutate elements that match the predicate's condition.</param>
        /// <param name="negAction">An action used to mutate elements that do not match the predicate's condition.</param>
        /// <returns>The elements in the sequence that matched the predicate's condition and were transformed by posAction.</returns>
        public static IEnumerable<TSource> ApplyMutateFilter<TSource>(this IEnumerable<TSource> source,
                                                                      Func<TSource, bool> predicate,
                                                                      Action<TSource> posAction,
                                                                      Action<TSource> negAction)
        {
            if (source != null)
            {
                foreach (TSource item in source)
                {
                    if (predicate(item))
                    {
                        posAction(item);
                    }
                    else
                    {
                        negAction(item);
                    }
                }
            }

            return source.Where(predicate);
        }
    }
}

This method takes one predicate and two actions.  A ‘predicate’ in this case just means a function that takes a parameter of type TSource and returns true or false.  Similarly an ‘action’ is just a function that takes a parameter of type TSource and returns void.

In the call to ApplyMutateFilter<T> in the event handler for the button’s Click event above, the predicate function is designed to return the result of the comparison between a Person object’s Age property and the value of selectedAge.  The two action functions are designed to set the value of the IsVisible property on a Person object to either true or false.

The ApplyMutateFilter<T> extension method body applies one of these actions to every item in the sequence, which action depends on the evaluation of the predicate.  If the predicate returns true the positive action is executed otherwise the negative action is executed resulting in the value of the IsVisible property changing.

Finally only the elements in the sequence that matched the predicate are returned with the call to the Where<T> extension method, thus the filter is applied.

It’s important to remember that setting the IsVisible property does not make the row disappear from the DataGrid; it’s the call to Where<T> that filters the sequence.  The IsVisible property is merely a convenient way of later finding which elements have been affected by the current filter in code, for example if you wanted to know what rows are currently being shown in the DataGrid after a filter has been applied.

Here is a screen shot after clicking the ‘Apply Filter’ button with a selected age of 21:

App Running Post-Filter

The FilteredPeople DataGrid now only displays people aged 21.  The grid is now bound to the filtered sequence returned from the call to ApplyMutateFilter<T> with a predicate comparing each Person object’s Age property to the selectedAge of 21.

The screenshot shows the rows have been filtered in the first DataGrid without affecting the Count of the original ObservableCollection<T> which the second DataGrid’s is still bound to, thus satisfying the first task we set out to achieve.

Finally it’s clear to see the second requirement has been fulfilled from the state of the IsVisible properties. The value of this property makes it easy to discern which rows in the original ObservableCollection<T> are currently filtered and which are displayed after the filter has been applied.

Altering the predicate is all that’s required to apply a filter to a different property/column or a combination of several properties/columns.

About these ads

3 Responses to “Filtering Silverlight DataGrid Rows with ApplyMutateFilter<T>”

  1. Stepi said

    Hi Peter,
    I have published recently a solution also for filtering the silverlight datagrid. if you are interested have a read at this article: http://www.codeproject.com/KB/silverlight/autofiltering_silverlight.aspx.

  2. Filtering Silverlight DataGrid Rows with ApplyMutateFilter « Peter McGrattan’s Weblog…

    Thank you for submitting this cool story – Trackback from DotNetShoutout…

  3. Hi Stepi,

    AutoFiltering – very nice – thanks for sharing.

    Regards,

    Peter

Sorry, the comment form is closed at this time.

 
Follow

Get every new post delivered to your Inbox.

%d bloggers like this: