排序、筛选和分页

这是微软官方教程Getting Started with Entity Framework 6 Code First using MVC 5 系列的翻译,这里是第三篇:排序、筛选和分页

原文: Sorting, Filtering, and Paging with the Entity Framework in an ASP.NET MVC Application

在之前的教程中你实现了一组使用Web页面对Student实体的的基本CRUD操作。在本教程中,您将为索引页添加排序、筛选和分页的功能。您还会创建一个简单的分组页面。

下图显示了当你完成本教程后的页面截屏。用户可以点击行标题来进行排序,并且多次点击可以让你在升序和降序之间切换。

将排序链接添加到学生的索引页

要为学生索引页添加排序功能,你需要往学生控制器中的索引方法和学生索引视图添加代码。

在索引方法中添加排序功能

使用下面的代码替换学生控制器的索引方法:

        public ActionResult Index(string sortOrder)
        {
            ViewBag.NameSortParm = string.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
            ViewBag.DateSortParm = sortOrder == "Date" ? "date_desc" : "Date";
            var students = from s in db.Students
                           select s;
            switch (sortOrder)
            {
                case "name_desc":
                    students = students.OrderByDescending(s => s.LastName);
                    break;
                case "Date":
                    students = students.OrderBy(s => s.EnrollmentDate);
                    break;
                case "date_desc":
                    students = students.OrderByDescending(s => s.EnrollmentDate);
                    break;
                default:
                    students = students.OrderBy(s => s.LastName);
                    break;
            }
            return View(students.ToList());
        }

这段代码从URL中接收sortOrder查询字符串,该字符串是由ASP.NET MVC作为参数传递给动作方法的。该参数将是"Name"或"Date"之一,这是作为升序的缺省的排序规则。还可能有一条下划线和"desc"来指示这是一个降序排序。

索引页面第一次请求时,没有任何查询字符串被传递,学生们按照LastName的升序排序显示。这是switch语句中的default指定的,当用户点击某列的标题超链接时,相应的sortOrder值通过查询字符串传递到控制器中。

两个ViewBag变量被用于为视图提供合适的查询字符串值。

ViewBag.NameSortParm = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
ViewBag.DateSortParm = sortOrder == "Date" ? "date_desc" : "Date";

这里使用了三元选择语句。第一个指定假如sortOrder参数为null或为空,则NameSortParm应设置为"name_desc",否则将其设置为空字符串。这两个语句为视图中列标题的超链接提供下列排序规则:

当前排序顺序Last Name超链接Date 超链接
Last Name 升序降序升序
Last Name 降序升序升序
Date 升序升序降序
Date 降序升序升序

该方法使用 LINQ to Entities 来指定要作为排序依据的列。代码在switch语句前创建了一个 IQueryable变量,然后在switch中修改它,并在switch语句后调用ToList方法。当您创建和修改IQueryable变量时,没有查询被实际发送到数据库执行。直到您将IQueryable 对象通过调用一种方法如ToList转换为一个集合时才进行真正的查询。因此,直到return View语句之前,这段代码的查询都不会执行。

作为为每个排序顺序编写不同的LINQ语句的替代方法,您可以动态地创建LINQ句。有关动态LINQ的信息,请参阅 Dynamic LINQ 。

为学生索引视图添加行标题超链接

在Views\Student\Index.cshtml中,使用下面的代码替换标题行的<tr>和<th>元素。

    <tr>
        <th>
            @Html.ActionLink("Last Name", "Index", new { sortOrder = ViewBag.NameSortParm})
        </th>
        <th>
             First Name
        </th>
        <th>
            @Html.ActionLink("Last Name", "Index", new { sortOrder = ViewBag.DateSortParm })
        </th>
        <th></th>
    </tr>

这段代码使用ViewBag的属性来设置超链接和查询字符串值。

运行该页面,点击Last Name和Enrollment Date行标题,观察排序的变化。

点击Last Name,学生排序将变为按照Last Name的降序排列。

向学生索引页中添加搜索框

要在索引页中增加搜索功能,你需要向视图中添加一个文本框及一个提交按钮并在Index方法中做相应的修改。文本框允许你输入要在名字和姓氏中检索的字符串。

向索引方法中添加筛选功能

