模型绑定(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: Home
public 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: Home
public 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; }
}