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
像这样比较简单的模型绑定,大家应该是很熟悉了吧!
或许已经烂熟于心了。
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