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

多列排序:从 Angular NgExTable 到源数据列表管理

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2019年9月28日

CPOL

22分钟阅读

viewsIcon

14902

downloadIcon

518

实现并描述 Angular 数据网格工具 NgExTable 的多列排序功能以及用于排序源数据列表的相关处理逻辑(已更新至 Angular 11)。

引言

我的文章《使用 Angular NgExTable 进行客户端和服务器端数据过滤、排序和分页》描述了自定义数据网格工具的基本结构和用法。这是 NgExTable 系列的第二篇文章,详细介绍了使用此 Angular UI 工具、API 数据服务和数据库存储过程的多列排序操作和工作流程。使用 Microsoft Visual Studio 和 C# 的读者可以下载源代码,并在前端、中间层数据服务和数据库后端的所有场景中运行代码。下载用于非 Microsoft 技术的任何平台源代码的读者仍然可以查看多列排序如何与 Angular 数据网格工具以及生成客户端源数据列表或服务器端模拟数据结果集的逻辑协同工作。如果您需要运行带有以前 Angular 版本(8、9 或 10)的示例应用程序,请参阅文章末尾“历史记录”部分中的详细信息。

示例应用程序演示了与数据列表排序相关的这些独特功能

  • 提供不同的方法在单列和多列排序类型之间切换
  • 仅当存在多个已排序列时才显示多列排序 UI 样式
  • 任何可排序列的排序顺序都可以通过自由轻松的选择进行更改
  • 使用“向下定位”规则智能切换现有排序序列号(详见“排序后的列和序列选择”部分)
  • 动态显示和隐藏多重排序命令面板,用于数据访问的“提交即生效”模式,以避免重复和冗余的数据调用,特别是对于服务器端分页操作
  • 可选地根据第一个排序列数据显示行分组线
  • 表格托管组件级别的不同列排序类型和相关选项配置,以便在应用程序中不同页面上使用网格工具时设置可以不同
  • 客户端 JavaScript 中用于多列排序的源数据列表、C# 中的 API 数据服务和 SQL Server 数据库存储过程的代码示例

设置并运行示例应用程序

注意:本节可能会重复《NgExTable 第一篇文章》中的大部分设置步骤。如果您已经从那篇文章下载了源代码,则项目和文件结构是相同的。只有在 AppDev\src\app\NgExTableDemo 文件夹下的 client-paging.component.tsserver-paging.component.ts 文件中的排序配置设置有所不同。

下载的源文件包含两种不同的 Visual Studio 解决方案/项目类型。请选择您喜欢的一种或两种,并在您的本地机器上进行设置。您还需要在本地机器上全局安装 node.js(推荐版本 14.x LTS 或更高版本)和 Angular CLI(推荐版本 11.x 或更高版本)。请查看 node.jsAngular CLI 文档以获取详细信息。

您可以在 C:\Program Files (x86)\Microsoft SDKs\TypeScript 文件夹中检查 Visual Studio 可用的 TypeScript 版本。示例应用程序的 ASP.NET 和 Core 类型都将 SM.NgExTable.Web.csproj 文件中 TypeScriptToolsVersion 节点下的 TypeScript for Visual Studio 版本设置为 4.0。如果您没有安装版本 4.0,请从 Microsoft 网站下载 安装包,或安装包含 TypeScript 4.0 的 Visual Studio 2019 版本 16.8.x。

NgExTable_AspNetCore_Cli

  1. 您需要在本地机器上使用 Visual Studio 2019(版本 16.8.x)。.NET Core 5.0 SDK 包含在 Visual Studio 安装中。

  2. 下载源代码文件并将其解压到您的本地工作区。

  3. 进入本地工作区的物理位置,依次双击 SM.NgExTable.Web\AppDev 文件夹下的 npm_install.batng_build.bat(如果未全局安装 Angular CLI,则双击 ng_build_local.bat)文件。

    注意ng build 命令可能在每次更改 TypeScript/JavaScript 代码后都需要执行,而 npm install 命令仅在节点模块包有任何更新时才需要执行。我没有启用 CLI/Webpack 热模块替换,因为它可能会破坏 Visual Studio 中调试的源代码映射。

  4. 使用Visual Studio 2019打开解决方案,并使用Visual Studio重新构建解决方案。

  5. 单击 IIS Express 工具栏命令(或按 F5)启动示例应用程序。

NgExTable_AspNet_Cli

  1. 下载源代码文件并将其解压到您的本地工作区。

  2. 转到本地工作区的物理位置,依次双击 SM.NgExTable.Web\ClientApp 文件夹下的 npm_install.batng_build.bat(如果未全局安装 Angular CLI,则双击 ng_build_local.bat)文件(另请参阅设置 NgTable_AspNetCore_Cli 项目时的相同注意点)。

  3. 使用 Visual Studio 2017 或 2019 打开解决方案,并使用 Visual Studio 重新构建解决方案。

  4. 单击 IIS Express 工具栏命令(或按 F5)启动示例应用程序。

NgTable_Ng11_Scripts_AnyPlatform.zip 文件仅包含客户端脚本源代码文件夹和文件。如果您使用非 Microsoft 平台系统,您可以将纯 Angular 源代码文件复制到您自己的网站项目,使用 npm 和 Angular CLI 命令执行必要的设置,然后在您的系统上运行示例应用程序。

由于加载了客户端分页的数据网格默认设置为单列排序,要查看使用多列排序的页面,您可以单击“服务器端分页”菜单项,然后单击搜索面板上的“Go”按钮。

您无需为“服务器端分页”演示设置来自数据提供服务应用程序的服务器数据源。默认情况下,示例应用程序使用 server-mock-data.service.ts 来模拟服务器端数据请求和响应模式及结果。这也有利于将 NgExTable_Scripts_AnyPlatform 代码文件添加到其网站项目并在非 Microsoft 平台上运行 NgExTable 示例的受众。

如果您使用 Visual Studio 运行示例应用程序,并希望为“服务器端分页”演示调用真实服务器数据源,我建议执行以下步骤

  1. 请参阅我的另一篇文章《ASP.NET Core:从 ASP.NET Web API 迁移的多层数据服务应用程序》,并按照说明设置 Core API 数据服务。任何版本的数据服务应用程序都是兼容的数据源提供程序。数据服务的源代码文件也可以在本文章的开头下载。

  2. 使用 Visual Studio 启动数据服务应用程序,并在后台保持运行解决方案和 IIS Express。

  3. ../app/NgExTableDemo/app.config.ts 文件中,将 ServerPagingDataSourcemock 更改为 server

    export const ServerPagingDataSource: 
        string = 'server'; //'mock' for simulating server data.
  4. 如果您的设置不正确,请编辑并启用活动服务 URL。

    export const WebApiRootUrl: string = "https://:7200/api/";  //Core 5.0
    
  5. 通过执行 ng_build.batng_build_local.bat 重建 Angular CLI。

  6. F5 运行 NgExTable 演示应用程序,选择左侧菜单项 Server-side Pagination,然后单击 Search Products 面板上的 Go 按钮。

提示:您可以修改源数据,使几行中的列具有相同的值,以便有意义地显示多列排序功能。

  • 客户端数据源文件位置:

    • NgExTable_AspNetCore_CliSM.NgExTable.Web/wwwroot/ClientApp/local-data-products.json
    • NgExTable_AspNet_CliSM.NgExTable.Web/ClientApp/local-data-products.json
    • NgExTable_Scripts_AnyPlateformClientApp/local-data-products.json
  • 数据服务中服务器数据源种子文件位置

    • SM.Store.CoreApi(任何版本):SM.Store.Api.DAL\DataContext\StoreDataInitializer.cs
    • SM.Store.WebApiSM.Store.Api.DAL\StoreDataContextInitializer.cs

    或者,您可以使用 SQL Server Management Studio 或查询命令直接更改 SQL Server 数据库中的数据值作为服务器数据源。

数据排序的 UI 结构

下图展示了用于 UI 数据排序过程的主要组件及其关系。

这些结构根据数据排序需要添加到表格托管的 HTML 模板中。

<table class="table table-condensed table-striped bottom-border"
        [table-main]="pagingParams" (tableChanged)="onChangeTable($event)">
    <thead>
        <tr class="option-board-tr">
            <th colspan="6">
                <options (optionChanged)="onChangeOptions($event)"></options>
            </th>
        </tr>
        <tr #trHead class="table-header-tr">
            <th>Product Name<column-sort sortBy="ProductName" 
                sortDirection="asc" sequence="3"></column-sort></th>
            <th>Category ID<column-sort sortBy="CategoryId"></column-sort></th>
            <th>Category Name<column-sort sortBy="CategoryName"></column-sort></th>
            <th>Unit Price<column-sort sortBy="UnitPrice"></column-sort></th>
            <th>Status<column-sort sortBy="StatusDescription" 
                sortDirection="asc" sequence="2"></column-sort></th>
            <th>Available Since<column-sort sortBy="AvailableSince" 
                sortDirection="desc" sequence="1"></column-sort></th>
        </tr>
        <tr>
            <th colspan="6" class="sort-box-th">
                <multi-sort-command (multiSortChanged)="onChangeTable($event)">
                </multi-sort-command>
            </th>
        </tr>
    </thead>
    - - -
