Filtering and Sorting a ListView in a UWP – Part 2 – The Code

I have to admit that after the ease of implementing the UX for filtering and sorting, I was expecting the implementation of filtering and sorting to be easy. After all, prior versions of the Windows SDK and XAML had a nice CollectionViewSource object that had Filter and SortDescriptions fields and I just hook into them, right? Well, it didn’t turn out to be that easy. How on earth do others do it?

Implementing a Filtered Store

After consulting with others, reading a whole bunch of StackOverflow, and delving into the documentation, I settled on an implementation. Obviously, CollectionViewSource was not going to do it, so I might as well roll my own wrapper. The ListView requires an object that implements IList (so that it can iterate over the list) and INotifyCollectionChanged (so it knows when to change asynchronously). These are easy to implement, so I created a Models/FilteredTaskStore.cs as follows:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Threading.Tasks;

namespace QuickStart.UWP.Models
{
    /// <summary>
    /// A filtered version of the task store.
    /// </summary>
    class FilteredTaskStore : INotifyCollectionChanged, IList
    {
        private TaskStore _store;
        private List<TaskItem> _view;
        private bool _filterCompleted = true;
        private string _sortMethod = null;

        /// <summary>
        /// Create a new FilteredTaskStore based on an existing TaskStore
        /// </summary>
        /// <param name="store">The TaskStore to base this filter on</param>
        public FilteredTaskStore(TaskStore <span class="hiddenGrammarError" pre="">store)
        {
            _store</span> = store;
            _view = new List<TaskItem>(this._store);
        }

        public bool IncludeCompletedItems
        {
            get
            {
                return _filterCompleted;
            }
            set
            {
                _filterCompleted = value;
                RefreshView();
            }
        }

        public string SortMethod
        {
            get
            {
                return _sortMethod;
            }
            set
            {
                _sortMethod = value;
                RefreshView();
            }
        }

In order to implement a filter, I need to keep the original (the TaskStore in my implementation) and I need to handle a view of the original (called _view). I’ve got a property for the filter – do I include completed items or not? I’ve also got a SortMethod which will be null or the field to be filtered. Note that when I set either the filter or the sort method, I call RefreshView() – more on that method later.

Since I am using Visual Studio, I right-clicked on the IList interface and told it to implement the interface based on the _view. This allows me to utilize this class as if it is a list. When you access it via the list specification, you are iterating over the view. In the code, I’ve surrounded this code as a #region so I can hide it. I’ve not touched that code from the scaffolding though.

In addition, I want to call this code as if it was a TaskStore. That means implementing the TaskStore methods and using a pass-through:

        #region TaskStore interface
        /// <summary>
        /// Create a new Task asynchronously
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        public async Task Create(TaskItem item)
        {
            await _store.Create(item);
            RefreshView();
        }

        /// <summary>
        /// Update a task asynchronously
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        public async Task Update(TaskItem item)
        {
            await _store.Update(item);
            RefreshView();
        }

        /// <summary>
        /// Delete a task asynchronously
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        public async Task Delete(TaskItem item)
        {
            await _store.Delete(item);
            RefreshView();
        }
        #endregion

Just like the setter in the filter and sort method, I’m calling RefreshView() in addition to calling the equivalent in the backend store. This allows me to update the view according to the new store values. That leaves me RefreshView() to write:

        /// <summary>
        /// Refresh the view, based on filters and sorting mechanisms.
        /// </summary>
        public void RefreshView()
        {
            var oldItems = _view.ToArray();

            var tasks = _store.Where<TaskItem>(task => IncludeCompletedItems || !task.Completed);

            _view.Clear();
            _view.AddRange(tasks);
            if (CollectionChanged != null)
            {
                // Call the event handler for the updated list.
                CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
            }

        }

This version only filters – it doesn’t sort yet (more on THAT later). The Where statement is dynamic LINQ. In this case, I’ve said “where either include completed items is on OR (includes completed items is off and the task is not complete)”. I then build the new view based on the filtered list. Finally, I emit a Reset event on the CollectionChanged event delegate channel. This tells the ListView that the whole list has been updated and it needs to re-render everything.

Wiring in the Filtered Store

The wiring for the filtered store is everywhere. Firstly, I need to initialize it. I do this in the constructor for the main page (in MainPage.xaml.cs):

