在 ASP.NET MVC 中充分利用 WebGrid (microsoft 官方示例)
2016-12-19 15:16
597 查看
在 ASP.NET MVC 中充分利用 WebGrid
https://msdn.microsoft.com/zh-cn/magazine/hh288075.aspxStuart Leeks
下载代码示例今年早些时候,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 入门
为了提供一个简单的 WebGrid 示例,我设置了一个 ASP.NET MVC 操作,它执行向视图传递 Ienumerable<Product> 的简单功能。 本文中我大多使用 Razor 视图引擎,但后面我也会讨论如何使用 WebForms 视图引擎。 我的 ProductController 类有如下操作:public ActionResult List() { IEnumerable<Product> model = _productService.GetProducts(); return View(model); }
List 视图中包含如下 Razor 代码,用于呈现图 1 所示的网格:
@model IEnumerable<MsdnMvcWebGrid.Domain.Product> @{ ViewBag.Title = "Basic Web Grid"; } <h2>Basic Web Grid</h2> <div> @{ var grid = new WebGrid(Model, defaultSort:"Name"); } @grid.GetHtml() </div>
![](https://i-msdn.sec.s-msft.com/zh-cn/magazine/hh288075.Leeks_Figure1new_hires(en-us,MSDN.10).jpg)
(单击进行缩放)
图 1 呈现的基本 Web 网格
该视图中的第一行指定型号(例如我们在视图中访问的 Model 属性的类型)为 IEnumerable<Product>。然后,我在 div 元素内通过传入型号数据实例化一个 WebGrid,我将代码放入 @{...} 代码块中是要告诉 Razor 不要试图呈现结果。 我还在构造函数中将 defaultSort 参数设置为“Name”,告知 WebGrid 传给它的数据已按 Name 排序。 最后,我用 @grid.GetHtml() 生成网格的 HTML 并在响应中呈现网格。
这段代码虽然不多,却提供了丰富的网格功能。 该网格限制了显示的数据量,并包含翻阅数据所需的分页器链接,而且列标题呈现为链接以支持分页。 如果需要自定义该行为,可在 WebGrid 构造函数和 GetHtml 方法中指定一些选项。 通过这些选项可以禁用分页和排序、更改每页显示的行数、更改分页器链接中的文本等等。 图 2 显示了 WebGrid 构造函数参数,图 3 显示了 GetHtml 参数。
图 2 WebGrid 构造函数参数
名称 | 类型 | 备注 |
source | IEnumerable<dynamic> | 要呈现的数据。 |
columnNames | IEnumerable<string> | 筛选呈现的列。 |
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 | 排序方向的查询字符串字段名称。 |
名称 | 类型 | 备注 |
tableStyle | string | 样式使用的表类。 |
headerStyle | string | 样式使用的标题行类。 |
footerStyle | string | 样式使用的页脚行类。 |
rowStyle | string | 样式使用的行类(仅限奇数行)。 |
alternatingRowStyle | string | 样式使用的行类(仅限偶数行)。 |
selectedRowStyle | string | 所选的样式行类。 |
caption | string | 显示为表标题的字符串。 |
displayHeader | bool | 指示是否应显示标题行。 |
fillEmptyRows | bool | 指示表中是否可以通过添加空行来保证 rowsPerPage 的行数。 |
emptyRowCellValue | string | 空行内填充的值,仅在设置了 fillEmptyRows 时使用。 |
columns | IEnumerable<WebGridColumn> | 用于自定义列呈现的列模型。 |
exclusions | IEnumerable<string> | 自动填充列时要排除的列。 |
mode | WebGridPagerModes | 分页器呈现模式(默认值为 NextPrevious 和 Numeric)。 |
firstText | string | 第一页链接的文本。 |
previousText | string | 上一页链接的文本。 |
nextText | string | 下一页链接的文本。 |
lastText | string | 最后一页链接的文本。 |
numericLinksCount | int | 要显示的数字链接的数量(默认值为 5)。 |
htmlAttributes | object | 包含为元素设置的 HTML 属性。 |
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: @<text>@Html.ActionLink((string)item.Name, "Details", "Product", new {id=item.ProductId}, null)</text>), grid.Column("ListPrice", header:"List Price", format: @<text>@item.ListPrice.ToString("0.00")</text>) ) )
![](https://i-msdn.sec.s-msft.com/zh-cn/magazine/hh288075.Leeks_Figure4_hires(en-us,MSDN.10).jpg)
图 4 采用自定义列的基本网格
虽然我指定格式时发生的情况看似有些神秘,但 format 参数实际就是一个 Func<dynamic,object>,即一个利用动态参数返回对象的委托函数。 Razor 引擎采用为 format 参数指定的代码段,并将其转变为一个委托。该委托采用一个名为 item 的动态参数,format 代码段中正是使用了这个 item 变量。 有关这些委托的工作方式的更多信息,请参见 Phil Haack 在以下地址发表的博客文章:bit.ly/h0Q0Oz。
由于 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<T>,如图 5 所示。 如您所见,这是个非常轻型的包装!图 5 创建派生 WebGrid
public class WebGrid<T> : WebGrid { public WebGrid( IEnumerable<T> 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<T, object> format = null, string style = null, bool canSort = true) { Func<dynamic, 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<T> Bind( IEnumerable<T> 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<T> Grid<T>( this HtmlHelper htmlHelper, ... parameter list omitted for brevity) { return new WebGrid<T>( source, ... parameter list omitted for brevity); } }
这样做有什么好处呢? 通过实现这个新的 WebGrid<T>,我添加了一个新的 Column 方法,该方法以 Func<T, object> 作为 format 参数,这意味着在调用扩展方法时不必再进行转换。 不仅如此,现在还能够获得 IntelliSense 支持和编译器检查(假定项目文件中已经打开 MvcBuildViews,它默认处于关闭状态)。
通过这种 Grid 扩展方法,您能够利用编译器针对范型参数的类型推断功能。 因此,本例中我们只需要编写 Html.Grid(Model),而不必编写新的 WebGrid<Product>(Model)。 无论采用哪种方式,返回的类型都是 WebGrid<Product>。
添加分页和排序
如您所见,WebGrid 能让我们毫不费力的获得分页和排序功能。 您还了解到如何通过 rowsPerPage 参数(位于构造函数中,或通过 Html.Grid 帮助程序实现)配置页面大小,使网格自动显示单页数据并呈现页面导航所使用的分页控件。 但是,这种默认行为可能满足不了您的需求。 为了说明这一点,我添加了一行代码,用于在呈现网格后显示数据源中包含的项数,如图 6 所示。![](https://i-msdn.sec.s-msft.com/zh-cn/magazine/hh288075.Leeks_Figure6_hires(en-us,MSDN.10).jpg)
图 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<Product> 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 方法负责。 对此可用下面代码说明:
<div> @{ var grid = new WebGrid<Product>(null, rowsPerPage: Model.PageSize, defaultSort:"Name"); grid.Bind(Model.Products, rowCount: Model.TotalRows, autoSortAndPage: false); } @grid.GetHtml(columns: grid.Columns( grid.Column("Name", format: @<text>@Html.ActionLink(item.Name, "Details", "Product", new { id = item.ProductId }, null)</text>), grid.Column("ListPrice", header: "List Price", format: @<text>@item.ListPrice.ToString("0.00")</text>) ) ) </div>
经过如此改造,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<Product> 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); }
AJAX:客户端改动
WebGrid 支持通过 AJAX 异步更新网格内容。 为了利用此功能,应确保包含网格的 div 有一个 id,然后通过 ajaxUpdateContainerId 参数将该 id 传入网格的构造函数。 还需要对 jQuery 的引用,但这已经包括在布局视图中。 指定 ajaxUpdateContainerId 以后,WebGrid 会修改自己的行为,使分页和排序链接能够利用 AJAX 进行更新:<div id="grid"> @{ var grid = new WebGrid<Product>(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: @<text>@Html.ActionLink(item.Name, "Details", "Product", new { id = item.ProductId }, null)</text>), grid.Column("ListPrice", header: "List Price", format: @<text>@item.ListPrice.ToString("0.00")</text>) ) ) </div>
尽管内置的使用 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 所示的脚本。
AJAX:服务器端改动
还有一点需要指出,就是脚本使用 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 的博客文章。
WebGrid 和 WebForms 视图引擎
到目前为止,所有举例都使用了 Razor 视图引擎。 在最简单的情况下,我们不必执行任何修改即可将 WebGrid 用于 WebForms 视图(暂不论视图引擎的语法差别)。 在前面的示例中,我演示了如何使用 format 参数自定义行数据的呈现:grid.Column("Name", format: @<text>@Html.ActionLink((string)item.Name, "Details", "Product", new { id = item.ProductId }, null)</text>),
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 提供有益的提示。Stuart Leeks 是英国高级开发支持团队的应用程序开发经理,他对于键盘快捷方式有着超乎寻常的热爱。他的博客站点在 blogs.msdn.com/b/stuartleeks,他在那里讨论自己感兴趣的技术主题(包括但不限于 ASP.NET MVC、实体框架和 LINQ)。
衷心感谢以下技术专家对本文的审阅:Simon Ince 和 Carl Nolan
相关文章推荐
- 在 ASP.NET MVC 中充分利用 WebGrid
- Microsoft ASP.NET MVC Preview 5 及LINQ TO SQL最新版开发示例演示(提供源码下载)
- 推荐一个基于Microsoft ASP.NET MVC Preview 2 的应用示例
- 在 ASP.NET MVC 中充分利用 WebGrid
- Microsoft ASP.NET MVC Preview 5 及LINQ TO SQL最新版开发示例演示(提供源码下载)
- 推荐一个基于Microsoft ASP.NET MVC Preview 2 的应用示例
- Microsoft ASP.NET MVC Preview 5 及LINQ TO SQL最新版开发示例演示(提供源码下载)
- Microsoft ASP.NET MVC Preview 5 及LINQ TO SQL最新版开发示例演示(提供源码下载)
- ASP.NET MVC 官方教程
- ASP.NET MVC中使用AJAX(XMLHttpRequest、Microsoft AJAX Library......)
- 免费 官方的ASP.NET MVC电子书-Professional ASP.NET MVC 1.0
- MSDN官方的ASP.Net异步页面的经典示例代码
- Microsoft ASP.NET MVC中Membership登陆的实现
- ASP.NET MVC中使用AJAX(XMLHttpRequest、Microsoft AJAX Library......) 推荐
- ASP.NET MVC 示例应用程序开发
- Asp.net MVC 示例项目"Suteki.Shop"分析之---安装篇
- 一个在ASP.NET MVC框架下开发的留言本示例(1)
- Asp.net Mvc Framework 十(测试方法及Filter的示例)
- 一个小Forum Web程序示例,ASP.NET MVC Framework,总体结构介绍(Part 1)
- MSDN官方的ASP.Net异步页面的经典示例代码