Nancy之ModelBinding(模型繫結)

Catcher8發表於2016-02-04

過年前的最後一篇部落格,決定留給Nancy中的ModelBinding

還是同樣的,我們與MVC結合起來,方便理解和對照

先來看看MVC中簡單的ModelBinding吧

 1         // POST: Authors/Create
 2         // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
 3         // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
 4         [HttpPost]
 5         [ValidateAntiForgeryToken]
 6         public ActionResult Create([Bind(Include = "AuthorId,AuthorName,AuthorGender,AuthorEmail,AuthorAddress,AuthorPhone")] Author author)
 7         {
 8             if (ModelState.IsValid)
 9             {
10                 db.Authors.Add(author);
11                 db.SaveChanges();
12                 return RedirectToAction("Index");
13             }
14             return View(author);
15         }  

上面的程式碼是我用下面型別的控制器生成的一個新增方法,裡面就用到了ModelBinding

像這樣比較簡單的模型繫結,大家應該是很熟悉了吧!

或許已經爛熟於心了。

MVC中關於Model Binding的詳細解讀可以參見下面的,真的超詳細,我就不再展開了

[ASP.NET MVC 小牛之路]15 - Model Binding

ModelBinder——ASP.NET MVC Model繫結的核心

下面就來看看Nancy中的model binding吧。

先來看個具體的例子,我們順著這個例子來講解這一塊的內容

這個例子我們要用到的引用有Nancy,Nancy.Hosting.Aspnet

我們先來看看它預設的繫結

先建立一個模型Employee

 1     public class Employee
 2     {
 3         public Employee()
 4         {
 5             this.EmployeeNumber = "Num1";
 6             this.EmployeeName = "Catcher8";
 7             this.EmployeeAge = 18;
 8         }
 9         public string EmployeeNumber { get; set; }
10         public string EmployeeName { get; set; }
11         public int EmployeeAge { get; set; }
12         public List<string> EmployeeHobby { get; set; }
13     }  

我們在這個模型中,給部分欄位設定了預設值。

建立一個檢視default.html,用於測試Nancy中預設的ModelBinding

 1 <!DOCTYPE html>
 2 <html>
 3 <head>
 4     <title>default</title>
 5     <meta charset="utf-8" />
 6 </head>
 7 <body>
 8     <form action="/default" method="post">
 9         <label>員工編號</label>
10         <input type="text" name="EmployeeNumber" /> <br />
11         <label>員工姓名</label>
12         <input type="text" name="EmployeeName" /> <br />
13         <label>員工年齡</label>
14         <input type="text" name="EmployeeAge" /> <br />
15         
16         <input type="checkbox" name="EmployeeHobby" value="籃球" />籃球
17         <input type="checkbox" name="EmployeeHobby" value="足球" />足球
18         <input type="checkbox" name="EmployeeHobby" value="排球" />排球
19         <input type="checkbox" name="EmployeeHobby" value="網球" />網球
20         <br />
21         <input type="submit" value="提交" />
22     </form>
23 </body>
24 </html>

然後我們建立一個TestModule.cs,在裡面演示了各種不同方式下的binding

為了減少描述,我在程式碼加了很多註釋
 1     public class TestModule : NancyModule
 2     {
 3         public TestModule()
 4         {
 5             Get["/default"] = _ =>
 6             {               
 7                 return View["default"];
 8             };
 9             Post["/default"] = _ =>
10             {
11                 Employee employee_Empty = new Employee();
12                 //這種寫法有問題,應該是 Employee xxx = this.Bind(); 才對!
13                 //因為這裡的this.Bind() 是 dynamic 型別,沒有直接指明型別
14                 //所以它會提示我們  “找不到此物件的進一步資訊”
15                 var employee_Using_Bind = this.Bind();
16                 
17                 //這裡在bind的時候指明瞭型別。這個會正常繫結資料。(推薦這種寫法)
18                 var employee_Using_BindWithTModel = this.Bind<Employee>();
19                 //這裡是將資料繫結到我們例項化的那個employee_Empty物件
20                 //執行到這裡之後,會發現employee_Empty的預設值被替換了!!
21                 var employee_Using_BindTo = this.BindTo(employee_Empty);
22                 //與上面的寫法等價!
23                 var employee_Using_BindToWithTModel = this.BindTo<Employee>(employee_Empty);
24                 //這個主要是演示“黑名單”的用法,就是繫結資料的時候忽略某幾個東西
25                 //這裡忽略了EmployeeName和EmployeeAge,所以得到的最終還是我們設定的預設值
26                 var employee_Using_BindAndBlacklistStyle1 = this.Bind<Employee>(e=>e.EmployeeName,e=>e.EmployeeAge);
27                 //與上面的寫法等價,演示不同的寫法而已!          
28                 var employee_Using_BindAndBlacklistStyle2 = this.Bind<Employee>("EmployeeName", "EmployeeAge");
29                 return Response.AsRedirect("/default");
30             };                  
31         }
32     }  

