Dynamically Sort, Filter And Page Data

This is the first in a new series of blog where I will illustrate how dynamic data and reactive extensions can be used to improve the performance of WPF applications. Answering this question on stack overflow has prompted me to write what I plan to be a 3 part mini-series. In this part I will examine paging of in-memory data to reduce how much data a grid binds to and in subsequent posts I will illustrate virtualising data, and finally I will look at injecting behaviours into visible rows.

It has become my custom to start each post with an image then I explain how I got there. So here’s the image, it shows a screen which can filter, sort and page in-memory data.

Paging with dynamic data

If you cannot be bothered reading the remainder of this article, the demo and code can be studied via the following links

All the code is in the demo project is here Dynamic data demo project on GitHub
View model PagedDataViewer.cs
View PagedDataView.xaml
Dynamic data source code here Github

Why use paging

So the first question is why would you page data when I can simply bind to all of it? That’s a reasonable question and mostly I would say there is no need. However for large collections or collections which rapidly update the main thread can often block whilst the collection is updating. I have found this to be the case even with virtualization enabled in the xaml. This is because the observable collection can only be updated on the main thread which is clearly problematic as it blocks. Additionally the ListCollectionView may have to apply sort operations which are very expensive as the ListCollectionView has to linearly find the correct position of each item. As the collection gets larger the linear find and replace operations get slower and slower. I have found from bitter experience that binding to more than 10,000 rows in WPF can be problematic.

Actually I could go on for a while about performance bottlenecks in WPF both from the data and the xaml perspective but that is debate which we can have on another day.

There are of course many solutions to the problem but now I will concentrate of using dynamic data to create a paged view.

Create controllers to dynamically change observables

Dynamic data provides a load of extensions and some controllers to dynamically interrogate the data. For our screen we need a few controllers to dynamically filter, sort and page.

    var pageController = new PageController();  
    var filterController = new FilterController<T>(); 
    var sortController = new SortController<T>(); 

