GridControl tutorial part11: Data filtering

Data filtering actually controls visibility of individual grid rows basing on rules set by the programmer. These rules should be convenient and easy to use, should not depend on data sorting or grouping and should be equally applied to static and real-time data. Data filtering should not depend on hierarchy level or on data being expanded or collapsed by parent rows.

Wpf GridControl column filters
Filtering methods

The grid provides three complementary methods of data filtering.

  • Row.Filtered - the easiest method of displaying or hiding data. However, it is doesn't support even-driven data model and therefore it is not useful for more complex applications. With this method the programmer has to control row visibility manually and to consider that data can be sorted or grouped and may have complex hierarchical structure.
  • IFilter interface - the most efficient and convenient filtering method. The programmer has to inherit IFilter interface and to implement IsFiltered method. This method uses row as argument for which the grid determines whether filtering is required.

    This method is called in the following cases:

    • when a filter is replaced, i.e. when GridControl.Filter property is called.
    • when new data is added to the grid for rows that are associated with newly added data.
    • when data is updated (i.e. when Row.Update() methods are called and when the grid gets notifications from INotifyPropertyChanged or IBindingList interface).
  • Column filters
Programmatic filter
C# 
//Quote class implementing INotifyPropertyChanged interface 
class Quote : INotifyPropertyChanged 
{ 
    private readonly int _quantity; 
    private double _price; 

    public Quote(int quantity, double price) 
    { 
        _quantity = quantity; 
        _price = price; 
    } 

    public int Quantity { get { return _quantity; } } 
    public double Price 
    { 
        get { return _price; } 
        set 
        { 
            if (_price != value) 
            { 
                _price = value; 
                if(PropertyChanged != null) 
                { 
                    PropertyChanged(this, new PropertyChangedEventArgs("Price")); 
                } 
            } 
        } 
    } 
    public event PropertyChangedEventHandler PropertyChanged; 
} 

//Set some filter: 
grid.Filter = new Filter(delegate(Row row) 
{ 
    double price = (double) row["Price"].Value; 
    return price < 1000; 
}); 

//Populate grid with some values: 
List<Quote> collection = new List<Quote>(); 
for (int i = 0; i < 5; ++i) 
{ 
    collection.Add(new Quote(i, 1000 + 100 * i)); 
} 
grid.ItemsSource = collection; 

//All 5 rows are displayed right now. Change price of a quote: 
collection[4].Price = 900; 

//The row becomes invisible. .Wpf GridControl displays only 4 rows
Column filters

Column filters provide user with visual controls to manage row filtering. Dapfor.Wpf.dll library provides a set of filters that can be used in most projects. These filters are provided in form of DataTemplate elements stored in Filters.FilterFactory

Developers can set filters with xaml code:

XAML 
<Window x:Class="SomeApplication.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" 
        xmlns:my="clr-namespace:Dapfor.Wpf.Controls;assembly=Dapfor.Wpf"
        xmlns:filters="clr-namespace:Dapfor.Wpf.Filters;assembly=Dapfor.Wpf">


  <my:GridControl>
        <my:GridControl.Headers>
            <my:Header>
                <my:Header.Columns>
                    <my:Column Id="Status" Title="Status" FilterTemplate="{x:Static filters:FilterFactory.CheckBoxFilter}" FilterAlignment="Stretch"/>
                    <my:Column Id="Established" Title="Established" FilterTemplate="{x:Static filters:FilterFactory.DateMultiFilter}"/>
                </my:Header.Columns>
            </my:Header>
        </my:GridControl.Headers>
    </my:GridControl>
</Window>

It can also be done programmatically:

C# 
Column column = gridControl1.Headers[0]["Established"];
column.FilterTemplate = FilterFactory.WildcardFilter;

When a filter template is set for a column, the grid automatically displays filter icon in its right part. When a user clicks the icon, the grid automatically opens a popup window and places the specified template to this window. The grid provides Column object to the template for data content. You can set the initial popup position withColumn.FilterAlignment property.

Column also provides the Column.CloseFilter() method to hide a control when it is no longer necessary. Filter is set when the Column.Filter property is called and the IFilter object is transferred to this property. The grid verifies filtering conditions for each row and hides or shows it as necessary. Users may change filtering conditions many times, and a new object has to be created for every such change IFilter. If you need to store filter values between calls, you can use the Column.Tag property or take the values from a previously installed filter.

Custom column filters

Now we will show you how to create a custom filter for Double type data.

Wpf GridControl Custom column filter

First of all, we create a CustomControl that will be displayed in a popup window:

XAML 
<UserControl x:Class="Dapfor.Wpf.Grid.Demo.Examples.Basics.CustomFilters.SalesFilter"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             x:Name="ThisControl">
    <Border BorderThickness=".5">
        <Slider x:Name="Slider" Height="23" HorizontalAlignment="Stretch" Value="100" Maximum="100" />
    </Border>
</UserControl>

To transfer a column to the control in xaml code, we add a DependencyProperty of the Column type:

