Click here to Skip to main content
15,892,298 members
Articles / Desktop Programming / WPF

Data Paging with Dynamic Data

Rate me:
Please Sign up or sign in to vote.
4.67/5 (2 votes)
8 Nov 2020MIT3 min read 7.7K   6  
Paging data collections using the Dynamic Data library
In this article, you will learn how to paginate data collections using Dynamic Data in a WPF-MVVM application.

Image 1

Introduction

If you have a large data collection, it becomes somewhat impractical, not to mention user unfriendly, to populate an items control with all the data in the collection. The best approach is to segment the data, so the items control only displays a subset of the data, and enables users to cycle through the data segments. Implementing such a pagination feature in .NET applications can be done using the Dynamic Data library and this article will take a look at how to do this in a WPF-MVVM application.

Dynamic Data

Image 2

ynamic Data is a portable class library that provides collections containing Reactive Extensions (Rx) features. A Dynamic Data collection can be either an observable list, of type SourceList<TObject>, or an observable cache, of type SourceCache<TObject, TKey>. These collections are managed using observable change sets that are created by calling a collection's Connect() operator and can be either of type IObservable<IChangeSet<TObject>> or IObservable<IChangeSet<TObject, TKey>>. Data manipulation operations like sorting, grouping, filtering, data virtualisation and pagination, are done using operators that can be chained together to carry out complex operations. As of the time of writing, the library has 60 collection operators.

To use Dynamic Data, your project must reference the Dynamic Data NuGet package.

Data Paging

As mentioned in the previous section, Dynamic Data provides two types of reactive collections which act as data sources. To paginate your data, you need to make use of the SourceCache<TObject, TKey> collection. In the sample project, such a collection is defined in an IEmployeesService implementation and will contain objects of type Employee.

C#
using Bogus;
using DynamicData;
using PagedData.WPF.Models;
using System;

namespace PagedData.WPF.Services
{
    public class EmployeesService : IEmployeesService
    {
        private readonly ISourceCache<Employee, int> _employees;

        public EmployeesService() => _employees = new SourceCache<Employee, int>(e => e.ID);

        public IObservable<IChangeSet<Employee, int>>
               EmployeesConnection() => _employees.Connect();

        public void LoadData()
        {
            var employeesFaker = new Faker<Employee>()
                .RuleFor(e => e.ID, f => f.IndexFaker)
                .RuleFor(e => e.FirstName, f => f.Person.FirstName)
                .RuleFor(e => e.LastName, f => f.Person.LastName)
                .RuleFor(e => e.Age, f => f.Random.Int(20, 60))
                .RuleFor(e => e.Gender, f => f.Person.Gender.ToString());

            _employees.AddOrUpdate(employeesFaker.Generate(1500));
        }
    }
}

In LoadData(), data is added to the observable cache by calling the collection's AddOrUpdate() method. This method has two overloads: one that takes a single object and another that takes a collection of objects. 1500 employee objects are added to the observable collection using Bogus, which generates fake data of employees between the ages of 20 and 60.

The collection's observable change set is publicly exposed by EmployeesConnection() which calls the collection's Connect() operator. The observable change set can then be bound to a ReadOnlyObservableCollection in a view model and other operators can also be called to carry out data management operations.

C#
public class MainWindowViewModel : ViewModelBase
{
    private const int PAGE_SIZE = 25;
    private const int FIRST_PAGE = 1;

    private readonly IEmployeesService _employeesService;
    private readonly ISubject<PageRequest> _pager;

    private readonly ReadOnlyObservableCollection<Employee> _employees;
    public ReadOnlyObservableCollection<Employee> Employees => _employees;

    public MainWindowViewModel(IEmployeesService employeesService)
    {
        _employeesService = employeesService;

        _pager = new BehaviorSubject<PageRequest>(new PageRequest(FIRST_PAGE, PAGE_SIZE));

        _employeesService.EmployeesConnection()
            .Sort(SortExpressionComparer<Employee>.Ascending(e => e.ID))
            .Page(_pager)
            .Do(change => PagingUpdate(change.Response))
            .ObserveOnDispatcher()
            .Bind(out _employees)
            .Subscribe();
    }

    ...
}

To paginate the data, it first has to be sorted. Then, you can call the Page() operator which takes an ISubject<PageRequest> that specifies the first page and the number of items in each page. The Do() operator provides updates when the collection mutates so I use it to update several view model properties using an IPagedChangeSet<TObject, TKey> response.

C#
private void PagingUpdate(IPageResponse response)
{
    TotalItems = response.TotalSize;
    CurrentPage = response.Page;
    TotalPages = response.Pages;
}

Populating the Data Source

Data will be added to the reactive collection when the application loads. This is done by the LoadDataCommand in the view model.

C#
private RelayCommand _loadDataCommand;
public RelayCommand LoadDataCommand =>
    _loadDataCommand ??= new RelayCommand(_ => LoadEmployeeData());

private void LoadEmployeeData() => _employeesService.LoadData();

Page Switching

Cycling through the pages of data is done using the previously defined ISubject<PageRequest> which has an OnNext() operator that is passed a PageRequest object.

C#
...

#region Previous page command
private RelayCommand _previousPageCommand;
public RelayCommand PreviousPageCommand => _previousPageCommand ??=
    new RelayCommand(_ => MoveToPreviousPage(), _ => CanMoveToPreviousPage());

private void MoveToPreviousPage() =>
    _pager.OnNext(new PageRequest(_currentPage - 1, PAGE_SIZE));

