Wednesday, October 2, 2013

WPF datagrid with filtering (MVVM)

Hi. In this post I would like to present a WPF Datagrid with the filtering capability implemented with pure MVVM approach.

From the user interface perspective, the filtering control is placed inside of the header of the column being filtered (in this case the Name column). The filter is collapsed by default, user needs to expand the expander in the column header to apply filtering. When filter is applied to the grid, the textbox with filter term is highlighted in red.



From the back-end perspective, the filtering is performed directly on the collection bound to the Datagrid. A collection that supports filtering natively is CollectionViewSource, in this case this collection is wrapped around the ObservableCollection. When filter is active it also applies to newly added elements.

Composition code

#region Composition Root
var products = new ObservableCollection<Product>(ProductsStaticSource());
var productsView = new CollectionViewSource { Source = products }.View;
var filteringVm = new FilteringSubViewModel();
DataContext = new ProductsViewModel(productsView, filteringVm, 
  new ProductNameContainsFilter(filteringVm));
#endregion


ProductsView.xaml

<Window x:Class="ExpandableHeader.ProductsView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
        xmlns:ExpandableHeader="clr-namespace:ExpandableHeader"
        Title="Products" Height="350" Width="625">
  <Grid>
    <Grid.Resources>
      <ExpandableHeader:ActivityToBrushConverter x:Key="activityToBrushConverter"/>
    </Grid.Resources>
    <DataGrid AutoGenerateColumns="False" ItemsSource="{Binding Products}">
      <DataGrid.Columns>
        <DataGridTextColumn Header="Id" Binding="{Binding Path=Id}" >
          <DataGridTextColumn.HeaderStyle>
            <Style TargetType="DataGridColumnHeader">
              <Setter Property="VerticalContentAlignment" Value="Top"/>
            </Style>
          </DataGridTextColumn.HeaderStyle>
        </DataGridTextColumn>
        <DataGridTemplateColumn Header="Name" CanUserSort="True" 
        SortMemberPath="Name" MinWidth="110" >
          <DataGridTemplateColumn.CellTemplate>
            <DataTemplate>
              <TextBlock Text="{Binding Name}" />
            </DataTemplate>
          </DataGridTemplateColumn.CellTemplate>
          <!--NAME COLUMN-->
          <DataGridTemplateColumn.HeaderTemplate>
            <DataTemplate>
              <Grid IsHitTestVisible="True">
                <Grid.ColumnDefinitions>
                  <ColumnDefinition/>
                  <ColumnDefinition/>
                </Grid.ColumnDefinitions>
                <TextBlock Grid.Column="0" Text="{TemplateBinding Content}"/>
                <!--FILTER EXPANDER-->
                <Expander Grid.Column="1" IsHitTestVisible="True" 
                VerticalAlignment="Top" Margin="60 -3 0 0" ToolTip="Filter">
                  <Border IsHitTestVisible="True" BorderThickness="1" 
                  Margin="-90 0 0 0" >
                    <StackPanel Margin="0 4 0 0">
                      <!--FILTER TEXTBOX-->
                      <TextBox 
                        Text="{Binding DataContext.FilteringVm.FilterTerm, 
                        RelativeSource={RelativeSource AncestorType=Window}}" 
                        Background="{Binding DataContext.FilteringVm.FilterActive, 
                        RelativeSource={RelativeSource AncestorType=Window}, 
                        Converter={StaticResource activityToBrushConverter}}" 
                        ToolTip="Enter filter term" Width="100" Height="18" FontSize="9" 
                        BorderThickness="1" />
                      <!--FILTER BUTTONS-->
                      <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
                        <TextBlock Margin="2">
                        <Hyperlink Command="{Binding DataContext.FilterApply, 
                          RelativeSource={RelativeSource AncestorType=Window}}">
                          Apply
                        </Hyperlink>
                        </TextBlock>
                        <TextBlock Margin="2">
                        <Hyperlink Command="{Binding DataContext.FilterRemove, 
                          RelativeSource={RelativeSource AncestorType=Window}}">
                          Clear
                        </Hyperlink>
                        </TextBlock>
                      </StackPanel>
                    </StackPanel>
                  </Border>
                </Expander>
              </Grid>
            </DataTemplate>
          </DataGridTemplateColumn.HeaderTemplate>

        </DataGridTemplateColumn>
        <DataGridTextColumn Header="Category" Binding="{Binding Path=Category}" />
        <DataGridTextColumn Header="European Article Number (EAN)" 
                    Binding="{Binding Path=EuropeanArticleNumber}" />
        <DataGridTextColumn Header="Description" Binding="{Binding Path=Description}" />
      </DataGrid.Columns>
    </DataGrid>
  </Grid>