在学生控制器中,使用下面的代码替换Index方法(高亮部分是我们所做出的修改):

        public ActionResult Index(string sortOrder, string searchString)
        {
            ViewBag.NameSortParm = string.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
            ViewBag.DateSortParm = sortOrder == "Date" ? "date_desc" : "Date";
            var students = from s in db.Students
                           select s;
            if (!string.IsNullOrEmpty(searchString))
            {
                students = students.Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())
                    || s.FirstMidName.ToUpper().Contains(searchString.ToUpper()));
            }
            switch (sortOrder)
            {
                case "name_desc":
                    students = students.OrderByDescending(s => s.LastName);
                    break;
                case "Date":
                    students = students.OrderBy(s => s.EnrollmentDate);
                    break;
                case "date_desc":
                    students = students.OrderByDescending(s => s.EnrollmentDate);
                    break;
                default:
                    students = students.OrderBy(s => s.LastName);
                    break;
            }
            return View(students.ToList());
        }

您已经将searchString参数添加到Index方法,您也已经添加用于在姓名中搜索指定字符串的Linq语句。搜索字符串是从文本框中接受的,稍后您将在视图中添加它。只有在搜索字符串有值时,搜索部分的语句才会执行。

注意:在大部分情况下,你在实体框架实体集合上或作为扩展方法去执行同一个的方法结果都是相同的,但某些情况下可能不同。

例如,.Net框架中的Contains方法实现为当你传递一个空字符串作为参数时,将返回所有行。但实体框架的SQL Server Compact 4.0提供程序将不返回任何行。因此示例中的代码(将Where语句放入一个if语句中)可以确保您在所有版本的SQL Server都能得到相同的结果。此外,Contains方法在.Net框架下默认是执行区分大小写的比较,而实体框架的SQL Server提供程序在默认情况下执行不区分大小写的比较。因此我们调用ToUpper方法使这里明确进行不区分大小写的比较。这样,将来如果您使用仓储库时,可以不需要进行多余的代码修正。那时将返回IEnumerable集合而不是IQueryable对象。(当你在IEnumerable集合上调用Contains方法,你将得到.Net框架版的Contains实现;当你在IQueryable对象上调用时,你将得到对应的数据库提供程序的方法实现。)

当你使用不同的数据库提供程序或使用IQueryable对象和IEnumable集合比较时,空值处理也可能不同。例如,在某些情况下一个where条件比如table.Column !=0可能不会返回包含NULL值的行。更多的信息,请参阅 Incorrect handling of null variables in 'where' clause 。

向学生索引视图中添加一个搜索框

在Views\Student\Index.cshtml中,在table元素之前添加下面高亮的代码以创建一个标题、一个文本框及一个搜索按钮。

<p>
    @Html.ActionLink("Create New", "Create")
</p>
@using (Html.BeginForm())
{
    <p>
        Find By Name: @Html.TextBox("SearchString")
        <input type="submit" value="Search" />
    </p>
}
<table class="table">
    <tr>
        <th>

运行索引页面,输入搜索字符串并提交,检查搜索功能是否正常工作。

注意该URL中并不包含搜索字符串,这意味着如果您将查询结果页面加入书签,通过使用书签打开该页面将无法得到筛选后的列表结果。稍后在本教程中我们将更改搜索按钮改用搜索字符串来过滤结果。

向学生索引页面添加分页

要向索引页面添加分页,你需要安装PagedList.Mvc NuGet包,然后你在索引方法及视图中进行相应的修改。PagedList.Mvc是一个很好的分页排序包,在此我们仅使用它来进行演示,下图显示附加了分页连接的索引页面。

安装PagedList.Mvc NuGet包

PagedList包作为PagedList.Mvc包的依赖项会自动安装到项目中。PagedList对IQueryable和IEnumable集合添加了PagedList集合类型及相应的扩展方法。这些扩展方法让你在使用这些集合时能方便地处理分页功能。

在程序包管理器控制台中输入以下命令来安装PagedList.Mvc包

Install-Package PagedList.Mvc 

将分页功能添加到索引方法中

在学生控制器中,添加PagedList命名空间:

using PagedList;

使用以下代码替换Index方法:

        public ActionResult Index(string sortOrder, string currentFilter, string searchString, int? page)
        {
            ViewBag.CurrentSort = sortOrder;
            ViewBag.NameSortParm = string.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
            ViewBag.DateSortParm = sortOrder == "Date" ? "date_desc" : "Date";
            if (searchString != null)
            {
                page = 1;
            }
            else
            {
                searchString = currentFilter;
            }
            ViewBag.CurrentFilter = searchString;

            var students = from s in db.Students
                           select s;
            if (!string.IsNullOrEmpty(searchString))
            {
                students = students.Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())
                    || s.FirstMidName.ToUpper().Contains(searchString.ToUpper()));
            }
            switch (sortOrder)
            {
                case "name_desc":
                    students = students.OrderByDescending(s => s.LastName);
                    break;
                case "Date":
                    students = students.OrderBy(s => s.EnrollmentDate);
                    break;
                case "date_desc":
                    students = students.OrderByDescending(s => s.EnrollmentDate);
                    break;
                default:
                    students = students.OrderBy(s => s.LastName);
                    break;
            }
            int pageSize = 3;
            int pageNumber = (page ?? 1);
            return View(students.ToPagedList(pageNumber, pageSize));
        }

