MVC中的扩展点(八)模型绑定
MVC可以将用户提交的数据绑定到Action参数,我们将这个过程称之为模型绑定,在模型绑定中有两个关键:一个是值提供器,用于确定数据来源,另一个称为模型绑定器,用于确定如何将值绑定到特性的数据模型。
MVC中默认的值提供器
值提供器是一组实现了IValueProvider接口的类,MVC中的值提供其使用了标准的抽象工厂设计模式,其类图如下:
MVC提供了四种默认的值提供器:
FormValueProvider:表单数据,对应于ASP.NET的Request.Form集合
QueryStringValueProvider:查询字符串,对应于ASP.NET的Request.QueryString集合
HttpFileCollectionValueProvider:文件集合,数据来源于Request.Files集合
RouteDataValueProvider:路由信息,对应于RouteData.Values集合
MVC为每一个值提供器提供了一个工厂:ValueProviderFactory,ValueProviderFactoryCollection是一个值提供器工厂集合,其中的GetValueProvider方法返回与当前控制器上下文匹配的值提供器。ValueProviderFactories是一个静态类,内部封装一个ValueProviderFactoryCollection集合(Factories属性),此集合默认包含MVC中4个默认的值提供器,所以,如果我们要使用自己的值绑定器,可以通过此类的Factories属性,使用InsertItem增加新的值提供器,使用SetItem方法将某个默认值提供器替换为我们的自定义值提供器。 需要注意的是,ValueProviderFactoryCollection的GetValueProvider方法实际返回的是一个ValueProviderCollection集合,但由于此集合类同样实现了IValueProvider接口(其GetValue方法会从集合中所有的值提供器中查找符合条件的项),所以GetValueProvider的返回类型仍然为IValueProvider。
MVC中默认的模型绑定器
模型绑定器用于将值提供器提供的数据映射到特定的模型类型,正因为有模型绑定器的存在,才使我们可以直接使用带参数的控制器方法:绑定器可以从值提供器中获取数据,并根据控制器方法参数类型,自动实例化参数并填充数据。
MVC中默认的模型绑定器类结构如下:
MVC中实现四种模型绑定器:
HttpPostedFileBaseModelBinder:用于处理HttpPostedFileBase类型
ByteArrayModelBinder:用于处理Byte[]类型
LinqBinaryModelBinder:用于处理Linq中的Binary类型
DefaultModelBinder:默认绑定器,如果某个类型没有特定的绑定器,则使用此绑定器。
ModelBinderDictionary是一个IModelBinder字典,键为类型,值为类型所对应的绑定器。DefaultBinder属性指定如果集合中不存在指定类型的绑定器时,应返回的默认绑定器,默认为DefaultModelBinder。我们可以通过此属性来指定我们自己的默认绑定器。ModelBinders是一个静态类,内部封装了ModelBinderDictionary(Binders属性),包含默认的绑定器列表。
CustomModelBinderAttribute是一个用于指定自定义绑定器的特性的抽象基类,方法GetBinder用于返回一个绑定器实例。ModelBinderAttribute是CustomModelBinderAttribute的一个实现,它根据指定的绑定器类型,返回其实例。MVC在查找适当的绑定器时,遵循以下顺序:
在参数上通过ModelBinderAttribute特性指定的绑定器
在ModelBinders.Binders中注册的绑定器
在类型上通过ModelBinderAttribute特性指定的绑定器
默认绑定器(通常为DefaultModelBinder)
BindAttribute特性用于指定默认绑定器在绑定时的具体行为:Exlude属性指定绑定器在填充数据时,不处理的属性列表,Include属性指定绑定器在填充数据时,只需处理的属性列表,Prefix用于指定类型的前缀,默认情况下,前缀为控制器方法中参数的名称。IsPropertyAllowed方法用于判断特定的属性是否需要被处理。
自定义值提供器
要实现自定义的值提供器,我们需要实现一个ValueProviderFactory和一个IValueProvider,之后将其添加到ValueProviderFactories.Factories集合中。下例实现一个CookieValueProvider,用于从Cookie中获取数据:
1、创建一个空MVC项目
2、实现CookieValueProviderFactory工厂及CookieValueProvider提供器
public class CookieValueProviderFactory : ValueProviderFactory{
public override IValueProvider GetValueProvider(ControllerContext controllerContext) { return new CookieValueProvider(controllerContext.HttpContext.Request.Cookies); } private class CookieValueProvider : IValueProvider{
private HttpCookieCollection Cookies; public CookieValueProvider(HttpCookieCollection cs) { Cookies = cs; } public bool ContainsPrefix(string prefix) {return Cookies.AllKeys.Contains(prefix);
} public ValueProviderResult GetValue(string key) {if (!ContainsPrefix(key))
return null;
string value = Cookies[key].Value;
return new ValueProviderResult(value,value, CultureInfo.CurrentCulture ); } } }3、在Application_Start中注册自定义值提供器工厂
ValueProviderFactories.Factories.Insert(0, new CookieValueProviderFactory());
4、实现一个测试控制器,HomeController:
public class HomeController : Controller { public ActionResult Index(DateTime? lastTime) { if (!Request.Cookies.AllKeys.Contains("lastTime")) { Response.Cookies.Add(new HttpCookie("lastTime", DateTime.Now.ToString())); } return Content(lastTime == null ? String.Empty : lastTime.ToString()); } }注意,参数类型为DateTime?可空类型,这样可防止默认绑定器在没有找到对应值时报错。测试时,首次运行,由于Cookie中没有lastTime项,所以页面为空,刷新一次页面,此时页面将显示上一次访问的时间。
自定义模型绑定器
通过实现IModelBinder接口可实现自定义模型绑定器,下例将实现一个XDocument类型的绑定器:
1、创建一个空MVC项目
2、实现XDocumentBinder
public class XDocumentBinder : IModelBinder { public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {string key = bindingContext.ModelName;
ValueProviderResult vpr = bindingContext.ValueProvider.GetValue(key);
if (vpr !=null && vpr.RawValue != null && !String.IsNullOrEmpty(vpr.AttemptedValue)) {//在模型状态中保存尝试值
bindingContext.ModelState.SetModelValue(key, vpr);
string tempString = ((String[])vpr.RawValue)[0]; XDocument xml = null;try
{
xml = XDocument.Parse(tempString);
} catch (XmlException) {//无法解析XML文本,则设置模型错误状态
bindingContext.ModelState.AddModelError(key, "Not valid XML");return null;
}//如果模型已经存在,则替换
XDocument existingModel = bindingContext.Model as XDocument; if (existingModel != null) { if (existingModel.Root != null) { existingModel.Root.ReplaceWith(xml.Root); }else
{
existingModel.Add(xml.Root); }return existingModel;
}else
{
return xml;
} }return null;
} }3、在Application_Start中注册XDocumentBinder
ModelBinders.Binders.Add(typeof(XDocument), new XDocumentBinder());
4、创建用于测试的HomeController及其View
public class HomeController : Controller { [ValidateInput(false)] public ActionResult Index(XDocument xml) { if (xml == null) {return View();
}else
{
Response.Clear(); return Content(xml.ToString(), "application/xml"); } } }
<div> <%using (Html.BeginForm()){%>
<%= Html.ValidationSummary() %> <%=Html.TextArea("xml", new { Rows=20, Cols=50 })%> <input type="submit" value ="submit" /> <% } %> </div>
关于注册XDocumentBinder,我们可以通过ModelBinders.Binders将某个类型映射到绑定器,也可以通过在参数上、类型上通过ModelBinderAttribute特性来指定。
本例中Index方法上,我们指定了[ValidateInput(false)]特性,用于跳过输入验证,这是因为默认设置下,MVC不允许提交XML类型的数据。
默认绑定器扩展
DefaultModelBinder默认绑定器通过Activator.CreateInstance来实例化模型类型,所以我们的模型必须要用无参构造器。要解除此限制,我们可以使用DI技术对默认绑定器进行扩展:从DefaultModelBinder继承一个新类,并重写CreateModel方法,使用DI来实例化模型。最后通过 ModelBinders.Binders.DefaultBinder属性指定我们的自定义默认绑定器。
关于DI,在MVC中的扩展点(三)控制器工厂中有所涉及,有兴趣的朋友可参照该示例来实现自定义默认绑定器。