【EntityFramework系列教程三,翻译】在ASP.NET MVC程序中使用EntityFramework对数据进行排序、过滤筛选以及实现分页

上一章中您已经为Students实体集实现了不同页面的增删改查的功能,本章中您将为Index页面给Students增加分页、过滤以及排序功能。你将创建一个实现简单分组功能的页面。

以下就是即将你要看到的效果——列标题变得可以点击,我们可以通过点击对特定列进行排序;重复点击同意标题两次将会导致对该列进行“升序”或是“降序”排列。

Students_Index_page_with_paging

【对Students的Index页面增加排序列】

为了增加排序列,你应当在Student控制器的Index方法中做如下变化:

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作为其参数,这个参数的值不是“Name”就是“Date”;后面可跟一个空格加上“desc”表示是升序还是降序排列。

第一次Index页面得到请求,故没有请求参数,所以学生信息全部按照“姓”升序排列;也就是按照默认创建、一路走下去的switch选择语句一样进行。当用户点击了列名,那么在请求的参数就自然就有了对应的值了。

两个“ViewBag”变量被用于配置记忆回传的请求数值:

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

以上代码是三元表达式。第一个先检测sortOrder是否未被赋值——如果是,被赋予空值;否则被赋予“Name Desc”字串。

这样一来排序总共有四种形式,取决于当前如何排序的:

  • 如果当前排序字段是“Last Name”(升序),那么Last Name的超链接在回传之后被制定为降序,同时Enrollment Date被指定为升序。
  • 如果当前的Last Name是降序,那么回传之后被指定为升序;Date就为降序了。
  • 如果Date是升序,那么Name变成升序,Date回传后降序。
  • 如果Date降序,回传后变升序;Last Name也为升序。

此方法使用了LINQ-TO-Entity指定待排序的列——此代码在switch之前创建了一个IQuerable对象,随即在switch对其进行了改变,在switch之后使用ToList方法并且把结果保存到View中。注意:当你创建IQuerable或者改变的时候并不会立即执行此方法——直到你调用了诸如ToList一类把IQuerable转化成对象集合的时候才被执行。因此该代码直到执行到return view的时候才生成单个数据集合。

【为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中的属性,对超链接设置了合适的请求数据值。
运行页面并且点击列名,来验证排序是否正常工作:

Students_Index_page_with_sort_hyperlinks

【对Index页面中“Students”数据增加过滤文本框】

为了在Index页中可以对Students进行过滤,您需要加入一个TextBox(文本框)以及一个Submit(提交)按钮,同时对Index方法做一些必要的改变。文本框允许您输入一个字符串,该字串可以是“姓”,也可以是“名”。

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(条件过滤)以便筛选出符合条件(包含该搜索字符串的“姓”或者是“名”)。要被搜索的字符串是你稍后在文本框中输入的,如果的确需要条件过滤,那么where表达式才会被执行:

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

注意:.NET Framework对于“Contains”的实现对于一个空字符串作为其搜索条件而言,将返回所有记录;不过对于SQL Server Compact将不会返回任何记录。因此实例中的代码(在if中的那个where)保证在任意类型的数据库都会返回相同结果;同样地,默认情况下Contains是区分大小写的,但是在SQL Server Compact则不然,因此采用ToUpper转化测试数据为不区分大小写的方法确保了即便您将代码使用库形式发布(那将返回IEnumerable而不是IQueryable——当您使用IEnumerable,默认采取.NET Framework内置形式;IQueryable则采用针对数据库默认实现的模式),照样可以正常运行。

2)为Index页面中Students数据添加搜索输入框:

在“Views\Student\Index.cshtml”中添加一个标题说明,一个文本框以及一个查询按钮,它们紧挨着table标签:

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

运行页面,输入要搜索的字串,点击“搜索”按钮检验其是否正常工作:
Students_Index_page_with_search_box

【为Index页Students数据提供分页功能】

为实现此功能,你应该使用NuGet工具安装PagedList(分页列表);然后您再对Index页面做一些补充,加入一些分页超链接。以下代码展示了此效果:

Students_index_page_with_paging

1)安装NuGet中的PagedList:

NuGet中的“PagedList”将安装一个PagedList类型的集合。当您把数据集合放入PagedList中,它内置的一些方法和属性将促使你完成分页工作。

在Visual Studio中先选中“项目”(不是“解决方案”!),点击左边的Online(在线)标签,然后在右上方的文本框中输入pagedlist,当搜索结果出来后,点击Install按钮。

PagedList_in_Add_Library_Package_Reference_box

2)在Index方法中增加分页功能:
在“Controllers\StudentController.cs”中加入一个using引入PagedList命名空间:

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”参数是未赋值的;如点击了页面链接,那么“page”变量将保存当前回传的页码。ViewBag属性提供了当前排序的顺序,因为这个顺序在分页时候仍然需要被用到,以便保证在分页时候仍旧保持原来的排序状态。

ViewBag.CurrentSort = sortOrder;

另外一个ViewBag属性提供了当前输入过滤的条件字符串,原因一是因为在页面重新加载之时仍旧需要把此信息显示到文本框上,其二是在分页的时候还要保持过滤的状态;如果在分页过程中检索字符串变更了,那么页码又重新从1开始——因为不同的过滤条件将导致不同的数据被呈现,原先的页码自然就作废了。

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

代码末尾处把Students结果转化成PagedList而不是List,这是因为可以把直接支持分页效果的对象集合传入View(视图)中,以下是代码:

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

“ToPagedList”方法需要一个页码作为其参数。两个问号是对“可空类型(可以是null)”的定义——如果变量是未被赋值的,返回1;否则就是返回变量自身的值。

3)为Index页Students增加分页超链接:
在“”中用下列代码替换已有的代码:

@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="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”表明你已经获得了PagedList而不是List对象。
文本框用当前搜索的字串初始化,这样用户在搜索的情况下输入的搜索字串不会遭致丢失。

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

列标题则通过请求字符串的方式把当前搜索的字串传入到控制器中,所以用户可以在不丢失搜索字串的情况下照样进行排序。

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

页面底部显示了一行:

Page [当前页] of [总页数] << < 前一页 后一页> >>

“<<”表示第一页,“<”是上一页……;如果用户目前所处第一页,那么“前一页”的链接将不可用;反之如果如果用户目前所处最后一页,那么“后一页”的链接将不可用。每个连接将携带者“页码”、“当前排序状态”以及“搜索字串”进入控制器,所以用户既可以分页,同时可以过滤和排序。
如果没有记录显示,“Page 0 of 0”将被显示出来(在此情况下“当前页”大于“总页数”,因为Model.PageNunber是1,而Model.PageCount是0)。运行页面结果如下:

Students_index_page_with_paging

在不同排序状态情况下尝试点击页码链接,以确保分页正常进行;然后输入过滤字串,确保三者都可以正常工作。

【创建一个显示学生静态信息的About页面】

Contoso大学网站的About页面将显示每一个enrollment日期有多少学生选课,这需要分组,并且对每个组进行简单统计。为达到此目的,你需要完成以下工作:

  • 创建一个对象视图模型类,这样你可以把数据传入其中并将它们回传到页面。
  • 在Home的控制器中改变About方法。
  • 更改About视图。

1)创建一个视图模型类:

创建一个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; }
}
}

2)改变Home控制器:

在HomeController.cs文件中增加以下using引入必要命名空间:

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

为数据库上下文对象增加以下类全局变量:

private SchoolContext db = new SchoolContext();

用下列代码替换About方法:

public ActionResult About()
{
var 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);
}

LINQ方法通过Enrollment日期对Students进行分组,统计每个组中的Students实体个数;并将数据存储到EnrollmentDataGroup对象视图模型中。
最后增加一个“销毁”方法:

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

2)改变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>

运行此页面,每个日期有多少学生选课就一目了然了。
现在你明白了如何创建一个数据模型实体,以及如何实现CRUD(增删改查)和分页、排序、过滤等功能,下一章您将深入了解一些关于扩展数据模型实体的高级教程。

关于其它EntityFramework资源您可以本系列最后一篇末尾处找到。

posted @ 2012-04-27 17:14  Serviceboy  阅读(1012)  评论(0编辑  收藏  举报