这段代码添加了一个page参数,一个当前排序顺序参数,添加了一个当前筛选参数到方法签名中:

public ActionResult Index(string sortOrder, string currentFiler, string searchString, int ? page)

页面第一次显示时,或者用户还没有点击分页或排序链接,所有参数都为null。如果点击分页链接,page变量将包含要显示的页面编号。

一个ViewBag属性被提供给视图用于指示当前的排序顺序,因为在点击了分页链接后必须要保持当前的排序顺序才能正确的对结果进行分页。

            ViewBag.CurrentSort = sortOrder;

另一个属性ViewBag.CurrentFiler被提供给视图用于指示当前的搜索字符串,分页连接同样必须包含此值以保持针对搜索结果进行分页。同时字符串还必须还原到搜索框中。如果在分页的过程中修改了搜索字符串,页码被重置为1,因为新的搜索字符串可能会导致不同的搜索结果集合。这一改变是在文本框中输入值并提交时。在这种情况下,searchString参数不为空。

            if (searchString != null)
            {
                page = 1;
            }
            else
            {
                searchString = currentFiler;
            }

在方法的结尾,学生IQueryable对象的ToPagedList扩展方法将学生查询转换为一个包含了单页的支持分页的集合类型。该学生集合的单页被传递给视图用于显示:

            int pageSize = ;
            int pageNumber = (page ?? );
            return View(students.ToPagedList(pageNumber, pageSize));

ToPagedList方法需要一个页码,两个问号表示null合成运算符,Null合成运算符定义了可为空类型的缺省值。在本例中,如果page的值不为空,则返回该值,如果page的值为空,则返回1。

向学生索引视图添加分页链接

在Views\Student\Index.cshtml中,使用下面的代码替换原来的,高亮部分显示了我们所做的更改:

@using PagedList.Mvc;
@model PagedList.IPagedList<ContosoUniversity.Models.Student>
<link href="~/Content/PagedList.css" type="text/css" rel="stylesheet" />

@{
    ViewBag.Title = "Students";
}

<h2>Students</h2>

<p>
    @Html.ActionLink("Create New", "Create")
</p>
@using (Html.BeginForm("Index","Student", FormMethod.Get))
{
    <p>
        Find By Name: @Html.TextBox("SearchString",ViewBag.CurrentFilter as string)
        <input type="submit" value="Search" />
    </p>
}
<table class="table">
    <tr>
        <th>
            @Html.ActionLink("Last Name", "Index", new { sortOrder = ViewBag.NameSortParm, currentFilter = ViewBag.CurrentFilter })
        </th>
        <th>
            First Name
        </th>
        <th>
            @Html.ActionLink("Enrollment Date", "Index", new { sortOrder = ViewBag.DateSortParm, currentFilter = ViewBag.CurrentFilter })
        </th>
        <th></th>
    </tr>

    @foreach (var item in Model)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.LastName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.FirstMidName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.EnrollmentDate)
            </td>
            <td>
                @Html.ActionLink("Edit", "Edit", new { id = item.ID }) |
                @Html.ActionLink("Details", "Details", new { id = item.ID }) |
                @Html.ActionLink("Delete", "Delete", new { id = item.ID })
            </td>
        </tr>
    }

</table>
<br />
Page @(Model.PageCount < Model.PageNumber ? 0 : Model.PageNumber) of @Model.PageCount

@Html.PagedListPager(Model, page => Url.Action("Index", new { page , sortOrder = ViewBag.CurrentSort , currentFilter = ViewBag.CurrentFilter }))

页面顶部的@model语句指示视图现在获取PagedList对象而不是List对象。

PagedList.Mvc的using语句使MVC帮助器可以使用PagedListPager扩展方法来生成分页按钮。

下面的代码使用了指定了FormMethod.Get的BeginForm的重载。

@using (Html.BeginForm("Index","Student", FormMethod.Get))
{
    <p>
        Find By Name: @Html.TextBox("SearchString",ViewBag.CurrentFilter as string)
        <input type="submit" value="Search" />
    </p>
}

