首页 > 代码库 > Kendo UI Grid 模型绑定
Kendo UI Grid 模型绑定
开篇
接触 Asp.net MVC 时间较长的童鞋可能都会了解过模型绑定(Model Binding),而且在一些做 Web 项目的公司或是Team面试中也经常会被问到。项目中有很多 Action 中都使用了自定义的模型绑定,但是业务逻辑太过复杂不适合做为例子与大家分享,而今天在做一个 Kendo UI 的功能时觉得可以用 Kendo UI 做为例子与大家分享与探讨一个典型的 Model Binding 的过程。
写的比较随性,欢迎大家讨论及拍砖!
背景介绍
Kendo UI: 它是一个非常出名的第三方控件公司,说直白点就是一个卖软件的公司,但主要的目标使用群体是开发人员或是软件开发公司(包括 Web 与 桌面软件)。Kendo UI 主要是针对 Web 开发,其 Kendo UI 中的各种控件(Widget)能够提供非常漂亮及友好的用户界面和用户体验,并且提供了大量的接口以方便使用编程的方式控制它的控件以达到特定的需求。
Kendo UI 中的发行包分为:javascript 脚本文件(kendo.all.min.js、kendo.asp.mvc.min.js等)以及 Wrapper (dll 程序集),Wrapper 即为本文重点。
Asp.net Model Binding: 当带有参数的请求(从Form表达提交或是拼接在 Url 中的 QueryString)到达 Asp.net MVC 程序后,会被解析成一个 HttpRequest 类型的对象并且成为 ControllerContext 中的成员变量,如果 debug 时在一个 Action 里打一个断点,然后在即时窗口(Immediate Window)中执行 this.Request.Params.AllKeys 就会得到此次请求的所有参数键名,当然你就可以通过 this.Request.Params["ParameterKey"] 找到任何参数值,所以 Model Binder 所做的工作就是把这个过程进行了封装,简而言之就是由模型绑定器(Model Binder)来完成 Action 参数列表里 ViewModel 的成员匹配,会使代码更加的简洁处理 ViewModel 绑定时更加的优雅。如果对代码有洁癖的人一定会喜欢它的。
这个功能在如下情境下会非常有用:
1. 一个复杂的页面需要将很多参数传递给后台(Action)进行处理,而这些参数可能需要一些计算、验证进而进行整合生成一个更加符合业务需求的 View Model。这样的需求在日常开发中应该会有不少,如果统统写在 Action 里,那代码估计都不忍真视。
2. 更加优雅的编写后台程序。
3. 把面试官侃晕。
将要介绍的 Kendo UI 中的 Model Binding 正好可以实现以上的需求。
Kendo UI Grid Model Binder
Kendo Ui 中的 DataSourceRequestModelBinder 就是今天的主角儿,我们可能通过 Reflector 或是 Resharper 得到反编译后的部分代码,从以下代码足以了解自定义模型绑定的实现原理。
在 DataSourceRequestModelBinder 类中,可以看出一个典型的自定义模型绑定器(Model Binder)需要实现 IModelBinder 接口,其中只有一个方法,当然就是 BindModel。而在 BindModel 方法中也只做了三件事:
- 从 HttpRequest.Params[] 找到具体的参数值,由 TryGetValue<T>() 方法中的 bindingContext.ValueProvider.GetValue(key) 完成这个工作(有兴趣的童鞋可以使用 resharper 跟进去看看)。
- 解析这些值(Create 或是 Deserialize)得到 Descriptor。
- 将这些 Descriptor 整合成一个 DataSourceRequest 对象,并返回。
1 public class DataSourceRequestModelBinder : IModelBinder 2 { 3 public string Prefix { get; set; } 4 5 public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) 6 { 7 DataSourceRequest dataSourceRequest = new DataSourceRequest(); 8 string result1; 9 if (this.TryGetValue< string>(bindingContext, GridUrlParameters.Sort, out result1)) 10 dataSourceRequest.Sorts = GridDescriptorSerializer.Deserialize<SortDescriptor>(result1); 11 int result2; 12 if (this.TryGetValue< int>(bindingContext, GridUrlParameters.Page, out result2)) 13 dataSourceRequest.Page = result2; 14 int result3; 15 if (this.TryGetValue< int>(bindingContext, GridUrlParameters.PageSize, out result3)) 16 dataSourceRequest.PageSize = result3; 17 string result4; 18 if (this.TryGetValue< string>(bindingContext, GridUrlParameters.Filter, out result4)) 19 dataSourceRequest.Filters = FilterDescriptorFactory.Create(result4); 20 string result5; 21 if (this.TryGetValue< string>(bindingContext, GridUrlParameters.Group, out result5)) 22 dataSourceRequest.Groups = GridDescriptorSerializer.Deserialize<GroupDescriptor>(result5); 23 string result6; 24 if (this.TryGetValue< string>(bindingContext, GridUrlParameters.Aggregates, out result6)) 25 dataSourceRequest.Aggregates = GridDescriptorSerializer.Deserialize<AggregateDescriptor>(result6); 26 return (object) dataSourceRequest; 27 } 28 29 private bool TryGetValue<T>(ModelBindingContext bindingContext, string key, out T result) 30 { 31 if (StringExtensions.HasValue( this.Prefix)) 32 key = this.Prefix + "-" + key; 33 ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(key); 34 if (valueProviderResult == null) 35 { 36 result = default (T); 37 return false; 38 } 39 else 40 { 41 result = (T) valueProviderResult.ConvertTo( typeof (T)); 42 return true; 43 } 44 } 45 }
相关的类 GridUrlParameters:
1 public static class GridUrlParameters 2 { 3 public static string Aggregates { get; set; } 4 5 public static string Filter { get; set; } 6 7 public static string Page { get; set; } 8 9 public static string PageSize { get; set; } 10 11 public static string Sort { get; set; } 12 13 public static string Group { get; set; } 14 15 public static string Mode { get; set; } 16 17 static GridUrlParameters() 18 { 19 GridUrlParameters.Sort = "sort"; 20 GridUrlParameters.Group = "group"; 21 GridUrlParameters.Page = "page"; 22 GridUrlParameters.PageSize = "pageSize"; 23 GridUrlParameters.Filter = "filter"; 24 GridUrlParameters.Mode = "mode"; 25 GridUrlParameters.Aggregates = "aggregate"; 26 } 27 28 public static IDictionary< string, string> ToDictionary( string prefix) 29 { 30 IDictionary< string, string> dictionary = ( IDictionary<string , string >) new Dictionary<string , string >(); 31 dictionary[GridUrlParameters.Group] = prefix + GridUrlParameters.Group; 32 dictionary[GridUrlParameters.Sort] = prefix + GridUrlParameters.Sort; 33 dictionary[GridUrlParameters.Page] = prefix + GridUrlParameters.Page; 34 dictionary[GridUrlParameters.PageSize] = prefix + GridUrlParameters.PageSize; 35 dictionary[GridUrlParameters.Filter] = prefix + GridUrlParameters.Filter; 36 dictionary[GridUrlParameters.Mode] = prefix + GridUrlParameters.Mode; 37 return dictionary; 38 } 39 }
Kendo Grid Descriptor 序列化器类:
1 public class GridDescriptorSerializer 2 { 3 private const string ColumnDelimiter = "~"; 4 5 public static string Serialize<T>( IEnumerable<T> descriptors) where T : IDescriptor 6 { 7 if (!Enumerable.Any<T>(descriptors)) 8 return "~"; 9 else 10 return string.Join( "~", Enumerable.ToArray<string >(Enumerable.Select<T, string>(descriptors, (Func <T, string >) (d => d.Serialize())))); 11 } 12 13 public static IList<T> Deserialize<T>( string from) where T : IDescriptor, new() 14 { 15 List<T> list = new List<T>(); 16 if (!StringExtensions.HasValue(from)) 17 return ( IList<T>) list; 18 foreach (string source in from.Split( "~".ToCharArray(), StringSplitOptions.RemoveEmptyEntries)) 19 { 20 T obj = new T(); 21 obj.Deserialize(source); 22 list.Add(obj); 23 } 24 return (IList<T>) list; 25 } 26 }
示例分析
需求场景
在了解了以上的背景知识后,我们来设定一个具体的需求场景:Kendo UI Grid 控件中可以在前端页面中实现 过滤(Filter)、分组(Group)以及 排序(Sort),当然 Kendo Grid 还有很多酷炫的功能,但今天的主角儿就是以上三位。现在用户希望在对 Grid 中的结果集进行了 filter、group 及 sort 过滤操作后,能将这些操作命令保存至数据库中的 UserProfile 表中,以便下次同一用户登录后能还原其过滤后的结果集。
分析
默认情况下后台的 Action 中都会使用 [DataSourceRequest]DataSouceRequest(这两个类都在 Wrapper 中) 显式的使用 Kendo UI 中的 ModelBinding,所以如果我们在初次加载页面时,或是刷新 Grid 时(只有当Grid使用 ajax 方式加载 DataSource 并且启用 Pageable 中的 refresh 功能)后台的 Action 会直接得到一个 DataSourceRequest 对象实例,里面就包裹了 filter、sort 以及 group 相对应的 Descriptor (只是一种封装,不必深究),然后在 Action 中从数据库取到数据后,再对结果集应用这些 Descriptor 就能在后台中实现过滤功能。
1 public ActionResult FilterMenuCustomization_Read([DataSourceRequest] DataSourceRequest request) 2 { 3 return Json(GetEmployees().ToDataSourceResult(request)); 4 }
Kendo Grid 发送的带有 filter、sort 以及 group 的 HttpRequest 都是由 kendo 前端 js 脚本完成的,所以我们不知道它是如何得到这些信息的,以及如何能够让它同时发一份到另一个 ActionB 中然后我们在 ActionB 中保存起来。
查看 js 文件? 不行的,它的文件可读性非常差,是专门处理过的。里面到处都是 a b c .... 无意义的字符做为变量名,所以除非高人在世。
思路
1. 在 js 中获取到 Grid 的过滤条件。
2. 使用 $.ajax() 把这些条件Post 至 Home/StoreGridCommans 中。
3. 使用 Kendo Wrapper 中的 DataSourceRequestAttribute 完成 ModelBinding。
整个实现过程很简单,在这里简单描述下:
1. $("#gridelementid").data("kendoGrid") 拿到 kendoObject,在 kendoObject.dataSource 中即有我们需要的所有过滤条件,如 kendoObject.dataSource._filter,kendoObject.dataSource._sort,kendoObject.dataSource._group。
2. 通过观察 Grid 的一次数据请求过程,即可了解 kendo 前端 js 会将过滤条件做怎样的处理后提交到后台,最终发现以如下形式传递这些过滤条件:
&sort=fieldname1-aes&filter=fieldname2~eq~and~fieldname3~neq&group=fieldname4-aes
3. 将 1 步中得到的过滤条件按第 2 步中的格式进行拼接并做为参数传递给 StoreGridCommands Action 中,可以直接放在 Url 中做为 QueryString,也可以封装进成一个 Json 对象传递。
需要留心的小问题
这里有一点需要注意的是对 DateTime 类型的列的处理会稍有不同。kendo Grid 接收到的都是经过 Json 序列化后的值 形如:“/Date(12345678)/”,这个值在它接收到后会做解析,但解析出的值会带有浏览器的本地时区 即:Mon 20 2014 13:12:00 (中国时间),而且在 kendoObject.dataSource._filter 中拿到值也是如此,而这个值在进行 ModelBinding 时是会出错的,ModelBinder 中对于时间类型的处理会以如下形式解析:
1 private static DateTime ParseDateTime(string value) 2 { 3 var result = DateTime.ParseExact(value, "yyyy-MM-ddTHH-mm-ss", null); 4 5 return result; 6 }
所以我们需要在处理过滤条件时特别留心 DateTime类型的列及其值,在 kendo Grid 加载后在前端可以使用 kendo.format() 来对时间类型的数据值进行格式化。
1 var filterCondition = ""; 2 3 var filterData =http://www.mamicode.com/ kendoObject.dataSource._filter; 4 5 if(filterData != undefined){ 6 7 var filters = kendoObject.dataSource._filter.filters; 8 9 if(filters.length>0) 10 { 11 for(var i=0;i<filters.length;i++){ 12 filterCondition += (filters[i].value.getTime?kendo.format("yyyy-MM-ddTHH-mm-ss",filters[i].value))+filterData.logic; 13 14 } 15 } 16 }
注:filterData.logic 在循环中是需要判断下是否要添加,如果有多个 filter 就添加该 logic,没有的话就不加。代码很简单,如果有这方面需要的童鞋可以自己尝试写写。