Model binding在HTTP请求中,通过浏览器发送的数据创建.NET对象的过程。每次我们定义一个带参数的action方法(参数对象由model binding创建),已经应用了model binding处理。这里我们要展示model binding系统的工作方式,同时为高级应用演示一些自定义的技巧。
理解model binding
设想有如下定义的action方法:
using System;
using System.Web.Mvc;
using MvcApp.Models;
namespace MvcApp.Controllers {
public class HomeController : Controller {
public ViewResult Person(int id) {
// get a person record from the repository
Person myPerson = null; //...retrieval logic goes here...
return View(myPerson);
}
    }
}
我们的action方法在HomeController类中定义,Visual Studio为我们创建的默认路由可以让我们调用action方法。这是默认代码:
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
当我们接受一个URL请求,比如/Home/Person/23,MVC Framework将映射request的细节,传递正确的值或者对象到我们的action方法。
负 责调用action方法的组件,负责在调用action方法前,获取值给参数。默认的action invoker——ControllerActionInvoker,依靠model binder。 model binder由IModelBinder接口定义,如下,IModelBinder接口:
namespace System.Web.Mvc {
public interface IModelBinder {
object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext);
}
}
在 MVC应用程序中,可以有多个 model binder,每一个 binder可以绑定一个或多个model类型。当action invoker 需要调用action方法时,它会查看model定义的参数,俄日每一个参数类型查找对应的model binder。在上例中,action invoker 将发现action方法有1个int参数,所以它将定位binder去负责绑定int值,同时调用它的BindModel方法。如果没有要操作int的 binder,那么就会使用默认的binder。
model binder负责生成合适的action方法参数值,这意味着转换一些request数据的元素,比如form或者query string值。但是MVC Framework不会对如何获得数据做任何限制。我们后面将会展示一些自定义binder的例子,展示你一些ModelBindingContext类 的细节,ModelBindingContext会传递给IModelBinder.BindMode方法。
使用默认的 Model Binder
尽管应用程序可以有多个binder,大多数还是仅仅依靠内建的binder类—— DefaultModelBinder。这个binder在action invoker 找不到绑定类型的自定义binder时使用。
默认的,这个model binder为了数据匹配要绑定的参数名,搜寻4个地方,如下所列,DefaultModelBinder搜寻参数数据的顺序:
7060aabaf87543a78ff1c6194ed80ecc
以上位置按顺序查询.比如,如果action方法入上面的例子, For example, in the case of the action method shown in
Listing 17-1, DefaultModelBinder 类检查我们的action方法,看到有一个参数命名是id,那么它会按如下顺序查找值。
1. Request.Form["id"]
2. RouteData.Values["id"]
3. Request.QueryString["id"]
4. Request.Files["id"]
注意:还有一个数据源,在接受JSON数据时候使用。我们会在后面详述。
查询值一旦找到,就停止查询,在我们的例子中,搜索了form数据和路由数据的值,但是由于路由片段带有id,在第2步就被找到,将不会在查找query string和upload file。
注意,你能看到action方法参数的名字的重要性了,参数名字和请求数据项的名字必须匹配,这样的话DefaultModelBinder类才可以找到并使用数据。
绑定简单类型
当处理简单参数类型时,DefaultModelBinder通过使用 System.ComponentModel.TypeDescriptor 尝试转换从请求数据中获得的string值为参数的类型。
如果值不能被转换,比如我们提供的apple作为参数值,但是真正需要的是一个int值,那么DefaultModelBinder将不会绑定到这个model。如果要避免这个问题,我们就需要修改参数。我们可以使用可空类型,如下:
public ViewResult RegisterPerson(int? id) {
如果使用这种方法,id参数的值如果没有匹配,或者数据转换失败的话,会设为null。或者,我们可以在没有有效数据的时候,给参数一个默认值,如下:
public ViewResult RegisterPerson(int id = 23) {
绑定复杂类型
当 action方法参数是复杂类型时,换句话就是不能通过TypeConverter类转换的类型,那么DefaultModelBinder 累使用反射来获得公共属性的集合,然后按顺序绑定他们中的每一个。入下面的例子,展示了Person类,我们使用此类来演示model binding。
public class Person {
[HiddenInput(DisplayValue=false)]
public int PersonId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
[DataType(DataType.Date)]
public DateTime BirthDate { get; set; }
public Address HomeAddress { get; set; }
public bool IsApproved { get; set; }
public Role Role { get; set; }
}
默认的model binder检查此类的属性,看看是否是简单类型。如果是,就在request中查询有相同名字的属性作为数据项,也就是说,FirstName属性会使得binder去查询FirstName数据项。
如果这个属性是另一个复杂类型,那么处理过程会为这个新类型重复此过程。公共属性的集合获得后,binder试着为它们找找值。不同点在于这些属性的名字是嵌套的。比如:Person类的HomeAddress 属性是Address类型的,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属性查询值时,model binder为HomeAddress.Line1查询值,换句话说,model对象中属性的名字和属性类型中属性的名字组合了。
创建EASY-TO-BIND HTML
创 建遵循命名格式的HTML最简单的方法就是使用templated view helper。在前面已经介绍过了。当我们在一view model是Person类的view中调用 @Html.EditorFor(m => m.FirstName)时,得到如下HTML:
<input class="text-box single-line" id="FirstName" name="FirstName"
type="text" value="Joe" />
当我们调用@Html.EditorFor(m =&gt; m.HomeAddress.Line1),那么得到的是如下代码:
<input class="text-box single-line" id="HomeAddress_Line1" name="HomeAddress.Line1"
type="text" value="123 North Street" />
你可以看到HTML的name属性被自动的设置为model binder查询的值。我们也可以手动创建HTML,但是对于此类工作,我们喜欢templated helpers 的便捷性。
设定自定义前缀
我们可以为默认的model binder设定自定义前缀,如果我们在发送给客户端的HTML中,包含额外的model对象,那么这就很有用了。下面是一个例子,增加额外的view model对象数据到响应:
@using MvcApp.Models;
@model MvcApp.Models.Person
@{
Person myPerson = new Person() {
FirstName = "Jane", LastName = "Doe"
};
}
@using (Html.BeginForm()) {
@Html.EditorFor(m =&gt; myPerson)
@Html.EditorForModel()
<input type="submit" value="Submit" />
}
为 了在view中创建和植入的Person对象,我们在view中使用EditorFor方法生成HTML,虽然这会很容易的通过ViewBag传递到 view。输入的拉姆达表达式是model对象,(由m表示)但是我们忽略这个,同时返回第二个person对象作为目标呈现。我们也可以调用 EditorForModel方法,这样发送给用户的HTML包含2个person对象的数据。
当我面呈现这样对象时,templated view helper在HTML元素的name属性上使用前缀。这就在主view model上分隔了数据。前缀从变量名中提取,myPerson。比如,下面是由View呈现的HTML,FirstName属性。
<input class="text-box single-line" id="myPerson_FirstName" name="myPerson.FirstName"
type="text" value="Jane" />
这个元素的name特性的值,通过对属性名和变量名加前缀创建——myPerson.FirstName。当查询数据的时候,model binder期待这个方法同时使用action方法参数的名字作为可能的前缀。如果我们的表单提交的action方法已经有如下签名:
第一个参数对象将被绑定到未加前缀的数据,第二个会绑定到有参数名作为前缀的数据,即myPerson.FirstName,myPerson.LastName等等。
如果我们不想以这种方法束缚我们的参数名字,那么我们可以通过Bind特性使用自定义前缀。如下代码所示,使用Bind特性知道自定义数据前缀:
public ActionResult Register(Person firstPerson,
[Bind(Prefix="myPerson")] Person secondPerson)
我们对myPerson设置了前缀属性。这意味着默认的 model binder将为数据项使用myPerson作为前缀,即使参数名字是secondPerson。
有选择的绑定属性
设 想,Person类的IsApproved属性非常敏感。使用之前介绍的技术,我们可以阻止这属性在HTMLModel中呈现,但是对于恶意的用户,恶意 简单的zaiURL后增加 ?IsAdmin=true,然后在提交表单。如果这样做了,model binder将很愉快的在绑定过程使用这个数据。
幸运的是,我们可以使用Bind特性在绑定过程中包括或者排除model属性。要知道仅仅某些属性可以包括,我需要设置Include特性的值,如下代码:
public ActionResult Register([Bind(Include="FirstName, LastName")] Person person) {
上述代码指定了仅有FirstName和LastName属性包括在绑定过程中,Person属性的其他值则被护绿。或者,我们可以指定这些属性为excluded,如下代码:
public ActionResult Register([Bind(Exclude="IsApproved, Role")] Person person) {
上述代码告诉midel binder在绑定过程包含所有的person类的属性,除了IsApproved和Role.
当我们这样使用绑定特性的时候,仅仅应用在一个单一的action方法上,如果我们要应用这个策略到所有的controller的所有的action方法,那么我们在model类上使用Bind特性,如下代码,在Model类上使用Bind特性。
[Bind(Exclude="IsApproved")]
public class Person {
[HiddenInput(DisplayValue=false)]
public int PersonId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
[DataType(DataType.Date)]
public DateTime BirthDate { get; set; }
public Address HomeAddress { get; set; }
public bool IsApproved { get; set; }
public Role Role { get; set; }
}
绑定到数组和集合
认的model binder能优雅的处理含有相同名字的多个数据项。比如,如下的view,view中的HTML元素有相同的name:
@{
ViewBag.Title = "Movies";
}
Enter your three favorite movies:
@using (Html.BeginForm()) {
@Html.TextBox("movies")
@Html.TextBox("movies")
@Html.TextBox("movies")
<input type=submit />
}
我们使用 Html.TextBox方法创建了3个input元素,name属性都是movies。
<input id="movies" name="movies" type="text" value="" />
<input id="movies" name="movies" type="text" value="" />
<input id="movies" name="movies" type="text" value="" />
我们可以使用如下的action方法接收用户输入的值,代码如下:
[HttpPost]
public ViewResult Movies(List<string> movies) {
...
model binder将查找所有的用户提供的值,在List<string>中,传递他们到Movies action方法。这个binder非常聪明,支持不同的参数类型。我们可以选择接收数据为stringp[]或者甚至是 IList<string>。
绑定到用户类型的集合
处理多值绑定的方法非常的优雅,但是如果我们想要它能支持自定义类型,那么我们必须以某种格式处理HTML,下面的代码展示了我们对Person对象数组该如何做:
@model List<MvcApp.Models.Person>
@for (int i = 0; i < Model.Count; i++) {
&lt;h4>Person Number: @i</h4>
@:First Name: @Html.EditorFor(m =&gt; m[i].FirstName)
@:Last Name: @Html.EditorFor(m =&gt; m[i].LastName)
}
这个templated helper生成HTML,在每个属性上用集合中对象的索引号加前缀,如下:
...
<h4>Person Number: 0</h4>
First Name: <input class="text-box single-line" name="[0].FirstName" type="text" value="Joe" />
Last Name: <input class="text-box single-line" name="[0].LastName" type="text" value="Smith" />
<h4>Person Number: 1</h4>
First Name: <input class="text-box single-line" name="[1].FirstName" type="text" value="Jane" />
Last Name: <input class="text-box single-line" name="[1].LastName" type="text" value="Doe" />
...
要绑定这种数据,我们只需要定义带有view model类型集合参数的action方法。如下代码
[HttpPost]
public ViewResult Register(List<Person> people) {
...
因为我们绑定到一个集合,默认的model binder将为加过前缀的Person类的属性查询值。当然,我们不必使用templated helpers老生成HTML。我们可以在view中显式的做,入下面例子演示的那样
<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")
只要我们确保index值正确的生成,model binder将能找到并且绑定我们所定义的全部元素。
绑定到无索引的集合
另一种定义集合项的方法是使用任意的字符串作为key。这对客户端使用javascript动态增加删除控件很有用而且不需要担心维护索引顺序。
使用这种方法,我们需要定义一个hidden input元素,命名为index,为item指定key,如下面的代码所示:
<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元素的name属性加前缀,以此匹配hidden 索引元素的值。model binder发现index,并且在处理期间,使用它关联数据值。
绑定到字典
默认的model binder能绑定到一个字典,但我们必须遵循一个非常特殊的命名序列。
<h4>First Person</h4>
<input type="hidden" name="[0].key" value="firstPerson"/>
First Name: @Html.TextBox("[0].value.FirstName")
Last Name: @Html.TextBox("[0].value.LastName")
<h4>Second Person</h4>
<input type="hidden" name="[1].key" value="secondPerson"/>
First Name: @Html.TextBox("[1].value.FirstName")
Last Name: @Html.TextBox("[1].value.LastName")
我们绑定到一个字典类Dictionary<string, Person>或者IDictionary<string, Person>,这个字典包含Person对象,我们可以使用如下的action方法接受数据
[HttpPost]
public ViewResult Register(IDictionary<string, Person> people) {
手动调用Model Binding
当一个action方法定义了参数,模型绑定过程就会自动执行,但是我们也能够直接控制绑定过程。对于初始化model对象,数据值在那里获得,怎样处理数据转换错误,我们有了更多的控制能力。
下例演示了action方法手动调用绑定过程。
[HttpPost]
public ActionResult RegisterMember() {
Person myPerson = new Person();
UpdateModel(myPerson);
return View(myPerson);
}
Update 方法接受我们之前创建的model对象作为参数,尝试使用标准的绑定过程为它的公共属性获得值,一个手动调用model binding的理由就是能在model对象中支持依赖注入(dependency injection (DI))。下面的例子增加了一个依赖注入到model对象创建。
[HttpPost]
public ActionResult RegisterMember() {
Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person));
UpdateModel(myPerson);
return View(myPerson);
}
如演示的那样,这不是仅有的方法引入DI到binding过程。我们在后面还会展示其他方法。
限制对指定数据源的绑定
当我们收到调用绑定过程,我们可以限制绑定过程在但一个的数据源上。默认的,binder查询4个地方,表单数据,路由数据,query string,和任何上传的文件。
下面代码演示了如何限制绑定在表单数据上。
[HttpPost]
public ActionResult RegisterMember() {
Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person));
UpdateModel(myPerson, new FormValueProvider(ControllerContext));
return View(myPerson);
}
这个版本的UpdateModel方法接收IValueProvider接口的实现,使之在绑定过程中是仅有的数据源。四个默认数据位置的每一个都有IValueProvider 实现,如下表:ed9cd034ae54435db549ea2bacc47fd5
限制数据源的最常用的方法是仅在form值中查询,我们可以使用一个简洁的绑定方法,意味着不需要创建FormValueProvider的实例。如下演示:
[HttpPost]
public ActionResult RegisterMember(FormCollection formData) {
Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person));
UpdateModel(myPerson, formData);
return View(myPerson);
}
FormCollection类实现了IValueProvider接口,如果我们定义了action方法带有这种类型的参数。那么model binder将提供我们一个可以直接传给UpdateModel方法的参数。
通过对UpdateModel方法的重载,允许我们指定一个前缀来查询,也允许我们指定哪个model属性应该被包含在绑定过程中。
处理绑定错误
用 户不可避免的会传递不能绑定到model属性的值,比如无效的日期,或者数字文本。当我们显式的调用model binding时,我们对任何诸如此类的错误负责。mdel binder通过抛出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) {
//...provide UI feedback based on 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)) {
//...proceed as normal
} else {
//...provide UI feedback based on ModelState
}
}
如果你不喜欢捕获或者处理异常,那么就使用TryUpdateModel 而不是UpdateModel 。在model bingding过程中,是没用任何差别的。
当model binding被自动的调用,绑定错误并不会由异常发信号,我们必须通过ModelState.IsValid属性,自己检查结果。
使用Model Binding接收文件上传
要接收上传的文件,我们要做的就是定义一个action 方法,带有HttpPostedFileBase 类型参数。如下代码演示:
public ActionResult Upload(HttpPostedFileBase file) {
// Save the file to disk on the server
string filename = "myfileName"; // ... pick a filename
file.SaveAs(filename);
// ... or work with the data directly
byte[] uploadedBytes = new byte[file.ContentLength];
file.InputStream.Read(uploadedBytes, 0, file.ContentLength);
// Now do something with uploadedBytes
}
我们已经以指定的格式创建了HTML表单,允许用户上传文件。演示代码如下:
@{
ViewBag.Title = "Upload";
}
<form action="@Url.Action("Upload")" method="post" enctype="multipart/form-data">
Upload a photo: <input type="file" name="photo" />
<input type="submit" />
</form>
关键点是将enctype属性设置为multipart/form-data.如果我们不这样做,浏览器会传送文件名字,而不是文件本身。(这是浏览器工作的方式,而不是MVC Framework的特性)
上述代码,我们使用HTML呈现了form元素,我们也可以使用 Html.BeginForm方法生成这个元素,但是只有通过重载方法才能满足4个参数,所以我们觉得使用纯HTML更加可读。
自定义Model Binding系统
我们已经展示了默认的model binding过程。和你想的一样,还有一些不同的方法使我们自定义binding system。接下来我们会展示几个例子。
创建一个自定义Value Provider
通过自定义Value provider,我们可以把我们自己的数据源加到model binding过程中。Value provider实现了IValueProvider接口,如下面的代码,IValueProvider接口
namespace System.Web.Mvc {
using System;
public interface IValueProvider {
bool ContainsPrefix(string prefix);
ValueProviderResult GetValue(string key);
}
}
ContainsPrefix 方法有model binder调用,它决定了value provider是否可以根据提供的前缀处理数据。GetValue方法返回一个有给定的数据key的值,或者provider没有合适的数据的话,则返 回null。下列代码展示了绑定了类型为timestamp的CurrentTime属性的provider的值。下面的代码只是一个演示:
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的请求。当我们得到这样的请求,我们返回DateTime.Now值。对所有的其他请求,则返回null。意味着我们不能提供其他数据。
我 们必须返回数据作为 ValueProviderResult类,此类有3个构造参数,第一个是我们想要和请求key关联的数据项。第二个参数用来追踪model binding错误的,不会应用于我们的例子中。最后一个参数是和value相关的cultrue信息。我们设定为InvariantCulture。
要注册我们 value provider ,我们需要创建一个工厂类,创建provider的实例。此类继承与抽象类ValueProviderFactory类。下面显示了一个针对CurrentTimeValueProvider的工厂类。
public class CurrentTimeValueProviderFactory : ValueProviderFactory {
public override IValueProvider GetValueProvider(ControllerContext controllerContext) {
return new CurrentTimeValueProvider();
}
}
当model binder想获得值时,GetValueProvider方法会被调用。我们的实现创建和返回了CurrentTimeValueProvider的实例。
最后一步是注册工厂类,在Global.asax中的Application Start方法中。如下代码:
protected void Application Start() {
AreaRegistration.RegisterAllAreas();
ValueProviderFactories.Factories.Insert(0, new CurrentTimeValueProviderFactory());
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
我 们通过增加实例到静态的ValueProviderFactories.Factories集合中注册我们的工厂类、我们之前解释过。model binder按照顺序查遍历value providers。如果我们想让自定义provider优先于内建的那些,那么我们必须使用Insert方法,使我们的工厂在集合的首位,如上述代码所 示。如果我们想要我们的provider作为后备,当其他provider不支持数据值的时候才启用,那么需要使用Add方法将工厂类追加到集合。就像这 样:
ValueProviderFactories.Factories.Add(new CurrentTimeValueProviderFactory());
我们可以通过定义一个含有DateTime类型参数的action方法,测试我们的provider。如下:
public ActionResult Clock(DateTime currentTime) {
    return Content("The time is " + currentTime.ToLongTimeString());
}
因为我们的value provider是model binder请求数据中的第一个,我们可以提供绑定到这个参数的值
使用ModelBinder特性
The final way of registering a custom model binder is to apply the ModelBinder attribute to the model
class, as shown in Listing 17-36.
最后一个注册自定义model binder的方法是应用ModelBinder特性到model类,如下代码:
[ModelBinder(typeof(PersonModelBinder))]
public class Person {
[HiddenInput(DisplayValue=false)]
public int PersonId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
[DataType(DataType.Date)]
public DateTime BirthDate { get; set; }
public Address HomeAddress { get; set; }
public bool IsApproved { get; set; }
public Role Role { get; set; }
}
ModelBinder 特性中的参数是model binder的类型,此类型在绑定这种类型对象时使用。我们指定我们的自定义PersonModelBinder类。我们趋向于实现 IModelBinderProvider接口来处理需求,这让人感觉和MVC Framework设计的其余部分更一致,或者在这种情况下使用 [ModelBinder] 。然而,既然所有的这些技术都导致相同的行为,因此我们不必介意使用哪种方式。

原文连接:http://cnn237111.blog.51cto.com/2359144/832069