使用 WebAPI 对 ng-grid 进行服务器端分页+过滤+排序

Posted

技术标签:

【中文标题】使用 WebAPI 对 ng-grid 进行服务器端分页+过滤+排序【英文标题】:Server-side paging+filtering+sorting for ng-grid with WebAPI 【发布时间】:2013-07-21 02:24:32 【问题描述】:

我正在尝试创建一个简单的工作示例,将 ng-grid 与 ASP.NET WebAPI 结合使用。因此,我从 ng-grid 示例页面 (http://angular-ui.github.io/ng-grid/) 中的服务器端分页示例开始;无论如何,我的网格总是显示空列,即使在调试时我可以确认数据已正确接收。可能我只是在网格设置中遗漏了一些东西,但我发现的所有样本看起来都与我的相似。有人可以帮忙吗?这是我所做的:

更新 #1:建议的解决方案似乎有效,但仅适用于第一页。每当我移动到新页面或执行任何其他需要刷新的操作时,即使服务器按预期返回数据更改,显示的数据也会保持不变。此外,从我发现的所有代码示例中,似乎设置数据的正确方法只是替换数组成员值,而不是清空并再次填充它。我按照https://groups.google.com/forum/#!searchin/angular/nggrid/angular/vUIfHWt4s_4/oU_C9w8j-uMJ 中的建议尝试了应用,但得到了相同的结果。


服务器端

只需创建一个新的 MVC4 应用程序,更新 NuGet 包并添加 angular 和 ng-grid 包。 我的假数据模型由 Item 类表示:

public sealed class Item

    public int Id  get; set; 
    public string Name  get; set; 
    public int Age  get; set; 
    public bool IsFemale  get; set; 

我还添加了几个模型来处理分页、过滤和排序各种数据集(我发现更容易拥有一个通用的分页基础模型 -PagedFilter- 和一些派生模型):

public class PagedFilter

    private int _nPageSize;
    private int _nPageNumber;

    public int PageSize
    
        get  return _nPageSize; 
        set
        
            if (value < 1) throw new ArgumentOutOfRangeException("value");
            _nPageSize = value;
        
    

    public int PageNumber
    
        get  return _nPageNumber; 
        set
        
            if (value < 1) throw new ArgumentOutOfRangeException("value");
            _nPageNumber = value;
        
    

    public int TotalItems  get; set; 

    public int TotalPages
    
        get  return (int)Math.Ceiling((double)(TotalItems / PageSize)); 
    

    public PagedFilter()
    
        _nPageSize = 20;
        _nPageNumber = 1;
    

这里是 ItemFilter:

public class ItemFilter : PagedFilter

    public List<string> SortFields  get; set; 
    public List<string> SortDirections  get; set; 
    public string Name  get; set; 
    public int? MinAge  get; set; 
    public int? MaxAge  get; set; 

然后我添加一个用于获取项目的 API 控制器:

public class ItemController : ApiController

    // fake data
    private readonly List<Item> _items;

    public ItemController()
    
        Random rnd = new Random();
        _items = new List<Item>();
        char c = 'a';

        for (int i = 0; i < 1000; i++)
        
            _items.Add(new Item
                            
                                Id = i,
                                Age = rnd.Next(1, 100),
                                IsFemale = ((i & 1) == 0),
                                Name = String.Format(CultureInfo.InvariantCulture, "0:00000-1",
                                    i, new string(c, 5))
                            );
            if (++c > 'z') c = 'a';
        
    

    public dynamic Get([FromUri] ItemFilter filter)
    
        var items = _items.AsQueryable();

        // filtering
        if (!String.IsNullOrEmpty(filter.Name))
            items = items.Where(i => i.Name.Contains(filter.Name));

        if (filter.MinAge.HasValue)
            items = items.Where(i => i.Age >= filter.MinAge.Value);

        if (filter.MaxAge.HasValue)
            items = items.Where(i => i.Age <= filter.MaxAge.Value);

        // ...sorting (using Dynamic Linq) omitted for brevity...

        // paging
        int nTotalItems = items.Count();
        items = items.Skip((filter.PageNumber - 1) * filter.PageSize)
                     .Take(filter.PageSize);
        return new
                   
                       totalItems = nTotalItems,
                       items = items.ToArray()
                   ;
    

客户端

在客户端,我的 Angular 应用程序只是一个以 ng-grid 示例为模型的控制器:因此我直接将属性添加到 $scope,即使在实际场景中我宁愿使用模型(可能从 TypeScript 类生成)。 html

<div ng-app="MyApp" ng-controller="MainController">
    <div ng-grid="gridOptions" style="height: 400px">
    </div>
</div>

JS:

var app = angular.module('MyApp', ['ngGrid']);

app.controller('MainController', ['$scope', '$http', function ($scope, $http, $apply) 
    $scope.items = [];

    // filter
    $scope.filterOptions = 
        filterText: "",
        useExternalFilter: true
    ;

    // paging
    $scope.totalServerItems = 0;
    $scope.pagingOptions = 
        pageSizes: [25, 50, 100],
        pageSize: 25,
        currentPage: 1
    ;

    // sort
    $scope.sortOptions = 
        fields: ["name"],
        directions: ["ASC"]
    ;

    // grid
    $scope.gridOptions = 
        data: "items",
        columnDefs: [
             field: "name", displayName: "Name", pinnable: true ,
             field: "age", displayName: "Age", width: "60" ,
             field: "isFemale", displayName: "F", width: "40" 
        ],
        enablePaging: true,
        enablePinning: true,
        pagingOptions: $scope.pagingOptions,        
        filterOptions: $scope.filterOptions,
        keepLastSelected: true,
        multiSelect: false,
        showColumnMenu: true,
        showFilter: true,
        showGroupPanel: true,
        showFooter: true,
        sortInfo: $scope.sortOptions,
        totalServerItems: "totalServerItems",
        useExternalSorting: true,
        i18n: "en"
    ;

    $scope.refresh = function() 
        setTimeout(function () 
            var p = 
                name: $scope.filterOptions.filterText,
                pageNumber: $scope.pagingOptions.currentPage,
                pageSize: $scope.pagingOptions.pageSize,
                sortFields: $scope.sortOptions.fields,
                sortDirections: $scope.sortOptions.directions
            ;

            $http(
                url: "/api/item",
                method: "GET",
                params: p
            ).success(function(data, status, headers, config) 
                $scope.totalServerItems = data.totalItems;
                // SUGGESTION #1 -- empty and fill the array
                /* $scope.items.length = 0;
                angular.forEach(data.items, function (item) 
                   $scope.items.push(item);
                ); 
                */
                // https://groups.google.com/forum/#!searchin/angular/nggrid/angular/vUIfHWt4s_4/oU_C9w8j-uMJ
                $scope.$apply(function ()  $scope.items = data.items; );
                if (!$scope.$$phase) 
                    $scope.$apply();
                
            ).error(function(data, status, headers, config) 
                alert(JSON.stringify(data));
            );
        , 100);
    ;

    // watches
    $scope.$watch('pagingOptions', function (newVal, oldVal) 
        if (newVal !== oldVal && newVal.currentPage !== oldVal.currentPage) 
            $scope.refresh();
        
    , true);

    $scope.$watch('filterOptions', function (newVal, oldVal) 
        if (newVal !== oldVal) 
            $scope.refresh();
        
    , true);

    $scope.$watch('sortOptions', function (newVal, oldVal) 
        if (newVal !== oldVal) 
            $scope.refresh();
        
    , true);

    $scope.refresh();
]);

在我的代码中,调用了成功回调,我可以浏览data.items中所有返回的项目。然而,网格中没有显示任何内容。控制台中没有出现错误。

【问题讨论】:

【参考方案1】:

经过一番试验,我想我找到了正确的代码。这篇关于 $apply 的帖子对我有所帮助:http://jimhoskins.com/2012/12/17/angularjs-and-apply.html。事实上,如果我理解得很好,那么根本不需要调用 apply ,因为我的数据来自 $http 已经提供了这个。所以,我只在成功回调中设置了范围项变量。这是完整的JS,希望这可以帮助像我这样的新手。现在我将使用 TypeScript 模型、服务和所有现实世界的东西来扩展测试:我担心我将不得不发布一些新帖子...... :)

var app = angular.module('MyApp', ['ngGrid']);

app.controller('MainController', ['$scope', '$http', function ($scope, $http, $apply) 
    $scope.items = [];

    // filter
    $scope.filterOptions = 
        filterText: "",
        useExternalFilter: true
    ;

    // paging
    $scope.totalServerItems = 0;
    $scope.pagingOptions = 
        pageSizes: [25, 50, 100],
        pageSize: 25,
        currentPage: 1
    ;

    // sort
    $scope.sortOptions = 
        fields: ["name"],
        directions: ["ASC"]
    ;

    // grid
    $scope.gridOptions = 
        data: "items",
        columnDefs: [
             field: "id", displayName: "ID", width: "60" ,
             field: "name", displayName: "Name", pinnable: true ,
             field: "age", displayName: "Age", width: "60" ,
             field: "isFemale", displayName: "F", width: "40" 
        ],
        enablePaging: true,
        enablePinning: true,
        pagingOptions: $scope.pagingOptions,        
        filterOptions: $scope.filterOptions,
        keepLastSelected: true,
        multiSelect: false,
        showColumnMenu: true,
        showFilter: true,
        showGroupPanel: true,
        showFooter: true,
        sortInfo: $scope.sortOptions,
        totalServerItems: "totalServerItems",
        useExternalSorting: true,
        i18n: "en"
    ;

    $scope.refresh = function() 
        setTimeout(function () 
            var sb = [];
            for (var i = 0; i < $scope.sortOptions.fields.length; i++) 
                sb.push($scope.sortOptions.directions[i] === "DESC" ? "-" : "+");
                sb.push($scope.sortOptions.fields[i]);
            

            var p = 
                name: $scope.filterOptions.filterText,
                pageNumber: $scope.pagingOptions.currentPage,
                pageSize: $scope.pagingOptions.pageSize,
                sortInfo: sb.join("")
            ;

            $http(
                url: "/api/item",
                method: "GET",
                params: p
            ).success(function(data, status, headers, config) 
                $scope.totalServerItems = data.totalItems;
                $scope.items = data.items;
            ).error(function(data, status, headers, config) 
                alert(JSON.stringify(data));
            );
        , 100);
    ;

    // watches
    $scope.$watch('pagingOptions', function (newVal, oldVal) 
        if (newVal !== oldVal) 
            $scope.refresh();
        
    , true);

    $scope.$watch('filterOptions', function (newVal, oldVal) 
        if (newVal !== oldVal) 
            $scope.refresh();
        
    , true);

    $scope.$watch('sortOptions', function (newVal, oldVal) 
        if (newVal !== oldVal) 
            $scope.refresh();
        
    , true);

    $scope.refresh();
]);

(作为旁注,您可以从代码中看到我传递一个字符串来排序数据,而不是两个数组来传递字段和方向。事实上,我找不到接收数组作为成员的正确方法我在 C# 控制器中的输入模型;所以我只是传递一个字符串,其中每个字段名称都以 + 或 - 为前缀,根据升序/降序方向)。

【讨论】:

【参考方案2】:

您将 ng-grid 上的数据源设置为 items,但是您永远不会在服务器成功回调时更新 items 数组。

在成功回调时做这样的事情

$scope.totalServerItems = data.totalItems;
angular.forEach(data.items, function(item) 
   $scope.items.push(item);
);

【讨论】:

谢谢,我没有意识到我需要填充 $scope.items!另一个问题:知道为什么我的控制器的 GET 方法现在被调用了两次吗? 客户端还是服务器端? 服务器端,WebApi 控制器的 Get 方法。此外,我的代码中肯定还有一些问题:当我更改页面时,服务器获取正确的参数并返回接下来的 N 项;如果我在 JS 成功处理程序中设置断点,我可以浏览这些新项目;但最后视图没有刷新,我一直看到第一页而不是第二页。【参考方案3】:

或许也有帮助

HTML 代码示例

<html ng-app="myApp">  
    <head lang="en">
        <meta charset="utf-8">
        <title>Getting Started With ngGrid code-sample</title>  
        <script type="text/javascript" src="angular.js"></script>
        <script type="text/javascript" src="ng-grid-1.3.2.js"></script>
    </head>
    <body ng-controller="MyCtrl">
        <div class="gridStyle" ng-grid="gridOptions"></div>
    </body>
</html>

AngulaJs 代码示例

var app = angular.module('myApp', ['ngGrid']);
app.controller('MyCtrl', function($scope, $http) 
    $scope.filterOptions = 
        filterText: "",
        useExternalFilter: true
    ; 
    $scope.totalServerItems = 0;
    $scope.pagingOptions = 
        pageSizes: [250, 500, 1000],
        pageSize: 250,
        currentPage: 1
    ;  
    $scope.setPagingData = function(data, page, pageSize)  
        var pagedData = data.slice((page - 1) * pageSize, page * pageSize);
        $scope.myData = pagedData;
        $scope.totalServerItems = data.length;
        if (!$scope.$$phase) 
            $scope.$apply();
        
    ;
    $scope.getPagedDataAsync = function (pageSize, page, searchText) 
        setTimeout(function () 
            var data;
            if (searchText) 
                var ft = searchText.toLowerCase();
                $http.get('jsonFiles/largeLoad.json').success(function (largeLoad)         
                    data = largeLoad.filter(function(item) 
                        return JSON.stringify(item).toLowerCase().indexOf(ft) != -1;
                    );
                    $scope.setPagingData(data,page,pageSize);
                );            
             else 
                $http.get('jsonFiles/largeLoad.json').success(function (largeLoad) 
                    $scope.setPagingData(largeLoad,page,pageSize);
                );
            
        , 100);
    ;

    $scope.getPagedDataAsync($scope.pagingOptions.pageSize, $scope.pagingOptions.currentPage);

    $scope.$watch('pagingOptions', function (newVal, oldVal) 
        if (newVal !== oldVal && newVal.currentPage !== oldVal.currentPage) 
          $scope.getPagedDataAsync($scope.pagingOptions.pageSize, $scope.pagingOptions.currentPage, $scope.filterOptions.filterText);
        
    , true);
    $scope.$watch('filterOptions', function (newVal, oldVal) 
        if (newVal !== oldVal) 
          $scope.getPagedDataAsync($scope.pagingOptions.pageSize, $scope.pagingOptions.currentPage, $scope.filterOptions.filterText);
        
    , true);

    $scope.gridOptions = 
        data: 'myData',
        enablePaging: true,
        showFooter: true,
        totalServerItems: 'totalServerItems',
        pagingOptions: $scope.pagingOptions,
        filterOptions: $scope.filterOptions
    ;
);

【讨论】:

【参考方案4】:

我最近一直在使用 ng-grid。我在引用新版本的 AngularJS 时遇到了类似的问题。确保您引用 angular min 文件 1.0.2。

这是我的带有分页的 ng-grid 的客户端代码。一旦实现了正确版本的 Angular JS,它就可以完美运行。

var app = angular.module('myApp', ['ngGrid']);

app.controller('MyCtrl', function ($scope, $http) 
// We needed to bring back mer becase we were using a variable that was being reassigned later on
var mer = [ Item: "Bottle", Pcode: 50, OHQ: 333, AQ: 33, Details: "CLICK" ,
     Item: "Bottle", Pcode: 43, OHQ: 2350, AQ: 1250, Details: "CLICK" ,
     Item: "Bottle", Pcode: 27, OHQ: 4000, AQ: 3000, Details: "CLICK" ,
     Item: "Bottle", Pcode: 29, OHQ: 55, AQ: 10, Details: "CLICK" ,
     Item: "Bottle", Pcode: 34, OHQ: 27, AQ: 2, Details: "CLICK" ,
     Item: "Bottle", Pcode: 50, OHQ: 111, AQ: 33, Details: "CLICK" ,
     Item: "Bottle", Pcode: 43, OHQ: 123, AQ: 1250, Details: "CLICK" ,
     Item: "Bottle", Pcode: 27, OHQ: 1234, AQ: 3000, Details: "CLICK" ,
     Item: "Bottle", Pcode: 29, OHQ: 5678, AQ: 10, Details: "CLICK" ,
     Item: "Bottle", Pcode: 34, OHQ: 0, AQ: 2, Details: "CLICK" ];


$scope.filterOptions = 
    filterText: "",
    useExternalFilter: false
;
$scope.totalServerItems = 0;
$scope.pagingOptions = 
    pageSizes: [5, 10],
    pageSize: 5,
    currentPage: 1
;

$scope.setPagingData = function (data, page, pageSize) 
    var pagedData = data.slice((page - 1) * pageSize, page * pageSize);
    $scope.myData = pagedData;
    $scope.totalServerItems = data.length;
    if (!$scope.$$phase) 
        $scope.$apply();
    
;

// I rearranged some of the code in this function.  I noticed we were calling the same function
// in the end just with a slightly different set of data....so instead of having 18-ish lines of code
// we have 12 (YAY)
$scope.getPagedDataAsync = function (pageSize, page, searchText) 
    setTimeout(function () 
        var data = mer;
        if (searchText) 
            var ft = searchText.toLowerCase();
            data = mer.filter(function (item) 
                JSON.stringify(item).toLowerCase().indexOf(ft) != -1;
            );
        
        $scope.setPagingData(data, page, pageSize);
    , 100);
;

$scope.getPagedDataAsync($scope.pagingOptions.pageSize, $scope.pagingOptions.currentPage);

$scope.$watch('pagingOptions', function (newVal, oldVal) 
    // Got rid of the other check here...this is what was causing the filter to not change the data when it changed.
    if (newVal !== oldVal) 
        $scope.getPagedDataAsync($scope.pagingOptions.pageSize, $scope.pagingOptions.currentPage, $scope.filterOptions.filterText);
    
, true);

$scope.$watch('filterOptions', function (newVal, oldVal) 
    if (newVal !== oldVal) 
        $scope.getPagedDataAsync($scope.pagingOptions.pageSize, $scope.pagingOptions.currentPage, $scope.filterOptions.filterText);
    
, true);

$scope.gridOptions = 
    data: 'myData',
    enablePaging: true,
    showFooter: true,
    totalServerItems: 'totalServerItems',
    pagingOptions: $scope.pagingOptions,
    filterOptions: $scope.filterOptions
;

);

【讨论】:

【参考方案5】:

最后一个文档对这个问题非常明确:http://ui-grid.info/docs/#/tutorial/308_external_filtering

我的结果代码:

var pagination = 
    pageNumber: 1,
    pageSize: 10,
    // list fields to be sorted
    sort: [field:'dup_percentage', direction:'desc'],
    // list fields to be filtered
    filter: []
;

$scope.gridOptions = 
    enableFiltering: true,
    useExternalFiltering: true,
    columnDefs: [...],
    onRegisterApi: function( gridApi ) 
        $scope.gridApi = gridApi;
        $scope.gridApi.core.on.filterChanged( $scope, function() 
        
                var grid = this.grid;

                // reset filters
                pagination.filter = [];

                // loop over all columns
                angular.forEach(grid.columns, function(column, i)
                
                    // loop over filters
                    if(typeof column.filters!==undefined)
                    
                        angular.forEach(column.filters, function(filter, j)
                        
                            // add column name and value to filter array
                            // to be send server side
                            if(typeof filter.term!=undefined && filter.term!==undefined)
                            
                                //console.log('add filter', column:column.name, search:filter.term);
                                pagination.filter.push(column:column.name, search:filter.term);
                            
                        );
                    
                );


                // when user types it's search term
                // server would be hitting too much 
                // so we add 500ms throttle
                if (angular.isDefined($scope.filterTimeout))
                
                    $timeout.cancel($scope.filterTimeout);
                
                $scope.filterTimeout = $timeout(function () 
                
                    // use pagination var which contains all info
                    // needed server side
                    getPage();
                , 500);
            );

好的,现在客户端完成了!你必须在服务器端处理它,我无法帮助你使用 .Net WebAPI,因为我是 php/mysql 驱动...

【讨论】:

【参考方案6】:

就像 Angular 网站上的示例一样:

$http(
            url: "/payments/GetPayments",
            method: "GET",
            params: p
        ).success(function(data, status, headers, config) 
            // Как в примере
            $scope.items = data.items;
            $scope.totalServerItems = data.totalItems;
            if (!$scope.$$phase) 
                $scope.$apply();
            

        ).error(function(data, status, headers, config) 
                alert(JSON.stringify(data));
        );

【讨论】:

以上是关于使用 WebAPI 对 ng-grid 进行服务器端分页+过滤+排序的主要内容,如果未能解决你的问题,请参考以下文章

ng-grid 和实时数据内存泄漏

从服务器更新后刷新 ng-grid 时出现空白数据

角度 highchart 和 ng-grid 标题不考虑帧宽度

angular.js学习-ng-grid

ui-grid 中 ng-grid 的“beforeSelectionChange”的等价物是啥?

ng-grid