2012年3月28日星期三

【译著】17 模型绑定 — 精通 MVC 3 框架

【译著】17 模型绑定 — 精通 MVC 3 框架



C H A P T E R 17
■ ■ ■

Model Binding
模型绑定

Model binding is the process of creating .NET objects using the data sent by the browser in an HTTP request. We have been relying on the model binding process each time we have defined an action method that takes a parameter—the parameter objects are created by model binding. In this chapter, we’ll show you how the model binding system works and demonstrate the techniques required to customize it for advanced use.
模型绑定是指,用浏览器以HTTP请求方式发送的数据来创建.NET对象的过程。每当我们定义具有参数的动作方法时,我们一直在依赖着模型绑定过程 — 这些参数对象是由模型绑定创建的。本章将介绍模型绑定系统如何工作,并演示对它进行定制以实现高级用法所需要的技术。

Understanding Model Binding
理解模型绑定

Imagine that we have defined an action method in a controller as shown in Listing 17-1.
假设我们已经在一个控制器中定义了一个动作方法,如清单17-1所示。

Listing 17-1. A Simple Action Method
清单17-1. 一个简单的动作方法

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); } }}

Our action method is defined in the HomeController class, which means the default route that Visual Studio creates for us will let us invoke our action method. As a reminder, here is the default route:
我们的动作方法是在HomeController类中定义的,这意味着Visual Studio创建的默认路由将使我们能够调用这个动作方法。作为提醒,以下是该默认路由:

routes.MapRoute(    "Default", // Route name    "{controller}/{action}/{id}", // URL with parameters    new { controller = "Home", action = "Index", id = UrlParameter.Optional });

When we receive a request for a URL such as /Home/Person/23, the MVC Framework has to map the details of the request in such a way that it can pass appropriate values or objects as parameters to our action method.
当我们接收到像/Home/Person/23这样的URL请求时,MVC框架需要对该请求的细节进行映射,其方式是,把相应的值或对象作为参数传递给这个动作方法。

The action invoker, the component that invokes action methods, is responsible for obtaining values for parameters before it can invoke the action method.
动作调用器,即调用动作方法的组件,负责在调用动作方法之前给参数获取相应的值。

The default action invoker, ControllerActionInvoker (introduced in Chapter 11), relies on model binders, which are defined by the IModelBinder interface, as shown in Listing 17-2.
默认动作调用器ControllerActionInvoker(第11章作过介绍),依赖于模型绑定器。而模型绑定器是由IModelBinder接口定义的,如清单17-2所示。

Listing 17-2. The IModelBinder Interface
清单17-2. IModelBinder接口

namespace System.Web.Mvc {
public interface IModelBinder { object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext); }}

There can be multiple model binders in an MVC application, and each binder can be responsible for binding one or more model types. When the action invoker needs to call an action method, it looks at the parameters that the method defines and finds the responsible model binder for each parameter type. In the case of Listing 17-1, the action invoker would find that the action method had one int parameter, so it would locate the binder responsible for binding int values and call its BindModel method. If there is no binder that will operate on int values, then the default model binder is used.
在一个MVC应用程序中,可以有多个模型绑定器,每个绑定器可以负责绑定一个或多个模型类型。当动作调用器需要调用一个动作方法时,它会考查这个方法所定义的参数,并查找各个参数类型所依赖的模型绑定器。在清单17-1的情况下,动作调用器会发现该动作方法具有一个int参数,因此,它会查找负责int值绑定的绑定器,并调用它的BindModel方法。如果没有对int值进行操作的绑定器,那么便使用默认的模型绑定器。

注:本章中必须分清:调用器、默认调用器、绑定器、默认绑定器等这样一些概念,还要理清:请求、请求数据、模型、模型属性、属性类型、控制器、动作方法、动作参数、参数类型、参数值、动作调用器、模型绑定器等概念之间的相互关系及其作用。这些对理解本章内容是很重要的。作为译者,我都深深感觉这些概念及其相互关系十分绕头。如果读者对这些没有清晰的概念,理解本章内容是很困难的 — 译者注

A model binder is responsible for generating suitable action method parameter values, and this usually means transforming some element of the request data (such as form or query string values), but the MVC Framework doesn’t put any limits on how the data is obtained. We’ll show you some examples of custom binders later in this chapter and show you some of the features of the ModelBindingContext class, which is passed to the IModelBinder.BindModel method (you can see details of the ControllerContext class, which is the other BindModel parameter, in Chapter 12).
模型绑定器负责生成适当的动作方法参数值,这通常意味着要对请求数据(如表单数据,或查询字符串值)的某些元素进行转换,但是MVC框架对如何获取这些数据并无任何限制。我们将在本章稍后演示一些自定义绑定器示例,并展示ModelBindingContext类的一些特性,这是传递给IModelBinder.BindModel方法的一个类(在第12章中,你可以看到ControllerContext类的细节,这是另一个BindModel参数)。

Using the Default Model Binder
使用默认模型绑定器

Although an application can have multiple binders, most just rely on the built-in binder class, DefaultModelBinder. This is the binder that is used by the action invoker when it can’t find a custom binder to bind the type.
虽然一个应用程序可以有多个绑定器,但大多数只依赖于内建的绑定器类,DefaultModelBinder。当动作调用器找不到对该类型进行绑定的自定义绑定器时,它是由动作调用器使用的一个绑定器。

By default, this model binder searches four locations, shown in Table 17-1, for data matching the name of the parameter being bound.
默认地,这个模型绑定器搜索四个位置,如表7-1所示,以获取与被绑定的参数名匹配的数据。

Table 17-1. The Order in Which the DefaultModelBinder Class Looks for Parameter Data
表17-1. DefaultModelBinder类查找参数数据的顺序
Source
Description
描述
Request.Form Values provided by the user in HTML form elements
由用户在HTML的form(表单)元素中提供的值
RouteData.Values The values obtained using the application routes
用应用程序路由获得的值
Request.QueryString Data included in the query string portion of the request URL
包含在URL请求的查询字串部分的数据
Request.Files Files that have been uploaded as part of the request (see the “Using Model Binding to Receive File Uploads” section for details of working with file uploading)
作为请求部分的上传文件(参见“用模型绑定接受文件上传”章节的文件上传细节)。

The locations are searched in order. For example, in the case of the action method shown in Listing 17-1, the DefaultModelBinder class examines our action method and finds that there is one parameter, called id. It then looks for a value as follows:
这些位置被依序搜索。例如,在清单17-1所示的动作方法情况下,DefaultModelBinder类会考查我们的动作方法,并发现有一个名为id的参数。然后它会查找以下的一个值:

  1. Request.Form["id"]
  2. RouteData.Values["id"]
  3. Request.QueryString["id"]
  4. Request.Files["id"]

■ Tip There is another source of data that is used when JSON data is received. We explain more about JSON and demonstrate how this works in Chapter 19.
提示:当接收JSON数据时,有另一个要用到的数据源。我们将在19章解释JSON,并演示其如何工作。