where the values can be changed like this

    //to change page
    pageController.Change(new PageRequest(1,100));
    //to change filter 
    filterController.Change(myobject=> //return a predicate);
    //to change sort
    sortController .Change( //return an IComparable<>);

Use these controller to build a filtered, sorted and paged dynamic data stream

As with all the example in this blog, the data is fed from a shared in-memory cache which is exposed through the ITradeService interface. The following code is only marginally different from code in previous posts.

    //this is an extension of observable collection optimised for dynamic data
    var collection = new ObservableCollectionExtended<TradeProxy>();

var loader = tradeService.All .Connect() 
   .Filter(filterController) // apply user filter
   .Transform(trade => new TradeProxy(trade), new ParallelisationOptions(ParallelType.Ordered, 5))
   .Sort(sortContoller, SortOptimisations.ComparesImmutableValuesOnly)
   .Page(pageController) // this applies the paging and returns on result effecting the current page
   .ObserveOn(schedulerProvider.MainThread)
    //ensure page parameters class knows which page we are on
   .Do(changes => _pageParameters.Update(changes.Response))
   .Bind(_data)     // update observable collection bindings
   .DisposeMany()   //dispose when no longer required
   .Subscribe();

In one line of code the data has been transformed, filtered, sorted and the current page is bound and reflected in the observable collection. And as if by magic the observable collection will self-maintain when any of the controller parameters change or when any of the data changes. At any time the parameters of the controllers can be changed to dynamically change the results of the current page.

The result with this small segment of code is that by applying the page operator we have significantly reduced the number of records bound to the grid and therefore reduced the work load on the main thread.

Hooray, let’s open the champagne. Almost that time, but not quite. We have not set the controller parameters yet nor have we created anything means for the user to changing the page, sort or apply a filter. For the user to enter these values I have created a couple supporting objects which are explained below.

Apply Page Changes

PageParameterData.cs is the class containing the latest page details as well as commands to move to the next and previous page. The commands are bound to the skip previous and next buttons and when these are pressed the page number property changes. This fires a notification which is observed using some simple Rx.

We observe the size and current page properties as followings.

//observe size and current page
var currentPageChanged = PageParameters.ObservePropertyValue(p => p.CurrentPage).Select(prop => prop.Value);
var pageSizeChanged = PageParameters.ObservePropertyValue(p => p.PageSize).Select(prop => prop.Value);

//combine values, create request object and change the controller.
var pageChanger = currentPageChanged.CombineLatest(pageSizeChanged,
                           (page, size) => new PageRequest(page, size))
                           .DistinctUntilChanged()
                           .Sample(TimeSpan.FromMilliseconds(100))
                           .Subscribe(pageController.Change);

The latest values of each are combined into a new PageRequest and the page controller is updated to this value. This reapplies the page logic producing a next page response which includes the next page of data.

Apply Filtering

As with several example screens in the dynamic data menu we have the SearchText property on the main view model. We observe changes, build a predicate and update the filter controller.

  var filterApplier = this.ObservePropertyValue(t => t.SearchText)
                .Throttle(TimeSpan.FromMilliseconds(250))
                .Select(propargs => BuildFilter(propargs.Value))
                .Subscribe(filterController.Change);

where the build filter function is as follows

private Func<Trade, bool> BuildFilter(string searchText)
 {
     if (string.IsNullOrEmpty(searchText)) return trade => true;
     return t => t.CurrencyPair.Contains(searchText, StringComparison.OrdinalIgnoreCase) 
                          || t.Customer.Contains(searchText, StringComparison.OrdinalIgnoreCase);
 }

Apply Sorting

SortParameterData.cs is the view model to bind the sorting data. The following code observes the selected item and applies the selected comparer to the new sort controller.

  var sortChange = SortParameters.ObservePropertyValue(t => t.SelectedItem).Select(prop=>prop.Value.Comparer)
          .ObserveOn(schedulerProvider.TaskPool)
          //Change the sort controller
          .Subscribe(sortContoller.Change);

Summary

This code is surprisingly easy with the main view model having about 100 lines of code. You probably would not believe if I said I wrote all of it in under 3 hours. Admittedly I have the infrastructure for the page changing and the sorting from another project but nonetheless I can assure you that when you are up to speed with dynamic data, you will regard the manipulation of collections of data very easy indeed.

Next time I will be doing something similar yet simpler by showing how dynamic data can virtualise data.

3 thoughts on “Dynamically Sort, Filter And Page Data

  1. I updated PageParameterData so it could show visible item indexes, as well as added page to start and page to end commands


    public class PageParameterData<TVm> : AbstractNotifyPropertyChanged
    {
    private int _currentPage;
    private int _pageCount;
    private int _pageSize;
    private int _totalSize;
    private int _startingIndex;
    private int _endingIndex;
    private readonly Command _cmdPageForward;
    private readonly Command _cmdPageToEnd;
    private readonly Command _cmdPageBackward;
    private readonly Command _cmdPageToStart;
    private readonly IObservableCollection<TVm> _masterList;
    private readonly IObservableCollection<TVm> _filteredList;
    public PageParameterData(int currentPage, int pageSize, IObservableCollection<TVm> filteredList, IObservableCollection<TVm> masterList)
    {
    _masterList = masterList;
    _filteredList = filteredList;
    _currentPage = currentPage;
    _pageSize = pageSize;
    _totalSize = masterList.Count;
    _masterList.CollectionChanged += _masterList_CollectionChanged;
    _filteredList.CollectionChanged += _filteredList_CollectionChanged;
    _cmdPageForward = new Command(() => CurrentPage = CurrentPage + 1, () => CurrentPage < PageCount);
    _cmdPageBackward = new Command(() => CurrentPage = CurrentPage – 1, () => CurrentPage > 1);
    _cmdPageToEnd = new Command(() => CurrentPage = PageCount, () => CurrentPage < PageCount);
    _cmdPageToStart = new Command(() => CurrentPage = 1, () => CurrentPage > 1);
    }
    void _filteredList_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
    StartingIndex = ((CurrentPage – 1) * PageSize) + 1;
    if (_filteredList.Count == 0)
    StartingIndex = EndingIndex = 0;
    else if (_currentPage == _pageCount && StartingIndex > 0)
    EndingIndex = StartingIndex + (_filteredList.Count – 1);
    else
    EndingIndex = _currentPage*_pageSize;
    }
    void _masterList_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
    if (CurrentPage == PageCount)
    EndingIndex = _masterList.Count;
    }
    public void Update(IPageResponse response)
    {
    CurrentPage = response.Page;
    PageSize = response.PageSize;
    PageCount = response.Pages;
    if (response.TotalSize != 0)
    TotalSize = response.TotalSize;
    _cmdPageForward.Refresh();
    _cmdPageBackward.Refresh();
    _cmdPageToEnd.Refresh();
    _cmdPageToStart.Refresh();
    }
    public ICommand CmdPageForward
    {
    get { return _cmdPageForward; }
    }
    public ICommand CmdPageBackward
    {
    get { return _cmdPageBackward; }
    }
    public ICommand CmdPageToEnd
    {
    get { return _cmdPageToEnd; }
    }
    public ICommand CmdPageToStart
    {
    get { return _cmdPageToStart; }
    }
    public int TotalSize
    {
    get { return _totalSize; }
    private set { SetAndRaise(ref _totalSize, value); }
    }
    public int PageCount
    {
    get { return _pageCount; }
    private set { SetAndRaise(ref _pageCount, value); }
    }
    public int CurrentPage
    {
    get { return _currentPage; }
    private set { SetAndRaise(ref _currentPage, value); }
    }
    public int PageSize
    {
    get { return _pageSize; }
    private set { SetAndRaise(ref _pageSize, value); }
    }
    public int StartingIndex
    {
    get { return _startingIndex; }
    private set { SetAndRaise(ref _startingIndex, value); }
    }
    public int EndingIndex
    {
    get { return _endingIndex; }
    private set { SetAndRaise(ref _endingIndex, value); }
    }
    }

    Like

  2. Pingback: What is Dynamic Data? | Dynamic Data

Leave a comment