首页 > 代码库 > Contoso 大学 - 3 - 排序、过滤及分页

Contoso 大学 - 3 - 排序、过滤及分页

原文地址:http://www.asp.net/mvc/tutorials/getting-started-with-ef-using-mvc/sorting-filtering-and-paging-with-the-entity-framework-in-an-asp-net-mvc-application

在上一个课程中,我们已经学习了如何使用 EF 对 Student 实体进行增、删、改、查处理。这次的课程我们将对学生的 Index 页面加入排序、过滤以及分页的功能。还要创建一个页面完成简单的分组。

下面的截图展示了完成之后的页面,列的标题作为链接支持用户通过点击完成排序,点击标题可以在升序和降序之间进行切换。

3-1  在 Students 的 Index 页面增加列标题链接

为 Index 页面增加排序的功能,我们需要修改 Student 控制器的 Index 方法,还需要为 Student 视图增加代码。

3-1-1  为 Index 方法增加排序功能

打开 Controllers\StudentController.cs,将 Index 方法替换为如下的代码。

public ViewResult 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 作为参数传递给 Action 方法。这个参数可以是  “Name” 或者 “Date”, 可能还有一个空格隔开的 desc 来指定降序。

当第一次请求 Index 的时候,没有参数,学生使用 LastName 的升序顺序显示。这是通过 switchdefault 代码段指定的,当用户点击一个列的标题链接的时候,合适的 sortOrder 值需要通过查询字符串传递进来。

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

ViewBag.NameSortParm = String.IsNullOrEmpty(sortOrder) ? "Name desc" : "";
ViewBag.DateSortParm = sortOrder == "Date" ? "Date desc" : "Date";

这里使用了条件语句,第一个用来指定当 sortOrder 参数为 null 或者空串的时候, ViewBag.NameSortParm 应用被设置为 Name desc,其他情况下,应该被设置为空串。

这里有四种可能,依赖于当前的排序情况:

  • 如果当前的排序规则为 LastName 升序,那么,LastName 链接应该设置为降序,Enrollment Date 链接必须被设置为按日期升序。
  • 如果当前的排序规则为 LastName 降序,那么,LastName 链接应该设置为升序,排序串应该为空串,日期为升序。
  • 如果当前排序的规则为 Date 升序,那么,链接应该为 LastName 升序和日期升序。
  • 如果当前的排序规则为 Date 降序,那么,链接应该为 LastName 升序和日期降序。

方法中使用 LINQ to Entities 来指定排序,在 switch 之前,代码首先创建一个 IQueryable 变量,在 switch 语句中修改这个查询表达式,最后调用 ToList 方法。在创建和修改查询表达式 IQueryable 的时候,并没有将查询发送到数据库中执行,查询直到将 IQueryable 对象驼工调用类似 ToList 方法转换到集合对象的时候才会执行,因此,代码中查询直到最后的 return View 才会被执行。

3-2-2  为 Index 视图增加列标题链接

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

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

这段代码使用 ViewBag 属性来设置超级链接中包含适当的查询字符串。

运行页面,点击列标题,来验证排序是否正常。

3-2  为 Index 页面增加搜索框

为 Index 页面增加过滤功能,需要增加一个文本框和一个提交按钮,然后,对 Index 方法进行一些修改,文本框允许你输入一个搜索字符串,用来在 FirstName 和 LastName 中进行搜索。

3-2-1  为 Index 方法增加过滤功能

打开 Controllers\StudentController.cs 文件,使用下面的代码替换 Index 方法。

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

现在,为 Index 方法增加了一个参数 searchString ,LINQ 语句中也增加了一个 where 子句,用来选择在 FirstName 或者 LastName 中包含过滤字符串的学生。搜索串来自文本框的输入,后面需要你在视图中加入它。增加的 where 条件子句仅仅在提供了搜索串的情况下才会被处理。

if (!String.IsNullOrEmpty(searchString))
{
students = students.Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())
|| s.FirstMidName.ToUpper().Contains(searchString.ToUpper()));
}