The search stops as soon as a value is found. In the case of our example, the form data and route data values are searched, but since a routing segment with the name id will be found in the second location, the query string and uploaded files will not be searched at all.
只要找到一个值,搜索便会停止。就我们的示例而言,将会搜索表单数据值和路由数据值,但由于在第二个位置便会找到带有id名的路由片段,则根本不会搜索查询字串和上载文件。

■ Note You can see how important the name of the action method parameter is—the name of the parameter and the name of the request data item must match in order for the DefaultModelBinder class to find and use the data.
注:由此可以看出,动作方法的参数名称很重要 — 参数名称和请求数据项名称必须与DefaultModelBinder类查找和使用数据的顺序相匹配。

Binding to Simple Types
绑定简单类型

When dealing with simple parameter types, the DefaultModelBinder tries to convert the string value that has been obtained from the request data into the parameter type using the System.ComponentModel.TypeDescriptor class.
当处理简单参数类型时,DefaultModelBinder会试图用System.ComponentModel.TypeDescriptor类把已经从请求数据获得的字符串值转换成参数类型。

If the value cannot be converted—for example, if we have supplied a value of apple for a parameter that requires an int value—then the DefaultModelBinder won’t be able to bind to the model.
如果这个值不能被转换 — 例如,如果我们给一个需要一个int值的参数提供了一个“apple”值 — 那么DefaultModelBinder便不能绑定该模型。

We can modify our parameters if we want to avoid this problem. We can use a nullable type, like this:
如果想避免这种问题,我们可以修改参数。我们可以用一个可空(nullable)类型,像这样:

public ViewResult RegisterPerson(int? id) {

If we take this approach, the value of the id parameter will be null if there is no matching, convertible data found in the request. Alternatively, we can make our parameter optional by supplying a default value to be used when there is no data available, like this:
如果采用这种方式,那么,在请求中没有匹配的可转换数据的情况下,id参数的值将为null。另一个办法是,我们可以在无数据可用时,通过给参数提供一个可用的默认值的办法,使该参数成为可选的,像这样:

public ViewResult RegisterPerson(int id = 23) {

CULTURE-SENSITIVE PARSING
文化敏感解析

The DefaultModelBinder class uses different culture settings to perform type conversions from different areas of the request data. The values obtained from URLs (the routing and query string data) are converted using culture-insensitive parsing, but values obtained from form data are converted taking culture into account.
DefaultModelBinder类对来自不同地域的请求数据采用不同的文化设置来执行类型转换。从URL获得的值(路由及查询字串数据)采用非文化敏感解析进行转换,但从表单数据获得的值则考虑文化因素进行转换。

注:这里的文化(Culture)是指,在计算机上设置不同的区域或地域(比如,中国、英国、美国等),则计算机便会采用相应的语言(即文化)(如中文、英语、美语等)。不同文化的差异主要表现在日期和货币的表示方式不同。下一段落描述了日期表示差异。货币表示差异不仅在于货币符上,还表现在货币的分位符和小数点的表示上,比如德国的货币小数点用的是逗号 — 译者注

The most common problem that this causes relates to DateTime values. Culture-insensitive dates are expected to be in the universal format yyyy-mm-dd. Form date values are expected to be in the format specified by the server. This means a server set to the U.K. culture will expect dates to be in the form dd-mm-yyyy, while a server set to the U.S. culture will expect the format mm-dd-yyyy, though in either case yyyy-mm-dd is acceptable too.
这种情况引起的最普遍的问题与DateTime值有关。我们希望非文化敏感日期采取通用格式yyyy-mm-dd。希望表单日期值是服务器设定的格式。意即,如果服务器的文化设置为U.K.(英国),希望日期的格式是dd-mm-yyyy,而服务器如果设置为U.S.(美国),则希望其格式为mm-dd-yyyy,而两种情况下都可以接受yyyy-mm-dd格式。

A date value won’t be converted if it isn’t in the right format. This means we must make sure that all dates included in the URL are expressed in the universal format. We must also be careful when processing date values that users provide. The default binder assumes that the user will express dates using the format of the server culture, something that is unlikely to always happen in an MVC application that has international users.如果日期值不是正确的格式,则不会被转换。意即,我们必须确保URL中的所有日期都被表示成通用格式。在处理用户提供的日期值时(表单中提供的日期值 — 译者注),我们也必须小心。默认绑定器假定,用户将用服务器的文化设置的格式来表示日期,这是具有国际用户的MVC应用程序不可能达到的愿望。

Binding to Complex Types
绑定复合类型

When the action method parameter is a complex type (in other words, any type that cannot be converted using the TypeConverter class), then the DefaultModelBinder class uses reflection to obtain the set of public properties and then binds to each of them in turn. Listing 17-3 shows the Person class we used in the previous chapter. We’ll use this again to demonstrate model binding for complex types.
当动作方法参数是复合类型时(即,不能用TypeConverter类进行转换的属性),那么,DefaultModelBinder类将用反射来获取public属性集,然后依次逐一进行绑定。清单17-3演示了我们在上一章使用的Person类。我们再次用这个类来演示复合类型的模型绑定。

Listing 17-3. A Complex Model Class
清单17-3. 一个复合模型类

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; }}

The default model binder checks the class properties to see whether they are simple types. If they are, then the binder looks for a data item in the request that has the same name as the property. That is, the FirstName property will cause the binder to look for a FirstName data item.
默认模型绑定器检查这个类的属性,以考查它们是否是简单类型。如果是,那么绑定器会在请求中查找与该属性同名的数据项。于是,FirstName属性将引发绑定器查找FirstName数据项。

If the property is another complex type, then the process is repeated for the new type; the set of public properties are obtained, and the binder tries to find values for all of them. The difference is that the property names are nested. For example, the HomeAddress property of the Person class is of the Address type, which is shown in Listing 17-4.
如果属性是一种复合类型,那么,该过程会针对新类型(所获得的public属性集)重复执行,绑定器会试图找出所有属性的值。不同的是这些属性名是嵌套的。例如,Person类的HomeAddress属性的类型是Address,如清单17-4所示。

Listing 17-4. A Nested Model Class
清单17-4. 一个嵌套的模型类

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; }}

When looking for a value for the Line1 property, the model binder looks for a value for HomeAddress.Line1—in other words, the name of the property in the model object combined with the name of the property in the property type.
在查找Line1属性值时,模型绑定器查找的是HomeAddress.Line1的值 — 即,模型对象的属性名(HomeAddress)与属性类型(Address)的属性名(Line1)的组合。(在这句话中,模型对象指Person、模型对象的属性名是Person中的HomeAddress属性、属性类型指HomeAddress属性的类型Address、属性类型的属性名是指Address中的Line1属性。绕头吧? — 译者注)。

CREATING EASY-TO-BIND HTML
创建易于绑定的HTML