private bool CanMoveToPreviousPage() => CurrentPage > FIRST_PAGE;
#endregion

#region Next page command
private RelayCommand _nextPageCommand;
public RelayCommand NextPageCommand => _nextPageCommand ??=
    new RelayCommand(_ => MoveToNextPage(), _ => CanMoveToNextPage());

private void MoveToNextPage() =>
    _pager.OnNext(new PageRequest(_currentPage + 1, PAGE_SIZE));

private bool CanMoveToNextPage() => CurrentPage < TotalPages;
#endregion

#region First page command
private RelayCommand _firstPageCommand;
public RelayCommand FirstPageCommand => _firstPageCommand ??=
    new RelayCommand(_ => MoveToFirstPage(), _ => CanMoveToFirstPage());

private void MoveToFirstPage() =>
    _pager.OnNext(new PageRequest(FIRST_PAGE, PAGE_SIZE));

private bool CanMoveToFirstPage() => CurrentPage > FIRST_PAGE;
#endregion

#region Last page command
private RelayCommand _lastPageCommand;
public RelayCommand LastPageCommand => _lastPageCommand ??=
    new RelayCommand(_ => MoveToLastPage(), _ => CanMoveToLastPage());

private void MoveToLastPage() =>
    _pager.OnNext(new PageRequest(_totalPages, PAGE_SIZE));

private bool CanMoveToLastPage() => CurrentPage < TotalPages;
#endregion

And that's all that's needed for the paging logic. The view model's properties and commands can then be bound to the necessary elements in the view.

XML
<mah:MetroWindow x:Class="PagedData.WPF.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:mah="http://metro.mahapps.com/winfx/xaml/controls"
               xmlns:iconPack="http://metro.mahapps.com/winfx/xaml/iconpacks"
               xmlns:behaviors="http://schemas.microsoft.com/xaml/behaviors"
               DataContext="{Binding Source={StaticResource VmLocator}, Path=MainWindowVM}"
               WindowStartupLocation="CenterScreen"
               mc:Ignorable="d"
               Title="Paged Data"
               Height="420" Width="580">
  <behaviors:Interaction.Triggers>
      <behaviors:EventTrigger>
          <behaviors:InvokeCommandAction Command="{Binding LoadDataCommand}"/>
      </behaviors:EventTrigger>
  </behaviors:Interaction.Triggers>

  <Grid>
      <Grid.RowDefinitions>
          <RowDefinition Height="*"/>
          <RowDefinition Height="Auto"/>
      </Grid.RowDefinitions>

      <DataGrid AutoGenerateColumns="False"
                IsReadOnly="True"
                EnableColumnVirtualization="True"
                EnableRowVirtualization="True"
                ItemsSource="{Binding Employees}">
          <DataGrid.Columns>
              <DataGridTextColumn Header="ID"
                                  Binding="{Binding ID}"/>
              <DataGridTextColumn Header="First Name"
                                  Binding="{Binding FirstName}"/>
              <DataGridTextColumn Header="Last Name"
                                  Binding="{Binding LastName}"/>
              <DataGridTextColumn Header="Age"
                                  Binding="{Binding Age}"/>
              <DataGridTextColumn Header="Gender"
                                  Binding="{Binding Gender}"/>
          </DataGrid.Columns>
      </DataGrid>

      <StackPanel Grid.Row="1" Margin="0,10" Orientation="Horizontal"
                  HorizontalAlignment="Center">
          <Button Style="{StaticResource CustomButtonStyle}"
                  Command="{Binding FirstPageCommand}">
              <iconPack:PackIconMaterial Kind="SkipBackward"/>
          </Button>
          <RepeatButton Margin="12,0,0,0"
                        Style="{StaticResource CustomRepeatButtonStyle}"
                        Command="{Binding PreviousPageCommand}">
              <iconPack:PackIconMaterial Width="15" Height="15"
                                          Kind="SkipPrevious"/>
          </RepeatButton>
          <TextBlock Margin="8,0" VerticalAlignment="Center">
              <TextBlock.Text>
                  <MultiBinding StringFormat="Page {0} of {1}">
                      <Binding Path="CurrentPage" />
                      <Binding Path="TotalPages" />
                  </MultiBinding>
              </TextBlock.Text>
          </TextBlock>
          <RepeatButton Style="{StaticResource CustomRepeatButtonStyle}"
                        Command="{Binding NextPageCommand}">
              <iconPack:PackIconMaterial Width="15" Height="15"
                                          Kind="SkipNext"/>
          </RepeatButton>
          <Button Margin="12,0,0,0"
                  Style="{StaticResource CustomButtonStyle}"
                  Command="{Binding LastPageCommand}">
              <iconPack:PackIconMaterial Kind="SkipForward"/>
          </Button>
      </StackPanel>

      <TextBlock Grid.Row="1" Margin="0,0,15,0"
                  HorizontalAlignment="Right" VerticalAlignment="Center"
                  Text="{Binding TotalItems, StringFormat={}{0} items}"/>
  </Grid>
</mah:MetroWindow>

Conclusion

I hope you have learnt something useful from this article. As mentioned before, Dynamic Data has quite a number of collection operators so do take a look at what else you can do with them. You can also download this article's sample project from the link at the top of the article.

History

  • 9th November, 2020: Initial post

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Software Developer
Kenya Kenya
Experienced C# software developer with a passion for WPF.

Awards,
  • CodeProject MVP 2013
  • CodeProject MVP 2012
  • CodeProject MVP 2021

Comments and Discussions

 
-- There are no messages in this forum --