        public MainPage()
        {
            store = new FilteredTaskStore(new TaskStore());
            this.InitializeComponent();

            SizeChanged += MainPage_SizeChanged;
            tasksListView.ItemsSource = store;

            // Set up the defaults for filtering by the store definition
            IncludeCompletedCheckbox.IsChecked = store.IncludeCompletedItems;
        }

One of the things I discovered while doing this was that you can only set the ItemsSource once. As a result, you want to set it in the constructor and then leave it alone. I want to refresh the view of the filtered source when I navigate to the main page:

        /// <summary>
        /// Refresh the contents of the list when the page is loaded
        /// </summary>
        /// <param name="e"></param>
        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            store.RefreshView();
        }

I also want to wire up the Filter option:

        private void FilterCompletedTasks_Clicked(object sender, RoutedEventArgs e)
        {
            var includeCompleted = (bool)((CheckBox)sender).IsChecked;
            store.IncludeCompletedItems = includeCompleted;
        }

The FilteredTaskStore object takes care of most of the refresh activities for me. Full disclosure – I also adjusted the XAML for the filter checkbox as follows:

                <AppBarButton Label="Filter">
                    <AppBarButton.Icon>
                        <SymbolIcon Symbol="Filter"/>
                    </AppBarButton.Icon>
                    <AppBarButton.Flyout>
                        <Flyout>
                            <StackPanel>
                                <CheckBox x:Name="IncludeCompletedCheckbox" IsChecked="True" Click="FilterCompletedTasks_Clicked">Completed</CheckBox>
                            </StackPanel>
                        </Flyout>
                    </AppBarButton.Flyout>
                </AppBarButton>

At this point you can run the code and see the filtering working. Pop open the filter box, uncheck the Include Completed checkbox, then check an item and watch it disappear. I did see a couple of visual bugs, but I’ll sort those out later.

Implementing Sorting

Sorting was now a case of wiring up the AppBarButton code to set the SortMethod and then implementing sorting in the RefreshView() method inside the FilteredTaskStore. To wire up the AppBarButton, I added a Click event handler to each RadioButton to call the same method:

                <AppBarButton Label="Sort">
                    <AppBarButton.Icon>
                        <SymbolIcon Symbol="Sort"/>
                    </AppBarButton.Icon>
                    <AppBarButton.Flyout>
                        <Flyout>
                            <StackPanel Orientation="Vertical">
                                <RadioButton x:Name="SortMethod_Unsorted" GroupName="SortOptions" IsChecked="True" Click="SortTasks_Clicked">Unsorted</RadioButton>
                                <RadioButton x:Name="SortMethod_ByTitle" GroupName="SortOptions" Click="SortTasks_Clicked">By Title</RadioButton>
                            </StackPanel>
                        </Flyout>
                    </AppBarButton.Flyout>
                </AppBarButton>

I need to implement the event handler in the MainPage.xaml.cs:

        private void SortTasks_Clicked(object sender, RoutedEventArgs e)
        {
            var b = ((RadioButton)sender).Name.Replace("SortMethod_","");
            store.SortMethod = (b.Equals("Unsorted")) ? null : b;
        }

Most of the interesting code is in the RefreshView() where I need to order the values:

        public void RefreshView()
        {
            var oldItems = _view.ToArray();

            var tasks = _store.Where(task => IncludeCompletedItems || !task.Completed);
            if (SortMethod != null)
            {
                if (SortMethod.Equals("ByTitle"))
                {
                    tasks = tasks.OrderBy(t => t.Title);
                }
            }

            _view.Clear();
            _view.AddRange(tasks);
            if (CollectionChanged != null)
            {
                // Call the event handler for the updated list.
                CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
            }
        }

Note that I’ve removed the specific model I’m using in the Where. There is no problem in the Where() clause but if you do that with the OrderBy, you’ll get an error that the method does not exist.

Wrap-up

Firstly, why did the folks who write this remove the filter and sort from the CollectionViewSource? That functionality would have made this so much easier! In that absence, there are definitely things I would implement in the FilteredTaskStore to make the UI look better. Most notably, I’d emit Add and Remove elements when I change the filtering mechanisms or add/remove an element. I’d also add an INotifyPropertyChanged interface when I update rather than updating the entire list.

So there are definitely things I can do to improve things, but as an example, this is great!

As always, you can find my code on my GitHub Repository.