The easiest way to create HTML that follows this naming format is to use the templated view helpers, which we described in Chapter 16. When we call @Html.EditorFor(m => m.FirstName) in a view with a Person view model, we get the following HTML:
创建遵循这种命名格式的HTML最容易的办法是使用我们在第16章描述的模板视图辅助器。当我们在带有Person视图模型的视图中调用@Html.EditorFor(m => m.FirstName)时,我们得到以下HTML:

<input  id="FirstName" name="FirstName"    type="text" value="Joe" />

and when we call @Html.EditorFor(m => m.HomeAddress.Line1), then we get the following:
而当我们调用@Html.EditorFor(m => m.HomeAddress.Line1)时,那么便得到以下结果:

<input  id="HomeAddress_Line1" name="HomeAddress.Line1"    type="text" value="123 North Street" />

You can see that the name attributes of the HTML are automatically set to values that the model binder looks for. We could create the HTML manually, but we like the convenience of the templated helpers for this kind of work.
你可以看到,HTML的name属性被自动设置为模型绑定器查找的值。我们可以用手工方式创建这种HTML,但我们更喜欢模板辅助器做这种工作的方便性。

Specifying Custom Prefixes
指定自定义前缀

We can specify a custom prefix for the default model binder to look for when it is searching for data items. This can be useful if we have included additional model objects in the HTML we sent to the client. As an example, consider the view shown in Listing 17-5.
在默认模型绑定器查找数据项时,我们可以为它指定一个自定义前缀。如果我们在发送给客户端的HTML中包含了额外的模型对象,这可能是有用的。作为一个例子,考虑清单17-5所示的视图。

Listing 17-5. Adding Additional View Model Object Data to a Response
清单17-5. 对响应添加额外的视图模型对象数据

@using MvcApp.Models;@model MvcApp.Models.Person
@{ Person myPerson = new Person() { FirstName = "Jane", LastName = "Doe" };}
@using (Html.BeginForm()) {
@Html.EditorFor(m => myPerson) @Html.EditorForModel()
<input type="submit" value="Submit" />}

We have used the EditorFor helper in this view to generate HTML for a Person object that we created and populated in the view, although this could as easily have been passed to the view through the ViewBag. The input to the lambda expression is the model object (represented by m), but we ignore this and return our second Person object as the target to render. We also call the EditorForModel helper so that the HTML sent to the user contains the data from two Person objects.
在这个视图中,我们使用了EditorFor辅助器,以便对我们在视图中创建并驻留的Person对象(myPerson)生成相应的HTML,尽管这可以很容易地通过ViewBag传递给视图。送入lambda表达式的是模型对象(由m表示),但我们把忽略了它,而把第二个Person对象作为渲染目标。我们也调用了EditorForModel辅助器,以便发送给用户的HTML含有这两个Person对象的数据。

When we render objects like this, the templated view helpers apply a prefix to the name attributes of the HTML elements. This is to separate the data from that of the main view model. The prefix is taken from the variable name, myPerson. For example, here is the HTML that was rendered by the view for the FirstName property:
当我们像这样渲染对象时,模板视图辅助器会把一个前缀运用于HTML元素的name属性。以此与主视图模型的数据加以区别。这个前缀取自变量名,myPerson。例如,以下是该视图对FirstName属性所渲染的HTML:

<input  id="myPerson_FirstName" name="myPerson.FirstName"    type="text" value="Jane" />

The value for the name attribute for this element has been created by prefixing the property name with the variable name—myPerson.FirstName. The model binder expects this approach and uses the name of action method parameters as possible prefixes when looking for data. If the action method our form posted to has the following signature:
该元素的name属性值是用该变量名作为该属性名的前缀来创建的 — myPerson.FirstName。模型绑定器会预测这种方式,并在查找数据时把动作方法参数作为可能的前缀。如果表单递交的动作方法具有以下签名:

public ActionResult Index(Person firstPerson, Person myPerson) {

the first parameter object will be bound using the unprefixed data, and the second will be bound by looking for data that starts with the parameter name—that is, myPerson.FirstName,myPerson.LastName, and so on.
第一个参数对象将用非前缀数据绑定,而第二个将以参数名为前缀查找的数据进行绑定 — 即,myPerson.FirstName、myPerson.LastName等等。

If we don’t want our parameter names to be tied to the view contents in this way, then we can specify a custom prefix using the Bind attribute, as shown in Listing 17-6.
如果我们不希望以这种方式把参数名绑定到视图内容,那么我们可以用Bind属性来指定一个自定义前缀,如清单17-6所示。

Listing 17-6. Using the Bind Attribute to Specify a Custom Data Prefix
清单17-6. 用Bind属性指定一个自定义数据前缀

public ActionResult Register(Person firstPerson,    [Bind(Prefix="myPerson")] Person secondPerson)

We have set the value of the Prefix property to myPerson. This means that the default model binder will use myPerson as the prefix for data items, even though the parameter name is secondPerson.
我们把Prefix属性的值设置为myPerson。这意味着,默认模型绑定器将用myPerson作为各数据项的前缀,即使参数名是secondPerson。

Selectively Binding Properties
有选择地绑定属性

Imagine that the IsApproved property of the Person class is especially sensitive. We can prevent the property being rendered in the model HTML using the techniques from Chapter 16, but a malicious user could simply append ?IsAdmin=true to a URL when submitting a form. If this were done, the model binder would happily discover and use the data value in the binding process.
假设Person类的IsApproved属性是特别敏感的。我们可以用第16章的技术来防止在模型的HTML中渲染这个属性,但恶意用户可以简单地把?IsAdmin=true加到URL上。如果是这样,模型绑定器会在绑定过程中开心地发现并使用这个数据值。

Fortunately, we can use the Bind attribute to include or exclude model properties from the binding process. To specify that only certain properties should be included, we set a value for the Include attribute property, as shown in Listing 17-7.
幸运的是,我们可以用Bind属性把模型属性包含到或排除出绑定过程。要指定只有某些属性应该被包含,我们给Include属性设置一个值,如清单17-7所示。

Listing 17-7. Using the Bind Attribute to Include Model Properties in the Binding Process
清单17-7. 使用Bind属性把模型属性包含到绑定过程

public ActionResult Register([Bind(Include="FirstName, LastName")] Person person) {

The listing specifies that only the FirstName and LastName properties should be included in the binding process; values for other Person properties will be ignored. Alternatively, we can specify that properties be excluded, as shown in Listing 17-8.
该清单指定,只有FirstName和LastName属性应该被包含在绑定过程中,其它Person属性的值将被忽略。另一种方法是,我们可以指定被排除的属性,如清单17-8所示。

Listing 17-8. Using the Bind Attribute to Exclude Model Properties from the Binding Process
清单17-8. 使用Bind属性把模型属性排除出绑定过程

public ActionResult Register([Bind(Exclude="IsApproved, Role")] Person person) {

This listing tells the model binder to include all of the Person properties in the binding process except for IsApproved and Role.
该清单告诉模型绑定器,在绑定过程中要包含除IsApproved和Role以外的所有Person属性。

When we use the Bind attribute like this, it applies only to a single action method. If we want to apply our policy to all action methods in all controllers, then we can use the Bind attribute on the model class itself, as shown in Listing 17-9.
当我们像这样使用Bind属性时,它只运用于一个单一的动作方法。如果我们希望把这个策略运用于所有控制器的所有动作方法,那么,我们可以在模型类上运用这个Bind属性,如清单17-9所示。

Listing 17-9. Using the Bind Attribute on a Model Class
清单7-9. 把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; }}

The attribute has the same effect as when applied to an action method parameter, but it is applied by the model binder any time that a Person object is bound.
该属性具有把它运用于一个动作方法参数的同样效果,但在Person对象被绑定的任何时候,模型绑定器都会运用它。

■ Tip If the Bind attribute is applied to the model class and to an action method parameter, a property will be included in the process only if neither application of the attribute excludes it. This means the policy applied to the model class cannot be overridden by applying a less restrictive policy to the action method parameter.
提示:如果Bind属性被运用于模型类,同时也被运用于一个动作方法参数,那么只有未被该注解属性排除的模型属性才会被包含在绑定过程之中。这意味着,运用于模型类的策略不能被运用于动作方法参数的弱限制策略所覆盖。

Binding to Arrays and Collections
绑定数组与集合

One elegant feature of the default model binder is how it deals with multiple data items that have the same name. For example, consider the view shown in Listing 17-10.
默认模型绑定器的优雅特性之一是它如何处理具有同名的多个数据项。例如,考虑清单17-10所示的视图。

Listing 17-10. A View That Renders HTML Elements with the Same Name
清单17-10. 渲染同名HTML元素的视图

@{ViewBag.Title = "Movies";}
Enter your three favorite movies:@using (Html.BeginForm()) {
@Html.TextBox("movies") @Html.TextBox("movies") @Html.TextBox("movies")
<input type=submit />}

We have used the Html.TextBox helper to create three input elements; these will all be created with a value of movies for the name attribute, like this:
我们用Html.TextBox辅助器创建了三个input元素,它们都会以movies作为其name属性的值,像这样:

<input id="movies" name="movies" type="text" value="" /><input id="movies" name="movies" type="text" value="" /><input id="movies" name="movies" type="text" value="" />

We can receive the values that the user enters with an action method like the one shown in Listing 17-11.
我们可以用一个如清单17-11所示的动作方法来接收用户输入的值。

Listing 17-11. Receiving Multiple Data Items in an Action Method
清单17-11. 在一个动作方法中接收多个数据项

[HttpPost]public ViewResult Movies(List<string> movies) {...

The model binder will find all the values supplied by the user and pass them to the Movies action method in a List<string>. The binder is smart enough to support different parameter types; we can choose to receive the data as a string[] or even as an IList<string>.
模型绑定器将会找出用户提供的所有值,并把它们以一个List<string>的形式传递给Movies动作方法。绑定器有足够的智能以支持不同的参数类型,我们可以有选择地把数据接收为一个string[],甚至一个IList<string>。

Binding to Collections of Custom Types
绑定自定义类型集

The multiple-value binding trick is very nice, but if we want it to work on custom types, then we have to produce HTML in a certain format. Listing 17-12 shows how we can do this with an array of Person objects.
多值绑定技巧很好,但如果我们希望它也能对自定义类型工作,那么我们就必须以某种格式来产生HTML。清单17-12演示了我们对一个Person对象数组如何做这种事。

Listing 17-12. Generating HTML for a Collection of Custom Objects
清单17-12. 生成自定义对象集的HTML

@model List<MvcApp.Models.Person>
@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)}

The templated helper generates HTML that prefixes the name of each property with the index of the object in the collection, as follows:
该模板辅助器用集合中对象的索引号作为各个属性名的前缀来生成HTML,如下所示:

...<h4>Person Number: 0</h4>First Name: <input name="[0].FirstName" type="text"    value="Joe" />Last Name: <input  name="[0].LastName"type="text"    value="Smith" /><h4>Person Number: 1</h4>First Name: <input name="[1].FirstName" type="text"    value="Jane" />Last Name: <input  name="[1].LastName"type="text"    value="Doe" />...

To bind to this data, we just have to define an action method that takes a collection parameter of the view model type, as shown in Listing 17-13.
为了绑定这种数据,我们只需要定义一个以视图模型类型的集合为参数的动作方法,如清单17-13所示。

Listing 17-13. Binding to an Indexed Collection
清单17-13. 绑定一个索引了的集合

[HttpPost]public ViewResult Register(List<Person> people) {...

Because we are binding to a collection, the default model binder will search for values for the properties of the Person class that are prefixed by an index. Of course, we don’t have to use the templated helpers to generate the HTML; we can do it explicitly in the view, as demonstrated by Listing 17-14.
因为我们绑定的是一个集合,默认模型绑定器会搜索以索引号为前缀的Person类的各个属性的值。当然,我们并不是必须用这种模板辅助器来生成HTML,我们可以在视图中明确地做这种事,如清单17-14所示。

Listing 17-14. Creating HTML Elements That Will Be Bound to a Collection
清单17-14. 创建绑定到一个集合的HTML元素

<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")

As long we ensure that the index values are properly generated, the model binder will be able to find and bind to all of the data elements we defined.
只要我们确保适当地生成索引值,模型绑定器就能够查找并绑定我们定义的所有数据元素。

Binding to Collections with Nonsequential Indices
绑定非序列化索引的集合

An alternative to sequential numeric index values is to use arbitrary string keys to define collection items. This can be useful when we want to use JavaScript on the client to dynamically add or remove controls and don’t want to worry about maintaining the index sequence. To use this option, we need to define a hidden input element called index that specifies the key for the item, as shown in Listing 17-15.
替代序列化数字索引值的另一种办法是,使用任意字符串键来定义集合的数据项。当我们希望在客户端用JavaScript来动态添加或删除控件,而且不想担心索引序列的维护时,这可能是有用的。为了选用这种办法,我们需要定义一个名为index的隐藏input元素,来指定数据项的键,如清单17-15所示。

Listing 17-15. Specifying an Arbitrary Key for an Item
清单17-15. 为数据指定任意键

<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")

We have prefixed the names of the input elements to match the value of the hidden index element. The model binder detects the index and uses it to associate data values together during the binding process.
我们给input元素的name加了与隐藏的index元素匹配的前缀(指[firtstPerson]、[secondPerson] — 译者注)。模型绑定器会检测到这个index元素,并在绑定过程中把它与数据值关联在一起。

Binding to a Dictionary
绑定字典

The default model binder is capable of binding to a dictionary, but only if we follow a very specific naming sequence. Listing 17-16 provides a demonstration.
默认模型绑定器能够绑定字典,但仅当我们遵循一种特殊的命名序列时才行。清单17-16提供了一个演示。

Listing 17-16. Binding to a Dictionary
清单17-16. 绑定字典

<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")

When bound to a Dictionary<string, Person> or IDictionary<string, Person>, the dictionary will contain two Person objects under the keys firstPerson and secondPerson. We can receive the data with an action method like this one:
当绑定Dictionary<string, Person>或IDictionary<string, Person>时,该字典在键firstPerson和secondPerson下将有两个Person对象。我们可以用下面这样的动作方法来接收数据:

[HttpPost]public ViewResult Register(IDictionary<string, Person> people) {...

Manually Invoking Model Binding
手工调用模型绑定

The model binding process is performed automatically when an action method defines parameters, but we can take direct control of the process if we want. This gives us more explicit control over how model objects are instantiated, where data values are obtained from, and how data parsing errors are handled. Listing 17-17 demonstrates an action method that manually invokes the binding process.
当动作方法定义了参数时,模型绑定过程是自动执行的,但只要我们想,我们可以直接控制这一过程。这使我们能够对,如何实例化模型对象以从中获得数据,以及如何处理数据解析错误等进行更明确的控制。清单17-17演示了手工调用绑定过程的一个动作方法。

Listing 17-17. Manually Invoking the Model Binding Process
清单17-17. 手工调用模型绑定过程

[HttpPost]public ActionResult RegisterMember() {
Person myPerson = new Person(); UpdateModel(myPerson); return View(myPerson);}

The UpdateModel method takes a model object we previously created as a parameter and tries to obtain values for its public properties using the standard binding process. One reason for invoking model binding manually is to support dependency injection (DI) in model objects. For example, if we were using an application-wide dependency resolver (which we described in Chapter 10), then we could add DI to the creation of our Person model objects, as Listing 17-18 demonstrates.
UpdateModel方法以前面生成的模型对象为参数,并试图用标准的绑定过程来获取其public属性的值。手工调用模型绑定过程的原因之一是为了支持模型对象的依赖性注入(DI)。例如,如果我们在使用应用程序范围的依赖性解析器(在第10章描述过),那么我们可以给Person模型对象的生成添加DI,如清单17-18所示。

Listing 17-18. Adding Dependency Injection to Model Object Creation
清单17-18. 对模型对象生成添加依赖性注入

[HttpPost]public ActionResult RegisterMember() {
Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person)); UpdateModel(myPerson); return View(myPerson);}

