首页 > 代码库 > 模型绑定
模型绑定
一、概念
模型绑定(Model Binding)是指用浏览器以http请求方式发送的数据来创建.NET对象的过程。每当我们定义具有参数的动作方法时,一直是在依赖着这种模型绑定过程——这些参数对象是通过模型绑定来创建的。
简单来说,模型绑定是利用用户在表单中输入的数据来构造动作方法所需要的参数对象的过程,数据的流向是从客户端的html表单到动作方法。
模型绑定过程是通过模型绑定器来实现的,其目的是用请求中所包含的数据来创建对象。特别是在调用动作方法时,为动作方法创建参数对象。绑定过程必然经过一些步骤:
- 检测目标对象(要创建的对象)的名称和类型。
- 通过对象名称查找数据源(请求),并找到可用数据(通常是字符串)。
- 根据对象类型将找到的数据值转换成目标类型。
- 通过对象名称、对象类型和这种经过处理的数据来构造目标对象。
- 将构造好的对象送给动作调用器,并由动作调用器将对象注入到目标动作方法中去。
二、使用默认模型绑定器
一个应用程序可以有多个绑定器,大多数只依赖于内建的绑定器类DefaultModelBinder。当动作调用器找不到绑定某个类型的自定义绑定器时,这是由动作调用器使用的一个绑定器。
默认情况下,这个绑定器会搜索四个位置,以获取与被绑定的参数名匹配的数据:
- Request.Form——由用户在html的form表单元素中提供的值。
- RouteData.Values——由应用程序路由获得的值。
- Request.QueryString——包含在请求url的查询字符串部分的数据。
- Request.Files——请求中的上传文件。
这些位置被依次搜索。例如,假设要请求的动作方法带有一个参数int id,那么DefaultModelBinder类会考查该动作方法,并发现有一个名为id的参数,然后它会按以下顺序查找值:
Requst.Form["id"]
RouteData.Values["id"]
Request.QueryString["id"]
Request.Files["id"]
只要找到一个值,搜索便会停止。例如,当接收到/Home/Person/23这样的请求时,搜索到第二个位置就会找到具有id名称的路由片段,后面的查询字符串和上传文件就不会再搜索了。
由此可见,动作方法的参数名称很重要——参数名称和请求数据项的名称必须匹配。
1、绑定简单类型
当处理简单参数类型时,DefaultModelBinder会试图用System.ComponentModel.TypeDescriptor类把已经从请求数据获得的字符串值转换成参数类型。
如果这个值不能被转换——例如,如果给需要一个int值的参数提供了一个字符串“apple”值——那么DefaultModelBinder就不能绑定该模型。
如果想避免这种问题,我们可以修改参数,可以用一个可空类型(nullable),像这样:
public ViewResult RegisterPerson(int? id)
如果采用这种方式,那么,在请求中没有匹配的可转换数据的情况下,id参数的值将为null,另一个办法是,可以在无数据可用时,给参数提供一个可用的默认值,以使该参数成为可选的:
public ViewResult RegisterPerson(int id=23)
2、绑定复合类型
当动作方法参数是复合类型时(即不能用TypeConverter类进行转换的属性),DefaultModelBinder类将用反射来获取public属性集,然后依次逐一进行绑定。例如,有复合模型类Person
public partial 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; } } [MetadataType(typeof(PersonMetadataSource))] public partial class Person { } [DisplayName("人员信息")] class PersonMetadataSource { [HiddenInput(DisplayValue = false)] public int PersonId { get; set; } //[UIHint("MultilineText")] [PlaceHolder("firstname")] public string FirstName { get; set; } [DataType(DataType.Date)] [Display(Name = "出生日期")] public DateTime BirthDate { get; set; } //[UIHint("Enum")] public Role Role { get; set; } }
默认模型绑定器检查这个类的各个属性,以考查它们是否是简单类型。如果是,那么绑定器会在请求中查找与该属性同名的数据项。
如果属性是一种复合类型,该过程会针对新类型重复执行,获取其public属性集,而绑定器也会试图找出所有这些属性的值。例如,Person类的HomeAddress属性的类型是Address,如下所示:
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; } }
在查找Line1属性值时,模型绑定器查找的是HomeAddress.Line1的值。
当我们在带有Person视图模型(View model)的视图中调用@Html.EditorFor(m=>m.FirstName)时,产生的html代码:
<input class="text-box single-line" id="FirstName" name="FirstName" type="text" value="Joe" />
而当我们调用@Html.EditorFor(m=>m.HomeAddress.Line1)时,会得到以下的html代码:
<input class="text-box single-line" id="HomeAddress_Line1" name="HomeAddress.Line1" type="text" value="123 North Street" />
html的name标签属性被自动设置为模型绑定器查找的值。
(1)指定自定义前缀
下面的例子在视图中生成了附加的视图模型对象数据。
新建mvc项目ModelBindApp,在解决方案管理器中的Models文件夹下新建类库文件ModelClass.cs
namespace ModelBindApp.Models{ public class DoubleP { public Person p1 { get; set; } public Person p2 { get; set; } } public partial 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; } } [MetadataType(typeof(PersonMetadataSource))] public partial class Person { } [DisplayName("人员信息")] class PersonMetadataSource { [HiddenInput(DisplayValue = false)] public int PersonId { get; set; } [PlaceHolder("firstname")] public string FirstName { get; set; } [PlaceHolder("lastname")] public string LastName { get; set; } [DataType(DataType.Date)] [Display(Name = "出生日期")] public DateTime BirthDate { 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 }}
注意这里使用了注解属性[PlaceHolder("firstname")],是我们自定义的PlaceHolderAttribute类来实现的,在解决方案管理器中新建一文件夹,名为Infrastructures,在文件夹中新建类库文件PlaceHolderAttribute.cs
namespace ModelBindApp.Infrastructures{ public class PlaceHolderAttribute : Attribute, IMetadataAware { private readonly string _placeholder; public PlaceHolderAttribute(string placeholder) { _placeholder = placeholder; } public void OnMetadataCreated(ModelMetadata metadata) { metadata.AdditionalValues["placeholder"] = _placeholder; } }}
另外,如果要让注解属性[PlaceHolder("firstname")]起作用,还需要创建分部视图(partial view)string.cshtml,路径为/Views/Shared/EditorTemplates/string.cshtml
@{ var placeholder = string.Empty; if (ViewData.ModelMetadata.AdditionalValues.ContainsKey("placeholder")) { placeholder = ViewData.ModelMetadata.AdditionalValues["placeholder"] as string; }}@Html.TextBox("", ViewData.TemplateInfo.FormattedModelValue, new { placeholder = placeholder })
接着本例在HomeController中定义了Index动作方法
namespace ModelBindApp.Controllers{ public class HomeController : Controller { [HttpGet] public ViewResult Index() { Person p1 = new Person { PersonId = 1, FirstName = "Joe", LastName = "Smith", BirthDate = DateTime.Parse("2014/6/12"), IsApproved = true, Role = Role.User, HomeAddress = new Address{ Line1 = "101 North Street", Line2 = "102 North Street", City = "ChongQing", PostalCode = "400041", Country = "CHN" } }; return View(p1); } [HttpPost] public ViewResult Index(Person firstPerson, Person myPerson) { DoubleP dp = new DoubleP { p1 = firstPerson, p2 = myPerson }; return View("Index2", dp); } }}
对于使用了注解属性[HttpGet]的动作方法Index,为它添加默认视图文件Index.cshtml
@using ModelBindApp.Models@model ModelBindApp.Models.Person@{ ViewBag.Title = "Index"; Person myPerson = new Person() { PersonId = 2, FirstName = "Jane", LastName = "Doe", BirthDate = DateTime.Parse("2001/3/8"), IsApproved = true, Role = Role.Admin, HomeAddress = new Address { Line1 = "1 West Street", Line2 = "2 West Street", City = "Chongqing", PostalCode = "65001", Country = "CHN" } };}<h2>Index</h2><h4>@Html.LabelForModel()</h4>@using (Html.BeginForm()){ <div class="column"> @Html.EditorForModel() @Html.EditorFor(m => m.HomeAddress) </div> <div class="column"> @Html.EditorFor(m => myPerson) @Html.EditorFor(m => myPerson.HomeAddress) </div> <input type="submit" value="Submit" />}
这个例子中就是演示了在发送给客户端的html中包含了额外的模型对象,在当前视图Index.cshtml中有两个数据对象,一个是通过动作方法传递过来的@model,另一个是在当前视图中定义的附加模型对象myPerson,也叫做额外对象。所谓额外对象,是在视图中创建并填充数据而形成的对象,是视图中除视图模型之外的另一个对象。
需要注意的是,从实验中可以看出,这里所创建的额外对象与试图模型对象@model各是独立的,各自具有不同的数据。
在第一列,用@Html.EditorForModel(),使用Editor辅助器将@model中的普通类型的属性产生出来,再使用@Html.EditorFor(m => m.HomeAddress)将其中一个复合类型的属性HomeAddress产生出来。
在第二列,用@Html.EditorFor(m => myPerson),使用Editor辅助器将额外对象myPerson中的普通类型的属性产生出来,再使用@Html.EditorFor(m => myPerson.HomeAddress) 将额外对象myPerson中的复合类型属性HomeAddress产生出来。
显示效果如图所示
在渲染额外对象时,模板视图辅助器会把一个前缀运用于html元素的name标签属性,以此与主视图模型的数据加以区别。
下面是第一列,由主视图模型对象的数据产生的FirstName标签和编辑框,请注意编辑框的id和name就是用的模型中对应的属性名“FirstName”
<div class="editor-label">
<label for="FirstName">FirstName</label>
</div><div class="editor-field">
<input id="FirstName" name="FirstName" placeholder="firstname" type="text" value="Joe" />
<span class="field-validation-valid" data-valmsg-for="FirstName" data-valmsg-replace="true"></span>
</div>
下面是第二列,由额外对象myPerson的数据产生的FirstName标签和编辑框,尤其注意编辑框的id变成了“myPerson_FirstName”,编辑框的name变成了“myPerson.FirstName”
<div class="editor-label">
<label for="myPerson_FirstName">FirstName</label>
</div><div class="editor-field">
<input id="myPerson_FirstName" name="myPerson.FirstName" placeholder="firstname" type="text" value="Jane" />
<span class="field-validation-valid" data-valmsg-for="myPerson.FirstName" data-valmsg-replace="true"></span>
</div>
那么,在把表单中的数据提交给动作方法时,如果提交的对象既有主视图模型对象,又有额外对象(像本例一样),那么模型绑定器在查找额外对象时,就默认添加上前缀来查找。例如:
[HttpPost] public ViewResult Index(Person firstPerson, Person myPerson) { DoubleP dp = new DoubleP { p1 = firstPerson, p2 = myPerson }; return View("Index2", dp); }
第一个参数对象将用非前缀数据绑定,也就是绑定主视图模型对象。而第二个参数对象将以参数名为前缀查找的数据进行绑定,也就是绑定额外对象(对应的属性为myPerson.FirstName、myPerson.LastName等)。
为处理Post的动作方法Index添加新的视图Index2.cshtml
@model ModelBindApp.Models.DoubleP@{ ViewBag.Title = "Index2";}<h2>Index2</h2><div class="column"> @foreach (System.Reflection.PropertyInfo p in (Model.p1).GetType().GetProperties()) {
<b>@p.Name : </b>@p.GetValue((Model.p1), null) <br /> } <br /><br /> HomeAddress:<br /> @foreach (System.Reflection.PropertyInfo p in (Model.p1.HomeAddress).GetType().GetProperties()) {
<b>@p.Name : </b>@p.GetValue((Model.p1.HomeAddress), null) <br /> }</div><div class="column"> @foreach (System.Reflection.PropertyInfo p in (Model.p2).GetType().GetProperties()) {
<b>@p.Name : </b>@p.GetValue((Model.p2), null) <br /> } <br /><br /> HomeAddress:<br /> @foreach (System.Reflection.PropertyInfo p in (Model.p2.HomeAddress).GetType().GetProperties()) {
<b>@p.Name : </b>@p.GetValue((Model.p2.HomeAddress), null) <br /> }</div>
这里使用了一个foreach,来遍历出一个对象里的所有属性名和对应的属性值。
foreach (System.Reflection.PropertyInfo p in 对象名.GetType().GetProperties())
这样,p.Name 访问到属性名;
p.GetValue(对象名,null) 访问到属性值。
因此,点击Submit提交后,产生的显示为:
当然,为了让标签和文本框在同一行,以及为了分列显示,修改和添加了一些css样式
在/Content/Site.css中修改.editor-label和.editor-field
.editor-label { margin: 1em 0 0 0; clear:left; float:left; min-width: 100px; vertical-align:middle;}.editor-field { margin: 0.5em 0 0 0; width:150px; float:left;}
并添加新样式:
.label { vertical-align:middle;}.check-box { vertical-align:bottom; margin: .5em 0 0 0;}div.column { float:left; width:30%;}.single-line { width: 100px;}
在动作方法
[HttpPost] public ViewResult Index(Person firstPerson, Person myPerson)
中,如果不想额外对象的前缀与第二个参数名相同,则可以指定自定义前缀,用下面的格式:
[HttpPost] public ViewResult Index(Person firstPerson, [Bind(Prefix="myPerson")]Person secondPerson)
这时,动作方法的第二个参数名可以任意取名字,只需要Prefix指定的前缀名与额外对象的名字相同就可以了。
(2)有选择地绑定属性
假设模型中有些属性是敏感的,可以用上一章的HiddenInput等方式防止在模型的html中渲染这个属性,但是恶意的用户可以简单地把“?属性名=属性值”附加到url上,如果是这样,模型绑定器会在绑定过程中正常发现并使用这个属性值。因此,我们在必要的时候可以用Bind注解属性把模型属性包含到或排除出绑定过程。
指定要包含的绑定属性,用Include。
例如,上面的例子,Post的Index动作方法改为:
[HttpPost]public ViewResult Index([Bind(Include="FirstName, LastName")]Person firstPerson, Person myPerson)
表示主视图模型对象在传递给第一个参数时,只绑定FirstName和LastName这两个属性,其他属性都没有。将Post的Index动作方法使用的视图文件Index2.cshtml对第一个参数对象的显示做以下修改:
<div class="column"> @foreach (System.Reflection.PropertyInfo p in (Model.p1).GetType().GetProperties()) {
<b>@p.Name : </b>@p.GetValue((Model.p1), null) <br /> } <br /><br /> HomeAddress:<br /> @if (Model.p1.HomeAddress != null) { foreach (System.Reflection.PropertyInfo p in (Model.p1.HomeAddress).GetType().GetProperties()) {
<b>@p.Name : </b>@p.GetValue((Model.p1.HomeAddress), null) <br /> } }</div>
注意,在显示复合类型HomeAddress属性的值时,先用if判断了下这个对象中的HomeAddress对象是否为null,如果为null就不再用foreach显示了。因为这里只绑定了FirstName和LastName属性,其他都没有,其他没有绑定的简单类型的属性就为默认值,复合类型的值就为null。执行后:
点击Submit提交按钮,产生显示为:
可以看到第一个对象因为只绑定了FirstName和LastName,所以只有这两个属性是正常提交过来的值,其他属性要么是默认值,要么为null,HomeAddress为null,没有显示。
排除要绑定的属性,用Exclude。
例如,上面的例子,Post的Index动作方法改为:
[HttpPost]public ViewResult Index([Bind(Exclude="FirstName, LastName")]Person firstPerson, Person myPerson)
排除掉属性FirstName和LastName
执行后,点击提交按钮,显示结果为:
除了以上单独在动作方法的属性指定Include和Exclude以外,还可以在模型类的定义直接指定,那就对所有动作方法都有效果了。例如:
public partial 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; } } [MetadataType(typeof(PersonMetadataSource))] public partial class Person { } [Bind(Exclude="FirstName,LastName")] [DisplayName("人员信息")] class PersonMetadataSource { [HiddenInput(DisplayValue = false)] public int PersonId { get; set; } [PlaceHolder("firstname")] public string FirstName { get; set; } [PlaceHolder("lastname")] public string LastName { get; set; } [DataType(DataType.Date)] [Display(Name = "出生日期")] public DateTime BirthDate { get; set; } public Role Role { get; set; } }
3、绑定到数组与集合
绑定到数组,意思是将html中传递过来的数据绑定形成数组。
默认模型绑定器可以处理具有相同名称的多个数据项。例如,在HomeController中,有动作方法Movie
[HttpGet] public ViewResult Movie() { return View(); } [HttpPost] public ViewResult Movie(List<string> movies) { return View("Movie2", movies); }
在[HttpGet]注解属性的Movie动作方法上添加视图Movie.cshtml
@{ ViewBag.Title = "Movie";}<h2>Movie</h2><h4>Enter your three favorite movies:</h4>@using (Html.BeginForm()){ @Html.TextBox("movies") @Html.TextBox("movies") @Html.TextBox("movies") <input type="submit" />}
这里用Html.TextBox辅助器创建了三个input编辑框,它们都会以movies作为其name属性值。产生的html代码如下:
<form action="/home/movie" method="post">
<input id="movies" name="movies" type="text" value="" />
<input id="movies" name="movies" type="text" value="" />
<input id="movies" name="movies" type="text" value="" />
<input type="submit" /></form>
点击了submit提交按钮后,是用[HttpPost]注解属性的Movie动作方法来接收提交的数据,注意这里Movie动作方法的参数List<string> movies,模型绑定器会找出用户提供的、所有name属性值为“movies”的数据,并把它们组装在一起以一个List<string>的形式传递给[HttpPost]注解属性的动作方法Movie。绑定器有足够的智能以支持不同的参数类型,还可以把数据接收成为数组string[],或者一个集合IList<string>。
(1)绑定到自定义类型集合
下面先演示如何用一个Person对象数组来产生html。在HomeController中添加[HttpGet]注解属性的动作方法Register11
[HttpGet] public ViewResult Register11() { List<Person> p=new List<Person>(); p.Add(null); p.Add(null); p.Add(null); return View(p); }
这里用List<Person>定义了数组p,并通过p.Add让它拥有了3个元素。使用return View(p)将数组p作为参数传递给对应的视图。为[HttpGet]注解属性的动作方法Register11添加视图Register11.cshtml,采用强类型ModelBindApp.Models.Person
@model List<ModelBindApp.Models.Person>@{ ViewBag.Title = "Register11";}<h2>Register11</h2>@for (int i = 0; i < Model.Count; i++){ <div class="column"> <h4>Person Number:@i</h4> @Html.EditorFor(m=>m[i]) </div>}
这里采用Html.EditorFor(m=>m[i])将数组里的每个元素(即每个Person对象)的属性产生出来。因为传递过来的数组中,每个对象都是null,所以初始都没有值。也没有单独去渲染HomeAddress属性,所以没有显出HomeAddress有关的属性。
另外,数组传递过来后,也可以用Html.EditorFor辅助器单独渲染个别属性,例如,将上面的Register11.cshtml修改为:
@model List<ModelBindApp.Models.Person>@{ ViewBag.Title = "Register11";}<h2>Register11</h2>@using (Html.BeginForm()){ for (int i = 0; i < Model.Count; i++) { <h4>Person Number:@i</h4> @:First Name: @Html.EditorFor(m => m[i].FirstName) @:Last Name: @Html.EditorFor(m => m[i].LastName) } <input type="submit" />}
执行后显示结果为:
如果提交后,要为[HttpPost]的动作方法上的参数绑定这种数据,只需要定义一个List<Person>类型的集合参数就可以,也就是绑定已索引的集合,就是数组。
[HttpPost] public ViewResult Register11(List<Person> people) { return View("Register12", people); }
为[HttpPost]注解属性的动作方法Register11添加视图Register12.cshtml,采用强类型
List<ModelBindApp.Models.Person>
@model List<ModelBindApp.Models.Person>@{ ViewBag.Title = "Register12";}<h2>Register12</h2>@foreach (ModelBindApp.Models.Person p in Model){ foreach (System.Reflection.PropertyInfo prop in p.GetType().GetProperties()) { <b>@prop.Name : </b>@prop.GetValue(p, null) <br /> } <br /><br />}
例如,输入如下数据
提交后产生的效果为:
在Register11.cshtml中,模板辅助器用集合中对象的索引号作为各个属性名的前缀来生成html。对比下,下面是Register11.cshtml的代码
@model List<ModelBindApp.Models.Person>@{ ViewBag.Title = "Register11";}<h2>Register11</h2>@using (Html.BeginForm()){ for (int i = 0; i < Model.Count; i++) { <h4>Person Number:@i</h4> @:First Name: @Html.EditorFor(m => m[i].FirstName) @:Last Name: @Html.EditorFor(m => m[i].LastName) } <input type="submit" />}
生成html后的html代码:
<h2>Register11</h2><form action="/home/register11" method="post"> <h4>Person Number:0</h4> First Name: <input name="[0].FirstName" placeholder="firstname" type="text" value="" /> Last Name: <input name="[0].LastName" placeholder="lastname" type="text" value="" /> <h4>Person Number:1</h4> First Name: <input name="[1].FirstName" placeholder="firstname" type="text" value="" /> Last Name: <input name="[1].LastName" placeholder="lastname" type="text" value="" /> <h4>Person Number:2</h4> First Name: <input name="[2].FirstName" placeholder="firstname" type="text" value="" /> Last Name: <input name="[2].LastName" placeholder="lastname" type="text" value="" /> <input type="submit" /></form>
提交后,绑定到[HttpPost]注解属性的动作方法Register11(List<Person> people)
由于这是绑定到集合(这里是已索引的集合,相当于数组),默认模型绑定器会搜索以索引号为前缀的Person类的各个属性的值,依据前缀的不同,自动分为不同的Person对象,组装成数组。
当然也可以不用EditorFor模板辅助器来生成html,例如,也可以用下面的形式,将Register11.cshtml改成下面的形式:
<h2>Register11</h2>@using (Html.BeginForm()){ @*for (int i = 0; i < Model.Count; i++) { <h4>Person Number:@i</h4> @:First Name: @Html.EditorFor(m => m[i].FirstName) @:Last Name: @Html.EditorFor(m => m[i].LastName) }*@ <h4>First Person</h4> @:First Name: @Html.TextBox("[0].FirstName") @:Last Name: @Html.TextBox("[0].LastName") <h4>Second Person</h4> @:First Name: @Html.TextBox("[1].FirstName") @:Last Name: @Html.TextBox("[1].LastName") <input type="submit" />}
这里在绑定到提交目的地的动作方法参数上的数组有两个元素。
另外,在cshtml中代码部分的注释为@* … *@,中间的部分为注释。
(2)绑定到非序列化索引的集合
替代序列化数字索引值的另一种方法是使用任意字符串键来定义集合的数据项。当希望在客户端用javascript来动态添加或删除控件,而又不想担心索引序列的维护时,这可能是有用的。
例如将上面例子中的Register11.cshtml修改为:
@model List<ModelBindApp.Models.Person>@{ ViewBag.Title = "Register11";}<h2>Register11</h2>@using (Html.BeginForm()){ <h4>First Person</h4> <input type="hidden" name="index" value="firstPerson" /> @:First Name:@Html.TextBox("[firstPerson].FirstName") @:Last Name:@Html.TextBox("[firstPerson].LastName") <h4>Second Person</h4> <input type="hidden" name="index" value="secondPerson" /> @:First Name:@Html.TextBox("[secondPerson].FirstName") @:Last Name:@Html.TextBox("[secondPerson].LastName") <input type="submit" />}
也可将数据绑定到:
[HttpPost] public ViewResult Register11(List<Person> people) { return View("Register12", people); }
三、手工调用模型绑定
当动作方法定义了参数时,模型绑定过程是自动执行的。也可以直接控制这一过程,使我们能够更明确地控制如何实例化模型对象、从何处获取数据,以及如何处理数据解析错误等问题。
下面是手工调用模型绑定过程的示例
[HttpPost] public ActionResult RegisterMember() { Person myPerson = new Person(); UpdateModel(myPerson); return View(myPerson); }
UpdateModel方法以myPerson模型对象为参数,并试图用标准的绑定过程来获取其public属性的值。手工调用模型绑定过程的原因之一是为了支持模型对象的依赖注入(DI)。例如,如果在使用应用程序范围的依赖性解析器,那么可以把DI添加到Person模型对象的创建过程:
[HttpPost] public ActionResult RegisterMember() { Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person)); UpdateModel(myPerson); return View(myPerson); }
这不是把DI引入到绑定过程的唯一方法,稍后将演示其他方法。
1、限制绑定到特定数据源
当手工调用绑定过程时,可以把绑定过程限制到一个单一的数据源。默认情况下,绑定器会查找四个地方:表单数据、路由数据、查询字符串、以及上传文件。
下面演示把绑定器限定到搜索单一位置——表单数据:
[HttpPost] public ActionResult RegisterMember() { Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person)); UpdateModel(myPerson, new FormValueProvider(ControllerContext)); return View(myPerson); }
UpdateModel方法的这一版本以IValueProvider接口的一个实现为参数,它成为绑定过程的唯一数据源。四个默认数据位置都有一个IValueProvider实现。
内建的IValueProvider实现
源 —— IValueProvider实现
Request.Form —— FormValueProvider
RouteData.Values —— RouteDataValueProvider
Request.QueryString —— QueryStringValueProvider
Request.Files —— HttpFileCollectionValueProvider
限制数据源最普遍的方式是只查找表单值,我们可以用一个雅致的绑定技巧,而不必创建FormValueProvider的实例:
[HttpPost] public ActionResult RegisterMember(FormCollection formData) { Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person)); UpdateModel(myPerson, formData); return View(myPerson); }
FormCollection类实现了IValueProvider接口,而且,如果把动作方法定义成以这个类型为参数,那么模型绑定器将为我们提供一个能够直接传递给UpdateModel方法的对象。
2、处理绑定错误
用户难免会提供一些不能绑定到相应模型属性的值,例如,无效日期,或对数值输入文本等。当显式调用模型绑定时,我们需要负责处理诸如此类的错误。模型绑定器通过抛出InvalidOperationException异常来表示绑定错误。错误的细节通过ModelState特性来查看。当使用UpdateModel方法时,必须做好捕捉该异常的准备,并用ModelState把错误消息表示给用户:
[HttpPost] public ActionResult RegisterMember(FormCollection formData) { Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person)); try { UpdateModel(myPerson, formData); } catch (InvalidOperationException ex) { //基于ModelState提供用户界面反馈 } return View(myPerson); }
另一个可选办法是,使用TryUpdateModel方法。绑定成功返回true,否则返回false:
[HttpPost] public ActionResult RegisterMember(FormCollection formData) { Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person)); if (TryUpdateModel(myPerson, formData)) { //正常处理 } else { //基于ModelState提供用户界面反馈 } }
当自动进行模型绑定时,绑定错误不会发出异常,必须通过ModelState.IsValid属性来检查结果。
四、使用模型绑定接收文件上传
对于接收文件上传,要做的全部工作就是定义一个以HttpPostedFileBase类型为参数的动作方法。模型绑定器会用与这个上传文件相应的数据来填充它.
五、自定义模型绑定系统
1、创建自定义的值提供器
通过定义一个自定义的值提供器,我们可以把自己的数据源添加到模型绑定过程。值提供器需要实现IValueProvider接口,IValueProvider接口如下:
public interface IValueProvider{ bool ContainsPrefix(string prefix); ValueProviderResult GetValue(string key);}
ContainsPrefix方法由模型绑定器调用,以确定这个值提供器是否可以解析给定前缀的数据。GetValue方法返回给定数据键的值,或者,在提供器无法得到合适的数据时,返回null。下面的例子演示了对名称为“CurrentTime”的属性绑定时间戳的一个值提供器。也就是说,在动作方法上如果有对名称为CurrentTime(不区分大小写)的参数请求值,默认就是用绑定的值提供器来提供值,所以表面上看起来并没有显式的传递参数值。
在ModelBindApp中的文件夹Infrastructures中新建类库文件CurrentTimeValueProviderFactory.cs
namespace ModelBindApp.Infrastructures{ public class CurrentTimeValueProviderFactory:ValueProviderFactory { public override IValueProvider GetValueProvider(ControllerContext controllerContext) { return new CurrentTimeValueProvider(); } } public class CurrentTimeValueProvider : IValueProvider { public bool ContainsPrefix(string prefix) { return string.Compare("CurrentTime", prefix, true) == 0; } public ValueProviderResult GetValue(string key) { return ContainsPrefix(key) ? new ValueProviderResult(DateTime.Now, null, CultureInfo.InvariantCulture) : null; } }}
上面的代码表示只响应对“CurrentTime”所作的请求。当接收到通过CurrentTime作为名字请求值时,便返回DateTime.Now的值,对于其他请求则返回null。
我们必须把数据值作为一个ValueProviderResult类型来返回,这个类有三个构造器参数,第一个参数是与请求键关联的数据值(本例中就是请求键CurrentTime,得到关联的值DateTime.Now),第二个参数用于跟踪模型绑定错误,这里没有使用,最后一个参数是与值相关的地域信息,这里指定为InvariantCulture。
为了把这个值提供器注册给应用程序,我们需要创建一个工厂类,以创建提供器的实例。这个工厂类派生于抽象类ValueProviderFactory,就是上面代码中的工厂类CurrentTimeValueProviderFactory。
当模型绑定器要在绑定过程中获取值时,会调用GetValueProvider方法,上述实现简单地创建并返回了CurrentTimeValueProvider类的一个实例。
最后,需要在应用程序中注册这个工厂类,可以在Global.asax的Applicaiton_Start方法中完成注册:
protected void Application_Start(){ AreaRegistration.RegisterAllAreas(); ValueProviderFactories.Factories.Insert(0, new CurrentTimeValueProviderFactory()); RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes);}
如果希望自定义值提供器优先于内建提供器,那么,必须用Insert方法把注册的工厂放到集合的第一个位置,如上所示。如果希望提供器在其他提供器不能提供数据值时作为一个备选,可以用Add方法把工厂追加到集合的末尾,如下所示:
ValueProviderFactories.Factories.Add(new CurrentTimeValueProviderFactory());
下面通过定义一个动作方法,该方法具有一个名为currentTime的参数,可以测试i这个提供器:
public ActionResult Clock(DateTime currentTime) { return Content("The time is " + currentTime.ToLongTimeString()); }
当请求Home/Clock时就会显示出当前时间。