</Window>


ProductsViewModel.cs - main view model to handle high level logic, UI commands and bindings.

using System;
using System.ComponentModel;
using System.Windows.Input;

namespace ExpandableHeader
{
  public class ProductsViewModel
  {
    private readonly IFilter _filter;
    public ICommand FilterApply { get; private set; }
    public ICommand FilterRemove { get; private set; }

    public IFilteringSubViewModel FilteringVm
    {
      get;
      private set;
    }

    public ICollectionView Products
    {
      get;
      private set;
    }

    public ProductsViewModel(ICollectionView products,
      IFilteringSubViewModel filteringSubViewModel, IFilter filter)
    {
      Products = products;
      FilteringVm = filteringSubViewModel;
      _filter = filter;
      FilterApply = new DelegateCommand(OnFilterApply);
      FilterRemove = new DelegateCommand(OnFilterRemove);
    }

    public void OnFilterApply()
    {
      FilteringVm.Apply();
      Products.Filter = _filter.Apply;
    }

    public void OnFilterRemove()
    {
      FilteringVm.Clear();
      Products.Filter = null;
    }
  }
}


FilteringSubViewModel.cs - a specific view model to handle filter state. It is a part of the main application view model presented above.

using System.ComponentModel;

namespace ExpandableHeader
{
  public interface IFilterOption
  {
    string FilterTerm { get; }
  }

  public interface IFilteringSubViewModel
  {
    string FilterTerm { get; }
    bool FilterActive { get; }
    void Apply();
    void Clear();
  }

  public class FilteringSubViewModel : INotifyPropertyChanged, 
                IFilterOption, IFilteringSubViewModel
  {
    public event PropertyChangedEventHandler PropertyChanged;

    public FilteringSubViewModel()
    {
      _filterTerm = string.Empty;
    }

    private bool _filterActive;
    private string _filterTerm;

    public string FilterTerm
    {
      get { return _filterTerm; }
      set
      {
        _filterTerm = value;
        NotifyPropertyChanged("FilterTerm");
      }
    }

    public bool FilterActive
    {
      get { return _filterActive; }
      set
      {
        _filterActive = value;
        NotifyPropertyChanged("FilterActive");
      }
    }

    public void Apply()
    {
      FilterActive = true;
    }

    public void Clear()
    {
      FilterTerm = string.Empty;
      FilterActive = false;
    }

    private void NotifyPropertyChanged(string propertyName)
    {
      var handler = PropertyChanged;
      if (handler != null)
      {
        handler(this, new PropertyChangedEventArgs(propertyName));
      }
    }
  }
}


ProductNameContainsFilter.cs - class for filtering of products by their name.

using System;

namespace ExpandableHeader
{
  public interface IFilter
  {
    bool Apply(object parameter);
  }

  public class ProductNameContainsFilter : IFilter
  {
    private readonly IFilterOption _filterOption;

    public ProductNameContainsFilter(IFilterOption filterOption)
    {
      _filterOption = filterOption;
    }

    public bool Apply(object parameter)
    {
      return ((Product)parameter).Name.IndexOf
               (_filterOption.FilterTerm, 
                StringComparison.InvariantCultureIgnoreCase)
               >= 0;
    }
  }
}


Download VS 2010 code

3 comments:

  1. It's very useful and helpful to me. Could it do filter in datagrid with paging? ex. Page1 is opened, I'd like to filter "boss", but "boss" is on the other pages, could it be shown on where on Page1 after filtered?

    ReplyDelete
  2. is there a better solution for making this a multi-column filters?

    ReplyDelete
  3. you could pass more than one IFilter and then do this:
    Products.Filter = obj =>_nameFilter.Apply(obj) && _otherFieldFilter.Apply(obj);

    ReplyDelete