As we’ll demonstrate, this isn’t the only way to introduce DI into the binding process. We’ll show you other approaches later in this chapter.
正如我们将要演示的那样,这不是把DI引入到绑定过程的唯一办法。我们将在本章稍后给你演示其它办法。

Restricting Binding to a Specific Data Source
将绑定限制到特定数据源

When we manually invoke the binding process, we can restrict the binding process to a single source of data. By default, the binder looks in four places: form data, route data, the query string, and any uploaded files. Listing 17-19 shows how we can restrict the binder to searching for data in a single location—in this case, the form data.
当手工调用绑定过程时,我们可以把绑定过程限制到一个单一的数据源。默认地,绑定器查看四个地方:表单数据、路由数据、查询字串、以及上载文件。清单17-19演示了可以如何把绑定器限制到搜索单一位置的数据 — 这里是表单数据。

Listing 17-19. Restricting the Binder to the Form Data
清单17-19. 将绑定器限制到表单数据

[HttpPost]public ActionResult RegisterMember() {
Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person)); UpdateModel(myPerson, new FormValueProvider(ControllerContext)); return View(myPerson);}

This version of the UpdateModel method takes an implementation of the IValueProvider interface, which becomes the sole source of data values for the binding process. Each of the four default data locations has an IValueProvider implementation, as shown in Table 17-2.
UpdateModel方法的这一版本以IValueProvider接口的一个实现为参数,它成为绑定过程的唯一数据源。四个默认数据位置都有一个IValueProvider实现,如表17-2所示。

Table 17-2. The Built-in IValueProvider Implementations
表17-2. 内建的IValueProvider实现
Source
IValueProvider Implementation
IValueProvider实现
Request.Form FormValueProvider
RouteData.Values RouteDataValueProvider
Request.QueryString QueryStringValueProvider
Request.Files HttpFileCollectionValueProvider

Each of the classes listed in Table 17-2 takes a ControllerContext constructor parameter, which we can obtain from the Controller property of the same name, as shown in the listing.
表17-2列出的各个类都以ControllerContext作为构造器参数,如上述清单所示,我们可以通过同名的Controller属性获得它。

The most common way of restricting the source of data is to look only at the form values. There is a neat binding trick that we can use that means we don’t have to create an instance of FormValueProvider, as shown in Listing 17-20.
限制数据源最普遍的方式是只查找表单值。我们可以用一个雅致的绑定技巧,使我们不必创建FormValueProvider的实例,如清单17-20所示。

Listing 17-20. Restricting the Binder Data Source
清单17-20. 限制绑定器数据源

[HttpPost]public ActionResult RegisterMember(FormCollection formData) {
Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person)); UpdateModel(myPerson, formData); return View(myPerson);}

The FormCollection class implements the IValueProvider interface, and if we define the action method to take a parameter of this type, the model binder will provide us with an object that we can pass directly to the UpdateModel method.
FormCollection类实现了IValueProvider接口,而且,如果我们把该动作方法定义成以这个类型为参数,那么,模型绑定器将给我们提供一个能够直接传递给UpdateModel方法的对象。

■ Tip Other overloaded versions of the UpdateModel method allow us to specify a prefix to search for and to specify which model properties should be included in the binding process.
提示:UpdateModel方法的另一个重载版本可以让我们指定搜索前缀,并指定绑定过程中应当包含哪些模型属性。

Dealing with Binding Errors
处理绑定错误

Users will inevitably supply values that cannot be bound to the corresponding model properties—invalid dates, or text for numeric values, for example. When we invoke model binding explicitly, we are responsible for dealing with any such errors. The model binder expresses binding errors by throwing an InvalidOperationException. Details of the errors can be found through the ModelState feature, which we describe in Chapter 18. When using the UpdateModel method, we must be prepared to catch the exception and use the ModelState to express an error message to the user, as shown in Listing 17-21.
用户不可避免地会提供一些不能绑定到相应模型属性的值 — 例如,无效日期,或对数字值输入文本等。当我们明确地调用模型绑定时,我们要负责处理诸如此类的任何错误。模型绑定器通过弹出InvalidOperationException异常来表示绑定错误。错误的细节通过ModelState特性来查看,我们将在第18章进行描述。当使用UpdateModel方法时,我们必须做好捕捉该异常的准备,并用ModelState把错误消息表示给用户,如清单17-21所示。

Listing 17-21. Dealing with Model Binding Errors
清单17-21. 处理模型绑定错误

[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);}

As an alternative approach, we can use the TryUpdateModel method, which returns true if the model binding process is successful and false if there are errors, as shown in Listing 17-22.
另一个可选的办法是,我们可以用TryUpdateModel方法。如果模型绑定成功,它会返回true,如果错误,返回false,如清单17-22所示。

Listing 17-22. Using the TryUpdateModel Method
清单17-22. 使用TryUpdateModel方法

[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 }}

