[翻译]在ASP.NET MVC中绑定数据(包括分页和排序)

本文介绍如何在ASP.NET MVC中构建具备排序和分页功能的数据Grid,通过使用泛型和反射等高级特性实现了对HTML的完全控制,并通过可测试的方法确保了功能的可靠性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

注:此文翻译的比较仓促,个别用语没有统一,也没有时间对译文做复查。

原作者的源代码是基于mvc2写的,我本机环境是mvc1,所以手工将源代码做了转换,提供下载的源代码里有两个目录,其中MVC2Grid目录下是原作者提供的代码,在我本地没有测试(基于MVC2),MVCGrid是我转换过的,在本地编译通过,运行成功。

代码下载:/Files/sansi/Mvc2Grid.rar


摘要:在此帖中我将向您展示如何在ASP.NET MVC中建立带排序和分页功能的Data Grid,你可以对生成的 HTML有足够的控制权,更重要的是这所有的操作都通过可测试的方法。另外当用户想分享某一页的排序数据时可以使用书签,只要在浏览器的地址栏里拷贝粘贴URL。我们避免使用JavaScript实现这个,这样经常禁用JavaScript的用户不会错过任何东西。

任何网站应用程序的首先的需求之一是在表格里展示数据。在web form时代这是个简单的任务,直接在.aspx页面中放置GridView控件,配置一下数据源就可以了。但是这种方法你丢掉了最重要的东西。对于生成的HTML代码没有足够的控件权,而且没有书签功能因为分页和排序信息要被存储在viewstate,这也是在开发web form程序的另一个问题。在此帖中,我将展示如何建立拥有GridView所有功能(像排序和分页)的Data Grid Helper方法,同时你还有对生成的HTML足够的控制权,并且你所做的这所有的一切都是可以测试的方法,在想分享某一页数据的时候使用书签功能。

为了在MVC.NET中创建任何HTML 都可以使用的辅助方法,你需要写一个HtmlHelpler类的扩展方法。因为我不想导入任何命名空间和web.config文件,我使用static类在System.Web.Mvc.Html命名空间里创建一个扩展方法。

 

首先我将展示一个有排序功能的Grid Data Helper,然后我将创建一个Pager Helper为这个Grid增加分页功能。我使用的Grid Data Helper代码是Stephen Walther在他的ASP.NET MVC framework Unleashed书中提到的,分页部分我将使用Steven Sanderson在他的Pro ASP.NET MVC framework书提到的代码,该代码为分页链接生成HTML标记。我将扩展这段代码创建动态分页,并且使用Dynamic Linq示例代码写简单的排序逻辑,避免使用太多的switch case代码块。

让我们先看一下代码,然后研究它是如何工作的:

ExpandedBlockStart.gif Grid Data Helper
using  System;
using  System.Collections.Generic;
using  System.Linq;
using  System.Web;
using  System.Web.UI;
using  System.IO;
 
