ASPNET MVC框架现在日趋流行,最近我也刚完成一个用ASPNET MVC框架做的项目,现在想做一些阶段性的总结。
很多 原来在WinForm下面的流行控件例如GridView,ListView,Repeater等表格呈现控件在ASPNET MVC下已经不能使用了,官方并没有提供现成的控件以供使用,因此我们也许经常会aspx页面中使用例如 <% For Or While语句 %>来呈现我们的表格了。但是一旦这样的页面多了,我们会发现这样还是比较繁琐且费时的。
在老赵的ASPNET MVC课程里面介绍过一个很有名气的类库MvcContrib。里面就有一个比较强大的类Grid来专门呈现这样强类型的数据的,在我的项目中,我也大量的采用了Grid的呈现方式,发现其还是非常好用的,而且扩展起来也是非常的方便的。
其实MvcContrib类库是一个很强大的类库,提供了诸如Ioc(控制反转)、ModelBinder、Filter、各种Input“控件”以及Grid,有兴趣的朋友可以去http://mvccontrib.codeplex.com/去获取更多信息。
从Grid使用说起
我 们首先从Grid(MvcContrib 下的Grid类,再次简称Grid,下同)的使用说起,做过Web开发的人都知道,表格式由行和列组成,而行又通常区分为标题行(Head)、数据行和尾 行三部分。Grid是通过一个名叫ColumnBuilder的类来实现IGridColumn列的累加,而IGridColumn接口则包含了控制单元 格属性的诸多功能。我们从一段简单代码说起
<%= Html.Grid(Model)
.Columns(c =>
{
c.For(x => x.CompanyName).Named("公司名称").Attributes(@class => "w10");
c.For(x => x.City).Named("城市").Attributes(@class => "w8");
c.For(x => x.ContactName).Named("联系名称").Attributes(@class => "w8");
}
)
.Empty("暂无数据").RowStart(c => string.Format("<tr class='{0}'>", c.IsAlternate ? "alt" : ""))
%>
这是一段较为简单的代码,比较容易看出来呈现的是三列,首先我对代码做一个简单说明。
Grid(Model) : HtmlHelper的扩展方法,实例化Grid类,包装数据,返回IGrid<T>接口,IGrid<T>接口继承了IGridWithOptions<T>接口。
Columns(…) : IGridWithOptions<T>接口提供的方法,作用是构建表格的列。
For(…) : ColumnBuilder<T>方法,返回IGridColumn<T>接口,作用是构建列,从而为后续的方法提供链式支持。
Name(…) : IGridColumn<T>方法,指定列名显示内容。
Attributes(…) : IGridColumn<T>方法,指定单元格的属性
Empty(…) : IGridWithOptions<T>接口提供的方法,作用是当列表为空时显示的内容(提示)。它和Columns方法一样同为链式方法(即返回IGridWithOptions<T>自身)。
RowStart(…) : HtmlHelper针对IGridWithOptions<T>的扩展方法,作用是自定义行的开头部分(即Tr)。
源码分析
上面只是一个简单的演示,其实提供的方法远比这多得多,要想更好的使用Grid,我们需对其源码进行分析。
Grid在设计上采取的面向接口编程,大大降低了耦合性,经过HtmlHelper进行扩展的方法主要是针对两个接口进行的,一类是 IGridColumn<T>,另一类是IGridWithOptions<T> 。我们在使用过程中通过VS2008的智能提示留意一下他们各自的返回值,因为返回的也是同一个接口,这样也就很好的提供了链式的方法,下面我久别分别说 说这两类接口。
IGridColumn<T>主要是针对特定列以及列下单元格的控制,接口的方法如下(为了便于理解,我添加了注释):
Action<RenderingContext> CustomHeaderRenderer { get; set; }
Action<RenderingContext, T> CustomItemRenderer { get; set; }
//定义单元格附加属性
IGridColumn<T> Attributes(Func<GridRowViewData<T>, IDictionary<string, object>> attributes);
//定义一个委托,指定单元格内容是否显示
IGridColumn<T> CellCondition(Func<T, bool> func);
//是否对单元格内容进行HTML编码
IGridColumn<T> DoNotEncode();
//对列名称进行分离(老外的名字一般由两部分组成)
IGridColumn<T> DoNotSplit();
//格式化单元格内容,示例:.Format("{0:C}")
IGridColumn<T> Format(string format);
//定义头部(单元格)附加属性
IGridColumn<T> HeaderAttributes(IDictionary<string, object> attributes);
//定义列名
IGridColumn<T> Named(string name);
//指定该列是否显示
IGridColumn<T> Visible(bool isVisible);
后面我添加过注释的方法相对来说比较容易,我就不再赘述,这里我们需要留意的是前两个Custom方法,分别是用来定义列头和单元格的,在HtmlHelper扩展方法中对应的是HeaderAttributes和Attributes方法,值得一提的它这里用到了名曰Hash的一个类,我们来看看其扩展方法签名:
public static IGridColumn<T> HeaderAttributes<T>(this IGridColumn<T> column, params Func<object, object>[] hash)
{
return column.HeaderAttributes(new Hash(hash));
}
public static IGridColumn<T> Attributes<T>(this IGridColumn<T> column, params Func<object, object>[] hash)
{
return column.Attributes(x => new Hash(hash));
}
再来看看Hash 类的写法
public class Hash : Hash<object>
{
public Hash(params Func<object, object>[] hash) : base(hash)
{
}
}
public class Hash<TValue> : Dictionary<string, TValue>
{
public Hash(params Func<object, TValue>[] hash)
: base(hash == null ? 0 : hash.Length, StringComparer.OrdinalIgnoreCase)
{
if (hash != null)
{
foreach (var func in hash)
{
Add(func.Method.GetParameters()[0].Name, func(null));
}
}
}
public static Dictionary<string, TValue> Empty
{
get { return new Dictionary<string, TValue>(0, StringComparer.OrdinalIgnoreCase); }
}
}
这里的Hash类继承了Hash<object> 类,而Hash<object> 类又继承了Dictionary<string, TValue> 类,再看看Hash<object>添加字典项的方法,留意这一句:
Add(func.Method.GetParameters()[0].Name, func(null));
它是遍历Func<object, TValue>数组后用反射的方法获取匿名方法的参数名称作为字典的Key(这里是string型),而Value则是通过执行匿名泛型函数func(null)得到的。所以这就是前面示例中Attributes(@class => "w10")中的写法,它获取方法的参数名称“Class”作为Key,而方法的返回值“w10”即是字典的Value了,是不是设计的很巧妙呢。
有了这个Hash类,我们在写IDictionary<string, object>属性的时候就方便多了,下面是几个示例:
<%-- 指定单元格css为grid_cell,id为gridcell_id,合并2列 --%>
.Attributes(new Hash(@class => "grid_cell", id => "gridcell_id", colspan => "2"))
<%-- 指定Grid表格css为grid,单元格间隙为2,间距为0 --%>
.Attributes(new Hash(@class => "grid").Add<int>("cellpadding", 2).Add<int>("cellspacing", 0))
<%-- 定义表头列的样式为 居中,加粗13像素 Verdana字体,点击后触发排序事件 --%>
.HeaderAttributes(new Hash(Style => "text-align:center;font:bold 13px/20px Verdana;", onclick => "javascript:sort()"))
另一类接口IGridWithOptions<T> 则是对Grid的外围如行属性,表格属性的扩展,同样我也做了注释。
[EditorBrowsable(EditorBrowsableState.Never)]
IGridModel<T> Model { get; }
//定义Grid属性
IGridWithOptions<T> Attributes(IDictionary<string, object> attributes);
//构建ColumnBuilder,对列(IGridColumn<T>)进行累加
IGridWithOptions<T> Columns(Action<ColumnBuilder<T>> columnBuilder);
//空数据时显示的文字内容
IGridWithOptions<T> Empty(string emptyText);
//定义头行属性,这里与html元素对应的应该是<tr>而不是<td>
IGridWithOptions<T> HeaderRowAttributes(IDictionary<string, object> attributes);
void Render();
//定义采取哪一种呈现器(IGridRenderer<T> )进行呈现
IGridWithOptions<T> RenderUsing(IGridRenderer<T> renderer);
//定义行属性,与HeaderRowAttributes的区别在于不是头行
IGridWithOptions<T> RowAttributes(Func<GridRowViewData<T>, IDictionary<string, object>> attributes);