默认情况下表单使用POST方式提交数据,这意味着参数使用HTTP消息体传递,而不是URL。当你指定使用HTTP GET时,表单数据通过URL来传递,这样用户就可以创建该URL的书签并重复使用。 W3C guidelines for the use of HTTP GET 推荐你在不会导致结果更新时使用GET方式来进行操作。

文本框使用当前的搜索字符串进行初始化,以便在分页的时候不会丢失搜索字符串。

 Find by name: @Html.TextBox("SearchString", ViewBag.CurrentFilter as string)  

列表器链接使用查询字符串将当前的搜索字符串传递给控制器,以便用户可以对搜索结果进行排序:

@Html.ActionLink("Last Name", "Index", new {
    sortOrder = ViewBag.NameSortParm,
    currentFilter = ViewBag.CurrentFilter
})

显示当前页数和总页数。

Page @(Model.PageCount < Model.PageNumber ? 0 : Model.PageNumber) of @Model.PageCount

如果没有要显示的页,则显示"Page 0 of 0"。(在这种情况下页面数字会大于总页数,因为PageNumber是1,而PageCount是0)

由PagedListPager帮助器显示分页按钮:

@Html.PagedListPager(Model, page = >Url.Action("Index", new {
    page,
    sortOrder = ViewBag.CurrentSort,
    currentFilter = ViewBag.CurrentFilter
}))

PagedListPager帮助器提供了很多选项,您可以自定义Url及样式,更多的信息请参阅 TroyGoode / PagedList 。

运行页面。

点击不同的排序顺序,并跳转到不同的页码,然后输入搜索字符串,并再次分页并验证排序及搜索过滤还可以正常工作。

创建关于我们页面来显示学生的统计信息

为Contoso大学的关于页面添加每日有多少个学生注册,需要用到分组及简单的计算,要做到这些,您需要执行下列操作:

  • 创建一个ViewModel用来传递数据给视图

  • 修改Home控制器中的About方法

  • 修改About视图

创建ViewModel

在项目文件夹中创建一个ViewModels文件夹,在该文件夹中添加一个新类,命名为EnrollmentDataGroup.cs,使用下面的代码替换自动生成的:

 using System;
 using System.ComponentModel.DataAnnotations;
 
 namespace ContosoUniversity.ViewModels
 {
     public class EnrollmentDateGroup
     {
         [DataType(DataType.Date)]
         public DateTime? EnrollmentDate { get; set; }
 
         public int StudentCount { get; set; }
     }
 }

修改Home控制器

在Home控制器中,在文件的顶部添加以下using语句:

using ContosoUniversity.DAL;
using ContosoUniversity.ViewModels;

在类定义后添加数据库上下文的类变量:

    public class HomeController : Controller
    {
        private SchoolContext db = new SchoolContext();

使用下面的代码替换About方法:

        public ActionResult About()
        {
            IQueryable<EnrollmentDateGroup> data = from student in db.Students
                                                   group student by student.EnrollmentDate into dateGroup
                                                   select new EnrollmentDateGroup()
                                                   {
                                                       EnrollmentDate = dateGroup.Key,
                                                       StudentCount = dateGroup.Count()
                                                   };
            return View(data.ToList());
        }

使用LINQ对学生实体按照注册日期进行分组,计算每个分组的实体数量并将结果存储在EnrollmentDateGroup视图模型对象的集合中。

添加Dispose方法:

        protected override void Dispose(bool disposing)
        {
            db.Dispose();
            base.Dispose(disposing);
        }

修改关于视图

将About.cshtml替换为如下代码:

 @model IEnumerable<ContosoUniversity.ViewModels.EnrollmentDateGroup>
 @{
     ViewBag.Title = "Student Body Statistics";
 }
 <h2>Student Body Statistics</h2>
 <table>
     <tr>
         <th>Enrollment Date</th>
         <th>Students</th>
     </tr>
     @foreach (var item in Model)
     {
         <tr>
             <td>@Html.DisplayFor(o => item.EnrollmentDate)</td>
             <td>@item.StudentCount</td>
 </tr>
     }
 </table>

运行程序,点击关于链接,你可以看到学生的统计信息了。

总结

到目前为止,你已经实现了基本的CRUD和排序、筛选、分页及分组功能,下一节中我们将通过扩展数据模型来介绍更高级的主题。

作者信息

Tom Dykstra - Tom Dykstra 是微软Web平台及工具团队的高级程序员,作家。