下载代码示例
今年早些时候,Microsoft 发布了 ASP.NET MVC 版本 3 (asp.net/mvc) 以及一款名为 WebMatrix 的新产品 (asp.net/webmatrix)。 该 WebMatrix 版本中提供了几个工作效率帮助组件,可以简化诸如图表和表格数据呈现等任务。 其中一个帮助组件是 WebGrid,该组件支持通过 AJAX 自定义列的格式、分页、排序和异步更新,使表格呈现变得非常简单。
本文介绍 WebGrid 及其在 ASP.NET MVC 3 中的使用方式,然后讨论如何在 ASP.NET MVC 解决方案中充分利用 WebGrid 的功能。 有关 WebMatrix 的概述以及本文所用 Razor 语法的相关信息,请参见 Clark Sell 在 2011 年 4 月期刊中的文章“WebMatrix 简介”(msdn.microsoft.com/magazine/gg983489)。
本文介绍如何在 ASP.NET MVC 环境中安装 WebGrid 组件,以提高表格数据的呈现效率。 我将从 ASP.NET MVC 角度重点介绍以下与 WebGrid 有关的功能:创建具有完全 IntelliSense 支持的强类型版 WebGrid;利用 WebGrid 支持实现服务器端分页;以及添加可在禁用脚本编写时从容降级的 AJAX 功能。 本文所用示例以一个现成的服务为基础进行构建,该服务通过实体框架提供对 AdventureWorksLT 数据库的访问。 如果您对数据访问代码感兴趣,可在代码下载部分下载这些代码,也可查阅 Julie Lerman 在 2011 年 3 月期刊中的文章“使用实体框架和 ASP.NET MVC 3 实现服务器端分页”(msdn.microsoft.com/magazine/gg650669)。
为了提供一个简单的 WebGrid 示例,我设置了一个 ASP.NET MVC 操作,它执行向视图传递 Ienumerable
- public ActionResult List()
- {
- IEnumerable
model = - _productService.GetProducts();
- return View(model);
- }
List 视图中包含如下 Razor 代码,用于呈现图 1 所示的网格:
- @model IEnumerable
.Domain.Product> - @{
- ViewBag.Title = "Basic Web Grid";
- }
>Basic Web Grid
>- @{
- var grid = new WebGrid(Model, defaultSort:"Name");
- }
- @grid.GetHtml()
(单击进行缩放)
图 1 呈现的基本 Web 网格
该视图中的第一行指定型号(例如我们在视图中访问的 Model 属性的类型)为 IEnumerable
这段代码虽然不多,却提供了丰富的网格功能。 该网格限制了显示的数据量,并包含翻阅数据所需的分页器链接,而且列标题呈现为链接以支持分页。 如果需要自定义该行为,可在 WebGrid 构造函数和 GetHtml 方法中指定一些选项。 通过这些选项可以禁用分页和排序、更改每页显示的行数、更改分页器链接中的文本等等。 图 2 显示了 WebGrid 构造函数参数,图 3 显示了 GetHtml 参数。
图 2 WebGrid 构造函数参数
名称 | 类型 | 备注 |
source | IEnumerable |
要呈现的数据。 |
columnNames | IEnumerable |
筛选呈现的列。 |
defaultSort | string | 指定作为排序依据的默认列。 |
rowsPerPage | int | 控制每页显示的行数(默认值为 10)。 |
canPage | bool | 启用或禁用数据分页。 |
canSort | bool | 启用或禁用数据排序。 |
ajaxUpdateContainerId | string | 网格中包含元素的 ID,用来启用 AJAX 支持。 |
ajaxUpdateCallback | string | 完成 AJAX 更新后调用的客户端函数。 |
fieldNamePrefix | string | 支持多个网格时查询字符串字段使用的前缀。 |
pageFieldName | string | 页码的查询字符串字段名称。 |
selectionFieldName | string | 所选行号的查询字符串字段名称。 |
sortFieldName | string | 排序列的查询字符串字段名称。 |
sortDirectionFieldName | string | 排序方向的查询字符串字段名称。 |
图 3 WebGrid.GetHtml 参数
名称 | 类型 | 备注 |
tableStyle | string | 样式使用的表类。 |
headerStyle | string | 样式使用的标题行类。 |
footerStyle | string | 样式使用的页脚行类。 |
rowStyle | string | 样式使用的行类(仅限奇数行)。 |
alternatingRowStyle | string | 样式使用的行类(仅限偶数行)。 |
selectedRowStyle | string | 所选的样式行类。 |
caption | string | 显示为表标题的字符串。 |
displayHeader | bool | 指示是否应显示标题行。 |
fillEmptyRows | bool | 指示表中是否可以通过添加空行来保证 rowsPerPage 的行数。 |
emptyRowCellValue | string | 空行内填充的值,仅在设置了 fillEmptyRows 时使用。 |
columns | IEnumerable |
用于自定义列呈现的列模型。 |
exclusions | IEnumerable |
自动填充列时要排除的列。 |
mode | WebGridPagerModes | 分页器呈现模式(默认值为 NextPrevious 和 Numeric)。 |
firstText | string | 第一页链接的文本。 |
previousText | string | 上一页链接的文本。 |
nextText | string | 下一页链接的文本。 |
lastText | string | 最后一页链接的文本。 |
numericLinksCount | int | 要显示的数字链接的数量(默认值为 5)。 |
htmlAttributes | object | 包含为元素设置的 HTML 属性。 |
前一段 Razor 代码将呈现每一行的所有属性,但您也可能希望对显示哪些列作出限制。 有多种方法可以实现这一目的。 第一种方法(也是最简单的方法)是将这一组列传递到 WebGrid 构造函数。 例如,以下代码只呈现 Name 和 ListPrice 属性:
- var grid = new WebGrid(Model, columnNames: new[] {"Name", "ListPrice"});
也可在 GetHtml 调用而不是在构造函数中指定这些列。 这种方法虽然要编写稍多的代码,但好处是可以指定更多关于如何呈现列的信息。 在下面的示例中,我指定了 header 属性,以使 ListPrice 列更便于阅读:
- @grid.GetHtml(columns: grid.Columns(
- grid.Column("Name"),
- grid.Column("ListPrice", header:"List Price")
- )
- )
在呈现一组项目时,我们通常希望让用户通过点击一个项目来导航到详细信息视图。 通过 Column 方法的 format 参数可以自定义数据项的呈现。 以下代码演示如何更改名称的呈现方式,以输出指向某个项目详细信息视图的链接。这段代码输出带两位小数的“List Price”(货币值惯用的小数位数),得到的输出如图 4 所示。
- @grid.GetHtml(columns: grid.Columns(
- grid.Column("Name", format: @
@Html.ActionLink((string)item.Name, - "Details", "Product", new {id=item.ProductId}, null)),
- grid.Column("ListPrice", header:"List Price",
- format: @
@item.ListPrice.ToString("0.00") )- )
- )
图 4 采用自定义列的基本网格
虽然我指定格式时发生的情况看似有些神秘,但 format 参数实际就是一个 Func
由于 item 参数属于动态类型,所以在编写代码时无法获得 IntelliSense 支持和编译器检查(请参见 Alexandra Rusina 在 2011 年 2 月期刊中发表的关于动态类型的文章msdn.microsoft.com/magazine/gg598922)。 而且,也不支持用动态参数调用扩展方法。 也就是说,当调用扩展方法时,一定要使用静态类型。正因为如此,我在前面的代码中调用 Html.ActionLink 扩展方法时,item.Name 转换成了 string。 由于 ASP.NET MVC 中对扩展方法的使用较为普遍,动态和扩展方法之间的这种冲突可能会让人疲于应付(在使用 T4MVC 等其他组件时情况甚至更糟:bit.ly/9GMoup)。
虽然动态类型化可能很适合 WebMatrix,但强类型化视图也有其优点。 实现强类型化的一种办法是创建一个派生类型 WebGrid
图 5 创建派生 WebGrid
- public class WebGrid
: WebGrid - {
- public WebGrid(
- IEnumerable
source = null, - ...
- parameter list omitted for brevity)
- : base(
- source.SafeCast<object>(),
- ...
- parameter list omitted for brevity)
- { }
- public WebGridColumn Column(
- string columnName = null,
- string header = null,
- Func
object> format = null, - string style = null,
- bool canSort = true)
- {
- Func
object> wrappedFormat = null; - if (format != null)
- {
- wrappedFormat = o => format((T)o.Value);
- }
- WebGridColumn column = base.Column(
- columnName, header,
- wrappedFormat, style, canSort);
- return column;
- }
- public WebGrid
Bind( - IEnumerable
source, - IEnumerable<string> columnNames = null,
- bool autoSortAndPage = true,
- int rowCount = -1)
- {
- base.Bind(
- source.SafeCast<object>(),
- columnNames,
- autoSortAndPage,
- rowCount);
- return this;
- }
- }
- public static class WebGridExtensions
- {
- public static WebGrid
Grid ( - this HtmlHelper htmlHelper,
- ...
- parameter list omitted for brevity)
- {
- return new WebGrid
( - source,
- ...
- parameter list omitted for brevity);
- }
- }
这样做有什么好处呢? 通过实现这个新的 WebGrid
通过这种 Grid 扩展方法,您能够利用编译器针对范型参数的类型推断功能。 因此,本例中我们只需要编写 Html.Grid(Model),而不必编写新的 WebGrid
如您所见,WebGrid 能让我们毫不费力的获得分页和排序功能。 您还了解到如何通过 rowsPerPage 参数(位于构造函数中,或通过 Html.Grid 帮助程序实现)配置页面大小,使网格自动显示单页数据并呈现页面导航所使用的分页控件。 但是,这种默认行为可能满足不了您的需求。 为了说明这一点,我添加了一行代码,用于在呈现网格后显示数据源中包含的项数,如图 6 所示。
图 6 数据源中的项数
可以看到,我们传递的数据中包含完整的产品列表(本例中为 295 个产品,但检索更多数据的情形想来并不少见)。 随着返回数据量的增加,虽然依旧是呈现单页数据,但服务和数据库所承受的负荷会越来越大。 但是有一种更好的办法:服务器端分页。 采用这种方式,只需要取回需要在当前页面中显示的数据(例如只显示五行数据)。
实现 WebGrid 服务器端分页的第一步是限制从数据源检索的数据量。 为此,需要知道请求的是哪一页数据,以便检索正确的数据页。 WebGrid 在呈现分页链接时,会重复使用页面的 URL,并在页码中附加一个查询字符串参数,例如 http://localhost:27617/Product/DefaultPagingAndSorting?page=3(该查询字符串参数的名称可通过帮助程序参数进行配置,这在支持同一页面中多个网格的分页时非常有用)。 也就是说,您可以在自己的操作方法中采用一个名为 page 的参数,然后使用查询字符串值填充该参数。
如果只是通过修改现有代码向 WebGrid 传递单页数据,则 WebGrid 只会看到单页数据。 由于它不知道还有别的页面,因而不再呈现分页器控件。 幸运的是,WebGrid 还有一种名为 Bind 的方法,可用来指定数据。Bind 不仅能够接受数据,而且有一个表示总行数的参数,从而据此计算页数。 为了使用此方法,需要更新 List 操作以检索更多信息并将其传入视图,如图 7 所示。
图 7 更新 List 操作
- public ActionResult List(int page = 1)
- {
- const int pageSize = 5;
- int totalRecords;
- IEnumerable
products = productService.GetProducts( - out totalRecords, pageSize:pageSize, pageIndex:page-1);
- PagedProductsModel model = new PagedProductsModel
- {
- PageSize= pageSize,
- PageNumber = page,
- Products = products,
- TotalRows = totalRecords
- };
- return View(model);
- }
利用这些附加信息,即可更新视图以使用 WebGrid 的 Bind 方法。 通过调用 Bind 可提供要呈现的数据和总行数,并将 autoSortAndPage 参数设置为 false。 autoSortAndPage 参数告知 WebGrid 不需要应用分页,因为这由 List 方法负责。 对此可用下面代码说明:
>- @{
- var grid = new WebGrid
>(null, rowsPerPage: Model.PageSize, - defaultSort:"Name");
- grid.Bind(Model.Products, rowCount: Model.TotalRows, autoSortAndPage: false);
- }
- @grid.GetHtml(columns: grid.Columns(
- grid.Column("Name", format: @
>@Html.ActionLink(item.Name, - "Details", "Product", new { id = item.ProductId }, null)),
- grid.Column("ListPrice", header: "List Price",
- format: @
>@item.ListPrice.ToString("0.00") )- )
- )
经过如此改造,WebGrid 又恢复了生机,重新呈现分页控件,但分页发生在服务中而不是视图中! 但是,由于关闭了 autoSortAndPage,排序功能遭到破坏。 WebGrid 利用查询字符串参数来传递排序列和方向,但我们已命令它不执行排序。 解决办法是在操作方法中添加 sort 和 sortDir 参数,然后将它们传入服务,让服务执行必要的排序,如图 8 所示。
图 8 在操作方法中添加排序参数
- public ActionResult List(
- int page = 1,
- string sort = "Name",
- string sortDir = "Ascending" )
- {
- const int pageSize = 5;
- int totalRecords;
- IEnumerable
products = - _productService.GetProducts(out totalRecords,
- pageSize: pageSize,
- pageIndex: page - 1,
- sort:sort,
- sortOrder:GetSortDirection(sortDir)
- );
- PagedProductsModel model = new PagedProductsModel
- {
- PageSize = pageSize,
- PageNumber = page,
- Products = products,
- TotalRows = totalRecords
- };
- return View(model);
- }
WebGrid 支持通过 AJAX 异步更新网格内容。 为了利用此功能,应确保包含网格的 div 有一个 id,然后通过 ajaxUpdateContainerId 参数将该 id 传入网格的构造函数。 还需要对 jQuery 的引用,但这已经包括在布局视图中。 指定 ajaxUpdateContainerId 以后,WebGrid 会修改自己的行为,使分页和排序链接能够利用 AJAX 进行更新:
id="grid">- @{
- var grid = new WebGrid
>(null, rowsPerPage: Model.PageSize, - defaultSort: "Name", ajaxUpdateContainerId: "grid");
- grid.Bind(Model.Products, autoSortAndPage: false, rowCount: Model.TotalRows);
- }
- @grid.GetHtml(columns: grid.Columns(
- grid.Column("Name", format: @
>@Html.ActionLink(item.Name, - "Details", "Product", new { id = item.ProductId }, null)),
- grid.Column("ListPrice", header: "List Price",
- format: @
>@item.ListPrice.ToString("0.00") )- )
- )
尽管内置的使用 AJAX 的功能很不错,但如果脚本编写被禁用,生成的输出将不起作用。 其原因在于,在 AJAX 模式下,WebGrid 在呈现定位标记时将 href 设置为“#”,并通过 onclick 处理程序注入 AJAX 行为。
我一直热衷于创建能在禁用脚本编写时从容降级的页面,最后往往发现做到这一点最好的办法是渐进式增强(基本原理是提供一个无需脚本即可正常工作的页面,然后通过脚本对该页面加以丰富)。 为达到此目的,可恢复为非 AJAX 的 WebGrid,然后创建图 9 所示的脚本以重新应用 AJAX 行为:
图 9 重新应用 AJAX 行为
- $(document).ready(function () {
- function updateGrid(e) {
- e.preventDefault();
- var url = $(this).attr('href');
- var grid = $(this).parents('.ajaxGrid');
- var id = grid.attr('id');
- grid.load(url + ' #' + id);
- };
- $('.ajaxGrid table thead tr a').live('click', updateGrid);
- $('.ajaxGrid table tfoot tr a').live('click', updateGrid);
- });
为使脚本只应用到一个 WebGrid 中,它利用 jQuery 选择器标识出设置了 ajaxGrid 类的元素。 脚本通过 jQuery live 方法 (api.jquery.com/live) 建立排序和分页链接的 click 处理程序(通过网格容器内的表标题和页脚进行标识)。 这将为符合选择器要求的现有和未来元素设置事件处理程序,由于脚本将取代内容,因此这样做非常方便。
updateGrid 方法被设置为事件处理程序,它首先要做的是调用 preventDefault 以抑制默认行为。 在此之后,该方法获取要使用的 URL(通过定位标记的 href 属性获取),然后通过调用 AJAX 将更新的内容加载到容器元素之中。 为了采用这种做法,一定要禁用默认的 WebGrid AJAX 行为,将 ajaxGrid 类添加到容器 div,然后加入图 9 所示的脚本。
还有一点需要指出,就是脚本使用 jQuery load 方法中的功能从返回的文档中分离出一个片段。 只需调用 load(‘http://example.com/someurl’) 就能加载 URL 的内容。 但是,load(‘http://example.com/someurl #someId’) 将从指定 URL 加载内容,然后返回 id 为“someId”的片段。这反映了 WebGrid 的默认 AJAX 行为,意味着不必通过更新服务器代码添加部分呈现行为。WebGrid 首先加载整个页面,然后从中剥离出新的网格。
尽管这样在快速获得 AJAX 功能方面非常有效,但也意味着需要通过网络发送不必要的数据,而且可能在服务器中也要查询不必要的数据。 幸运的是,ASP.NET MVC 能够轻松解决这个问题。 基本做法是将要在 AJAX 及非 AJAX 请求中共享的呈现内容提取到一个部分视图中。 随后,控制器中的 List 操作既可以为 AJAX 调用仅呈现部分视图,也可以为非 AJAX 调用呈现完整视图(该完整视图又使用该部分视图)。
这种做法非常简单,只需在操作方法内部测试 Request.IsAjaxRequest 扩展方法的结果即可。 当 AJAX 与非 AJAX 代码途径之间的差别非常小时,这种方法十分适用。 然而,两者之间的差别往往比较大(例如,完全呈现需要的数据比部分呈现多)。 在这种情况下,可能需要编写一个 AjaxAttribute,以便单独编写相应的方法,然后让 MVC 框架根据请求是否为 AJAX 请求来选择合适的方法(与 HttpGet 和 HttpPost 属性的工作方式相同)。 关于这方面的例子,请参阅我在 bit.ly/eMlIxU 的博客文章。
到目前为止,所有举例都使用了 Razor 视图引擎。 在最简单的情况下,我们不必执行任何修改即可将 WebGrid 用于 WebForms 视图(暂不论视图引擎的语法差别)。 在前面的示例中,我演示了如何使用 format 参数自定义行数据的呈现:
- grid.Column("Name",
- format: @
@Html.ActionLink((string)item.Name, - "Details", "Product", new { id = item.ProductId }, null)),
format 参数实际上是一个 Func,但 Razor 视图引擎对我们隐藏了这一点。 不过,您还是可以传递 Func,例如用 lambda 表达式:
- grid.Column("Name",
- format: item => Html.ActionLink((string)item.Name,
- "Details", "Product", new { id = item.ProductId }, null)),
借助于这种简单的转换,现在我们可以轻松地在 WebForms 视图引擎中使用 WebGrid!
本文介绍了如何通过几项简单的调整,在不牺牲强类型化、IntelliSense 和高效服务器端分页的情况下利用 WebGrid 为我们提供的功能。 WebGrid 有一些非常棒的功能,可帮助我们提高表格数据的呈现效率。 希望本文能为您在 ASP.NET MVC 应用程序中充分利用 WebGrid 提供有益的提示。