模型绑定的本质
任何控制器方法的执行都受action invoker组件(下文用invoker代替)控制。对于每个Action方法的参数,这个invoker组件都会获取一个Model Binder Object(模型绑定器对象)。Model Binder的职责包括为Action方法参数寻找一个可能的值(从HTTP请求上下文)。每个参数都可以绑定到不同的Model Binder;但是大部分情况我们都使用的是默认模型绑定器-DefaultModelBinder(如果我们没有显式设置使用自定义的Model Binder的话)。
每个Model Binder都使用它自己的特定算法来为Action方法参数设置值。默认模型绑定器对象大量使用反射机制。具体来说,对于每个Action方法参数,Model Binder都试图根据参数名去请求参数中寻找匹配的值。比如某个Action方法参数名为Text,那么ModelBinder会去请求上下文中寻找拥有相同名字的名值对(Entry)。如果找到,则Model Binder继续将Entry的值转换为Action方法参数类型。如果类型转换成功,转换后的值就被赋给那个Action方法参数,否则会抛出异常。
注意只要遇到第一个不能成功转换类型或者在请求上下文中找不到匹配的参数(而且这个参数的类型为不可空类型),那么就会立刻抛异常。也就是说只有所有声明的参数都被Model Binder成功解析,Action方法才会被调用。并且注意Model Binder产生的异常无法在Action方法中捕获,我们必须在global.asax中设置一个全局错误处理器(global error handler)来捕获处理这些异常。还须注意只有当方法参数不能赋值为null才会抛异常。所以对于下面这种情况:
public ActionResult TestAction(string name, Int32 age) {
// ...
return View();
}
如果请求上下文中不包含名为name的Entry不会有任何问题,name参数值被ModelBinder设为null。但是age参数就不同了,因为Int32类型是基本值类型,不能赋null值。如果我们需要允许不传age参数,那么我们只需要简单地修改代码为如下或者为age参数提供一个默认值:
public ActionResult TestAction(string name, Nullable<Int32> age) {
// ...
return View();
}
默认模型绑定器从HTTP请求上下文中查找参数值的顺序如下:
请求数据来源 | 说明 |
Request.Form | 通过表单提交的参数 |
RouteData.Values | 路由参数 |
Request.QueryString | 查询参数,类似http://abc.com?name=jxq,这里的name=jxq即查询参数 |
Request.Files | 随请求上传的文件 |
当我们有多个要上传的参数时,我们最好不要为每个请求参数都创建一个Action方法参数,以避免方法参数列表过长。
对于默认模型绑定器,我们可以将参数列表封装成一个参数类型,然后默认模型绑定器会按照与上面相同的按名(属性名和请求参数名进行匹配)匹配算法去设置这个参数对象的属性。
手动调用模型绑定
默认情况下模型绑定会自动调用,但是我们也可以手动进行模型绑定。比如下面的代码示例:
[HttpPost]
public ActionResult RegisterMember() {
Person myPerson = new Person();
UpdateModel(myPerson);
return View(myPerson);
}
上面的UpdateModel(myPerson)即手动模型绑定。
手动进行模型绑定的最大好处就是使得模型绑定过程更灵活。比如我们可以限制类型绑定只取表单提交参数,那么我们可以像下面这么做:
[HttpPost]
public ActionResult RegisterMember() {
Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person));
UpdateModel(myPerson, new FormValueProvider(ControllerContext));
return View(myPerson);
}
FormValueProvider实现了IValueProvider接口,其他几种参数对应的IValueProvider实现如下:
请求数据来源 | IValueProvider实现 |
Request.Form | FormValueProvider |
RouteData.Values | RouteDataValueProvider |
Request.QueryString | QueryStringValueProvider |
Request.Files | HttpFileCollectionValueProvider |
除了上面的方法可以限制类型绑定的数据来源外,我们还可以利用直接利用FormCollection作为IValueProvider,如下:
[HttpPost]
public ActionResult RegisterMember(FormCollection formData) {
Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person));
UpdateModel(myPerson, formData);
return View(myPerson);
}
手动进行模型绑定过程可能发生异常,可以用两种方法处理:第一种方法是直接捕获异常;第二种方法是利用TryUpdateModel方法。第一种方法如下:
[HttpPost]
public ActionResult RegisterMember(FormCollection formData) {
Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person));
try
{
UpdateModel(myPerson, formData);
}
catch(InvalidOperationException e)
{
// 处理异常
}
return View(myPerson);
}
第二种方法如下:
[HttpPost]
public ActionResult RegisterMember(FormCollection formData) {
Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person));
if(TryUpdateModel(myPerson, formData))
{
// 正常处理
}
else {
// 处理异常
}
return View(myPerson);
}
第一节已经说过当使用默认模型绑定器时,我们无法在Action方法捕获模型绑定过程抛出的异常,可以在global.ascx中配置错误处理器来捕获处理。除此之外,我们还可以通过ModelState.IsValid来判断默认模型绑定是否成功。
定制模型绑定器系统
我们可以定制IValueProvider实现,IValueProvider接口定义如下:
namespace System.Web.Mvc {
using System;
public interface IValueProvider {
bool ContainsPrefix(string prefix);
ValueProviderResult GetValue(string key);
}
}
我们定制一个IValueProvider实现如下:
using System;
using System.Globalization;
using System.Web.Mvc;
namespace CustomeModelBinderDemo.Controllers.ValueProvider
{
public class MyValueProvider: IValueProvider
{
public bool ContainsPrefix(string prefix)
{
return System.String.Compare("curTime", prefix, StringComparison.OrdinalIgnoreCase) == 0;
}
public ValueProviderResult GetValue(string key)
{
return ContainsPrefix(key) ? new ValueProviderResult(DateTime.Now, null, CultureInfo.CurrentCulture) : null;
}
}
}
上面的GetValue(string key)方法即根据Action方法参数名从HTTP请求上下文获取匹配的值存到ValueProviderResult对象,ValueProviderResult类型包含一个ConvertTo(Type type)方法,用于将它封装的值转换成指定类型,这正好与第一节中讲的类型转换吻合(也说明XValueProvider对象负责模型绑定过程中的类型转换工作,因为模型绑定器会调用XValueProvider对象进行类型转换)。
然后我们定义一个ValueProvider工厂类:
public class CurrentTimeValueProviderFactory : ValueProviderFactory {
public override IValueProvider GetValueProvider(ControllerContext controllerContext) {
return new CurrentTimeValueProvider();
}
}
然后我们在Global.asax的Application_Start方法中注册这个工厂类:
protected void Application_Start() {
AreaRegistration.RegisterAllAreas();
ValueProviderFactories.Factories.Insert(0, new CurrentTimeValueProviderFactory()); // 注册ValueProvider工厂对象
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
最后我们在Action中使用如下:
public ActionResult Clock(DateTime curTime) {
return Content("The time is " + curTime.ToLongTimeString());
}
我们也可以定制模型绑定器对象,有两种方法:第一种方法是继承DefaultModelBinder类,然后重写它的CreateModel方法,并且在Application_Start方法中设置ModelBinders.Binders.DefaultBinder为这个模型绑定器(表明现在的默认模型绑定器用的是我们自己定义的):
public class DIModelBinder : DefaultModelBinder {
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) {
return DependencyResolver.Current.GetService(modelType) ?? base.CreateModel(controllerContext, bindingContext, modelType);
}
}
然后在global.asax的Application_Start方法中设置默认模型绑定器为DIModelBinder:
protected void Application_Start() {
// ...
ModelBinders.Binders.DefaultBinder = new DIModelBinder();
// ...
}
第二种方法继承IModelBinder接口,然后实现它的接口方法:
public class PersonModelBinder : IModelBinder {
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
// 检查是否有现成的model对象,如果没有创建一个(如果使用手动绑定则bindingContext.Model就不会为null)
Person model = (Person) bindingContext.Model ?? (Person)DependencyResolver.Current.GetService(typeof(Person));
// find out if the value provider has the required prefix
bool hasPrefix = bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName); // bindingContext.ModelName返回当前模型的名称
string searchPrefix = (hasPrefix) ? bindingContext.ModelName + "." : "";
// 填充model对象的字段
model.PersonId = int.Parse(GetValue(bindingContext, searchPrefix, "PersonId"));
model.FirstName = GetValue(bindingContext, searchPrefix, "FirstName");
model.LastName = GetValue(bindingContext, searchPrefix, "LastName");
model.BirthDate = DateTime.Parse(GetValue(bindingContext, searchPrefix, "BirthDate"));
model.IsApproved = GetCheckedValue(bindingContext, searchPrefix, "IsApproved");
model.Role = (Role)Enum.Parse(typeof(Role), GetValue(bindingContext, searchPrefix, "Role"));
return model;
}
private string GetValue(ModelBindingContext context, string prefix, string key) {
ValueProviderResult vpr = context.ValueProvider.GetValue(prefix + key);
return vpr == null ? null : vpr.AttemptedValue;
}
private bool GetCheckedValue(ModelBindingContext context, string prefix, string key) {
bool result = false;
ValueProviderResult vpr = context.ValueProvider.GetValue(prefix + key);
if (vpr != null) {
result = (bool)vpr.ConvertTo(typeof(bool));
}
return result;
}
}
然后同样地我们需要注册模型绑定器,可以全局注册,方法是在Application_Start方法中添加下面代码:
protected void Application_Start() {
// ...
ModelBinders.Binders.Add(typeof(Person), new PersonModelBinder());
// ...
}
也可以通过Attribute为某个Action参数设置模型绑定器,如下:
public ActionResult Index(
[ModelBinder(typeof(DateTimeModelBinder))] DateTime theDate)
亦或者像下面这样进行模型绑定器与类型的绑定:
[ModelBinder(typeof(PersonModelBinder))]
public class Person {
[HiddenInput(DisplayValue=false)]
public int PersonId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
最后我们来看看如何定制ModelBinderProvider,它主要用于有多个模型绑定器的情况下来选择用哪个某型绑定器,我们需要实现IModelProvider接口:
using System;
using System.Web.Mvc;
using MvcApp.Models;
namespace MvcApp.Infrastructure {
public class CustomModelBinderProvider : IModelBinderProvider {
public IModelBinder GetBinder(Type modelType) {
return modelType == typeof(Person) ? new PersonModelBinder() : null;
}
}
}
然后又是老套的在Application_Start方法中注册:
ModelBinderProviders.BinderProviders.Add(new CustomModelBinderProvider());
参考资料:
https://www.simple-talk.com/dotnet/asp.net/the-three-models-of-asp.net-mvc-apps/
<Pro ASP.NET MVC3 Framework, 3rd Edition>.Chapter 17: Model Binding