65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Dynamic Data 进行数据分页

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (2投票s)

2020年11月9日

MIT

3分钟阅读

viewsIcon

9149

使用 Dynamic Data 库对数据集合进行分页

引言

如果您拥有庞大的数据集合,那么将所有数据填充到项目控件中是不切实际的,更不用说对用户不友好了。最好的方法是对数据进行分段,以便项目控件仅显示数据的子集,并允许用户循环浏览数据段。在 .NET 应用程序中实现这样的分页功能可以使用 Dynamic Data 库,本文将介绍如何在 WPF-MVVM 应用程序中执行此操作。

动态数据

ynamic Data 是一个可移植的类库,提供包含 Reactive Extensions (Rx) 功能的集合。Dynamic Data 集合可以是类型为 SourceList<TObject> 的可观察列表,也可以是类型为 SourceCache<TObject, TKey> 的可观察缓存。这些集合使用通过调用集合的 Connect() 运算符创建的可观察变更集进行管理,变更集的类型可以是 IObservable<IChangeSet<TObject>>IObservable<IChangeSet<TObject, TKey>>。排序、分组、筛选、数据虚拟化和分页等数据操作是使用可以链接在一起以执行复杂操作的运算符完成的。在撰写本文时,该库有 60 个集合运算符。

要使用 Dynamic Data,您的项目必须引用 Dynamic Data NuGet 包。

数据分页

如上一节所述,Dynamic Data 提供了两种类型的反应式集合,它们充当数据源。要对数据进行分页,您需要使用 SourceCache<TObject, TKey> 集合。在示例项目中,这样的集合在 IEmployeesService 实现中定义,并将包含 Employee 类型的对象。

  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));
          }
      }
  }    

LoadData() 中,通过调用集合的 AddOrUpdate() 方法将数据添加到可观察缓存。此方法有两个重载:一个接受单个对象,另一个接受对象集合。使用 Bogus 将 1500 个 employee 对象添加到可观察集合中,Bogus 生成 20 到 60 岁之间的 employee 的虚假数据。

集合的可观察变更集通过调用集合的 Connect() 运算符的 EmployeesConnection() 公开。然后,可观察变更集可以绑定到视图模型中的 ReadOnlyObservableCollection,并且还可以调用其他运算符来执行数据管理操作。

  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();
      }

      ...
  }    

要对数据进行分页,首先必须对其进行排序。然后,您可以调用 Page() 运算符,该运算符接受指定第一页和每页项目数的 ISubject<PageRequest>Do() 运算符在集合发生变化时提供更新,因此我使用它来使用 IPagedChangeSet<TObject, TKey> 响应更新多个视图模型属性。

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

填充数据源

当应用程序加载时,数据将被添加到反应式集合中。这是通过视图模型中的 LoadDataCommand 完成的。

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

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

页面切换

使用先前定义的 ISubject<PageRequest> 循环浏览数据页面,该 ISubject<PageRequest> 具有一个 OnNext() 运算符,该运算符传递一个 PageRequest 对象。

  ...
  
  #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    

这就是分页逻辑所需的全部内容。然后,可以将视图模型的属性和命令绑定到视图中的必要元素。

  <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>    

结论

我希望您从本文中学到了一些有用的东西。如前所述,Dynamic Data 有相当多的集合运算符,因此请查看您还可以用它们做什么。您还可以从文章顶部的链接下载本文的示例项目。

历史

  • 2020 年 11 月 9 日:首次发布
© . All rights reserved.