下面來看看執行的結果

我們在表單填下了這些內容,現在我們監視上面的各個值的變化

 
 可以看到employee_Using_Bind的繫結是一種錯誤的寫法,會出錯,這個的正確寫法,我在註釋給出了。
 
employee_Empty、employee_Using_BindWithTModel、employee_Using_BindingTo、employee_Using_BindingToWithTModel

這幾個最終都是一樣的效果!!這裡說最終,是因為我們的employee_Empty剛例項化時,應該是我們設定的預設值。

employee_Using_BindAndBlacklistStyle1和employee_Using_BindAndBlacklistStyle2是在Bind後面帶了引數的,

這些引數就是所謂的黑名單,就是繫結的時候忽略掉。然後結果就是我們設定的預設值Catcher8和18。

至於它為什麼這樣就能繫結上,我們看了自定義的繫結之後可能會清晰不少。

接下來就是使用自定義的繫結方法:

模型我們還是用剛才的Employee.cs

此處新新增一個檢視custom.html,基本和前面的default.html一致,換了個action

 1 <!DOCTYPE html>
 2 <html>
 3 <head>
 4     <title>custom</title>
 5     <meta charset="utf-8" />
 6 </head>
 7 <body>
 8     <form action="/custom" method="post">
 9         <label>員工編號</label>
10         <input type="text" name="EmployeeNumber" /> <br />
11         <label>員工姓名</label>
12         <input type="text" name="EmployeeName" /> <br />
13         <label>員工年齡</label>
14         <input type="text" name="EmployeeAge" /> <br />
15         <input type="checkbox" name="EmployeeHobby" value="籃球" />籃球
16         <input type="checkbox" name="EmployeeHobby" value="足球" />足球
17         <input type="checkbox" name="EmployeeHobby" value="排球" />排球
18         <input type="checkbox" name="EmployeeHobby" value="網球" />網球
19         <br />
20         <input type="submit" value="提交" />
21     </form>
22 </body>
23 </html>

 

至關重要的一步!!!編寫我們的ModelBinder,這個ModelBinder要實現IModelBinder這個介面!

 1     public class MyModelBinder : IModelBinder
 2     {
 3         public bool CanBind(Type modelType)
 4         {
 5             return modelType == typeof(Employee);
 6         }
 7         public object Bind(NancyContext context, Type modelType, object instance, BindingConfig configuration, params string[] blackList)
 8         {
 9             var employee = (instance as Employee) ?? new Employee();
10             employee.EmployeeName = context.Request.Form["EmployeeName"] ?? employee.EmployeeName;
11             employee.EmployeeNumber = context.Request.Form["EmployeeNumber"] ?? employee.EmployeeNumber;
12             employee.EmployeeAge = 24;//我們把年齡寫死,方便看見差異 
13             employee.EmployeeHobby = ConvertStringToList(context.Request.Form["EmployeeHobby"]) ?? employee.EmployeeHobby;
14             return employee;
15         }
16         
17         private List<string> ConvertStringToList(string input)
18         {
19             if (string.IsNullOrEmpty(input))
20             {
21                 return null;
22             }
23             var items = input.Split(',');
24             return items.AsEnumerable().ToList<string>();
25         }
26     }  

