有人已经在翻译这本书了,好像翻译到了第16章,等第17章没等到,自己译出来看看。只为自己学习使用。不对的地方大家多批评。谢谢。
第十七章 模型绑定
模型绑定是实用浏览器发送的数据请求创建.net对象的过程。每次我们定义了一个带有参数的action方法时我们都依赖了模型绑定过程——参数对象是被模型绑定创建的。在本章我们将给你展示模型绑定系统怎样工作,并展示要求自定义他的这些技术的高级应用。
理解模型绑定
设想我们在Controller中定义了像清单17-1展示的一个action方法。
清单17-1 A Simple Action Method
using System.Web;
using System.Web.Mvc;
using MvcApp.Models;
namespace MvcApp.Controllers
{
public class HomeController : Controller
{
public ActionResult Person(int id)
{
//get a person record from the repository
Person myPerson = null;//...获取逻辑在这里编写
return View(myPerson);
}
}
}
我们的action方法定义在HomeController中,这意味着Virtual Studio为我们创建的默认路由将调用我们的action方法。提醒一下,下面是默认路由:
routes.MapRoute(
"Default", // 路由名称
"{controller}/{action}/{id}", // 带有参数的URL
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // 参数默认值
);
当我们接受到一个像 Home/Person/32这样的请求时,MVC框架必须以这样的方式映射请求细节:它能把期望的值或对象作为参数传递给我们的acton方法。
调用acton的组建,Action调用器,在调用action方法前,负责为参数获取值。默认的action调用器,ControllerActonInvoker(在11章介绍过)依赖于模型绑定器,模型绑定器被接口IModelBinder定义,如清单17-2.
清单17-2 IModelBinder接口
namespace System.Web.Mvc
{
public interface IModelBinder
{
object BindModel(ControllerContext contrellerContext, ModelBindingContext bingContext);
}
}
在MVC应用程序中可以有多个模型绑定器,每个绑定器能够负责一个或者多个模型类型。当action调用器需要调用一个action方法时,它(模型绑定器——我)查看action定义的参数并且负责为每个参数类型找到负责的模型绑定器。在17-1的例子中,action调用器将找到带有一个int类型的参数的action方法,因此它将找到负责绑定int值的绑定器并且调用它的BindModel方法。如果没有负责int类型数据的绑定器,那么将使用默认的绑定器。
一个绑定器负责生成合适的action方法参数值,这通常意味着转换一些请求数据的元素(如表单或查询字符串值),但是MVC框架并不限制数据怎样被获取。我们将在本章会面展示给你一些自定义绑定器的例子并展示一些ModelBindingContext类的特点,这个类被传递给IModelBinder.BindModel方法(你可以在12章看到其他BindModel参数——ControllerContext类的细节。)TODO:
使用默认的模型绑定器
尽管一个应用程序可以有多个绑定器,大多数只依赖一个内建的绑定器类,DefaultModelBinder。当应用程序不能找到自定义的绑定器绑定类型时,它将使用默认绑定器。
默认的,绑定器查找如表17-1所示的四个地方,根据参数的名字来匹配和绑定数据。
表17-1 DefaultModelBinder类查找参数数据的顺序
源 |
描述 |
Request.Form |
用户的HTML表单元素提供的值 |
Route.Values |
通过应用程序的路由获取到的值 |
Request.QueryString |
请求Url的查询字符串包含的数据 |
Request.File |
作为请求的一部分而被上传的文件(查看“使用模型绑定接受上传文件”一节了解文件上传细节)。 |
这些地方按顺序被查询,在清单17-1的例子中展示的action方法,DefaultModelBinder类检查我们的action方法并找到一个叫做id的参数,它然后找向下面的一个值:
- Request.Form[“id”]
- Route.Values[“id”]
- Requset.QueryString[“id”]
- Request.Files[“id”]
提示:当JSON数据被接受时,这也是一个可用的数据源。我们会在19章中解释更多JSON并展示他怎么工作。 |
当找到一个值后搜索停止,基于我们的例子,表单数据,路由数据值将被搜索。因为在第二个位置中会找到路由中id这一节,所以查询字符串和上传的文件根本不会被搜索。
提示:你会看到action方法中参数的名字是多么重要——参数的名字和请求数据项的名字必须匹配,以便DefaultModelBinder类找到并使用数据。 |
绑定简单类型
当处理简单数据类型时,DefaultModelBinder 尝试使用System.ComponentModel.TypeDescripyter类把从请求数据中获取的string值转换成参数的类型。
如果这个值不能被转换——例如,我们提供了一个apple给一个int类型的参数,DefaultModelBinder将不能绑定参数给模型。
如果我们想避免这个问题,我们可以修改我们的参数。我们可以用一个可用类型,像这样:
public ActionResult Person(int? id) {
如果我们使用了这个方法,从请求中找到的可转换的值,如果没有匹配,id参数的值将是空。对应的,当没有可成功转换的数据时,我们可以提供一个默认值,使我们的参数可选。像这样:
public ActionResult Person(int? id=23) {
文化敏感解释 |
DefaultModelBinder类根据不同地区的请求数据为平台使用不同的文化设置。从Url中获取的值被转换时使用了文化敏感解释,从数据中获取的值被转换时会考虑文化。 DateTime类型的值常常引起问题。我们期望文化敏感的日期使用通用的格式 yyyy-mm-dd。期望表单日期值的格式与服务器指定的格式一致。这意味着,设定英国文化的服务器期望日期是这种格式dd-mm-yyyy,设定美国文化的服务器希望日期格式是mm-dd-yyyy,尽管yyyy-mm-dd这种格式的日期也可以被接受。 如果一个日期值得格式不正确,那么它将不能不转换。这意味着所有日期格式数据包括Url中的,必须使用通用格式。当我们处理用户提供的日期值时,我们也必须小心。默认的绑定器假定用户以服务器文化表示日期数据,MVC用户都是国际用户是不太可能发生的。 |
绑定复杂类型
当action方法参数是复杂类型时(换句话说,任何类型都不能使用TypeConverter类转换),DefaultModelbinder使用反射获取公共属性集合,并按顺序绑定它们。清单17-3展示了我们在前面章节中使用的Person类。我们将再次使用它来演示复杂类型的模型绑定。
清单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 Birthday { get; set; }
public Address HomeAddress { get; set; }
public bool IsApproved { get; set; }
public Role Role { get; set; }
}
默认的模型绑定器检查这个类的属性是不是简单类型。如果是,绑定器从请求数据中查找与属性名相同的数据项。这就是说FirstName属性将使绑定器查找名称为FirstName的数据项。
如果属性是复杂类型,绑定器将为这个新类型重复上面的操作;公共属性的集合将被获取,然后绑定器尝试为他们找到对应的值。不同的地方是,属性名是嵌套的,例如Person类中的HomeAddress属性是Address类型的,如清单17-4所示
清单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; }
}
当为Line1属性查找值时,模型绑定器将查找HomeAddress.Line1的值——换句话说,模型对象的属性名与属性类型中的属性名组合了。
创建易邦定的HTML |
遵循这种格式的最简单的创建HTML的方式是使用我们在第16章介绍的模板视图辅助器。当我们在一个带有Person类型的视图模型的视图中调用@Html.EditorFro(m=>m.FirstName)时,我们会的到如下的HTML: <input class=”text-box sigle-line” id=”FirstName” name=”FirstName” type=”text” Value=”Joy”/> 当调用@Html.EditorFro(m=>m.HomeAddress.Line1)时 ,得到的如下 <input class=”text-box sigle-line” id=”HomeAddress_Line1” name=” HomeAddress.Line1” type=”text” Value=”123 North Street”/> 你能看到HTML的name属性被自动的设置了值,这个值是模型绑定器需要查询的。我们可以手动的创建HTML,但是我们喜欢由模型绑定器做这种事情带来的方便。 |
制定自定义前缀
当模型绑定器查询数据项的时候我们可以为它制定一个自定义的前缀。如果在发送给客户端的HTML中包含了额外的模型对象,那么这是十分有用的。作为一个例子, 考虑清单17-5展示的视图。
清单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" />
}
虽然可以很容易的使用ViewBag传递视图,但在我们创建的视图中,我们创建了一个Person对象,并使用了EditorFor辅助器来为它生成HTML。Lambda表达式的输入是模型对象(用m表示),但是我们忽略了它并范围我们的第二个作为渲染目标的Person对象。我们也调用了EditerForModel辅助器以便发送给用户的HTML中的数据来自两个Person对象。
当我们像这样渲染视图时,模板视图辅助器给HTML元素的name属性申请一个前缀,这用来与主视图模型数据区分。前缀的名称来自变量名,myPerson。例如下面的HTML是视图中被渲染的FirstName属性
<input class="text-box single-line" id="myPerson_FirstName" name="myPerson.FirstName" type="text" value="Jane" />
这个元素的name属性的值被创建时,通过变量名——myPerson.FirstName 给属性名赋值。模型绑定器期望这种方式,并且当查找数据的时候,它可能使用action方法的参数名作为前缀。如果我们提交表单的action方法具有如下的签名:
public ActionResult Index(Person firstPerson, Person myPerson) {
第一个参数对象将使用没有前缀的数据来绑定,第二个参数绑定是将查找以参数名开头 的数据,可以是myPerson.FirstName, myPerson.LastName等。
如果我们不想让我们的参数名与视图的内容以这种方式绑定,我们可以使用Bind特性
指定一个自定义的前缀,如清单17-6所示
清单17-6 使用Bind特性指定自定义数据前缀
public ActionResult Register(Person firstPerson, [Bind(Prefix = "myPerson")]Person secondPerson) {
我们把前缀设置为myPerson,它的意思是默认模型绑定器将使用myPerson作为数据项的前缀,即使参数的名称是secondPerson。
可选的绑定属性
设想一下,Person类的IsApproved属性是非常敏感的,我们可以组织模型中的属性被渲染成HTML时使用第16章的技术,但是一个恶意用户提交表单时可以简单的在Url后添加?IsAdmin=true。如果他这样做了,模型绑定器会发现并在处理绑定的时候使用这些数据。
幸运的是,我们可以使用Bind特性在处理绑定时包含或排除模型的属性。要指定只有特定的属性被包含在内,我们可以Bind特性的Include属性赋值,如清单17-7所示
清单17-7 使用Bind特性来包含模型属性到绑定处理程序
public ActionResult Register([Bind(Include = "FirstName,LastName")]Person person) {
这个清单指定了只有FirstName,LastName属性被包含在绑定程序中;Person类的其他属性将被忽略。相应的,我们可以指定被排除在外的属性,如清单17-8所示
清单17-8使用Bind特性从绑定处理程序排除模型属性
public ActionResult Register([Bind(Exclude = "IsApproved,Role")]Person person) {
这个清单告诉模型绑定器,包含所有属性到绑定处理程序,除了IsApproved和Role。
当我们像这样使用Bind特性的时候,它只应用于单个的Action方法。如果我们想把我们的规则应用到所有Controller中的所有action方法,我们可以在模型类本身中使用Bind特性,如清单17-9。
清单17-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 Birthday { get; set; }
public Address HomeAddress { get; set; }
public bool IsApproved { get; set; }
public Role Role { get; set; }
}
这样使用特性与应用它到单个的action方法参数有相同的作用,但是任何使用模型绑定器绑定Person对象的时候它都会被应用。
提示:如果Bind特性被同时应用到了模型类和action方法参数,任何一个都不排除的属性才会被包含到绑定处理程序中。这意味着应用于模型类的规则不会被包含更少限制的应用于action方法参数的规则重写。 |
绑定数组和集合
默认模型绑定器的一个优雅特性是他如何处理具有相同名称的多个数据项。例如,考虑清单17-10展示的视图。
清单17-10 渲染具有相同名称name属性的HTML元素的视图
@{
ViewBag.Title = "Movies";
}
Enter your three favorite movies:
@using (Html.BeginForm())
{
@Html.TextBox("movies")
@Html.TextBox("movies")
@Html.TextBox("movies")
<input type="submit" />
}
我们已经使用了Html.EditorFor辅助器创建了三个输入元素;这些元素将被创建并且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="" />
我们可以用如清单17-11所示的action方法接受用户输入的值。
清单17-11 在一个action方法中接受多个数据项
[HttpPost]
public ActionResult Movies(List<string> movies){
… …
模型绑定器将找到所有用户提供的数据,并且把他们放到一个List<string>中传递给Movies动作方法。绑定器为不同的参数类型提供了灵敏的支持。我们可以选择用string[]或IList<string>接受数据。
为自定义类型绑定集合
多值绑定的技巧非常好,但是如果我们想让它应用于自定义类型,我们不得不编写特定格式的HTML。清单17-12展示了我们怎样为一个Person对象的数组做这件事。
清单17-12 为自定义对象集合生成HTML
… …
<h4>Person number :0</h4>
First Name:<input class="text-box single-line" name="[0].FirstName" type="text" value="Tom" />
Last Name:<input class="text-box single-line" name="[0].LastName" type="text" value="Green" />
<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="Smith" />
… ….
为了绑定这些数据,我们只需要定义一个接收视图对象类型的集合参数的action方法,如清单17-13所示
清单17-13 绑定可索引的集合
[HttpPost]
public ActionResult Register(List<Person> people) {
因为我们要绑定集合,默认的模型绑定器将根据以索引下标为前缀的值为Person类查找值。当然我们不能用模板辅助器来生成HTML;我们会在视图中明确的指定它,如清单17-14演示的。
清单 17-14 创建绑定集合的HTML元素
… …
<h4>First Person</h4>
First Name:@Html.TextBox("[0].FirstName")
Last Name:@Html.TextBox("[0].LastName")
<h4>Scond Person</h4>
First Name:@Html.TextBox("[1].FirstName")
Last Name:@Html.TextBox("[1].LastName")
… …
只要我们确保索引值被适当的生成了,模型绑定器就能找到并绑定我们定义的数据元素。
绑定无序索引的集合
顺序的数字索引值的另一方面是,使用任意的字符型键来定义集合数据项。这在我们想使用客户端Javascript来动态添加或移除控件并且不用担心维护索引序列时是十分有用的。使用这个功能,我们需要定义一个被称为索引的隐藏数据元素来指定数据项的键,如清单17-15。
清单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>Scond Person</h4>
<input type="hidden" name="index" value="secondPerson" />
First Name:@Html.TextBox("[secondPerson].FirstName")
Last Name:@Html.TextBox("[secondPerson].LastName")
我们给输入元素的name值加上前缀使他能匹配隐藏索引元素。模型绑定器检测索引并在绑定处理过程中使用它来关联数据值。
绑定字典
在绑定字段时,默认模型绑定器也是可用的,但是我们只能遵循一个非常明确的命名序列。清单17-16提供了一个演示。
清单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>Scond 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>或Dictionary<string,Person>时,字典将包含键为firstName和lastName的Person对象。我们可以用这样的action方法接收数据:
[HttpPost]
public ActionResult Register(IDictionary<string,Person> people) {
… …
手动调用模型绑定
当action方法定义参数后,模型绑定处理程序会自动执行,但是我们也可以直接控制执行。这使我们对模型对象的实例化过程中数据值从哪里获取,数据解析错误如何被处理,有了更精确的控制。清单17-17演示了action方法中如何手动调用绑定处理程序。
清单17-17 手动调用模型绑定程序
[HttpPost]
public ActionResult RegisterMember()
{
Person myPerson = new Person();
UpdateModel(myPerson);
return View(myPerson);
}
UpdateModel方法用我们之前创建的模型对象作为参数并尝试使用标准绑定处理程序从这个对象的公共属性中获取值。使用手动模型绑定的原因之一是为了在模型对象中支持依赖注入(DI)。例如,如果我们使用了应用程序范围的依赖分析器(我们在第10章中描述过),我们可以再创建Person模型对象中添加DI,如清单17-18演示。
清单17-18 为模型对象创建添加依赖注入
[HttpPost]
public ActionResult RegisterMember()
{
Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person));
UpdateModel(myPerson);
return View(myPerson);
}
我们演示的不是唯一的在绑定处理程序中引入DI的方式,我们会在本章中展示其他的方式。
绑定特定的数据源
当我们手动调用绑定处理程序时,我们可以限制绑定处理程序到简单的数据源。默认地,绑定器查找四个地方:表单数据,路由数据,查询字符串和上传的文件。清单17-19展示了我们怎样限制绑定器只搜索单一的位置——在本例中是表单数据。
清单17-19 限制绑定器到表单数据
[HttpPost]
public ActionResult RegisterMember()
{
Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person));
UpdateModel(myPerson,new FormValueProvider(ControllerContext));
return View(myPerson);
}
在这个版本的UpdateModel方法中,它接受一个实现了IValueProvider接口的实现类作为绑定程序获取数据值的唯一数据来源。这四个地方中的每个都有一个IValueProvider的实现,如表17-2所示
表17-2 内置的IValueProvider实现
源 |
IValueProvider实现 |
Request.Form |
FormValueProvider |
Route.Values |
RouteDataValueProvider |
Requset.QueryString |
QueryStringValueProvider |
Request.Files |
HttpFileCollectionValueProvider |
表17-2中列出的没有个类都接收一个ControllerContext的构造器参数,我们可以从Controller的同名属性中得到。属性名称如上表展示。
约束数据来源的最常用的方式是:只查看表单中的值。在使用时,有一个非常灵巧的技巧,我们不用创建一个FormValueProvider的实例,如清单17-20所示。
清单17-20 限制绑定器的数据来源
[HttpPost]
public ActionResult RegisterMember(FormCollection formData)
{
Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person));
UpdateModel(myPerson, formData);
return View(myPerson);
}
FormCollection类实现了IValueProvider接口,并且如果我们定义了一个接收FormCollection类型参数的action方法,模型绑定器将为我们提供一个可以直接传递给UpdateModel方法的对象。
提示:UpdateModel的其他重载版本,允许我们为查询指定一个前缀,并且允许我们指定模型的那些属性被包含到绑定处理程序中。 |
处理绑定错误
用户提供的值不能被绑定到相应的模型属性,这是不可避免的,如不正确的日期值,文本对应数字值。当我们明确的调用模型绑定是,我们需要处理这样的错误。模型绑定器抛出InvalidOperationException异常来提示错误。错误的细节可以在ModelState(我们在第18章中描述过)中找到。当我们调用UpdateModel方法时,我们要准备好获取异常并使用ModelState给用户表示错误消息。如清单17-21。
清单17-21 处理模型绑定错误
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。如清单17-22。
清单17-22 使用TryUpdateModel方法
[HttpPost]
public ActionResult RegisterMember(FormCollection formData)
{
Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person));
if (TryUpdateModel(myPerson,formData))
{
// process as normal
}
else
{
//provide UI feedback based on ModelState
}
return View(myPerson);
}
我们更喜欢TryUpdateModel方法的唯一原因是我们不想捕获并处理异常。这两方法在绑定处理程序中并没有什么不同。
提示:当模型绑定自动被调用时,绑定错误并不抛出异常。我们必须检查ModelState.IsValid属性的结果。我们会在第18章中介绍。 |
使用模型绑定接收上传文件
对于接受上传文件,所有我们需要做的是定义一个参数类型为HttpPostedFileBase的action方法。模型绑定器将把数据作为一个上传的文件来处理。清单17-23展示了action方法接受上传文件。
清单17-23 action方法接受上传文件
[HttpPost]
public ActionResult Upload(HttpPostedFileBase file)
{
//Save the file to disk on the server
string fileName = "myFileName";//pick a name
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表单来允许用户上传文件,如清单17-24所示。
清单17-24 允许用户上传文件的视图
@{
ViewBag.Title = "Upload";
}
<h2>Upload</h2>
<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框架)。
在这个清单中我们使用了逐字的HTML来渲染表单控件,我们也可以用Html.BeginForm来生成,但是只能使用具有四个参数的重载版本,我们认为逐字的HTML更容易阅读。
自定义模型绑定系统
我们已经展示了默认模型绑定处理过程,正如你可能期望的,我们有一些不同的方式来自定义绑定系统。在下面的章节中我们会展示一些例子。
创建自定义值提供器
自定义值提供器,我们可以为模型绑定处理程序添加我们自己的数据源,值提供器实现了IValueProvider接口,如清单17-25所示:
清单17-25 IValueProvider接口
namespace System.Web.Mvc {
using System;
public interface IValueProvider {
bool ContainsPrefix(string prefix);
ValueProviderResult GetValue(string key);
}
}
被模型绑定器调用的ContainsPrefix方法用来判断值提供器是否可以为给定的前缀解析数据。GetValue方法为给定的数据键返回值,当提供器没有匹配的数据时会返回null,清单17-26展示了值提供器为CurrentTime属性绑定时间戳,我们在实际应用程序中很少这么做,但这是一个很好的演示。
清单17-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;
}
}
我们想在请求CurrentTime是做出响应。当我们得到一个这样的请求,我们返回静态的DateTime.Now属性的值。对于所有其他的请求我们返回null,这表明我们不提供数据。
我们必须以ValueProviderResult的形式返回我们的数据值。这个类有三个构造器参数,第一个是我么想与请求的键关联的数据项,第二个参数用于跟踪模型绑定错误,我们并不在我们的例子中使用。最后一个参数是与值相关的文化信息,我们制定了InvariantCulture。
为应用程序注册我们的值提供器,我们需要创建一个用于生成我们的提供器的工厂类,这个类派生自抽象类ValueProviderFactory。清单17-27展示了为CurrentTimeProvider创建 的工厂类。
清单17-27 自定义值提供器工厂类
public class CurrentTimeValueProviderFactory : ValueProviderFactory
{
public override IValueProvider GetValueProvider(ControllerContext controllerContext)
{
return new CurrentTimeValueProvider();
}
}
当模型绑定器为绑定处理程序获取值得时候GetValueProvider方法被调用。在我们的实现中,我们只简单的创建并返回了CurrentTimeValueProvider的一个实例。
最有一步是为应用程序注册工厂类,我们在Global.asax文件的Application_Start方法中操作。如清单17-28。
清单17-28 注册值提供器工厂
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
ValueProviderFactories.Factories.Insert(0, new CurrentTimeValueProviderFactory());
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
我们通过向静态的ValueProviderFactories.FactoriesCollection中添加一个实例来注册我们的工厂类。正如我们之前解释的那样,模型绑定器按顺序查看值提供器。如果我们想让我们自定义的提供器优先于内建的提供器,我们必须使用Insert方法把我们的提供器插入到集合的第一个位置,正如清单展示的那样。如果我们想在其他的提供器不能提供数据时使用我们的提供器,我们需要使用Add方法把我们的提供器缀到集合的末尾,像这样:
ValueProviderFactories.Factories.Add(new CurrentTimeValueProviderFactory());
… …
我们可以定义一个带有DateTime类型的CurrentTime参数的action方法来测试我们的提供器。
如清单17-29。
清单17-29 使用自定义提供器的action方法
public ActionResult Clock(DateTime currentTime)
{
return Content("The Time is" + currentTime.ToLongTimeString());
}
以为我们的提供器是模型绑定器请求数据的第一个提供器,我们能够为参数绑定一个值。
创建依赖敏感的模型绑定器(Creating a Dependency-Aware Model Binder)
我们展示过怎样为绑定处理程序引入依赖注入的手动模型绑定,但是更优雅的方式是创建一个源于DefaultModelBinder类并重载CreateModel方法的DI-Aware的绑定器,如清单17-30。
清单17-30 创建DI-Aware的模型绑定器
using System.Web;
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);
}
}
}
这个类使用应用程序范围的依赖解析器创建模型对象,并且必要时调用基类的实现(使用System.Activator的默认构造器创建实例)。
提示:查看第十章了解NInject依赖解析器类了解详细。 |
我们必须把我们的绑定器作为默认绑定器注册到应用程序中,我们在Global.asax文件的Application_Start方法中操作。如清单17-31。
清单17-31 注册默认模型绑定器
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
ModelBinders.Binders.DefaultBinder = new DIModelBinder();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
模型绑定处理程序现在可以创建具有依赖性的模型对象了。
创建自定义模型绑定器
我们能通过为指定的类型创建自定义的模型绑定器来重载默认绑定器的行为。清单17-32提供了一个演示。
清单17-32 自定义模型绑定器
public class PersonModelBinder:IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
// see if there is a 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.Birthday = DateTime.Parse(GetValue(bindingContext, searchPrefix, "Birthday"));
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;
}
}
这个类有很多内容,我们会打碎它并以我们的方式一步一步解析它。首先我们获取我们将要绑定的模型对象,如下
Person model = (Person)bindingContext.Model ?? (Person)DependencyResolver.Current.GetService(typeof(Person));
当我们手动调用模型绑定处理程序时,我们传递一个模型对象给UpdateModel方法,这个对象对于BindingContext方法的Model属性是可用的。一个好的模型绑定器将检查是否有模型对象可用,如果有,把它用于绑定处理程序。否则我们用应用程序范围的依赖解析器创建一个模型对象(第十章中讨论过)。
下一步我们看值提供器提供的请求数据值是否需要应用前缀:
bool hasPrefix = bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName);
string searchPrefix = (hasPrefix) ? bindingContext.ModelName + "." : "";
bindingContext.ModelName属性返回我们正绑定的模型的名字,如果我们已经在视图中渲染了模型对象,我们生成的HTML将不带有前缀,但是不管怎样ModelName属性将返回action方法的参数名。我们可以检查值提供器看是否有前缀存在。
我们通过BindingContext.ValueProvider方法值提供器,这给了我们对于所有可用的值提供器访问的统一入口,并且请求将按照之前我们在本章中讨论的顺序传递给他们。如果数据值中存在前缀,我们将使用它。
下一步我们所有值提供器从Person模型对象中获取值,下面是例子:
model.PersonId = int.Parse(GetValue(bindingContext, searchPrefix, "PersonId"));
我们在模型绑定器中定义了一个GetValue方法,它用来从统一的值提供器中获取ValueProviderResult对象并且从AttempedValue属性中摘录一个string类型的值。
我们在第15章解释了,当我们渲染Checkbox时,HTML辅助器方法创建一个隐藏输入元素来确保当没有选项被选中时我们能接收到一个值。这给我们带来了一个小问题,当模型绑定的时候,值提供器将以一个数组的形式给我们两个值,解决它,我们可以使用ValueProviderResult.ConvertTo方法来判断并给我们一个正确的值:
result = (bool)vpr.ConvertTo(typeof(bool));
提示:在这个绑定器中,我们不进行输入验证,我们乐观的假定了用户为Person类的属性提供的数据都是有效的。我们在第18章中讨论验证,此刻我们更关注基本的模型绑定处理。 |
一但我们为我们感兴趣的属性设置了值(为了简化程序,我们省略了HomeAddress属性)我们将返回我们构建的模型对象。注册我们的模型绑定器,我们在Global.asax文件的Application_Start方法中操作。如清单17-33。
清单17-33 注册自定义模型绑定器
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
ModelBinders.Binders.Add(typeof(Person), new PersonModelBinder());
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
我们使用ModelBinders.Binders.Add方法注册我们的绑定器,传入我们绑定器支持的类型并实例化绑定器类。
创建模型绑定器提供器
另一个注册模型绑定器的方法是通过实现IModelBinderProvider接口创建一个模型绑定器提供器。如清单17-34所示。
清单17-34 自定义模型绑定器提供器
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;
}
}
}
如果我们有多种类型的多个绑定器或者我们有多个提供器需要维护,这正方式更灵活。我们在Global.asax文件的Application_Start方法中注册我们的绑定器提供器,如清单17-35。
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
ModelBinderProviders.BinderProviders.Add(new CustomModelBinderProvider());
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
使用模型绑定器特性
最后一种注册自定义的模型绑定器的方式是为模型类应用ModelBinder特性,如清单17-36所示。
清单17-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 Birthday { get; set; }
public Address HomeAddress { get; set; }
public bool IsApproved { get; set; }
public Role Role { get; set; }
}
ModelBinder特性的唯一参数是绑定某种类型对象的模型绑定器的类型。我们指定了PersonModelBinder。对于这三种方式,我们倾向于使用实现IModelBinderProvider接口来处理复杂的需求。它感觉与MVC框架的其他部分更一致。或者简单的使用[ModelBinder]特性来处理只关联一种特定模型类型的模型绑定器。不管怎样,所有这些方式产生相同的行为,使用哪一种方式并不十分在重要。
小结
在本章中,我们介绍了模型绑定处理程序的工作,展示了默认模型绑定器的操作和不同的自定义处理程序的方式。许多MVC应用程序只需要使用默认的模型绑定器,它能很好的与处理辅助器生成的HTML结合,但是,对于许多高级的应用程序,创建自定义绑定器来以明确、高效的方式创建模型对象是非常有用的。下一张我们将演示怎样校验模型对象和当接受到无效数据时怎样向用户程序更有意义的错误。