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 方法中也只做了三件事:

  1. 從 HttpRequest.Params[] 找到具體的參數值,由 TryGetValue<T>() 方法中的 bindingContext.ValueProvider.GetValue(key) 完成這個工作(有興趣的童鞋可以使用 resharper 跟進去看看)。
  2. 解析這些值(Create 或是 Deserialize)得到 Descriptor。
  3. 將這些 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 = 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,沒有的話就不加。代碼很簡單,如果有這方面需要的童鞋可以自己嘗試寫寫。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM