天天看點

Nancy之ModelBinding(模型綁定)

原文: Nancy之ModelBinding(模型綁定)

過年前的最後一篇部落格,決定留給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

Nancy之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     }        

下面來看看運作的結果

Nancy之ModelBinding(模型綁定)

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

Nancy之ModelBinding(模型綁定)

 可以看到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             };          

下面看看運作效果

Nancy之ModelBinding(模型綁定)

我們還是在表單輸入這些内容,同時對employee1和employee2添加監視

Nancy之ModelBinding(模型綁定)

清楚的看到,我們自定義的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             };            

運作看看效果

Nancy之ModelBinding(模型綁定)

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

Nancy之ModelBinding(模型綁定)

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

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

ModelBinding在Nancy這個項目下面,裡面的内容如下:

Nancy之ModelBinding(模型綁定)

很明顯,我們應該先看看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可以修改我們綁定的行為

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

Nancy之ModelBinding(模型綁定)

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

繼續閱讀