注意:在传递一个空串的时候,.NET 实现的 Contains 方法将会返回所有的数据行,但是 EF Provider for SQL Server Compact 4.0 对于空串不返回任何行。因此,代码中增加了一个 if 判断语句,以确保对于所有的 SQL Server 都有一致的处理结果。另外,.NET 实现的 Contains 默认进行区分大小写的字符串比较,因此,通过调用 ToUpper 方法显式转换字符串为大写,

以确保在转换到使用资源库模式的时候不需要修改代码。那个时候将会返回一个 IEnumerable 集合而不是 IQueryable 对象 ( 在调用 IEnumerable 集合上的 Contains 方法的时候,使用 .NET 实现的方法,在调用 IQueryable 对象上的 Contains 方法的时候,使用数据库 Provider 提供的实现 )。

3-2-2  在 Student 视图上加入搜索框

在视图 Views\Student\Index.cshtml 上,table 开始标记之前,增加一个标题,一个文本框,以及一个 Search 按钮。

@using (Html.BeginForm())
{
<p>
Find by name: @Html.TextBox("SearchString")
<input type="submit" value=http://www.mamicode.com/"Search" /></p>
}

运行程序,输入一个搜索串,然后点击 Search 按钮来查看过滤的效果。



3-3  在 Student 的 Index 视图上增加分页

为了支持分页,你需要通过 NuGet 包管理器安装 PagedList ,然后,需要在 Index 方法中增加一些代码,在视图中增加分页的链接,下面的截图展示了分页的链接。

3-3-1  安装 PagedList 包

NuGet 中的 PagedList 包将会增加一种类型:PagedList,当将查询结果传入到 PagedList 中后,它提供的一系列属性和方法使得排序更加简单。

在 Visual Studio 中,确信选中了当前的项目,而不是解决方案。在 Tools 菜单中,选择 Library Package Manager,然后选择 Add Library Package Reference。

在 Add Library Package Reference 对话框中,点击左边的 Online 窗格,然后在搜索框中输入 pagedlist ,在看到 PagedList 包之后,点击 Install。

3-3-2  为 Index 方法增加分页功能

打开 Controllers\StudentController.cs,在代码的前面为 PagedList 命名空间增加 using 语句.

using PagedList;

将 Index 方法替换成如下的代码。

 public ViewResult 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 (Request.HttpMethod == "GET")
{
searchString = currentFilter;
}
else
{
page = 1;
}
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 ViewResult Index(string sortOrder, string currentFilter, string searchString, int? page)

当第一次显式这个页面的时候,或者用户没有点击分页链接的时候,page 参数将会是 null。如果分页链接被点击了,page 参数将会包含需要显示的页码。

ViewBag 中的 CurrentSort 属性用来提供当前的排序顺序,它必须被包含到当前的分页链接中,以便在分页处理过程中保持当前的排序规则。

ViewBag.CurrentSort = sortOrder;

其它的 ViewBag 属性为视图提供当前的过滤串,因为这个过滤串在页面被重新显示的时候,必须重新回到文本框中,另外,这个串也必须包含在分页链接中,以便在分页过程中,保持过滤效果。最后,如果在分页的过程中修改了过滤串,那么页码将会回到第一页,因为新的过滤规则返回了不同的数据,很可能原来的页码在这时候已经不再存在了。

if (Request.HttpMethod == "GET")
{
searchString = currentFilter;
}
else
{
page = 1;
}
ViewBag.CurrentFilter = searchString;

在方法的最后,查询学生的表达式被转换为 PagedList ,而不再是通常的 List,这样传递到视图中的就是支持分页的集合,代码如下:

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

ToPagedList 方法需要一个页码值,两个问号用来为可空的页码提供一个默认值,表达式 ( page ?? 1 ) 意味着如果 page 有值得话返回这个值,如果是 null 的话,返回 1。

3-3-3  为视图增加分页链接

Views\Student\Index.cshtml中,使用下面的代码替换原有代码。

@model PagedList.IPagedList<ContosoUniversity.Models.Student>

@{
ViewBag.Title = "Students";
}

<h2>Students</h2>

<p>
@Html.ActionLink("Create New", "Create")
</p>
@using (Html.BeginForm())
{
<p>
Find by name: @Html.TextBox("SearchString", ViewBag.CurrentFilter as string)
<input type="submit" value=http://www.mamicode.com/"Search" /></p>
}
<table>
<tr>
<th></th>
<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>
</tr>

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

