過年前的最後一篇部落格,決定留給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_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之靜態檔案處理。
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中也一致
下面看看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 是判斷是否能轉化為目的型別,
Nancy預設的Converters包含了Collection、DateTime、Fallback和Numeric
當然,有了這個介面,我們可以實現更多的擴充,怎麼用著方便怎麼來!
當然不能忘了我們自定義模型繫結用到的介面 IModelBinder
1 public interface IModelBinder : IBinder 2 { 3 bool CanBind(Type modelType); 4 }
IModerBinder這個介面除了自己定義的CanBind方法外,還繼承了IBinder這個介面,所以我們自定義ModelBinder的時候只需要
實現這個介面就可以了。作用就是繫結資料到相應的模型中。
最後就講講“黑名單”的內容!
“黑名單”的實現,還用到了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