The only reason to favor TryUpdateModel over UpdateModel is if you don’t like catching and dealing with exceptions; there is no functional difference in the model binding process.
喜欢TryUpdateModel而不用UpdateModel的唯一原因是,你不喜欢捕捉并处理异常,在模型绑定过程方面,两者没有差别。

■ Tip When model binding is invoked automatically, binding errors are not signaled with exceptions. Instead, we must check the result through the ModelState.IsValid property. We explain ModelState in Chapter 18.
提示:当自动调用模型绑定时,绑定错误不会发出异常信号。因此,我们必须通过ModelState.IsValid属性来检查结果。我们将在第18章解释ModelState。

Using Model Binding to Receive File Uploads
用模型绑定接收文件上载

All we have to do to receive uploaded files is to define an action method that takes a parameter of the HttpPostedFileBase type. The model binder will populate it with the data corresponding to an uploaded file. Listing 17-23 shows an action method that receives an uploaded file.
对于接收文件上载,我们要做的全部工作就是定义一个以HttpPostedFileBase类型为参数的动作方法。模型绑定器会用与这个上载文件相应的数据来填充它。清单17-23演示了一个接收文件上载的动作方法。

Listing 17-23. Receiving an Uploaded File in an Action Method
清单17-23. 在一个动作方法中接收一个上载文件

[HttpPost]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}

We have to create the HTML form in a specific format to allow the user to upload a file, as shown by the view in Listing 17-24.
我们必须以一种特殊的格式创建HTML表单,以允许用户上载一个文件,如清单17-24的视图所示。

Listing 17-24. A View That Allows the User to Upload a File
清单17-24. 允许用户上载一个文件的视图

@{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>

The key is to set the value of the enctype attribute to multipart/form-data. If we don’t do this, the browser will just send the name of the file and not the file itself. (This is how browsers work—it isn’t specific to the MVC Framework).
关键是把enctype属性的值设置为multipart/form-data。如果我们不这么做,浏览器将只会发送文件名,而不是发送文件本身。(这是浏览器的工作方式 — 并不是MVC框架所特有的)。

In the listing, we rendered the form element using literal HTML. We could have generated the element using the Html.BeginForm helper, but only by using the overload that requires four parameters. We think that the literal HTML is more readable.
在这个清单中,我们用字面HTML渲染了form元素。我们可以用Html.BeginForm辅助器来生成这个元素,但只能使用需要四个参数的重载版本。我们认为这种字面HTML可读性更好。

Customizing the Model Binding System
定制模型绑定系统

We have shown you the default model binding process. As you might expect by now, there are some different ways in which we can customize the binding system. We show you some examples in the following sections.
我们已经向你演示了默认模型绑定过程。正如你现在所期望的,有一些我们可以对绑定系统进行定制的不同方法。在以下章节中,我们将向你演示一些示例。

Creating a Custom Value Provider
创建一个自定义的值提供器

By defining a custom value provider, we can add our own source of data to the model binding process. Value providers implement the IValueProvider interface, which is shown in Listing 17-25.
通过定义一个自定义的值提供器,我们可把把自己的数据源添加到模型绑定过程。值提供器实现IValueProvider接口,如清单17-25所示。

Listing 17-25. The IValueProvider Interface
清单17-25. IValueProvider接口

namespace System.Web.Mvc {    using System;
public interface IValueProvider { bool ContainsPrefix(string prefix); ValueProviderResult GetValue(string key); }}

The ContainsPrefix method is called by the model binder to determine whether the value provider can resolve the data for a given prefix. The GetValue method returns a value for a given data key or returns null if the provider doesn’t have any suitable data. Listing 17-26 shows a value provider that binds a timestamp for properties called CurrentTime. This isn’t something that we would likely need to do in a real application, but it provides a simple demonstration.
ContainsPrefix方法由模型绑定器调用,以确定这个值提供器是否可以解析给定前缀的数据。GetValue方法返回给定数据键的值,或者,如果提供器无法得到合适的数据时,返回null。清单17-26演示了对名为CurrentTime的属性绑定时间戳的一个值提供器。这并不是我们在一个真实的应用程序中可能要做的事情,但它提供了一个简单的演示。

Listing 17-26. A Custom IValueProvider Implementation
清单16-26. 一个自定义的IValueProvider实现

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; }}

We want to respond only to requests for CurrentTime. When we get such a request, we return the value of the static DateTime.Now property. For all other requests, we return null, indicating that we cannot provide data.
我们希望只对CurrentTime的请求进行响应。当获得这样一个请求时,便返回静态DateTime.Now属性的值。对于所有其它请求,都返回null,以指示不能提供数据。

We have to return our data value as a ValueProviderResult class. This class has three constructor parameters. The first is the data item that we want to associate with the requested key. The second parameter is used to track model binding errors and doesn’t apply to our example. The final parameter is the culture information that relates to the value; we have specified the InvariantCulture.
我们必须把数据值作为一个ValueProviderResult类来返回。这个类有三个构造器参数。第一个是我们希望与请求键关联的数据项。第二个用于跟踪模型绑定错误,在我们的例子中并未运用。最后一个是与值相关的地域信息,这里我们已经指定为InvariantCulture。

To register our value provider with the application, we need to create a factory class that will create instances of our provider. This class be is derived from the abstract ValueProviderFactory class. Listing 17-27 shows a factory class for our CurrentTimeValueProvider.
为了把这个值提供器注册给应用程序,我们需要创建一个工厂类,以生成这个提供器的实例。这个类派生成于抽象ValueProviderFactory类。清单17-27演示了用于CurrentTimeValueProvider的工厂类。

Listing 17-27. A Custom Value Provider Factory Class
清单17-27. 一个自定义值提供器工厂类

public class CurrentTimeValueProviderFactory : ValueProviderFactory {
public override IValueProvider GetValueProvider(ControllerContext controllerContext) { return new CurrentTimeValueProvider(); }}

The GetValueProvider method is called when the model binder wants to obtain values for the binding process. Our implementation simply creates and returns an instance of the CurrentTimeValueProvider class.
当模型绑定器要在绑定过程中获取值时,会调用GetValueProvider方法。我们的实现简单地创建并渲染了CurrentTimeValueProvider类的一个实例。

The last step is to register the factory class with the application, which we do in the Application_Start method of Global.asax, as shown in Listing 17-28.
最后一步是在应用程序中注册这个工厂类,我们在Global.asax的Application_Start方法中做这件事,如清单17-28所示。

Listing 17-28. Registering a Value Provider Factory
清单17-28. 注册值提供器工厂

protected void Application_Start() {    AreaRegistration.RegisterAllAreas();
ValueProviderFactories.Factories.Insert(0, new CurrentTimeValueProviderFactory());
RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes);}