</table>

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

@if (Model.HasPreviousPage)
{
@Html.ActionLink("<<", "Index", new { page = 1, sortOrder = ViewBag.CurrentSort, currentFilter=ViewBag.CurrentFilter })
@Html.Raw(" ");
@Html.ActionLink("< Prev", "Index", new { page = Model.PageNumber - 1, sortOrder = ViewBag.CurrentSort, currentFilter=ViewBag.CurrentFilter })
}
else
{
@:<<
@Html.Raw(" ");
@:< Prev
}

@if (Model.HasNextPage)
{
@Html.ActionLink("Next >", "Index", new { page = Model.PageNumber + 1, sortOrder = ViewBag.CurrentSort, currentFilter=ViewBag.CurrentFilter })
@Html.Raw(" ");
@Html.ActionLink(">>", "Index", new { page = Model.PageCount, sortOrder = ViewBag.CurrentSort, currentFilter=ViewBag.CurrentFilter })
}
else
{
@:Next >
@Html.Raw(" ")
@:>>
}
</div>

视图最前面的 @model 语句指定现在传递到视图的不再是 List 而是 PagedList 。

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

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

列的标题链接使用查询串来传递当前的搜索串,以便传递给控制器当前的搜索和排序。

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

在当前页面的最后,通过一行来显示分页的导航 UI。

Page [current page number] of [total number of pages] << < Prev Next > >>

<< 符号连接到第一页, < Prev 链接到上一页,等等。如果用户当前就在第一页,那么,链接到第一页的链接就会被禁用,类似地,如果用户当前在最后一页,导航到最后一页就会被禁用,每一个分页链接传递页码以及当前的排序串和搜索串到控制器,这使得可以在分页的同时维护排序和过滤规则。

如果没有页可以显示,将会显示 “Page 0 of 0 “,在这种情况下,页面数字就会大于页数,因为 Model.PageNumber 是 1,但是 Model.PageCount 为 0。

运行页面。

在不同的排序规则下,点击分页链接,确认分页在正常工作。然后输入一个过滤串,再次点击分页的链接,确认在排序和过滤的同时,分页可以正常工作。

3-4  创建 About 页面显示学生的统计情况

在 Contoso 大学网站的 About 页面,我们希望能够显示每个注册日有多少学生注册。这需要进行分组,然后在每个组上进行简单地计算,需要完成下列工作:

  • 创建用于传递数据到视图的 ViewModel
  • 修改 Home 控制器中的 About 方法
  • 修改 About 视图

3-4-1  创建 ViewModel

创建 ViewModels 文件夹,在文件夹中,创建 EnrollmentDateGroup.cs 类文件,将代码替换为如下代码:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.ViewModels
{
public class EnrollmentDateGroup
{
[DisplayFormat(DataFormatString = "{0:d}")]
public DateTime? EnrollmentDate { get; set; }

public int StudentCount { get; set; }
}
}

3-4-2  修改 Home 控制器

增加如下的 using 语句。

using ContosoUniversity.DAL;
using ContosoUniversity.Models;
using ContosoUniversity.ViewModels;

增加一个数据库上下文变量。

private SchoolContext db = new SchoolContext();

使用如下的代码替换 About 方法。

public ActionResult About()
{
var data = http://www.mamicode.com/from student in db.Students
group student by student.EnrollmentDate into dateGroup
select new EnrollmentDateGroup()
{
EnrollmentDate = dateGroup.Key,
StudentCount = dateGroup.Count()
};
return View(data);
}

LINQ 语句通过注册日期对学生进行分组,计算每一组中的实体数量,最后将查询结果保存为 EnrollmentDateGroup 对象。

3-4-3  增加 Dispose 方法

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

3-4-4  修改 About 视图

打开 Views\Home\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>
@String.Format("{0:d}", item.EnrollmentDate)
</td>
<td>
@item.StudentCount
</td>
</tr>
}
</table>

运行页面,每个注册日注册学生的数量显示在表格中。

现在,你已经看到了如何创建数据模型,以及实现基本的增、删、改、查处理,排序、过滤、分页和分组功能。下一次,我们将会扩展数据模型开始更加高级的内容。

Contoso 大学 - 3 - 排序、过滤及分页