namespace  System.Web.Mvc.Html
{
    
public   static   class  DataGridHelper
    {
        
public   static   string  DataGrid < T > ( this  HtmlHelper helper)
        {
            
return  DataGrid < T > (helper,  null null );
        }
 
        
public   static   string  DataGrid < T > ( this  HtmlHelper helper,  object  data)
        {
            
return  DataGrid < T > (helper, data,  null );
        }
 
        
public   static   string  DataGrid < T > ( this  HtmlHelper helper,  object  data,  string [] columns)
        {
            
// Get items
            var items  =  (IEnumerable < T > )data;
            
if  (items  ==   null )
                items 
=  (IEnumerable < T > )helper.ViewData.Model;
 
            
// Get column names
             if  (columns  ==   null )
                columns 
=   typeof (T).GetProperties().Select(p  =>  p.Name).ToArray();
 
            
// Create HtmlTextWriter
            var writer  =   new  HtmlTextWriter( new  StringWriter());
 
            
// Open table tag
            writer.RenderBeginTag(HtmlTextWriterTag.Table);
 
            
// Render Table Header
            writer.RenderBeginTag(HtmlTextWriterTag.Thead);
            RenderHeader(helper, writer, columns);
            writer.RenderEndTag();
 
            
//  Render table body
            writer.RenderBeginTag(HtmlTextWriterTag.Tbody);
            
foreach  (var item  in  items)
                RenderRow
< T > (helper, writer, columns, item);
            writer.RenderEndTag();
 
            
// Close  table tag
            writer.RenderEndTag();
 
            
// return the string
             return  writer.InnerWriter.ToString();
        }
 
        
private   static   void  RenderHeader(HtmlHelper helper, HtmlTextWriter writer,  string [] columns)
        {
            writer.RenderBeginTag(HtmlTextWriterTag.Tr);
            
foreach  (var columnName  in  columns)
            {
                writer.RenderBeginTag(HtmlTextWriterTag.Th);
                var currentAction 
=  ( string )helper.ViewContext.RouteData.Values[ " action " ];
                var link 
=  helper.ActionLink(columnName, currentAction,  new  { sort  =  columnName });
                writer.Write(link);
                writer.RenderEndTag();
            }
            writer.RenderEndTag();
        }
 
        
public   static   void  RenderRow < T > (HtmlHelper helper, HtmlTextWriter write,  string [] columns, T item)
        {
            write.RenderBeginTag(HtmlTextWriterTag.Tr);
            
foreach  (var columnName  in  columns)
            {
                write.RenderBeginTag(HtmlTextWriterTag.Td);
                var value 
=   typeof (T).GetProperty(columnName).GetValue(item,  null ??  String.Empty;
                write.Write(helper.Encode(value.ToString()));
                write.RenderEndTag();
            }
            write.RenderEndTag();
        }
 
    }
}

 

这个helpers的核心代码仅仅创建一个表格。正如你看到的这个helpers是泛型方法,你可以在所有的领域模型类中使用。你可以通过参数传递模型数据和列名,或者你仅仅通过ViewModel传递让helper为你做这个事。该第一个有趣的部分是我们使用了反射查找我们模型的列名。

columns  =   typeof (T).GetProperties().Select(p  =>  p.Name).ToArray();

 

在这行代码中,我们获取所有的传递给helper模型的所有属性名称,把它放到列数据组,然后我们使用这个数据创建我们Grid表头的HTML代码,并且我们将使用它生成的必须的用来调用action方法的链接,将列名做为排序表达式传递给它。

然后我们开始创建标记,正如你所看到的我们使用HtmlTextWriter类型实现这个任务更加简洁,可读,更少错误验证,当然也更加可测。但你也可以使用StringBuiler类或者其它合作你觉得舒服的方法。

你所看到更加有兴趣的代码是RenderHeader和RenderRow方法,让我们来看一下:
RenderHeader:在这个方法中,我们打算让表头列做为链接来调用排序方法。为了创建这些链接我们需要通过下面这行代码猎取当前的动作。

var currentAction  =  ( string )helper.ViewContext.RouteData.Values[“action”];

然后我们使用 Html.ActionLink辅助方法生成实际的链接标记:

 var link  =  helper.ActionLink(columnName, currentAction,  new  { sort  =  columnName });

RenderRow:在这个方法中我使用反射获取方法的属性填充 每个单元格,并且我们使用列数组指定属性的名称:

 var value  =   typeof (T).GetProperty(columnName).GetValue(item,  null ??  String.Empty;

同样注意我们使用Html.Encode方法传递合法的Html标记到视图避免XSS或者其它JS攻击:

 write.Write(helper.Encode(value.ToString()));

这样我们包含排序的GridData辅助方法就完成了,但分页怎么办呢?
我打算创建更有利于复用的分布辅助方法,这样我在Grid Data之外也可以使用。而且基于SoC的原因,我对将分页逻辑从排序逻辑分离出来非常感兴趣。首先来检查一下源代码然后分析它是如何工作的:

ExpandedBlockStart.gif 代码
  namespace  System.Web.Mvc.Html
 {
     
public   static   class  PageLinkHelper
     {
         
public   static   string  PageLink( this  HtmlHelper html,  int  currentPage,  int  totalPages, Func < int string >  pageUrl)
         {
  
             var diff 
=   1 ;
             StringBuilder result 
=   new  StringBuilder();
             var TotalPages 
=  totalPages;
  
             
if  (currentPage  <   1 )
                 currentPage 
=   1 ;
  
             
if  ((currentPage  +  diff)  <  totalPages)
                 totalPages 
=  currentPage  +  diff;
             var startPage 
=   1 ;
             
if  ((currentPage  -  diff)  >  startPage)
                 startPage 
=  currentPage  -  diff;
  
             
if  ((currentPage  -  diff)  >   1 )
             {
                 TagBuilder tag3 
=   new  TagBuilder( " a " );
                 tag3.Attributes.Add(
" href " , pageUrl( 1 ));
                 tag3.InnerHtml 
=   " 1 " ;
                 result.AppendLine(tag3.ToString());
             }
  
             
if  ((currentPage  -  (diff  +   1 ))  >   1 )
             {
                 TagBuilder tag2 
=   new  TagBuilder( " a " );
                 tag2.Attributes.Add(
" href " , pageUrl(currentPage  -  (diff  +   1 )));
                 tag2.InnerHtml 
=   " ... " ;
                 result.AppendLine(tag2.ToString());
             }
  
             
for  ( int  i  =  startPage; i  <=  totalPages; i ++ )
             {
                 TagBuilder tag 
=   new  TagBuilder( " a " );
                 tag.Attributes.Add(
" href " , pageUrl(i));
                 tag.InnerHtml 
=  i.ToString();
                 
if  (i  ==  currentPage)
                     tag.AddCssClass(
" pageSelected " );
                 result.AppendLine(tag.ToString());
             }
  
             
if  ((currentPage  +  (diff  +   1 ))  <  TotalPages)
             {
                 TagBuilder tag2 
=   new  TagBuilder( " a " );
                 tag2.Attributes.Add(
" href " , pageUrl(currentPage  +  (diff  +   1 )));
                 tag2.InnerHtml 
=   " ... " ;
                 result.AppendLine(tag2.ToString());
             }
  
             
if  ((currentPage  +  diff)  <  TotalPages)
             {
                 TagBuilder tag3 
=   new  TagBuilder( " a " );
                 tag3.Attributes.Add(
" href " , pageUrl(TotalPages));
                 tag3.InnerHtml 
=  TotalPages.ToString();
                 result.AppendLine(tag3.ToString());
             }
             
return  result.ToString();
         }
     }
}

这个辅助方法获取当前页和总页数,还有一个方法创建链接。
你将很快看到我们直接在视图中传递这些参数。我只是想展示这个分页助手的核心代码:

ExpandedBlockStart.gif 代码
for  ( int  i  =  startPage; i  <=  totalPages; i ++ )
{
TagBuilder tag 
=   new  TagBuilder( " a " );
tag.Attributes.Add(
" href " , pageUrl(i));
tag.InnerHtml 
=  i.ToString();
if  (i  ==  currentPage)
tag.AddCssClass(
" pageSelected " );
result.AppendLine(tag.ToString());
}

在这里我们创建每一页的标记,为当前页加上CSS类,剩余的代码只是在创建"..."链接,检查当前页与首页或末页的区别。我创建只是显示首页,末页和当前页的逻辑,还有上一页和下一页的分页链接和获取更多分页链接的"..."。很明显,你可以改变它实现你的需求,我期待在能在评论中看到更多更有效更好的实现这些逻辑的方法。

到此为止我们所做的所有事情只是生成在视图上使用的基本的HTML标记!我们需要在控制器中创建核心的逻辑。但是首先我想解释一些重要的观点。首先,在这个示例中我将使用默认的路由配置,并且借助于Repository和视图模型模式。我使用的数据库是Northwind示例数据库,我使用EF和ORM,并且使用简单的DI模式从repository中分离控制器,使得它仅仅在依赖控制的结构。

 

好,让我们来检查一下控制器的源代码后看看它是如何工作的:

ExpandedBlockStart.gif 代码
namespace  Mvc2Application1.Controllers
{
  
public   class  ProductController : Controller
  {
      
private  IProductsRepository _repository;
      
private   int  _pageSize;
      
public  ProductController(): this ( new  ProductsRepositoryEF())
      {

      }

      
public  ProductController(IProductsRepository repository)
      {
          _repository 
=  repository;
          _pageSize 
=   10 ;
      }

      
public  ActionResult List( string  sort,  int ?  page)
      {
          var currentPage 
=  page  ??   1 ;
          ViewData[
" SortItem " =  sort;
          sort 
=  sort  ??   " Name " ;
          ViewData[
" CurrentPage " =  currentPage;
          ViewData[
" TotalPages " =  ( int )Math.Ceiling(( float )_repository.Products.Count()  /  _pageSize);

          var products 
=  from p  in  _repository.Products
                         select 
new  ProductViewModel()
                         {
                             Name 
=  p.ProductName,
                             Price 
=  p.UnitPrice  ??   0 ,
                             Category 
=  p.Category.CategoryName
                         };
          var sortedProducts 
=  products
              .OrderBy(sort)
              .Skip((currentPage 
-   1 *  _pageSize)
              .Take(_pageSize);
          
return  View(sortedProducts);
      } 
   }
}

 

你看到,我们通过参数传递排序和分页信息给动作方法,通过ViewData字典传递SortItem,CurrentPage和TotalPages给视图。我使用实图模型模式传递所需的列给视图,并且让复杂的对象相对简单的类。最有趣部分是取代传递lambda表达式,我传递了属性名做为排序条件。如果你尝试这样会得到错误。为了能够通过LINQ使用这种动态查询,你需要添加包括在Visual Studio中的Dynamic.cs类到你的工程 ,并且为你的控制器类添加System.Linq.Dynamic 命名空间,更多有关使用DynamicLinq的请参考Scott Guthrie’s blog 文章。这样我们的控制器足够简单并能工作,让我们转到视图代码完成我们的杰作吧。

ExpandedBlockStart.gif 代码
<% @ Page Title = ""  Language = " C# "  MasterPageFile = " ~/Views/Shared/Site.Master "  Inherits = " System.Web.Mvc.ViewPage<IEnumerable<Mvc2Application1.Models.ViewModels.ProductViewModel>> "    %>
<% @ Import Namespace = " Mvc2Application1.Models.ViewModels "   %>
< asp:Content  ID ="Content1"  ContentPlaceHolderID ="TitleContent"  runat ="server" >
    List
</ asp:Content >
 
< asp:Content  ID ="Content2"  ContentPlaceHolderID ="MainContent"  runat ="server" >
 
    
< h2 > List of Product </ h2 >
    
<% = Html.DataGrid < ProductViewModel > ()  %>
    
< div >
        
<% = Html.PageLink(( int )ViewData[ " CurrentPage " ],
            (
int )ViewData[ " TotalPages " ],
            p 
=>  Url.Action( " List " ,
                
new  { page  =  p,
                    sort 
=  ( string )ViewData[ " SortItem " ] })) %>
    
</ div >
</ asp:Content >

首先,请注意我导入了必须的命名空间。你看到视图的代码非常简单,我们通过Html.DataGrid助力展示Data Grid,对于分页部分我们只是调用了刚刚创建的Html.PageLink助手。最有趣的部分是我们传递给这个助手的方法:

p => Url.Action("List",new { page = p,sort = (string)ViewData["SortItem"]})

这种方法让每个分页链接调用同一个动作方法(List),传递分页和排序参数,这样用户能通过传递Url的方式共享内容,因为所有的分页和排序信息都可以在地址览中看到。这就是我们刚刚创建的 Data Grid,现在我们可以完全控制生成的HTML,并且我们可以轻松的测试我们的助手类。正如Rob Connery说的,每一次在你的视图中有一个if就创建一个助手类!这种你的视图更容易测试,并且HTML更友好。


总结:我们刚刚创建了两个有用的辅助方法,我们在我们的所有展示带排序和分页功能的Data Grid的工程中使用。另外,我们使用了动态Linq写简单的排序逻辑,并且实现我Data Grid 辅助类我们利用了C#的一些高级特性如:泛型和反射。我们还使用了TagBuilder和 HtmlTextWriter类写了简洁和可测试的辅助类。我们分页逻辑就完全从我们DataGrid中分离了,这样我们在任何数据列表中使用了。

原文地址:http://mvcsharp.wordpress.com/2010/02/11/building-a-data-grid-in-asp-net-mvc/

 

转载于:https://www.cnblogs.com/sansi/archive/2010/05/26/1744496.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值