C# 
public partial class SalesFilter : UserControl
{
    public SalesFilter()
    {
        InitializeComponent();
    }

    #region ColumnProperty
    public static readonly DependencyProperty ColumnProperty = DependencyProperty.Register("Column", 
                                                                                            typeof (Column), 
                                                                                            typeof (SalesFilter), 
                                                                                            new FrameworkPropertyMetadata(null, OnChangeColumn));
    private static void OnChangeColumn(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
    }
    public Column Column
    {
        get { return (Column) GetValue(ColumnProperty); }
        set { SetValue(ColumnProperty, value); }
    }
    #endregion
}

After that we can use binding to set control background and foreground so that it does not differ too much from the current grid style. We can also add an event handler for the slider value change event. Full xaml code of our control now may look as follows:

XAML 
<usercontrol x:class="Dapfor.Wpf.Grid.Demo.Examples.Basics.CustomFilters.SalesFilter"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    mc:ignorable="d" x:name="ThisControl">
    <Border Background="{Binding ElementName=ThisControl, Path=Column.Header.Grid.Background}"
            BorderBrush="{Binding ElementName=ThisControl, Path=Column.Header.Grid.AppearanceColumnBackground}" 
            BorderThickness=".5">
        <Slider x:Name="Slider" Height="23" HorizontalAlignment="Stretch" Value="100" Maximum="100" ValueChanged="OnSliderValueChanged" />
    </Border>
</usercontrol>

 

Sorting conditions change when the slider position changes. We will create rules displaying only rows with values not exceeding the slider value.

C# 
//Our filtration rules
private class FilterRules : IFilter
{
    private readonly Column _column;
    private readonly double _value;

    //The second parameter indicates the current value of our slider
    public FilterRules(Column column, double value)
    {
        _column = column;
        _value = value;
    }

    //This method is called each time when the GridControl needs to show or hide the row
    public bool IsFiltered(Row row)
    {
        var cell = row[_column.Id];
        var v = cell != null ? cell.Value : null;
        return (v as double?) > _value;
    }
}

public partial class SalesFilter : UserControl
{
   ...

    //Event handler is called when slider's value is changed
    private void OnSliderValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
    {
        //Set a new filter
        Column.Filter = new FilterRules(Column, e.NewValue);
    }
}

It would be great to hide the control when user presses the Esc key. We will add a corresponding event handler and use the Column.CloseFilter() method.

C# 
protected override void OnPreviewKeyDown(KeyEventArgs e)
{
    base.OnPreviewKeyDown(e);
    if (e.Key == Key.Escape)
    {
        Column.CloseFilter();
    }
}

We also need to preserve the current slider value for the case if user suddenly closes the control. We can do it by saving values in Column.Tag. Finally we will add the _suspendUpdates variable so that we don't have to recreate the IFilter object on control initialization. The full code of our control will now look as follows:

C# 
public partial class SalesFilter : UserControl
{
    private bool _suspendUpdates;

    public SalesFilter()
    {
        InitializeComponent();
    }

    private void OnSliderValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
    {
        if (!_suspendUpdates && Column != null)
        {
            Column.Tag = e.NewValue;
            Column.Filter = new FilterRules(Column, e.NewValue);
        }
    }

    protected override void OnPreviewKeyDown(KeyEventArgs e)
    {
        base.OnPreviewKeyDown(e);
        if (e.Key == Key.Escape)
        {
            Column.CloseFilter();
        }
    }

    #region ColumnProperty
    public static readonly DependencyProperty ColumnProperty = DependencyProperty.Register("Column", typeof (Column), typeof (SalesFilter), new FrameworkPropertyMetadata(null, OnChangeColumn));
    private static void OnChangeColumn(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        //Restore persisted data
        var control = (SalesFilter) d;
        if (control.Column != null && control.Column.Tag is double)
        {
            control._suspendUpdates = true;
            control.Slider.Value = (double) control.Column.Tag;
            control._suspendUpdates = false;
        }
    }
    public Column Column
    {
        get { return (Column) GetValue(ColumnProperty); }
        set { SetValue(ColumnProperty, value); }
    }
    #endregion
}

Setting our slider in grid column is quite easy. To do it, we will create DataTemplate resource and put our control inside it. Don't forget to transfer the produced column to the control:

XAML 
<UserControl x:Class="SomeControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d">

  <UserControl.Resources>
      <!-- Sales templates -->
      <DataTemplate x:Key="SalesFilter">
          <customFilters:SalesFilter Column="{Binding}"/>
      </DataTemplate>
  </UserControl.Resources>  

</UserControl>

Finally we have to transfer our template to grid column:

XAML 
<my:GridControl>
    <my:GridControl.Headers>
        <my:Header>
            <my:Header.Columns>
                <my:Column Id="sales" Title="Sales" FilterTemplate="{StaticResource SalesFilter}"/>
            </my:Header.Columns>
        </my:Header>
    </my:GridControl.Headers>
</my:GridControl>

Back to Wpf GridControl tutorial