Month: February 2022

Filtering a collection in WPF using CollectionView

Today I will show you how to use CollectionView for filtering a collection.

As per microsoft docs, CollectionView is defined as

Represents a view for grouping, sorting, filtering, and navigating a data collection.

Among various usages mentioned above let us have a look at how filtering can be used.

Requirement:

We have a list of stations and we want to filter the list based on user input received through a textbox.

Solution:

I will use ListView to populate the list of the stations and will filter this list using the text entered in the search text box.

Here is the raw code which contains a text box and a list view.

       <StackPanel Width="300" HorizontalAlignment="Center">
            <TextBox x:Name="txtSearch" Text="{Binding SearchCriteria, UpdateSourceTrigger=PropertyChanged}" TabIndex="0">
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="TextChanged">
                        <i:InvokeCommandAction Command="{Binding SearchTextChangedCommand}"/>
                    </i:EventTrigger>
                    <i:EventTrigger EventName="LostFocus">
                        <i:InvokeCommandAction Command="{Binding SearchLostFocusCommand}"/>
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </TextBox>

            <ListView x:Name="listViewStation"
                ItemsSource="{Binding StationList}"
                      DisplayMemberPath="StationName"
                      SelectedValue="{Binding SelectedStation}" TabIndex="1">
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="LostFocus">
                        <i:InvokeCommandAction Command="{Binding ListViewLostFocusCommand}"/>
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </ListView>
        </StackPanel>

I have used Prism framework for auto wiring view model and to define the commands.

Here is the underlying view model’s code for the above mentioned view.

     public class MainWindowViewModel : BindableBase
    {
        private string searchCriteria;

        public string SearchCriteria
        {
            get { return searchCriteria; }
            set { SetProperty(ref searchCriteria, value); }
        }

        private Station selectedStaion;
        public Station SelectedStation
        {
            get { return selectedStaion; }
            set { SetProperty(ref selectedStaion, value); }
        }

        public DelegateCommand WindowLoadedCommand { get; set; }
        public DelegateCommand SearchTextChangedCommand { get; set; }
        public DelegateCommand ListViewLostFocusCommand { get; set; }
        public ObservableCollection<Station> StationList { get;  set;} = new ObservableCollection<Station>();

        public List<Station> stations = new List<Station>();

        public MainWindowViewModel()
        {
            WindowLoadedCommand = new DelegateCommand(WindowLoaded);
            SearchTextChangedCommand = new DelegateCommand(SearchTextChanged);
            ListViewLostFocusCommand = new DelegateCommand(ListViewLostFocus);
        }

        private void ListViewLostFocus()
        {
            //do something with the selected station
        }

        private void SearchTextChanged()
        {
            CollectionViewSource.GetDefaultView(StationList).Refresh();
        }

        private void LoadStationList()
        {
            stations = new List<Station>();
            stations.Add(new Station { StationName = "Mogra", StationId = 1 });
            stations.Add(new Station { StationName = "Dahisar", StationId = 2 });
            stations.Add(new Station { StationName = "Dahanukar Wadi", StationId = 3 });
            stations.Add(new Station { StationName = "Andheri", StationId = 4 });
            stations.Add(new Station { StationName = "National Park", StationId = 5 });
            stations.Add(new Station { StationName = "Gundavali", StationId = 6 });
            stations.Add(new Station { StationName = "Goregaon", StationId = 7 });
            StationList.AddRange(stations);
        }

        private void WindowLoaded()
        {
            LoadStationList();
            CollectionViewSource.GetDefaultView(StationList).Filter = FilterStations;
        }

        private bool FilterStations(object obj)
        {
            if (string.IsNullOrEmpty(SearchCriteria))
                return true;

            Station station = (Station)obj;

            var result = station.StationName.ToLower().StartsWith(SearchCriteria)
                || station.StationName.ToLower().Contains(SearchCriteria);

            return result;
        }
    }

CollectionViewSource class provides a static method GetDefaultView, which accepts the object as a source and returns an object of ICollectionView type. This is the default view which is attached to the given source collection.

ICollectionView has a Predicate named as Filter. This predicate is used to determine if the object passed to it can be included in the view.

I have used this predicate and provided my own implementation by attaching the FilterStations method in WindowLoaded event.

So until now we have set the filter criteria. Now we have to monitor the TextChanged event of text box and have to refresh the ICollectionView of the station collection.
This will trigger the FilterStations delegate and filter out the not required station.
Station names containing the text entered in text box is filtered and shown in the list view.

This filter operation do not change the underlying collection source. It just changes the view.
This is an awesome feature provided by WPF.

Code can be downloaded from Git