模型绑定(Model Binding)是指,用浏览器以Http请求方式发送的数据来创建.Net对象的过程。
准备示例项目
新建一个空的MVC项目,名叫MvcModels,接下去会以此项目来演示各种功能。
在Models文件夹中创建一个Person.cs类文件,代码如下图所示:
namespace MvcModels.Models
{public class Person{public int PersonId { get; set; }public string FirstName { get; set; }public string LastName { get; set; }public DateTime BirthDate { get; set; }public Address HomeAddress { get; set; }public bool IsApproved { get; set; }public Role Role { get; set; }}public class Address{public string Line1 { get; set; }public string Line2 { get; set; }public string City { get; set; }public string PostalCode { get; set; }public string Country { get; set; }}public enum Role{Admin,User,Guest}
}
定义一个Home控制器,代码如下图所示:
public class HomeController : Controller{private Person[] personData = {new Person { PersonId = 1,FirstName = "Adam",LastName = "Freeman" },new Person { PersonId = 2,FirstName = "Jacqui",LastName = "Griffyth"},new Person { PersonId = 3,FirstName = "John",LastName = "Smith" },new Person { PersonId = 4,FirstName = "Anne",LastName = "Jones"}};// GET: Homepublic ActionResult Index(int id){Person dataItem = personData.Where(p => p.PersonId == id).First();return View(dataItem);}}
新增Index控制器对应的Index.cshtml页面,代码如下:
@model MvcModels.Models.Person@{ViewBag.Title = "Index";Layout = "~/Views/Shared/_Layout.cshtml";
}<h2>Person</h2>
<div><label>ID:</label> @Html.DisplayFor(m => m.PersonId)</div>
<div><label>First Name:</label>@Html.DisplayFor(m => m.FirstName)</div>
<div><label>Last Name:</label>@Html.DisplayFor(m => m.LastName)</div>
<div><label>Roles:</label>@Html.DisplayFor(m => m.Role)</div>
新增_layout.cshtml布局页面,代码如下:
<!DOCTYPE html><html>
<head><meta name="viewport" content="width=device-width" /><title>@ViewBag.Title</title><style>label {display:inline-block; width:100px;font-weight:bold;margin:5px;}form label {float:left;}input.text-box {float:left;margin:5px}button[type=submit] {margin-top:5px;float:left;clear:left;}form div {clear:both;}</style>
</head>
<body><div>@RenderBody()</div>
</body>
</html>
运行程序,并导航到/Home/Index/1,结果如下图所示:
默认的动作绑定器ControllerActionInvoker要依靠模型绑定器来生成调用动作所需的数据对象。模型绑定器由IModelBinder接口所定义,接口如下图所示:
public interface IModelBinder
{object BindModel(ControllerContext controllerContext,ModelBindingContext bindingContext)
}
在一个MVC应用程序中,可以有多个模型绑定器,而每个绑定器可以负责绑定一个或者多个模型类型。它会考察该方法所定义的参数,并查找各个参数类型所依赖的模型绑定器。
在上述示例中,动作调用器会检查Index方法,并发现它具有一个int型的参数。于是会查找负责int值绑定的绑定器,并调用BindModel方法。
使用默认的模型绑定器
虽然程序可以定义自定义的模型绑定器,大多数程序都是依靠内建的模型绑定器DefaultModelBinder.当动作调用器找不到绑定某个类型的自定义绑定器时,这个默认的模型绑定器便是由动作调用器所使用的一个绑定器。默认情况下,这个模型绑定器会搜索四个位置:
源 | 描述 |
Request.Form | 由用户在HTML的form(表单)元素中提供的值 |
RouteData.Values | 用应用程序路由获得的值 |
Request.QueryString | 包含在请求URL中的查询字符串部分的数据 |
Request.Files | 请求中上传的文件 |
这些位置被依序搜索。例如,在上述简单示例中,DefaultModelBinder会为id参数查找以下的一个值:
1、Request.Form["id"]
2、RouteData.Values["id"]
3、Request.QueryString["id"]
4、Request.Files["id"]
只要找到值,便会停止搜索。在上述例子中,搜索到第二步就停了,不会到第三步。
当处理简单参数类型时,DefaultModelBinder会尝试使用 System.ComponentModel.TypeDescriptor类。将已经从请求数据获得的字符串值转化成参数类型。如果无法转为这个值:例如给int值传一个“apple”,程序就会报错:
解决这个问题有两种办法:
一、在动作方法参数中设置可空类型(nullable),这为绑定器提供一个退路,一个可空的int参数可以不必为数字值,这让模型绑定器在调用动作时,这可以让动作方法参数设置为Null。
public ActionResult Index(int? id)
二、 在动作方法中运用默认值,当模型绑定器无法为id参数找到一个值时,将默认值1来代替,如下所示:
public ActionResult Index(int id = 1)
绑定复杂类型
当动作方法的参数是复合类型时,DefaultModelBinder类将用反射来获取public属性集。
在Home控制器中,新增如下两个动作方法:
public class HomeController : Controller{private Person[] personData = {new Person { PersonId = 1,FirstName = "Adam",LastName = "Freeman" },new Person { PersonId = 2,FirstName = "Jacqui",LastName = "Griffyth"},new Person { PersonId = 3,FirstName = "John",LastName = "Smith" },new Person { PersonId = 4,FirstName = "Anne",LastName = "Jones"}};// GET: Homepublic ActionResult Index(int? id = 1){Person dataItem = personData.Where(p => p.PersonId == id).First();return View(dataItem);}public ActionResult CreatePerson(){return View(new Person());}[HttpPost]public ActionResult CreatePerson(Person model){return View("Index",model);}}
为没有参数的CreatePerson控制器方法创建一个对应的视图:CreatePerson.cshtml ,代码如下图所示:
@model MvcModels.Models.Person
@{ViewBag.Title = "CreatePerson";Layout = "~/Views/Shared/_Layout.cshtml";
}<h2>CreatePerson</h2>
@using (Html.BeginForm())
{<div>@Html.LabelFor(m => m.PersonId) @Html.EditorFor(m => m.PersonId)</div><div>@Html.LabelFor(m => m.FirstName) @Html.EditorFor(m => m.FirstName)</div><div>@Html.LabelFor(m => m.LastName) @Html.EditorFor(m => m.LastName)</div><div>@Html.LabelFor(m => m.Role) @Html.EditorFor(m => m.Role)</div><button type="submit">Submit</button>
}
运行导航到/Home/CreatePerson,结果如下图所示:
点击submit按钮后,可以看到已经将输入的数据 传到 Index 界面了:
在表单传递给CreatePerson方法时,形成了一种不同的模型绑定情况。默认的模型绑定器发现,动作方法需要一个Person对象,于是会依次处理每个属性。 对于每个简单类型的属性,绑定器会视图查找请求中的一个值,就如同上一个示例所做的那样。因此,当遇到 PersonId属性时,绑定器会查找personId的数据值,它将在请求的表单中发现这个值。
如果一个属性需要另一个复合类型,那么,该过程会针对新类型重复执行。获取该类型的public属性集,而绑定器会视图找出这些属性的值。不同的是,这些属性是嵌套的。例如,Person类的HomeAddress 属性 是Address类型。
创建易于绑定的HTML
更新CreatePerson.cshtml中的代码,以便为Address类型捕获一些属性:
@model MvcModels.Models.Person
@{ViewBag.Title = "CreatePerson";Layout = "~/Views/Shared/_Layout.cshtml";
}<h2>CreatePerson</h2>
@using (Html.BeginForm())
{<div>@Html.LabelFor(m => m.PersonId) @Html.EditorFor(m => m.PersonId)</div><div>@Html.LabelFor(m => m.FirstName) @Html.EditorFor(m => m.FirstName)</div><div>@Html.LabelFor(m => m.LastName) @Html.EditorFor(m => m.LastName)</div><div>@Html.LabelFor(m => m.Role) @Html.EditorFor(m => m.Role)</div><div>@Html.LabelFor(m => m.HomeAddress.City)@Html.EditorFor(m => m.HomeAddress.City)</div><div>@Html.LabelFor(m => m.HomeAddress.Country)@Html.EditorFor(m => m.HomeAddress.Country)</div><button type="submit">Submit</button>
}
更新Index.html中的代码,如下图所示:
@model MvcModels.Models.Person@{ViewBag.Title = "Index";Layout = "~/Views/Shared/_Layout.cshtml";
}<h2>Person</h2>
<div><label>ID:</label> @Html.DisplayFor(m => m.PersonId)</div>
<div><label>First Name:</label>@Html.DisplayFor(m => m.FirstName)</div>
<div><label>Last Name:</label>@Html.DisplayFor(m => m.LastName)</div>
<div><label>Roles:</label>@Html.DisplayFor(m => m.Role)</div><div><label>City:</label>@Html.DisplayFor(m => m.HomeAddress.City)</div>
<div><label>County:</label>@Html.DisplayFor(m => m.HomeAddress.Country)</div>
运行导航到/Home/CreatePerson,如下图所示:
点击Submit按钮后,数据传递成功,情况如下:
简而言之,模型绑定器查找的是 HomeAddress.Country,即,模型对象的属性名(HomeAddress)与属性类型(Address)d的属性名(Country)的组合。
指定自定义前缀
偶尔有些时候,生成的HTML与一种类型的对象有关,但是希望绑定到另一个对象。这意味着包含的前缀与模型绑定器期望的结构不对应。这个时候需要用到属性注解了。
在Models文件夹中创建了一个新的类文件,名称为AddressSummary.cs,如下图所示:
public class AddressSummary{public string City { get; set; }public string Country { get; set; }}
在Home控制器中增加一个动作方法,如下图所示:
public ActionResult DisplaySummary(AddressSummary summary){return View(summary);}
修改CreatePerson.cshtml文件中表达提交的目标:
@model MvcModels.Models.Person
@{ViewBag.Title = "CreatePerson";Layout = "~/Views/Shared/_Layout.cshtml";
}<h2>CreatePerson</h2>
@using (Html.BeginForm("DisplaySummary","Home"))
{<div>@Html.LabelFor(m => m.PersonId) @Html.EditorFor(m => m.PersonId)</div><div>@Html.LabelFor(m => m.FirstName) @Html.EditorFor(m => m.FirstName)</div><div>@Html.LabelFor(m => m.LastName) @Html.EditorFor(m => m.LastName)</div><div>@Html.LabelFor(m => m.Role) @Html.EditorFor(m => m.Role)</div><div>@Html.LabelFor(m => m.HomeAddress.City)@Html.EditorFor(m => m.HomeAddress.City)</div><div>@Html.LabelFor(m => m.HomeAddress.Country)@Html.EditorFor(m => m.HomeAddress.Country)</div><button type="submit">Submit</button>
}
导航到/Home/CreatePerson方法:
点击submit方法后,结果如下图所示:
由于Country和City的前缀改变了,由HomeAddress变成了AddressSummary,故绑定器无法实现绑定。只需对动作方法的参数运用Bind注解属性即可,代码如下图所示:
public ActionResult DisplaySummary([Bind(Prefix ="HomeAddress")]AddressSummary summary)
{return View(summary);
}
重新运行代码,即可看到运行成功:
有选择性的绑定属性
如果希望对某一属性不需要模型绑定器进行绑定,可以使用如下代码:
public ActionResult DisplaySummary([Bind(Prefix ="HomeAddress",Exclude ="Country")]AddressSummary summary)
{return View(summary);
}
也可以设置模型绑定器只绑定某一属性,代码如下图所示:
[Bind(Include ="City")]public class AddressSummary{public string City { get; set; }public string Country { get; set; }}
绑定到数组
默认模型绑定器的一个雅致的特性是它支持动作方法参数作为数组。在Home控制器中添加一个方法,如下图所示:
public ActionResult Names(string[] names){names = names ?? new string[0];return View(names);}
创建Names方法对应的视图Names.csthml,如下图所示:
@model string[]
@{ViewBag.Title = "Names";Layout = "~/Views/Shared/_Layout.cshtml";
}<h2>Names</h2>
@if (Model.Length == 0)
{using (Html.BeginForm()) {for (int i = 0; i < 3; i++){<div><label>@(i + 1):</label>@Html.TextBox("names")</div>}<button type="submit">Submit</button>}
}
else
{foreach (string str in Model){<p>@str</p>}@Html.ActionLink("Back", "Names")
}
导航到/Home/Names,如下图所示:
点击Submit后,如下图所示:
递交表单时,默认的模型绑定器明白动作方法需要一个字符串数组。于是会查找与参数具有同样名称的数据项。在本例中,意味着会将所有input元素的内容聚集到一起用以填充数组。
绑定到集合
能绑定的不仅仅是数组,还可以使用.Net集合类。
修改names动作方法为强类型集合,如下图所示:
public ActionResult Names(IList<string> names){names = names ?? new List<string>();return View(names);}
并修改Names.cshtml页面代码,如下所示:
@model IList<string>
@{ViewBag.Title = "Names";Layout = "~/Views/Shared/_Layout.cshtml";
}<h2>Names</h2>
@if (Model.Count() == 0)
{using (Html.BeginForm()) {for (int i = 0; i < 3; i++){<div><label>@(i + 1):</label>@Html.TextBox("names")</div>}<button type="submit">Submit</button>}
}
else
{foreach (string str in Model){<p>@str</p>}@Html.ActionLink("Back", "Names")
}
运行结果如下图所示:
点击提交后:
绑定到自定义模型集合
可以将一些单个的数据属性绑定成一个自定义类型的数组。如上述的AddressSummary模型类.
在控制器中增加一个新的动作方法,如下图所示:
public ActionResult Address(IList<AddressSummary> addresses){addresses = addresses ?? new List<AddressSummary>();return View(addresses);}
添加Address动作方法对应的页面,代码如下图所示:
@using MvcModels.Models
@model IList<AddressSummary>
@{ViewBag.Title = "Address";Layout = "~/Views/Shared/_Layout.cshtml";
}<h2>Address</h2>
@if (Model.Count() == 0)
{using (Html.BeginForm()){for (int i = 0; i < 3; i++){<fieldset><legend>Address @(i + 1)</legend><div><label>City:</label>@Html.Editor("[" + i + "].City")</div><div><label>Country:</label>@Html.Editor("[" + i + "].Country")</div></fieldset>}<button type="submit">Submit</button>}
}
else
{foreach (AddressSummary str in Model){<p> @str.City, @str.Country</p>}@Html.ActionLink("Back","Address");
}
导航到/Home/Address,并输入内容,页面如下图所示:
点击提交后,正确显示页面:
可以看到生成的HTML代码:
当表单被提交时,默认的模型绑定器知道它需要创建的是一个AddressSummary对象集合,并利用 name 标签属性中的数组索引前缀获取对象的类型。以【0】为前缀的那些属性表示一个AddressSummary对象,以【1】为前缀表示第二个对象。以此类推。
手工调用模型绑定
当动作方法定义了参数时,模型绑定过程是自动执行的,但是只要你愿意,也可以直接控制这一过程。如下将演示如何将Home控制器的Address动作方法修改成手动调用绑定过程。
修改Address方法,代码如下图所示:
public ActionResult Address(IList<AddressSummary> addresses){addresses = addresses ?? new List<AddressSummary>();UpdateModel(addresses);return View(addresses);}
UpdataModel方法以上一条语句定义的时候的模型对象为参数,并试图用标准的绑定该过程来获取其public属性的值。
当手工调用绑定时,可以将绑定过程限制到单一的数据源。默认情况下,绑定器会搜索四个地方:表单数据、路由数据、查询字符串,以及上传文件。一下代码演示了如何将绑定器限制到搜索单一位置的数据——表单数据。
public ActionResult Address(IList<AddressSummary> addresses){addresses = addresses ?? new List<AddressSummary>();UpdateModel(addresses,new FormValueProvider(ControllerContext));return View(addresses);}
UpdateModel方法的这一版本以IValueProvider接口的一个实现为参数,该实现也成为了绑定过程的唯一的数据源。四个默认的数据位置的每一个都由一个IValueProvider实现表示:
源 | IValueProvider |
Request.Form | FormValueProvider |
RouteData.Value | RouteDataValueProvider |
Request.QueryString | QueryStringValueProvider |
Request.Files | HttpFileCollectionValueProvider |
可以用另一种跟优雅的写法来表示:
public ActionResult Address(FormCollection formData){List<AddressSummary> addresses = new List<AddressSummary>();UpdateModel(addresses,formData);return View(addresses);}
处理绑定错误
用户难免会提供一些不能绑定到相应模型属性的值,需要对这些情况抛出些异常。特别是使用UpdateModel方法时,必须做好捕捉该异常的准备,代码如下图所示:
public ActionResult Address(FormCollection formData){List<AddressSummary> addresses = new List<AddressSummary>();try{UpdateModel(addresses, formData);}catch (InvalidCastException ex){//给用户提供反馈}return View(addresses);}
另一个可选的办法是,可以使用TryUpdateModel方法。如果模型绑定成功,返回ture;否则返回false;
public ActionResult Address(FormCollection formData){List<AddressSummary> addresses = new List<AddressSummary>();if (TryUpdateModel(addresses,formData)){//正常处理}else{//给用户提供反馈}return View(addresses);}
这两种方式的唯一区别是,你是否喜欢捕捉并处理异常。
定制模型绑定系统
还有一些不同的方式,可以对绑定系统进行定制。
通过定义一个自定义的值提供器,可以将自己的数据源添加到模型绑定过程。值提供器(Valueprovider)需要实现IValueProvider接口,如下图所示:
public interface IValueProvider{bool ContainsPrefix(string prefix);ValueProviderResult GetValue(string key);}
ContainsPrefix方法由模型绑定器调用,以确定这个值提供器是否可以解析给定前缀的数据。
GetValue方法返回给定数据键的值,或者在提供器无法得到合适的数据时返回null。
新建一个CountryValueProvider类,实现以上接口:
public class CountryValueProvider : IValueProvider{public bool ContainsPrefix(string prefix){return prefix.ToLower().IndexOf("country") > -1;}public ValueProviderResult GetValue(string key){if (ContainsPrefix(key)){return new ValueProviderResult("USA", "USA", CultureInfo.InvariantCulture);}else{return null;}}}
该值提供器只对请求Country属性的值进行响应,而且总是返回 USA 。对于其他请求,返回 NULL,表示无法提供数据。
返回值必须提供一个ValueProviderResult类来返回。这个类有三个构造器参数:第一个参数是与请求键关联的数据项,第二个参数是作为HTML页面一部分的该数据的安全显示形式,第三个参数是该值相关的文化信息。这里已经指定为了InvariantCulture。
为了在应用程序中对这个值进行注册,需要一个工厂类,以便在MVC框架需要时为这个提供器创建实例。这个工厂类必须派生于抽象类ValueProviderFactory。代码如下图所示:
public class CustomValueProviderFactory : System.Web.Mvc.ValueProviderFactory
{public override IValueProvider GetValueProvider(ControllerContext controllerContext){return new CountryValueProvider();}
}
当模型绑定器要为绑定过程获取值时,会调用这个GetValueProvider方法。上述实现了简单的创建并返回了CurrentTimeProvider类的一个实例,但你可以使用ControllerContext参数提供的数据,以便创建不同的值提供器,对不同种类的请求进行响应。
然后在Global.asax的Application_Start方法中注册:
protected void Application_Start(){AreaRegistration.RegisterAllAreas();RouteConfig.RegisterRoutes(RouteTable.Routes);ValueProviderFactories.Factories.Insert(0,new CustomValueProviderFactory());}
如果希望这一提供器在其他提供器不能提供数据值时作为一个备选,那么可以用Add方法把工厂追加到集合末尾:
ValueProviderFactories.Factories.Add(new CustomValueProviderFactory());
运行程序,导航到/Home/Address,如下图所示:
点击Submit,如下图所示:
创建自定义模板绑定器
通过创建一个特定类型的自定义模型绑定器,可以覆盖默认绑定器的行为。自定义模型绑定器需要实现IModelBinder接口。创建一个AddressSummaryBinder.cs类文件,如下图所示:
public class AddressSummaryBinder : IModelBinder{public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext){AddressSummary model = (AddressSummary)bindingContext.Model ?? new AddressSummary();model.City = GetValue(bindingContext,"City");model.Country = GetValue(bindingContext,"Country");return model;}private string GetValue(ModelBindingContext context,string name){name = (context.ModelName == "" ? "" : context.ModelName + ".") + name;ValueProviderResult result = context.ValueProvider.GetValue(name);if (result == null || result.AttemptedValue == ""){return "<Not Specified>";}else{return (string)result.AttemptedValue;}}}
BindModel方法的参数是一个ControllerContext对象,可以用它来访问当前请求的细节。另一个是ModelBindingContext对象,该对象提供了当前寻找的模型对象的细节,并能访问MVC应用程序中其他模型绑定工具。
属性 | 描述 |
Model | 如果手工调用了绑定,可返回传递给UpdataModel方法的模型对象 |
ModelName | 返回被绑定模型的名称 |
ModelType | 返回被创建模型的类型 |
ValueProvider | 返回能用于请求中获得数据值的IValueProvider实现 |
在调用BindModel方法时,检查已经是否设置了 ModelBindingContext 对象 的Model属性,如果已经设置,则该模型便是将要为之生成数据值的对象,如没有设置,则创建AddressSummary类的一个实例。通过调用GetValue方法获取City和Country属性的值,然后返回已经过填充的AddressSummary对象。
在GetValue方法中,通过了ModelBindingContext.ValueProvider属性获得的IValueProvider实现,以获取模型对象属性的值。
ModelName属性能够告诉我们,对正在寻找的属性的名称,是否需要追加一个前缀。当无法为一个属性找到值,或者该属性为空字符串时,便提供一个默认值<Not Specified>.
然后在Global.asax的Application_Start方法中注册该模型绑定器:
protected void Application_Start(){AreaRegistration.RegisterAllAreas();RouteConfig.RegisterRoutes(RouteTable.Routes);ModelBinders.Binders.Add(typeof(AddressSummary), new AddressSummaryBinder());}
导航到/Home/Address,并输入内容,如下图所示:
提交后,结构如下图所示:
用注解属性注册模型绑定器
可以在模型类上使用ModelBinder注解属性进行修饰,来注册自定义模型绑定器。不必使用Global.asax文件。如下图所示:
[ModelBinder(typeof(AddressSummaryBinder))]public class AddressSummary{public string City { get; set; }public string Country { get; set; }}