然後在我們的TestModule.cs中新增如下程式碼

 1             Get["/custom"] = _ =>
 2             {
 3                 return View["custom"];
 4             };
 5             Post["/custom"] = x =>
 6             {
 7                 //此時就會呼叫我們自己定義的Binder了
 8                 var employee1 = this.Bind<Employee>();
 9                 Employee employee2 = this.Bind();              
10                 return Response.AsRedirect("/custom");
11             };    

下面看看執行效果

我們還是在表單輸入這些內容,同時對employee1和employee2新增監視

清楚的看到,我們自定義的binder生效了,年齡就是我們設定的24!

Nancy中,還有比較方便的是json和xml也同樣能繫結。由於這兩個很相似,所以這裡就只介紹json。

同樣的,例子說話!

新增一個json.html檢視

 1 <!DOCTYPE html>
 2 <html>
 3 <head>
 4     <title>default</title>
 5     <meta charset="utf-8" />
 6     
 7     <script src="../../content/jquery-1.10.2.min.js"></script>
 8     <script type="text/javascript">     
 9         $(document).ready(function(){            
10             var dat = "{\"EmployeeName\":\"catcher1234\", \"EmployeeAge\":\"33\"}";
11             $.ajax({
12                 type: "POST",
13                 url: "/json",
14                 contentType: "application/json",
15                 data: dat,
16                 success: function (data) {
17                     alert("Response:\n" + data);
18                 }
19             });
20         });                              
21     </script>
22 </head>
23 <body>   
24 </body>
25 </html>

在這裡,偷懶了(節省點時間),我是直接寫死了兩個值,然後列印出這個employee的相關屬性。

還有一點要注意的是。引用的js檔案,不想寫convention配置就把js放到content資料夾,具體的可參見我前面的bolg Nancy之靜態檔案處理

不然會發現這個錯誤"$ is not defined"
 
然後在TestModule.cs中新增如下程式碼
 1             Get["/json"] = _ =>
 2             {
 3                 return View["json"];
 4             };
 5             Post["/json"] = _ =>
 6             {
 7                 var employee = this.Bind<Employee>();
 8                 var sb = new StringBuilder();
 9                 sb.AppendLine("繫結的employee的值:");
10                 sb.Append("編號: ");
11                 sb.AppendLine(employee.EmployeeNumber);
12                 sb.Append("姓名: ");
13                 sb.AppendLine(employee.EmployeeName);
14                 sb.Append("年齡: ");
15                 sb.AppendLine(employee.EmployeeAge.ToString());                
16                 return sb.ToString();
17             };      

 

執行看看效果

再來看看我們監視的情況!!

很nice,正是我們想要的結果,編號沒有賦值,自動取了預設值!

 

跟往常一樣,簡單分析一下這一塊的原始碼。

ModelBinding在Nancy這個專案下面,裡面的內容如下:

很明顯,我們應該先看看DefaultBinder.cs,因為所有的預設實現,Nancy都會帶Default的字樣

DefaultBinder實現了IBinder這個介面,這個介面裡面就一個東西Bind

 1     /// <summary>
 2     /// Binds incoming request data to a model type
 3     /// </summary>
 4     public interface IBinder
 5     {
 6         /// <summary>
 7         /// Bind to the given model type
 8         /// </summary>
 9         /// <param name="context">Current context</param>
10         /// <param name="modelType">Model type to bind to</param>
11         /// <param name="configuration">The <see cref="BindingConfig"/> that should be applied during binding.</param>
12         /// <param name="blackList">Blacklisted property names</param>
13         /// <param name="instance">Existing instance of the object</param>
14         /// <returns>Bound model</returns>
15         object Bind(NancyContext context, Type modelType, object instance, BindingConfig configuration, params string[] blackList);
16     }  

 

這就是我們ModelBinding的關鍵所在!

DefaultBinder裡面的實現就是

先判斷繫結的型別是不是陣列集合,是的話,一種處理策略,不是的話,另一種處理策略,

在裡面的判斷中還有一個重要的概念是Binding Configuration。因為這個Configuration可以修改我們繫結的行為