We register our factory class by adding an instance to the static ValueProviderFactories.Factories collection. As we explained earlier, the model binder looks at the value providers in sequence. If we want our custom provider to take precedence over the built-in ones, then we have to use the Insert method to put our factory at the first position in the collection, as shown in the listing. If we want our provider to be a fallback that is used when the other providers cannot supply a data value, then we can use the Add method to append our factory class to the end of the collection, like this:
我们通过把一个实例添加到静态ValueProviderFactories.Factories集合的方式来注册这个工厂类。正如我们之前所解释的那样,模型绑定器会依次查看这些值提供器。如果我们希望自定义值提供器优先于内建提供器,那么,我们必须用Insert方法把我们的工厂放到集合的第一个位置,如上述清单所示。如果希望我们的提供器在其它提供器不能提供数据值时作为一个备选,那么,我们可以用Add方法把我们的工厂追加到集合的末端,像这样:

ValueProviderFactories.Factories.Add(new CurrentTimeValueProviderFactory());

We can test our provider by defining an action method that has a DateTime parameter called currentTime, as shown in Listing 17-29.
我们可以通过定义一个动作方法来测试我们的提供器,该动作方法具有一个名为currentTime的DateTime参数,如清单17-29所示。

Listing 17-29. An Action Method That Uses the Custom Value Provider
清单17-29. 使用自定义值提供器的动作方法

public ActionResult Clock(DateTime currentTime) {    return Content("The time is " + currentTime.ToLongTimeString());}

Because our value provider is the first one that the model binder will request data from, we are able to provide a value that will be bound to the parameter.
因为我们的值提供器是模型绑定器用来请求数据的第一个提供器,我们便能够提供一个绑定到这个参数的值。

Creating a Dependency-Aware Model Binder
创建依赖性感知的模型绑定器

We showed you how we can use manual model binding to introduce dependency injection to the binding process, but a more elegant approach is to create a DI-aware binder by deriving from the DefaultModelBinder class and overriding the CreateModel method, as shown in Listing 17-30.
我们向你演示过能够如何用手工绑定把依赖性注入引入绑定过程,但是,更优雅的办法是,通过对DefaultModelBinder类进行派生,并覆盖其CreateModel方法,来创建一个DI感知的绑定器,如清单17-30所示。

Listing 17-30. Creating a DI-Aware Model Binder
清单17-30. 创建一个DI感知的模型绑定器

using System;using System.Web.Mvc;
namespace MvcApp.Infrastructure {
public class DIModelBinder : DefaultModelBinder { protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) {
return DependencyResolver.Current.GetService(modelType) ?? base.CreateModel(controllerContext, bindingContext, modelType); } }}

This class uses the application-wide dependency resolver to create model objects and falls back to the base class implementation if required (which uses the System.Activator class to create a model instance using the default constructor).
这个类使用应用程序范围的依赖性解析器来创建模型对象,并在必要时回退到基类实现(它用System.Activator类创建一个使用默认构造器的模型实例)。

■ Tip See Chapter 10 for details of our Ninject dependency resolver class.
提示:详见第10章的Ninject依赖性解析器类。

We have to register our binder with the application as the default model binder, which we do in the Appliction_Start method of Global.asax, as shown in Listing 17-31.
我们必须在应用程序中把我们的绑定器注册为默认模型绑定器,在Global.asax的Application_Start方法中做这件事,如清单17-31所示。

Listing 17-31. Registering a Default Model Binder
清单17-31. 注册一个默认模型绑定器

protected void Application_Start() {
AreaRegistration.RegisterAllAreas();
ModelBinders.Binders.DefaultBinder = new DIModelBinder();
RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes);}

The model binding process will now be able to create model objects that have dependencies.
现在,模型绑定过程将能够创建具有依赖性的模型对象。

Creating a Custom Model Binder
创建自定义模型绑定器

We can override the default binder’s behavior by creating a custom model binder for a specific type. Listing 17-32 provides a demonstration.
我们可以针对一个特定类型创建一个自定义模型绑定器来覆盖默认绑定器的行为。清单17-32提供了一个演示。

Listing 17-32. A Custom Model Binder
清单17-32. 一个自定义模型绑定器

public class PersonModelBinder : IModelBinder {
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
// see if there is an existing model to update and create one if not Person model = (Person)bindingContext.Model ?? (Person)DependencyResolver.Current.GetService(typeof(Person));
// find out if the value provider has the required prefix bool hasPrefix = bindingContext.ValueProvider .ContainsPrefix(bindingContext.ModelName);
string searchPrefix = (hasPrefix) ? bindingContext.ModelName + "." : "";
// populate the fields of the model object model.PersonId = int.Parse(GetValue(bindingContext, searchPrefix, "PersonId")); model.FirstName = GetValue(bindingContext, searchPrefix, "FirstName"); model.LastName = GetValue(bindingContext, searchPrefix, "LastName"); model.BirthDate = DateTime.Parse(GetValue(bindingContext, searchPrefix, "BirthDate")); model.IsApproved = GetCheckedValue(bindingContext, searchPrefix, "IsApproved"); model.Role = (Role)Enum.Parse(typeof(Role), GetValue(bindingContext, searchPrefix, "Role")); return model; }
private string GetValue(ModelBindingContext context, string prefix, string key) { ValueProviderResult vpr = context.ValueProvider.GetValue(prefix + key); return vpr == null ? null : vpr.AttemptedValue; }
private bool GetCheckedValue(ModelBindingContext context, string prefix, string key) { bool result = false; ValueProviderResult vpr = context.ValueProvider.GetValue(prefix + key); if (vpr != null) { result = (bool)vpr.ConvertTo(typeof(bool)); } return result; }}

There is a lot going on in this class, so we’ll break it down and work our way through step by step. First we obtain the model object that we are going to bind to, as follows:
在这个类中要做很多事,因此我们将把它分解下来,一步步地进行我们的工作。首先,获得打算绑定的模型对象,如下所示:

Person model = (Person)bindingContext.Model ??    (Person)DependencyResolver.Current.GetService(typeof(Person));

When the model binding process is invoked manually, we pass a model object to the UpdateModel method; that object is available through the Model property of the BindingContext class. A good model binder will check to see whether a model object is available and, if there is, use it for the binding process. Otherwise, we are responsible for creating a model object, which we do using the application-wide dependency resolver (which we discussed in Chapter 10).
当手工调用模型绑定过程时,我们把一个模型对象传递给UpdateModel方法,通过BindingContext类的Model属性,该对象是可用的。一个好的模型绑定器应该检查模型对象是否可用,而且,如果可用,便把它用于绑定过程。否则,我们要负责创建一个模型对象,我们用应用程序范围的依赖性解析器来做这件事(在第10章作过描述)。

Our next step is to see whether we need to use a prefix to request data values from the value providers:
下一步是通过值提供器来考查对请求数据值是否需要用一个前缀:

bool hasPrefix = bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName);string searchPrefix = (hasPrefix) ? bindingContext.ModelName + "." : "";

The BindingContext.ModelName property returns the name of the model we are binding to. If we have rendered the model object in a view, there will be no prefix in the HTML that was generated, but the ModelName will return the action method parameter name anyway, so we check with the value provider to see whether the prefix exists.
BindingContext.ModelName属性返回我们绑定的模型名。如果在一个视图中已经渲染了这个模型对象,那么在所生成的HTML中就将没有前缀,但ModelName总会返回动作方法的参数名,因此,我们用值提供器来检查前缀是否存在。

We access the value providers through the BindingContext.ValueProvider property. This gives us consolidated access to all of the value providers that are available, and requests will be passed on to them in the order we discussed earlier in the chapter. If the prefix exists in the value data, we will use it.
我们通过BindingContext.ValueProvider属性来访问值提供器。这让我们能够统一地访问所有可用的值提供器,而且按本章前面讨论的次序把请求传递给它们。如果值数据有前缀,我们将使用它。

Next we use the value providers to obtain values for the properties of our Person model object. Here is an example:
下一步我们用值提供器来获取Person模型对象各属性的值。以下是一个例子:

model.FirstName = GetValue(bindingContext, searchPrefix, "FirstName");

We have defined a method called GetValue in the model binder class that obtains ValueProviderResult objects from the consolidated value provider and extracts a string value through the AttemptedValue property.
我们在模型绑定器类中定义了一个名为GetValue的方法,它通过统一的值提供器获取ValueProviderResult对象,并通过AttemptedValue属性来提取一个字符串。

We explained in Chapter 15 that when we render checkboxes, the HTML helper methods create a hidden input element to make sure we receive a value when the checkbox is unchecked. This causes us a mild problem when it comes to model binding, because the value provider will give us both values as a string array. To resolve this, we use the ValueProviderResult.ConvertTo method to sort this out and give us the correct value:
我们在第15章解释过,当我们渲染一个检查框(checkbox)时,HTML辅助器方法会创建一个隐藏的input元素,以确保在检查框未选择时可以得到一个值。在模型绑定时,这会产生一点问题,因为值提供器会以字符串数组形式给出两个值。为了解决这一问题,我们用ValueProviderResult.ConvertTo方法进行挑选,并给出一个正确的值:

result = (bool)vpr.ConvertTo(typeof(bool));

■ Tip We don’t perform any input validation in this model binder, meaning that we blithely assume that the user has provided valid values for all of the Person properties. We discuss validation in Chapter 18, but for the moment we want to focus on the basic model binding process.
提示:在这个模型绑定器中,我们不必执行任何输入校验,意即,我们开心地假设用户对所有Person属性提供了有效值。我们将在第18章讨论校验,但此刻我们想集中关注于基本的模型绑定过程。

Once we have set the properties we are interested in (we omitted the HomeAddress property for simplicity), we return the populated model object. We have to register our model binder, which we do in the Application_Start method of Global.asax, as demonstrated by Listing 17-33.
一旦我们已经设置了感兴趣的属性(出于简化,我们忽略了HomeAddress属性),便返回装配好的模型对象。我们必须注册我们模型绑定器,仍是在Global.asax的Application_Start方法中做这件事,如清单17-33所演示的那样。

Listing 17-33. Registering a Custom Model Binder
清单17-33. 注册自定义模型绑定器

protected void Application_Start() {    AreaRegistration.RegisterAllAreas();
ModelBinders.Binders.Add(typeof(Person), new PersonModelBinder());
RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes);}

We register our binder through the ModelBinders.Binders.Add method, passing in the type that our binder supports and an instance of the binder class.
我们通过ModelBinders.Binders.Add方法注册了我们的绑定器,在其中传递了该绑定器支持的类型,以及绑定器类的一个实例。

Creating Model Binder Providers
创建模型绑定器提供器

An alternative means of registering custom model binders is to create a model binder provider by implementing the IModelBinderProvider, as shown in Listing 17-34.
注册自定义模型绑定器的另一种手段是通过实现IModelBinderProvider来创建一个模型绑定器提供器,如果清单17-34所示。

Listing 17-34. A Custom Model Binder Provider
清单17-34. 一个自定义模型绑定器提供器

using System;using System.Web.Mvc;using MvcApp.Models;
namespace MvcApp.Infrastructure {
public class CustomModelBinderProvider : IModelBinderProvider {
public IModelBinder GetBinder(Type modelType) { return modelType == typeof(Person) ? new PersonModelBinder() : null; } }}

This is a more flexible approach if we have custom binders that work on multiple types or have a lot of providers to maintain. We register our binder provider in the Application_Start method of Global.asax, as shown in Listing 17-35.
如果有对多个类型进行操作的自定义模型绑定器,或有许多提供器要维护,这是一种更灵活的办法。仍在Global.asax的Application_Start方法中注册我们的绑定器提供器,如清单17-35所示。

Listing 17-35. Registering a Custom Binder Provider
清单17-35. 注册一个自定义绑定器提供器

protected void Application_Start() {    AreaRegistration.RegisterAllAreas();
ModelBinderProviders.BinderProviders.Add(new CustomModelBinderProvider());
RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes);}

Using the ModelBinder Attribute
使用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.
注册一个自定义模型绑定器的最后一种方式是把ModelBinder属性运用于模型类,如清单17-36所示。

Listing 17-36. Specifying a Custom Model Binder Using the ModelBinder Attribute
清单16-36. 用ModelBinder属性指定一个自定义模型绑定器

[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; }}

The sole parameter to the ModelBinder attribute is the type of the model binder that should be used when binding this kind of object. We have specified our custom PersonModelBinder class. Of the three approaches, we tend toward implementing the IModelBinderProvider interface to handle sophisticated requirements, which feels more consistent with the rest of the design of the MVC Framework, or simply using [ModelBinder] in the case of just associating a custom binder with a specific model type. However, since all of these techniques result in the same behavior, it doesn’t really matter which one you use.
送给ModelBinder属性的唯一参数是,在绑定这种对象时应当使用的模型绑定器类型。我们指定了我们的自定义PersonModelBinder类。在这三种方法中,我们倾向于实现IModelBinderProvider接口,以处理复杂的需求,这让人感觉与MVC框架设计的其余部分更和谐。或者,恰好把一个自定义绑定器与特定模型类型关联的情况下,简单地使用[ModelBinder]。然而,由于所有这些技术都导致同样的结果,使用哪种技术并无多大关系。

Summary
小结

In this chapter, we have introduced you to the workings of the model binding process, showing you how the default model binder operates and the different ways in which the process can be customized. Many MVC Framework applications will need only the default model binder, which is nicely aligned to process the HTML that the helper methods generate. But for more advanced applications, it can be useful to create custom binders that create model objects in a more efficient or more specific way. In the next chapter, we’ll show you how to validate model objects and how to present the user with meaningful errors when invalid data is received.
在本章中,我们给你介绍了模型绑定过程的工作过程,向你演示了默认模型绑定器如何操作,以及可以对该过程进行定制的不同方式。许多MVC框架应用程序都只需要默认模型绑定器,它很好地协调处理辅助器方法所生成的HTML。但是,在一些更高级的应用程序中,创建自定义绑定器,以更有效或更特殊的方式来生成模型对象,可能是很有用的。在下一章中,我们将向你演示如何校验模型对象,以及在接收非法数据时,如何向用户表现有意义的错误信息。



TAG:ASP NET MVC