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