</table>

以下是关于排序相关的指令和组件结构的一些关键点。

  • TableMainDirective:这个用于 HTML 表格的属性指令是一个没有 HTML 模板的组件。它作为当前表格的私有本地服务。它可以被导入并注入到任何只在当前表格范围内声明的子组件中。因此,在示例应用程序中,OptionsComponentColumnSortingComponentMultiSortingCommandComponent 这些子组件可以将 tableMainDirective 作为通信的中心点。

  • OptionsComponent:它放置在 HTML 表格标题内。该组件及其 HTML 模板在 NgExTable 库包中预定义,因此客户端调用方(示例应用程序的 NgExTableDemo 部分)中的代码将非常简单。该组件通过 tableMainDirective 传输数据并提交更改。

  • ColumnSortingComponent:它用于显示和处理排序图标和序列号。可以为可排序列定义多个组件实例,如上面的 HTML 代码所示。因此,父 TableMainDirective 和子 ColumnSortingComponent 实例之间存在一对多关系。如果从父级启动子代码片段的执行,则所有子实例中的相同代码都将迭代。

  • MultiSortingCommandComponent:它渲染多列排序命令面板,用于以“提交即生效”模式提交或取消排序请求。此组件也仅与 TableMainDirective 通信,通过它执行输入和输出数据操作。UI 面板根据所需的多列排序操作动态渲染和显示(详见后面章节“多列排序命令面板”)。

排序类型和配置

尽管多列排序功能可用,但在许多情况下,单列数据选项的排序仍然实用。示例应用程序可以配置为仅运行单列排序类型,或单列和多列排序类型的组合。这些配置必须在表格托管组件级别进行,而不是全局级别,因为应用程序中可能使用具有不同设置的多个数据网格。

仅单列排序

这是数据网格显示的传统操作模式。如果设计不需要多列排序,或者数据源不提供多列排序选项,您可以使用此类型。

要仅以单列排序模式运行应用程序,请在表格托管组件类的 ngOnInit 方法中设置此行

this.tableMainDirective.sortingRunMode = 0; //0: single column sorting only, 
                                            //1: single/multiple column sorting (default)

在示例应用程序中,客户端分页演示页面仅加载单列排序类型的数据网格。您可以注释掉此配置行或将其值设置为 1,以启用此页面的可切换单列和多列排序类型。

可切换的单列和多列排序

通过此设置,单列和多列排序类型可以同时存在于数据网格中。但问题是如何轻松智能地在单列和多列排序类型之间切换。我们将在下一节中详细讨论这个主题,包括两种方法:下拉选择和 Ctrl(或 Shift)键操作。

要运行可切换的应用程序,请在表格托管组件类的 ngOnInit 方法中设置此行。您也可以省略此行,因为它是默认设置。

this.tableMainDirective.sortingRunMode = 1;

默认情况下,sortingTypeSwitch 设置为使用下拉选择。有关使用 Ctrl/Shift 键方法的详细信息,请参阅下一节。

如果使用下拉选择,您可能还需要为单列或多列排序设置默认加载类型。否则,默认设置是加载单列排序作为活动选择类型。

this.tableMainDirective.sortingOption = 'multiple'; //'single' (default) or 
                                  // 'multiple' - used only for sortingRunMode = 1

在示例应用程序中,所有列排序的配置项都在 TableMainDirective 控制器类中用注释定义。

//Values here are defaults and can be overwritten from table-hosting component-level settings.    
sortingRunMode: number = 1;     //0: single column sorting only, 
                                //1: single/multiple column sorting
sortingTypeSwitch: number = 0;  //0: dropdown selection mode, 
                                //or 1: Ctrl/Shift key mode - used only for sortingRunMode = 1
sortingOption: string = 'multiple'; //'single' or 'multiple' - initial loading type 
                                    // used only for sortingRunMode = 1
enableOptionBoard: string = ''; //'yes' or 'no'('' the same as 'no').
showOptionBoardContent: string = '';//'yes' or 'no'('' the same as 'no')
showGroupingLines: string = ''; //yes' or 'no'('' the same as 'no')

所有配置项也都在 ClientPagingComponentServerPagingComponent 类的 ngOnInit 方法中列出并附有注释,以演示如何使用或覆盖默认值。在您了解了示例应用程序的所有详细信息后,请随意启用、禁用或更改项目行以测试数据排序结果和显示。

在单列和多列排序选项之间切换

示例应用程序演示了两种不同的方法来在单列和多列排序类型之间切换,每种方法都有其优缺点。对于您的实际应用程序开发,您可以根据您的偏好和业务需求实现其中一种方法。

从下拉列表中选择排序类型

TableMainDirective.sortingTypeSwitch 的值为 1 时,此方法配置为使用下拉选择。

sortingTypeSwitch = 1; //dropdown selection,

OptionsComponent 及其模板用于下拉列表和选择显示。在 OptionsComponent 中,下拉选定值被分配给 TableMainDirective.sortingOption。当选项更改时,会调用 switchSortingOption 方法来处理排序逻辑和数据结果中的更改。由于 TableMainDirective 被注入到 OptionsComponent 类的契约中,因此可以直接在 OptionsComponent 中访问 TableMainDirective 中的属性和方法。

onSortingOptionChange($event: any) {
    this.tableMainDirective.sortingOption = this.sortingOption;        
    this.tableMainDirective.switchSortingOption();
}  

默认加载多列排序时,选项面板和列标题如下所示

当切换到单列排序时,该过程会自动从之前的多列排序中取第一个排序的列。

通过显式选择,用户可以清楚地知道当前的排序类型和所执行操作的后果。但是,这种方法需要额外的组件和空间用于页面上的下拉显示。

使用 Ctrl 或 Shift 键单击排序图标

此方法或功能在示例应用程序中默认不加载。您需要在表格托管组件控制器类的 ngOnInit 方法中将 TableMainDirective.sortingTypeSwitch 的值设置为 0 来启用它。

sortingTypeSwitch = 0; //Using Ctrl or Shift key,

通过这种方法,在任何可排序列上常规单击任何排序图标始终以单列排序类型进行。当按住 CtrlShift 键时,在第二个可排序列上单击任何排序图标,该过程将进入多列排序工作流。ColumnSortingComponent 中的 toggelSort 方法会检查此类用户操作。

toggleSort(obj: any, $event: any) {

    if (this.config.sortingTypeSwitch == 0 && ($event.ctrlKey || $event.shiftKey) &&
        !this.tableMainDirective.baseSequenceOptions.find(a => a.value > 1)) {
        if (this.config.sortingOption == 'single') {
            //Switch to multiple mode.
            this.tableMainDirective.sortingOption = 'multiple';
        }
    }
    - - -
}

此时,任何用户操作都将遵循多列排序场景,与从下拉列表中选择多列排序时使用的场景相同。尽管如此,不需要任何选择的显式结构。这种方法的缺点也很明显。

  • 用户可能不清楚 Ctrl 或 Shift 键的操作,因此最好提供额外的说明。
  • 对于切换回单列排序类型,如果仍然使用 CtrlShift 键,会很奇怪。因此,需要考虑其他方法。

您可以点击序列下拉菜单中的“x”按钮来移除已排序的列,直到数据网格上只剩下一个已排序的列,此时它就变成了单列排序类型。然而,这对于该目的来说无疑是一种繁琐的方式。为了快速切换到单列排序类型,多重排序命令面板上添加了一个“Go to Single Column Sorting”按钮。该面板的详细信息将在后面的部分中描述,但这里只显示了带有该按钮的面板,该按钮仅在 CtrlShift 键激活模式下渲染到面板上。点击该按钮将立即切换到单列排序类型,保留之前第一个已排序的列。您还可以注意到,没有显示排序类型下拉菜单。

列和序列选择

