Tuesday, February 23, 2016

Filter DataGrid in C#/WPF using ICollectionView

Today I tried to find a bug in a ICollectionView-based WPF filtering action. The problem was fortunately easy to find, but I was shocked that I didn't find an easy example in our projects to explain my colleague the way how this all works... So, here is my easy example of a grid which can be filtered:

I created a standard WPF project and installed the nuget-package mvvm-light (see: http://www.codeproject.com/Articles/321806/MVVMLight-Hello-World-in-10-Minutes if you don't know the nuget package). Long story short: it provides some base classes and some assets like a relay-command which is useful to start your work without the need of creating boiler-plate code again and again. The mvvmlight's viewmodel-locator is defined as a static resource in app.xaml so you can define view-models (and its instances) centrally using an "inversion of control"-container (called SimpleIoc). Here nothing has to be changed, because it creates a MainViewModel (as an example) we will use to create this sample.
I kept the MainWindow.xaml which was created by default and put the needed controls in there:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<Window x:Class="FilteringGrids.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:FilteringGrids"
        mc:Ignorable="d"
        Title="Filter-Test" Height="350" Width="525"
        DataContext="{Binding Main, Source={StaticResource Locator}}">
    <Grid Margin="10">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="1*" />
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="Auto" />
 
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="1*" />
        </Grid.RowDefinitions>
 
        <TextBox Grid.Row="0" Text="{Binding FilterText}" Margin="0,0,10,10"></TextBox>
        <Button Grid.Row="0" Grid.Column="1" Margin="0,0,10,10" Height="25" Width="50" Command="{Binding RefreshCommand}">Refresh</Button>
        <Button Grid.Row="0" Grid.Column="2" Margin="0,0,0,10" Height="25" Width="50"  Command="{Binding FilterCommand}">Filter</Button>
        <DataGrid Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3" ItemsSource="{Binding Data}" />
    </Grid>
</Window>

As we can see in line 9: the datacontext is wired up with the viewmodel instance created in the ViewModelLocator, so we can use the paradigm of MVVM. In the following XAML code we bind the text to filter, the data and two buttons to the background viewmodel...


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.CommandWpf;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Data;
using System.Windows.Input;
namespace FilteringGrids.ViewModel
{
    /// <summary>
    /// This class contains properties that the main View can data bind to.
    /// <para>
    /// Use the <strong>mvvminpc</strong> snippet to add bindable properties to this ViewModel.
    /// </para>
    /// <para>
    /// You can also use Blend to data bind with the tool's support.
    /// </para>
    /// <para>
    /// See http://www.galasoft.ch/mvvm
    /// </para>
    /// </summary>
    public class MainViewModel : ViewModelBase
    {
        public class Model
        {
            public string Name { get; set; }
            public string Value1 { get; set; }
            public string Value2 { get; set; }
            public string Value3 { get; set; }
            public string Value4 { get; set; }
        }
        /// <summary>
        /// Initializes a new instance of the MainViewModel class.
        /// </summary>
        public MainViewModel()
        {
            #region Filter
            FilterText = "";
            Predicate<object> filterFunction = (object raw) =>
            {
                Model dataToFilter = raw as Model;
                return
                    dataToFilter.Name.Contains(FilterText) ||
                    dataToFilter.Value1.Contains(FilterText) ||
                    dataToFilter.Value2.Contains(FilterText) ||
                    dataToFilter.Value3.Contains(FilterText) ||
                    dataToFilter.Value4.Contains(FilterText);
            };
            FilterCommand = new RelayCommand(() => DataCollectionView.Refresh(),
                                             () => true);
            #endregion
            #region Init / Refresh
            RefreshCommand = new RelayCommand(() =>
            {
                Data = new List<Model> {
                    new Model() { Name="one",   Value1="1", Value2="1.0", Value3="1.00", Value4="1.000" },
                    new Model() { Name="two",   Value1="2", Value2="2.0", Value3="2.00", Value4="2.000" },
                    new Model() { Name="three", Value1="3", Value2="3.0", Value3="3.00", Value4="3.000" },
                    new Model() { Name="four",  Value1="4", Value2="4.0", Value3="4.00", Value4="4.000" },
                };
                DataCollectionView = CollectionViewSource.GetDefaultView(Data);
                DataCollectionView.Filter = filterFunction;
                this.RaisePropertyChanged("Data");
            }, () => true);
            // init data
            RefreshCommand.Execute(null);
            #endregion
        }
        public string FilterText { get; set; }
        public List<Model> Data { get; set; }
        private ICollectionView DataCollectionView { get; set; }
        public ICommand RefreshCommand { get; set; }
        public ICommand FilterCommand { get; set; }
    }
}


What we see here is that 99% of the magic happens in the constructor of the viewmodel. This is bad style, but keeps the things easy for the example, so please forgive me here. We see here that all members (defined at the end of the class) are bound by the xaml-code except the CollectionView which is used for the actual filtering.
Important here is:

  • if you change the reference of your data source then populate that to the UI (INPC -> RaisePropertyChanged)
  • Filtering over ICollectionView is easily achievable over a Predicate-function
  • recreate the ICollectionView if the original instance of the data changes (you probably don't need a reference to this)
  • if the filter-result might change call CollectionViewInstance.Refresh()

kind regards,
Daniel

No comments: