在 ASP.NET MVC 中充分利用 WebGrid

转自:http://msdn.microsoft.com/zh-cn/magazine/hh288075.aspx


Stuart 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 类有如下操作:


  
  
  1.           public ActionResult List()
  2.   {
  3.     IEnumerable<Product> model =
  4.       _productService.GetProducts();
  5.  
  6.     return View(model);
  7.   }
  8.         

List 视图中包含如下 Razor 代码,用于呈现图 1 所示的网格:


  
  
  1.           @model IEnumerable<MsdnMvcWebGrid.Domain.Product>
  2. @{
  3.   ViewBag.Title = "Basic Web Grid";
  4. }
  5. <h2>Basic Web Grid</h2>
  6. <div>
  7. @{
  8.   var grid = new WebGrid(Model, defaultSort:"Name");
  9. }
  10. @grid.GetHtml()
  11. </div>
  12.         


(单击进行缩放)

图 1 呈现的基本 Web 网格

该视图中的第一行指定型号(例如我们在视图中访问的 Model 属性的类型)为 IEnumerable<Product>。然后,我在 div 元素内通过传入型号数据实例化一个 WebGrid,我将代码放入 @{...} 代码块中是要告诉 Razor 不要试图呈现结果。我还在构造函数中将 defaultSort 参数设置为“Name”,告知 WebGrid 传给它的数据已按 Name 排序。最后,我用 @grid.GetHtml() 生成网格的 HTML 并在响应中呈现网格。

这段代码虽然不多,却提供了丰富的网格功能。该网格限制了显示的数据量,并包含翻阅数据所需的分页器链接,而且列标题呈现为链接以支持分页。如果需要自定义该行为,可在 WebGrid 构造函数和 GetHtml 方法中指定一些选项。通过这些选项可以禁用分页和排序、更改每页显示的行数、更改分页器链接中的文本等等。图 2 显示了 WebGrid 构造函数参数,图 3 显示了 GetHtml 参数。

图 2 WebGrid 构造函数参数

名称类型备注
sourceIEnumerable<dynamic>要呈现的数据。
columnNamesIEnumerable<string>筛选呈现的列。
defaultSortstring指定作为排序依据的默认列。
rowsPerPageint控制每页显示的行数(默认值为 10)。
canPagebool启用或禁用数据分页。
canSortbool启用或禁用数据排序。
ajaxUpdateContainerIdstring网格中包含元素的 ID,用来启用 AJAX 支持。
ajaxUpdateCallbackstring完成 AJAX 更新后调用的客户端函数。
fieldNamePrefixstring支持多个网格时查询字符串字段使用的前缀。
pageFieldNamestring页码的查询字符串字段名称。
selectionFieldNamestring所选行号的查询字符串字段名称。
sortFieldNamestring排序列的查询字符串字段名称。
sortDirectionFieldNamestring排序方向的查询字符串字段名称。

图 3WebGrid.GetHtml 参数

名称类型备注
tableStylestring样式使用的表类。
headerStylestring样式使用的标题行类。
footerStylestring样式使用的页脚行类。
rowStylestring样式使用的行类(仅限奇数行)。
alternatingRowStylestring样式使用的行类(仅限偶数行)。
selectedRowStylestring所选的样式行类。
captionstring显示为表标题的字符串。
displayHeaderbool指示是否应显示标题行。
fillEmptyRowsbool指示表中是否可以通过添加空行来保证 rowsPerPage 的行数。
emptyRowCellValuestring空行内填充的值,仅在设置了 fillEmptyRows 时使用。
columnsIEnumerable<WebGridColumn>用于自定义列呈现的列模型。
exclusionsIEnumerable<string>自动填充列时要排除的列。
modeWebGridPagerModes分页器呈现模式(默认值为 NextPrevious 和 Numeric)。
firstTextstring第一页链接的文本。
previousTextstring上一页链接的文本。
nextTextstring下一页链接的文本。
lastTextstring最后一页链接的文本。
numericLinksCountint要显示的数字链接的数量(默认值为 5)。
htmlAttributesobject包含为元素设置的 HTML 属性。

前一段 Razor 代码将呈现每一行的所有属性,但您也可能希望对显示哪些列作出限制。有多种方法可以实现这一目的。第一种方法(也是最简单的方法)是将这一组列传递到 WebGrid 构造函数。例如,以下代码只呈现 Name 和 ListPrice 属性:


  
  
  1.           var grid = new WebGrid(Model, columnNames: new[] {"Name""ListPrice"});
  2.         

也可在 GetHtml 调用而不是在构造函数中指定这些列。这种方法虽然要编写稍多的代码,但好处是可以指定更多关于如何呈现列的信息。在下面的示例中,我指定了 header 属性,以使 ListPrice 列更便于阅读:


  
  
  1.           @grid.GetHtml(columns: grid.Columns(
  2.  grid.Column("Name"),
  3.  grid.Column("ListPrice", header:"List Price")
  4.  )
  5. )
  6.         

在呈现一组项目时,我们通常希望让用户通过点击一个项目来导航到详细信息视图。通过 Column 方法的 format 参数可以自定义数据项的呈现。以下代码演示如何更改名称的呈现方式,以输出指向某个项目详细信息视图的链接。这段代码输出带两位小数的“List Price”(货币值惯用的小数位数),得到的输出如图 4 所示。


  
  
  1.           @grid.GetHtml(columns: grid.Columns(
  2.  grid.Column("Name", format: @<text>@Html.ActionLink((string)item.Name,
  3.             "Details""Product"new {id=item.ProductId}, null)</text>),
  4.  grid.Column("ListPrice", header:"List Price"
  5.              format: @<text>@item.ListPrice.ToString("0.00")</text>)
  6.  )
  7. )
  8.         

图 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


  
  
  1.           public class WebGrid<T> : WebGrid
  2.   {
  3.     public WebGrid(
  4.       IEnumerable<T> source = null,
  5.       ...
  6.           parameter list omitted for brevity)
  7.     : base(
  8.       source.SafeCast<object>(), 
  9.       ...
  10.           parameter list omitted for brevity)
  11.     { }
  12.   public WebGridColumn Column(
  13.               string columnName = null
  14.               string header = null
  15.               Func<T, object> format = null
  16.               string style = null
  17.               bool canSort = true)
  18.     {
  19.       Func<dynamic, object> wrappedFormat = null;
  20.       if (format != null)
  21.       {
  22.         wrappedFormat = o => format((T)o.Value);
  23.       }
  24.       WebGridColumn column = base.Column(
  25.                     columnName, header, 
  26.                     wrappedFormat, style, canSort);
  27.       return column;
  28.     }
  29.     public WebGrid<T> Bind(
  30.             IEnumerable<T> source, 
  31.             IEnumerable<string> columnNames = null
  32.             bool autoSortAndPage = true
  33.             int rowCount = -1)
  34.     {
  35.       base.Bind(
  36.            source.SafeCast<object>(), 
  37.            columnNames, 
  38.            autoSortAndPage, 
  39.            rowCount);
  40.       return this;
  41.     }
  42.   }
  43.  
  44.   public static class WebGridExtensions
  45.   {
  46.     public static WebGrid<T> Grid<T>(
  47.              this HtmlHelper htmlHelper,
  48.              ...
  49.           parameter list omitted for brevity)
  50.     {
  51.       return new WebGrid<T>(
  52.         source, 
  53.         ...
  54.           parameter list omitted for brevity);
  55.     }
  56.   }
  57.         

这样做有什么好处呢?通过实现这个新的 WebGrid<T>,我添加了一个新的 Column 方法,该方法以 Func<T, object> 作为 format 参数,这意味着在调用扩展方法时不必再进行转换。不仅如此,现在还能够获得 IntelliSense 支持和编译器检查(假定项目文件中已经打开 MvcBuildViews,它默认处于关闭状态)。

通过这种 Grid 扩展方法,您能够利用编译器针对范型参数的类型推断功能。因此,本例中我们只需要编写 Html.Grid(Model),而不必编写新的 WebGrid<Product>(Model)。无论采用哪种方式,返回的类型都是 WebGrid<Product>。

添加分页和排序

如您所见,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 操作


  
  
  1.           public ActionResult List(int page = 1)
  2. {
  3.   const int pageSize = 5;
  4.  
  5.   int totalRecords;
  6.   IEnumerable<Product> products = productService.GetProducts(
  7.     out totalRecords, pageSize:pageSize, pageIndex:page-1);
  8.             
  9.   PagedProductsModel model = new PagedProductsModel
  10.                                  {
  11.                                    PageSize= pageSize,
  12.                                    PageNumber = page,
  13.                                    Products = products,
  14.                                    TotalRows = totalRecords
  15.                                  };
  16.   return View(model);
  17. }
  18.         

利用这些附加信息,即可更新视图以使用 WebGrid 的 Bind 方法。通过调用 Bind 可提供要呈现的数据和总行数,并将 autoSortAndPage 参数设置为 false。autoSortAndPage 参数告知 WebGrid 不需要应用分页,因为这由 List 方法负责。对此可用下面代码说明:


  
  
  1.           <div>
  2. @{
  3.   var grid = new WebGrid<Product>(null, rowsPerPage: Model.PageSize, 
  4.     defaultSort:"Name");
  5.   grid.Bind(Model.Products, rowCount: Model.TotalRows, autoSortAndPage: false);
  6. }
  7. @grid.GetHtml(columns: grid.Columns(
  8.  grid.Column("Name", format: @<text>@Html.ActionLink(item.Name, 
  9.    "Details", "Product", new { id = item.ProductId }, null)</text>),
  10.   grid.Column("ListPrice", header: "List Price", 
  11.     format: @<text>@item.ListPrice.ToString("0.00")</text>)
  12.   )
  13.  )
  14.  
  15. </div>
  16.         

经过如此改造,WebGrid 又恢复了生机,重新呈现分页控件,但分页发生在服务中而不是视图中!但是,由于关闭了 autoSortAndPage,排序功能遭到破坏。WebGrid 利用查询字符串参数来传递排序列和方向,但我们已命令它不执行排序。解决办法是在操作方法中添加 sort 和 sortDir 参数,然后将它们传入服务,让服务执行必要的排序,如图 8 所示。

图 8 在操作方法中添加排序参数


  
  
  1.           public ActionResult List(
  2.            int page = 1
  3.            string sort = "Name"
  4.            string sortDir = "Ascending" )
  5. {
  6.   const int pageSize = 5;
  7.  
  8.   int totalRecords;
  9.   IEnumerable<Product> products =
  10.     _productService.GetProducts(out totalRecords,
  11.                                 pageSize: pageSize,
  12.                                 pageIndex: page - 1,
  13.                                 sort:sort,
  14.                                 sortOrder:GetSortDirection(sortDir)
  15.                                 );
  16.  
  17.   PagedProductsModel model = new PagedProductsModel
  18.   {
  19.     PageSize = pageSize,
  20.     PageNumber = page,
  21.     Products = products,
  22.     TotalRows = totalRecords
  23.   };
  24.   return View(model);
  25. }
  26.         

AJAX:客户端改动

WebGrid 支持通过 AJAX 异步更新网格内容。为了利用此功能,应确保包含网格的 div 有一个 id,然后通过 ajaxUpdateContainerId 参数将该 id 传入网格的构造函数。还需要对 jQuery 的引用,但这已经包括在布局视图中。指定 ajaxUpdateContainerId 以后,WebGrid 会修改自己的行为,使分页和排序链接能够利用 AJAX 进行更新:


  
  
  1.           <div id="grid">
  2.  
  3. @{
  4.   var grid = new WebGrid<Product>(null, rowsPerPage: Model.PageSize, 
  5.   defaultSort: "Name", ajaxUpdateContainerId: "grid");
  6.   grid.Bind(Model.Products, autoSortAndPage: false, rowCount: Model.TotalRows);
  7. }
  8. @grid.GetHtml(columns: grid.Columns(
  9.  grid.Column("Name", format: @<text>@Html.ActionLink(item.Name, 
  10.    "Details", "Product", new { id = item.ProductId }, null)</text>),
  11.  grid.Column("ListPrice", header: "List Price", 
  12.    format: @<text>@item.ListPrice.ToString("0.00")</text>)
  13.  )
  14. )
  15.  
  16. </div>
  17.         

尽管内置的使用 AJAX 的功能很不错,但如果脚本编写被禁用,生成的输出将不起作用。其原因在于,在 AJAX 模式下,WebGrid 在呈现定位标记时将 href 设置为“#”,并通过 onclick 处理程序注入 AJAX 行为。

我一直热衷于创建能在禁用脚本编写时从容降级的页面,最后往往发现做到这一点最好的办法是渐进式增强(基本原理是提供一个无需脚本即可正常工作的页面,然后通过脚本对该页面加以丰富)。为达到此目的,可恢复为非 AJAX 的 WebGrid,然后创建图 9 所示的脚本以重新应用 AJAX 行为:

图 9 重新应用 AJAX 行为


  
  
  1.           $(document).ready(function () {
  2.  
  3.   function updateGrid(e) {
  4.     e.preventDefault();
  5.     var url = $(this).attr('href');
  6.     var grid = $(this).parents('.ajaxGrid'); 
  7.     var id = grid.attr('id');
  8.     grid.load(url + ' #' + id);
  9.   };
  10.   $('.ajaxGrid table thead tr a').live('click', updateGrid);
  11.   $('.ajaxGrid table tfoot tr a').live('click', updateGrid);
  12.  });
  13.         

为使脚本只应用到一个 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 参数自定义行数据的呈现:


  
  
  1.           grid.Column("Name"
  2.   format: @<text>@Html.ActionLink((string)item.Name, 
  3.   "Details""Product"new { id = item.ProductId }, null)</text>),
  4.         

format 参数实际上是一个 Func,但 Razor 视图引擎对我们隐藏了这一点。不过,您还是可以传递 Func,例如用 lambda 表达式:


  
  
  1.           grid.Column("Name"
  2.   format: item => Html.ActionLink((string)item.Name, 
  3.   "Details""Product"new { id = item.ProductId }, null)),
  4.         

借助于这种简单的转换,现在我们可以轻松地在 WebForms 视图引擎中使用 WebGrid!

总结

本文介绍了如何通过几项简单的调整,在不牺牲强类型化、IntelliSense 和高效服务器端分页的情况下利用 WebGrid 为我们提供的功能。WebGrid 有一些非常棒的功能,可帮助我们提高表格数据的呈现效率。希望本文能为您在 ASP.NET MVC 应用程序中充分利用 WebGrid 提供有益的提示。

Stuart Leeks 是英国高级开发支持团队的应用程序开发经理,他对于键盘快捷方式有着超乎寻常的热爱。他的博客站点在blogs.msdn.com/b/stuartleeks,他在那里讨论自己感兴趣的技术主题(包括但不限于 ASP.NET MVC、实体框架和 LINQ)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值