多列排序功能通过复杂的处理逻辑实现,用于选择排序的列和序列。如果不需要修改逻辑,用户可能不必了解内部代码细节,但下面概述了实现和实践的要点。

  1. 每个可排序列都会创建一个 ColumnSortingComponent 对象实例。这些实例在列标题上设置排序图标和带下拉列表的序列显示。

  2. TableMainDirective 对象通过数据成员 sortableListbaseSequenceOptions 数组来管理 ColumnSortingComponent 的多个实例。例如,sortableItemColumnSortingComponent 中创建,然后添加到 TableMainDirective 中的 sortableList 数组。

    ColumnSortingComponent

    //Populate sortableItem and add sortable column to sortableList in parent.
    this.sortableItem = {
        sortBy: this.sortBy,
        sortDirection: this.sortDirection || '',
        sequence: this.sequence || -1
    };
    
    this.tableMainDirective.initSortableList(this.sortableItem);    
    

    TableMainDirective

    //Called from each columnSortComponent.
    initSortableList(sortableItem: SortableItem) {
        - - -
        this.sortableList.push(sortableItem);
        - - -  
    }
  3. ColumnSortingComponent 中的 toggleSort 方法处理已排序列选择中的更改并更新相应 sortableItem 的排序方向。

    toggleSort() {
        - - -                
        switch (this.sortableItem.sortDirection) {
            case "asc":
                this.sortableItem.sortDirection = "desc";
                break;
            case "desc":
                this.sortableItem.sortDirection = 
                      this.toggleWithOriginalDataOrder ? "" : "asc";
                break;
            default:
                //Existing sortDirection is ''.
                this.sortableItem.sortDirection = "asc";
                break;
        }
        - - -
    }
  4. 当排序序列改变时,ColumnSortingComponentonSequenceChange 方法的代码需要调用父 TableMainDirective 来重新排列序列值并刷新整体排序设置,因为列中的任何改变都会影响其他列的行为。

    onSequenceChange(event: any) {
        let oldSeq: number = this.sortableItem.sequence;
        this.sortableItem.sequence = event.value;
                           
        //Call to re-arrange sequence numbers. 
        this.tableMainDirective.rearrangeSequence
                (oldSeq, this.sortableItem.sequence, this.sortBy);                
                
        //Call TableMainDirective and then back call each ColumnSortComponent 
        //to reset sorting settings if changed.
        this.tableMainDirective.refreshSortSettings();
        - - -        
    }
  5. 排序序列的排列基于这些规则

    • 下一个可用的序列号会自动分配给任何新添加的用于排序的列。

    • 最大可用序列号会根据活动已排序列的数量动态更新。例如,如果选择了三个已排序列,则序列下拉列表只包含 1、2 和 3。

    • 通过从下拉列表中选择数字来更改任何列序列,将自动将序列号向较大数字侧重新排列。例如,如果将序列号从 4 更改为 2,则之前的 2 将变为 3,之前的 3 变为 4。TableMainDirective 中的 rearrangeSequence 方法实现了所需的规则和逻辑。

      rearrangeSequence(oldSeq: number, newSeq: number, sortBy: string) {
          if (oldSeq > 0 && newSeq == -1) {
              //Re-arrange sequence if any sortableItem.sequence reset to -1.
              this.sortableList.forEach((item: SortableItem, index: number) => {
                  if (item.sequence > oldSeq) {
                      item.sequence--;
                  }
              });
          }
          else {
              //Change any sequence number in positive range.
              this.sortableList.forEach((item: SortableItem, index: number) => {
                  if (item.sortBy != sortBy) {
                      if (item.sequence >= newSeq && item.sequence < oldSeq) {
                          item.sequence++;
                      }
                      else if (item.sequence <= newSeq && item.sequence > oldSeq) {
                          item.sequence--;
                      }
                  }
              });            
          }	
  6. TableMainDirective 中,排序列选择和/或序列中的任何升级更改都将传输到 pagingParams.sortList 数组,该数组可以发送到表格托管组件以刷新数据网格。如果当前排序选项是单列类型,则 pagingParams.sortList 仅填充 sortableList 中的第一个 sortableItem

    updatePagingParamsWithSortSettings() {
        //Transfer active sortable items to pagingParams.sortList.
        this.pagingParams.sortList = [];
        for (let num: number = 1; num <= this.sortableList.length; num++) {
            for (let idx: number = 0; idx < this.sortableList.length; idx++) {
                if (this.sortableList[idx].sequence == num) {
                    let sortItem: SortItem = {
                        sortBy: this.sortableList[idx].sortBy,
                        sortDirection: this.sortableList[idx].sortDirection
                    };
                    this.pagingParams.sortList.push(sortItem);
                    break;
                }
            }
            if (this.sortingOption == 'single' && num == 1) {
                break;
            }
        }
    }

多列排序命令面板

对于多列排序,大多数在互联网和软件市场中可以找到的 Angular 数据网格工具都使用“随更改即访问”数据访问模式,其中对排序的列选择或序列的任何更改操作都会重新加载数据项到网格中,无论这是否是中间步骤。结果是,任何指定的排序操作,如果包含多个步骤,都将多次调用数据并刷新网格。

为了解决这个问题,NgExTable 引入了 MultiSortingCommandComponent 及其 HTML 模板,采用“提交即访问”数据访问模式。该面板可以在表格标题下方动态展开和折叠。

  1. <multi-sort-command> 标签添加到表格托管组件模板中 <thead> 的第二行。

    <thead>
        <tr #trHead>
            <!--Normal th headers -->
            - - -
        </tr>
        <tr>
            <th colspan="6" class="sort-box-th">
                <multi-sort-command (multiSortChanged)="onChangeTable($event)">
                </multi-sort-command>
            </th>
        </tr>
    </thead>
  2. 当满足以下任一条件集时,命令面板将自动显示。

    条件集 #1

    • 当前排序选项处于多列类型模式,并且至少有一个现有排序列。
    • 点击任何其他列上的排序图标以开启排序或更改排序方向。

    条件集 #2

    • 已经至少有两个已排序的列。
    • 点击任何序列下拉菜单并已更改序列号。

    下面显示了命令面板,其中打开了下拉选择选项切换类型。

  3. 当命令面板打开时,用户可以舒适地重复对列和序列选择进行任何所需的更改。排序请求只有在点击“OK”按钮后才会提交。另外,用户可以取消更改或清除所有排序设置以获取未排序的数据集。

  4. 无论何时点击 OKCancelClear Column Settings 按钮,或者如果面板打开,点击面板外部的任何操作,面板都将自动关闭。命令面板不是模态类型,因此面板外部的任何操作都将被视为取消操作。

  5. 面板的显示和隐藏由 MultiSortingCommandComponentshowMultiSortPanel 变量的值控制。获取和设置变量值的请求通过订阅方法从其他组件发送。

    • 当点击命令面板上的任何按钮时,直接在 MultiSortingCommandComponent 中将 showMultiSortPanel 变量设置为 false。面板将在相应操作完成后随此操作关闭。

    • 由于多列排序的大部分顶级处理在 TableMainDirective 中执行,并且需要从父级到子组件的调用,因此用于获取和设置 showMultiSortPanel 值的方法已添加到 TableMainDirective 中。这些方法可以从 TableMainDirective 本身和 ColumnSortingComponents 的多个位置调用。

      getShowMultiSortPanelFlag(): boolean {
          let subjectParam: NameValueItem = {
              name: 'getShowMultiSortPanelFlag',
              value: undefined
          }
          this.multiSortCommandComponent.next(subjectParam);
          return subjectParam.value;
      }
      
      setShowMultiSortPanelFlag(flagValue: boolean) {
          let subjectParam: NameValueItem = {u
              name: 'setShowMultiSortPanelFlag',
              value: flagValue
          }
          this.multiSortCommandComponent.next(subjectParam);
      }
    • 方法调用在 MultiSortingCommandComponent 的构造函数中被订阅。当从父 TableMainDirective 调用时,它实际上在 MultiSortingCommandComponent 中获取并设置 showMultiSortPanel 变量的值。

      let pThis: any = this;
      //Called from TableMainDirective to open this panel.
      this.tableMainDirective.multiSortCommandComponent$.subscribe(
          (subjectParam: NameValueItem) => {
              if (subjectParam.name == "setShowMultiSortPanelFlag") {
                  //subjectParam.value: true or false.
                  pThis.showMultiSortPanel = subjectParam.value;
              }
              else if (subjectParam.name == "getShowMultiSortPanelFlag") {                    
                  subjectParam.value = pThis.showMultiSortPanel;
              }
          }
      );

排序参数的默认设置

使用 NgExTable,排序参数的默认值可以在两个地方之一指定。

  1. 在表格托管 HTML 模板的 column-sort 标签中添加 sortDirectionsequence 属性和值。

    <tr>
        <th>Product Name<column-sort sortBy="ProductName" 
            sortDirection="asc" sequence="3"></column-sort></th>
        <th>Category ID<column-sort sortBy="CategoryId"></column-sort></th>
        <th>Category Name<column-sort sortBy="CategoryName"></column-sort></th>
        <th>Unit Price<column-sort sortBy="UnitPrice"></column-sort></th>
        <th>Status<column-sort sortBy="StatusDescription" 
            sortDirection="asc" sequence="2"></column-sort></th>
        <th>Available Since<column-sort sortBy="AvailableSince" 
            sortDirection="desc" sequence="1"></column-sort></th>
    </tr>
  2. 在表格托管组件的 ngOnInit() 中向 pagingParams.sortList 数组添加元素。元素顺序决定了列排序序列。

    this.pagingParams = {
        pageSize: pageSize !== undefined ? pageSize : 10,
        pageNumber: 1,
        sortList: [{
            sortBy: 'AvailableSince',
            sortDirection: 'desc'
        }, {
            sortBy: 'StatusDescription',
            sortDirection: 'asc'
        }, {
            sortBy: 'ProductName',
            sortDirection: 'asc'
        }],
        changeType: TableChange.init
    }

如果不存在默认项和值,则网格最初将在没有任何列数据排序的情况下填充。

排序数据列表的显示增强

通过在 AfterViewInit 事件处理程序中或数据加载后更改 nativeElement 项的样式,可以将许多排序数据列表的显示选项添加到页面中。示例应用程序展示了一个根据第一个排序列向网格显示添加行分组线的示例。主要方法 setRowGroupLinesTableMainDirective 中创建,用于动态处理基于 DOM 的样式设置。读者可以在 table-main.directive.ts 文件中查看代码详细信息。

这种样式设置是在库中完成的,而不是在 UI 中,这只是一个示例,以节省 UI 部分的代码。仍然需要将 DOM 元素从表格托管组件传递到 TableMainDirective。因此,在表格托管组件中定义了两个 ViewChild 对象,其中 trHeadtrItems 分别为模板变量 #trHead#trItems 创建。

//Row group lines.
@ViewChild('trHead', { static: true }) trHead: ElementRef;
@ViewChildren('trItems') trItems: QueryList<any>;

数据加载后,代码会通过传递两个 ViewChild 对象实例来调用 setRowGroupLines 方法。

//Show first sortBy group lines in grid. 
this.tableMainDirective.setRowGroupLines(this.trHead, this.trItems);

showGroupingLines 选项也可以使用选项切换板上的复选框启用和禁用。如果按列排序的行非常独特,您可能希望关闭分组线,因为在这种情况下分组线可能意义不大。

以下屏幕截图显示了在 AvailableSince 作为第一个排序列时行分组线的实际效果。

使用上述方法还可以增强各种其他显示样式,例如更改行背景颜色、文本字体类型和颜色、合并单元格,甚至添加子行。请随意自行尝试。

客户端分页的排序源数据

NgExTable/client-pagination.service.ts 中的 TypeScript 代码执行客户端分页数据列表的排序逻辑。在示例应用程序中,服务器模拟数据提供程序也使用了相同的原生 JavaScript array.sort 函数和相关进程。

//Sorting logic.
changeSort(pagingParams: PagingParams, data: Array<any>): Array<any> {        
    let pThis: any = this;
    let rtnArr: any = data.sort((previous: any, current: any) => {
        //Sort firstly-available column with different comparison items along the sortList.
        let idx: number = 0; 
        while (idx < pagingParams.sortList.length) {
            if (current[pagingParams.sortList[idx].sortBy] 
                     !== previous[pagingParams.sortList[idx].sortBy]) {
                return pThis.doSort(previous, current, pagingParams.sortList, idx);
            }
            idx++;
        }            
        return 0;            
    });        
    return rtnArr;
}        

private doSort(previous: any, current: any, 
               sortList: Array<SortItem>, idx: number): number {        
    //Null is sorted to the last for both asc and desc.
    if (previous[sortList[idx].sortBy] === null) {
        return 1;
    }
    else if (current[sortList[idx].sortBy] === null) {
        return -1;
    }
    else if (previous[sortList[idx].sortBy] > current[sortList[idx].sortBy]) {
        return sortList[idx].sortDirection === 'desc' ? -1 : 1;
    }
    else if (previous[sortList[idx].sortBy] < current[sortList[idx].sortBy]) {
        return sortList[idx].sortDirection === 'asc' ? -1 : 1;
    }
    return 0;
}

以下是代码的重要之处

  • 代码处理单列和多列排序场景。

  • 在实际上是一个外层循环的 data.sort 方法中,内层 while 循环用于在 sortList 数组中搜索按序列排序的列。

  • while 循环调用的 doSort 比较函数对每个迭代的列组进行实际数据排序。

  • 逻辑将 null 值排在最后,无论排序方向如何。如果您对 null 值应用不同的规则,您可能会更改 doSort 函数中的返回索引号,例如,在第一个 ifelse if 条件之间切换 -11,使 null 值始终排在最前面。

服务器端分页的排序源数据

示例应用程序提供了使用 LINQ Expression 和数据库存储过程进行服务器端数据排序管理的示例。这些示例使用 Microsoft 技术实现,因此读者需要使用 Visual Studio 工具打开源代码项目才能查看演示结果。本地设置 Visual Studio API 数据服务项目的详细信息请参阅“设置和运行示例应用程序”部分。或者,您可以直接查看文章《ASP.NET Core:从 ASP.NET Web API 迁移的多层数据服务应用程序》中的说明。

LINQ 表达式

LINQ 表达式方法是针对一些简单请求的选择,无需复杂的业务规则、许多表连接或多个数据库实例。此方法的本质是使用传递的 sortList 集合作为单列或多列排序参数来构造 LINQ 表达式树节点。该逻辑由 SM.Store.CoreApi_[版本]\SM.Store.Api.Common\Classes\GenericMultiSorterPager.cs 中的 AsSortedQueryable 方法处理。注释行解释了代码执行的操作。

public static IOrderedQueryable<t> AsSortedQueryable<t>
       (this IQueryable<t> source, List<sortitem> sortList)
{
    if (sortList.Count == 0)
    {
        return null;
    }
    //Create parameter expression node.
    var param = Expression.Parameter(typeof(T), string.Empty);
            
    //Create member expression for accessing properties for first sorted column.
    var property = Expression.PropertyOrField(param, sortList[0].SortBy);

    //Create lambda expression as delegation type for first sorted column.
    var sort = Expression.Lambda(property, param);

    //Call to create sorting expression for the first sorted column.
    MethodCallExpression orderByCall = Expression.Call(
        typeof(Queryable),
        "OrderBy" + (sortList[0].SortDirection == "desc" ? "Descending" : string.Empty),
        new[] { typeof(T), property.Type },
        source.Expression,
        Expression.Quote(sort));

    //Call to create multiple column sorting expressions if more than one sorted column passed.
    if (sortList.Count > 1)
    {
        for (int idx = 1; idx < sortList.Count; idx++)
        {
            var item = sortList[idx].SortBy;

            //The same logic used as for the first column sorting.
            param = Expression.Parameter(typeof(T), string.Empty);
            property = Expression.PropertyOrField(param, item);
            sort = Expression.Lambda(property, param);

            //Use "ThenBy" for more than one column sorting. 
            orderByCall = Expression.Call(
                typeof(Queryable),
                "ThenBy" + (sortList[idx].SortDirection == "desc" ? 
                "Descending" : string.Empty),
                new[] { typeof(T), property.Type },
                orderByCall,
                Expression.Quote(sort));
        }
    }
    //Generate and return LINQ of IOrderedQueryable type.
    return (IOrderedQueryable<t>)source.Provider.CreateQuery<t>(orderByCall);
}

如果您想测试 LINQ 表达式方法,请在 Angular 示例应用程序的 NgExTableDemo\services\app.config.ts 中启用该行。如果 API 数据服务 URL 与下载的原始 URL 不同,您可以调整它。

export const ServerPagingDataSource: string = 'server';

export const WebApiRootUrl: string = 'https://:7200/api/'; 

数据库存储过程

使用存储过程是检索分页、过滤和排序数据列表的首选方法,特别是在需要多列排序、多个数据库实例或复杂业务规则处理时。ASP.NET Core API 数据服务示例应用程序包含在 SM.Store.CoreApi_[版本]\SM.Store.Api.DAL\DataContext\StoreDataInitializer.cs 文件中创建存储过程 GetPagedProductList 的代码。运行数据服务应用程序时,存储过程可以自动添加到 appsettings.json 配置文件中指定的 SQL Server 数据库中。如果使用内存数据库设置,存储过程可能无法正常工作。

使用存储过程进行多列排序代码相当简单直接。

  • 从 Angular 示例应用程序代码中,将 sortList 包含在 paginationRequest 对象中,用于 AJAX 调用(详见 ../SM.NgExTable.Web/ClientApp/app/NgExTableDemo/server-paging.component.ts)。

    //Parameters for data access
    getProductListRequest(): any {
        let req: any = {
            - - -
            paginationRequest: {
                sortList: []
            },
        };
        - - -
        if (this.pagingParams.sortList.length > 0) {
            req.paginationRequest.sortList = this.pagingParams.sortList;
        }
        return req;
    };
    	
  • 数据服务控制器方法 Post_GetPagedProductListSpsortList 泛型列表转换为具有 SQL ORDER BY 子句格式的 sortString

    //Multiple column sorting
    if (request.PaginationRequest.SortList != null && 
        request.PaginationRequest.SortList.Count > 0)
    {
        foreach (var item in request.PaginationRequest.SortList)
        {
            if (sortString != " ") sortString += ", ";
            sortString += item.SortBy + " " + item.SortDirection;
        }
    }	
  • sortString 被传递给 GetPagedProductList 存储过程并嵌入到其中的动态查询中。

    IF @SortString != ''
    SET @Query = @Query + ' ORDER BY ' + @SortString	

如果感兴趣,读者还可以在包含的文件 StoreCF8.sql 中查看存储过程的完整脚本,该文件可从 ApiDataServices.zip 下载。

摘要

多列排序功能是数据网格显示 Web 应用程序的一个很好的附加功能。本文和示例应用程序可以作为在客户端 UI 和服务器处理端实现此类功能的有用资源。

历史

  • 2019年9月28日
    • 原始帖子使用 Angular 8 CLI
  • 2020年12月20日

    • 使用 Angular 11 CLI 更新了源代码
    • 使用 ASP.NET Core 5.0 更新了网站项目
    • 编辑了部分章节的文章文本以进行更新
    • 如果您需要使用之前的 Angular 版本 8、9 或 10 运行示例应用程序,您可以下载应用程序的 package.json 文件 Package.json_Ng8-9-10.zip,用您想要的版本替换现有应用程序中的 package.json 文件,然后按照 Set Up and Run Sample Application 部分中的说明进行操作。示例应用程序的 Angular 11 源代码与 Angular 版本 8、9 和 10 完全兼容,没有大的破坏性更改。
© . All rights reserved.