這裡我直接截了官網文件的圖來展示

BodyOnly設定為true的時候,一旦主體被繫結,binder就會立刻停止。

IgnoreErrors為false時,就不會在繼續進行繫結,為true時就會繼續繫結,預設值是false。

Overwrite為ture時,允許binder去覆蓋我們設定的那些預設值,為false時,就是不允許,預設值是true!

 

DefaultBinder裡面有個GetDataFields的私有方法

 1         private IDictionary<string, string> GetDataFields(NancyContext context)
 2         {
 3             var dictionaries = new IDictionary<string, string>[]
 4                 {
 5                     ConvertDynamicDictionary(context.Request.Form),
 6                     ConvertDynamicDictionary(context.Request.Query),
 7                     ConvertDynamicDictionary(context.Parameters)
 8                 };
 9             return dictionaries.Merge();
10         }  

從中我們可以看出,它處理繫結的時候用到了字典,包含了表單的資料、url的引數,這點與mvc裡面的基本一致!

所以我們在寫頁面的時候,我們只要把表單元素的name屬性設定為對應的欄位名就可以,同樣的,這個在mvc中也一致

我們在mvc檢視中用的 @Html.EditorFor之類的強型別繫結,生成的頁面都是把name設定為欄位名稱!

下面看看ITypeConverter這個介面

1      public interface ITypeConverter
2     {       
3         bool CanConvertTo(Type destinationType, BindingContext context);
4      
5         object Convert(string input, Type destinationType, BindingContext context);
6     }  

這個介面提供了一種轉換型別的方法

CanConvertTo 是判斷是否能轉化為目的型別,

Convert才是真正的轉化!
 

Nancy預設的Converters包含了Collection、DateTime、Fallback和Numeric

當然,有了這個介面,我們可以實現更多的擴充,怎麼用著方便怎麼來!

 

當然不能忘了我們自定義模型繫結用到的介面 IModelBinder

1     public interface IModelBinder : IBinder
2     {
3         bool CanBind(Type modelType);
4     }  

IModerBinder這個介面除了自己定義的CanBind方法外,還繼承了IBinder這個介面,所以我們自定義ModelBinder的時候只需要

實現這個介面就可以了。作用就是繫結資料到相應的模型中。

我們前面自定義就是用的這個,把資料繫結到了Employee中。

最後就講講“黑名單”的內容!

“黑名單”的實現,還用到了DynamicModelBinderAdapter這個東西,但最為主要的是

DefaultBinder裡面的CreateBindingContext這個私有方法!

 1         private BindingContext CreateBindingContext(NancyContext context, Type modelType, object instance, BindingConfig configuration, IEnumerable<string> blackList, Type genericType)
 2         {
 3             return new BindingContext
 4             {
 5                 Configuration = configuration,
 6                 Context = context,
 7                 DestinationType = modelType,
 8                 Model = CreateModel(modelType, genericType, instance),
 9                 ValidModelBindingMembers = GetBindingMembers(modelType, genericType, blackList).ToList(),
10                 RequestData = this.GetDataFields(context),
11                 GenericType = genericType,
12                 TypeConverters = this.typeConverters.Concat(this.defaults.DefaultTypeConverters),
13             };
14         }

 

從中我們可以看到GetBindingMembers用到了blackList,再看看這個方法

1         private static IEnumerable<BindingMemberInfo> GetBindingMembers(Type modelType, Type genericType, IEnumerable<string> blackList)
2         {
3             var blackListHash = new HashSet<string>(blackList, StringComparer.Ordinal);
4 
5             return BindingMemberInfo.Collect(genericType ?? modelType)
6                 .Where(member => !blackListHash.Contains(member.Name));
7         }

看到這個,行了,基本就懂了!

member => !blackListHash.Contains(member.Name)

這個表示式就是起到了真正的過濾作用啦!!!

 

ModelBinding就講到這裡了。

 

最後獻上本次的程式碼示例:

 https://github.com/hwqdt/Demos/tree/master/src/NancyDemoForModelBinding

相關文章