一:简单的模型绑定
在ASP.NET MVC中是模型绑定来解析客户端传过来的数据的,简单的来说就更近一步来封装了获得数据的手段,让用户更方便的来获取数据了
我们来简单做一个例子
public ActionResult Index() { return View(); } [HttpPost] public ActionResult Index(string username) { ViewData["username"] = username; return View(); }
这里我们定义了一个Action,[httpPost]限定Action只能被post请求访问,我们在其参数中定义一个名字username的字符串
view视图中的代码如下
@{ ViewBag.Title = "Index"; } <form method="post"> <h1>简单模型绑定</h1> 姓名:<input type="text" name="username" id="username" /> <input type="submit" value="提交" /> </form> @if (ViewData["username"] != null) { <h1>@ViewData["username"]</h1> }
这里注意模型绑定的name属性需要和view视图中的input标签工具name相同
二:使用FormCollection来进行模型绑定
在Asp.net中我们习惯了用request.Form来获取提交的表单的值,当然在MVC中我们可以很方便的获取其值
public ActionResult Index() { return View(); } [HttpPost] public ActionResult Index(FormCollection e) { ViewData["username"] = e["username"]; ViewData["Age"] = e["Age"]; return View(); }
在视图中代码为
<form method="post"> <h1>简单模型绑定</h1> 姓名:<input type="text" name="username" id="username" /> 年龄:<input type="text" name="Age" id="Age" /><br /> <input type="submit" value="提交" /> </form> @if (ViewData["username"] != null) { <label>姓名:</label><h1>@ViewData["username"]</h1> <label>年龄:</label><h1>@ViewData["Age"]</h1> }
我们可以通过这种方法清晰的看到一样是能获取数据
三:模型绑定之传递对象
之前我们讨论的都是通过表单传递一些简单的值,现在如果要传递一个model对象能不能呢?我们先在Model层添加一个People得吧
public class People { public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } }
在Controller将代码修改
public ActionResult Index() { return View(); } [HttpPost] public ActionResult Index(People people) { ViewData["Id"] = people.Id; ViewData["Name"] = people.Name; ViewData["Age"] = people.Age; return View(); }
此时我们将模型绑定为People类型
View中视图修改为
<form method="post"> Id:<input type="text" name="Id" id="Id" /> 姓名:<input type="text" name="Name" id="Name" /> 年龄:<input type="text" name="Age" id="Age" /><br /> <input type="submit" value="提交" /> </form> @if (ViewData["Id"] != null) { <label>Id:</label><h1>@ViewData["Id"]</h1> <label>姓名:</label><h1>@ViewData["Name"]</h1> <label>年龄:</label><h1>@ViewData["Age"]</h1> }
当然这里我们将定义的Input标签的name属性与类people名字一样
到这里你可能又会觉得那能不能传递多个对象呢?
我们将Controller的代码改动一下
public ActionResult Index() { return View(); } [HttpPost] public ActionResult Index(People people1, People people2) { ViewData["Id1"] = people1.Id; ViewData["Name1"] = people1.Name; ViewData["Age1"] = people1.Age; ViewData["Id2"] = people2.Id; ViewData["Name2"] = people2.Name; ViewData["Age2"] = people2.Age; return View(); }
<form method="post"> Id1:<input type="text" name="people1.Id" id="people1.Id" /> 姓名1:<input type="text" name="people1.Name" id="people1.Name" /> 年龄1:<input type="text" name="people1.Age" id="people1.Age" /><br /> Id2:<input type="text" name="people2.Id" id="people2.Id" /> 姓名2:<input type="text" name="people2.Name" id="people2.Name" /> 年龄2:<input type="text" name="people2.Age" id="people2.Age" /><br /> <input type="submit" value="提交" /> </form> @if (ViewData["Id1"] != null) { <label>Id1:</label><h1>@ViewData["Id1"]</h1> <label>姓名1:</label><h1>@ViewData["Name1"]</h1> <label>年龄1:</label><h1>@ViewData["Age1"]</h1> <label>Id2:</label><h1>@ViewData["Id2"]</h1> <label>姓名2:</label><h1>@ViewData["Name2"]</h1> <label>年龄2:</label><h1>@ViewData["Age2"]</h1> }
注意:View视图我们必须将标签的name属性设置和模型绑定名字一样
Model Binding(模型绑定)是Http请求和Action方法之间的桥梁,它根据Action方法中的Model类型创建.NET对象,并将Http请求数据经过转换赋给该对象
模型绑定原理
以HomeController下Index的简单例子来查看
public ActionResult Index(int id = 0) { viewdata["Id"]=id; return view(); }
Action方法是由默认的Action Invoke(即ControllerActionInvoker类)来调用,Action方法是由默认的Action Invoker(即ControllerActionInvker类)来调用,Action Invoker 依靠 Model Binder(模型绑定器) 来创建调用 Action 方法需要的数据对象。我们可以通过 Model Binder 实现的接口来了解它的功能,该接口是 IModelBinder
namespace System.Web.Mvc { public interface IModelBinder { object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext); } }
当 action invoker 需要调用一个 action 方法时,它先看这个 action 方法需要的参数,然后为每个参数找到和参数的类型对应的 Model Binder。对于我们以上示例,Action Invoker 会先检查action 方法,发现它有一个int的参数,然后它会定位到负责给其类型提供值的 Binder,并调用该 Binder 的 BindModel 方法。该方法再根据 Action 方法参数名称从路由信息中获取其类型的值,最后把该值提供给 Action Invoker。
MVC 框架内置默认的 Model Binder 是 DefaultModelBinder 类
当Action Invoker没有找到自定义的Binder时,则默认使用DefaultModelBinder,默认情况下DefaultModelBinder从如下4中途径找到绑定的值
1.Request.Form,HTML form 元素提供的值。
2.RouteData.Values,通过应用程序路由提供的值。
3.Request.QueryString,所请求 URL 的 query string 值。
4.Request.Files,客户端上传的文件。
DefaultModelBinder会按照上面的顺序来查找对应的值
1.Request.Form["id"]
2.RouteData.Values["id"]
3.Request.QueryString["id"]
4.Request.Files["id"]
复合类型的绑定
复合类型:任何不能被 TypeConverter 类转换的类型(大多指自定义类型),否则称为简单类型,对于复合类型,DefaultModelBinder 类通过反射获取该类型的所有公开属性,然后依次进行绑定。
依然看上面的例子
public class People { public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } }
在Action方法中
public ActionResult CreatePerson(People model) { return View(model); }
但是如果一个复合类型的属性也是复合类型
public class People { public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } public Address HomeAddress { get; set; } } public class Address { public string City { get; set; } public string Country { get; set; } }
表单提交后,model binder 会在 Request.Form["HomeAddress.Country"] 中查找到 People.HomeAddress 的 Country 属性的值。当Model binder 检查到 People类型参数的 HomeAddress 属性是一个复合类型,它会重复之前的查找工作,为 HomeAddress 的每个属性查找值,唯一不同的是,查找的时候用的名称不一样。
应用 Bind 特性
有时候我们还会遇到这样的情况,某个 action 方法的参数类型是某个对象的属性的类型,如下面这个 DisplayAddress action 方法:
public ActionResult DisplayAddress(Address address) { return View(address); }
@model MvcApplication1.Models.Person ... @using(Html.BeginForm("DisplayAddress", "Home")) { <div>@Html.LabelFor(m => m.PersonId)@Html.EditorFor(m=>m.PersonId)</div> <div> @Html.LabelFor(m => m.HomeAddress.City) @Html.EditorFor(m=> m.HomeAddress.City) </div> <div> @Html.LabelFor(m => m.HomeAddress.Country) @Html.EditorFor(m=> m.HomeAddress.Country) </div> <button type="submit">Submit</button> }
那么我们如何把 Person 类型的对象传递给 DisplayAddress(Address address) 方法呢?点提交按钮后,Binder 能为 Address 类型的参数绑定 Person 对象中的 HomeAddress 属性值吗?
经过验证视图中的值确实不能传递给DisplayAddress中的参数address,不能模型绑定的原因是问题在于生成 form 表单的 name 属性有 HomeAddress 前缀(name="HomeAddress.Country"),model binder 会通过request.form[“Country”]去进行绑定,因为提交上来的name有HoneAddress.Country有了HomeAddress所以无法绑定
这时我们利用Bind绑定特性就可以解决问题
public ActionResult DisplayAddress([Bind(Prefix="HomeAddress")]Address address) { return View(address); }
注意:使用 Bind 特性指定了前缀后,需要提交的表单元素的 name 属性必须有该前缀才能被绑定。
Bind 特性还有两个属性,Exclude 和 Include
Exclude:在绑定时不会对 Address 这个 Model 的 Country 属性绑定值
public ActionResult DisplayAddress([Bind(Prefix = "HomeAddress", Exclude = "Country")]Address address) { return View(address); }
Include:Binder 在给 Address 模型绑定时只会给 Country 属性绑定值
[Bind(Include = "Country")] public class Address { public string City { get; set; } public string Country { get; set; } }
绑定到数组
public ActionResult Names(string[] names) { names = names ?? new string[0]; return View(names); }
@model string[] @{ ViewBag.Title = "Names"; } <h2>Names</h2> @if (Model.Length == 0) { using (Html.BeginForm()) { for (int i = 0; i < 3; i++) { <div><label>@(i + 1):</label>@Html.TextBox("names")</div> } <button type="submit">Submit</button> } } else { foreach (string str in Model) { <p>@str</p> } @Html.ActionLink("Back", "Names"); }
重要:绑定到集合
先创建一个带有 IList<Address> 参数的 action 方法:
public ActionResult Address(IList<Address> addresses) { addresses = addresses ?? new List<Address>(); return View(addresses); }
在 View 中表单元素的 name 属性应该怎样命名才能被 Model Binder 识别为集合呢?
@using MvcApplication1.Models @model IList<Address> @{ ViewBag.Title = "Address"; } <h2>Addresses</h2> @if (Model.Count() == 0) { using (Html.BeginForm()) { for (int i = 0; i < 2; i++) { <fieldset> <legend>Address @(i + 1)</legend> <div><label>City:</label>@Html.Editor("[" + i + "].City")</div> <div><label>Country:</label>@Html.Editor("[" + i + "].Country")</div> </fieldset> } <button type="submit">Submit</button> } } else { foreach (Address str in Model) { <p>@str.City, @str.Country</p> } @Html.ActionLink("Back", "Address"); }
当 Model Binder 发现 Address action 方法需要一个 Address 集合作为参数时,它便从提交的数据中从索引 [0] 开始查找和 Address 的属性名称相同的数据值.
手动调用model Bind方法
使用UpdateModel来指定调用model Bind的方法来指定只从某一数据源来啊获取数据
public ActionResult Address() { IList<Address> addresses = new List<Address>(); UpdateModel(addresses, new FormValueProvider(ControllerContext)); return View(addresses); }
自定义Value Provider
通过自定义 Value Provider 我们可以为 Model Binding 添加自己的数据源。前面我们讲到了四种内置 Value Provider 实现的接口是 IValueProvider,我们可以实现这个接口来自定义一个 Value Provider。先来看这个接口的定义:
namespace System.Web.Mvc { public interface IValueProvider { bool ContainsPrefix(string prefix); ValueProviderResult GetValue(string key); } }
ContainsPrefix 方法是 Model Binder 根据给定的前缀用来判断是否要解析所给数据。GetValue 方法根据数据的key返回所需要值。下面我们添加一个 Infrastructure 文件夹,创建一个名为 CountryValueProvider 的类来实现这个接口,代码如下:
public class CountryValueProvider : IValueProvider { public bool ContainsPrefix(string prefix) { return prefix.ToLower().IndexOf("country") > -1; } public ValueProviderResult GetValue(string key) { if (ContainsPrefix(key)) return new ValueProviderResult("China", "China", CultureInfo.InvariantCulture); else return null; } }
这就自定义好了一个 Value Provider,当需要一个 Country 的值时,它始终返回"China",其它返回 null。ValueProviderResult 类的构造器有三个参数,第一个参数是原始值对象,第二个参数是原始对象的字符串表示,最后一个是转换这个值所关联的 culture 信息。
为了让 Model Binder 调用这个 Value Provider,我们需要创建一个能实现化它的类,这个类需要继承 ValueProviderFactory 抽象类。如下我们创建一个这样的类,名为 CustomValueProviderFactory:
public class CustomValueProviderFactory : ValueProviderFactory { public override IValueProvider GetValueProvider(ControllerContext controllerContext) { return new CountryValueProvider(); } }
当 model binder 在绑定的过程中需要获取值时会调用这里的 GetValueProvider 方法。这里我们没有做别的处理,直接返回了一个 CountryValueProvider 实例。
最后我们需要在 Global.asax 文件中的 Application_Start 方法中进行注册,如下:
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); ValueProviderFactories.Factories.Insert(0, new CustomValueProviderFactory()); ...
自定义 Model Binder
我们也可以为特定的 Model 自定义 Model Binder。前面讲了默认的 Model Binder 实现的接口是 IModelBinder(前文列出了它的定义),自定义的 Binder 自然也需要实现该接口。下面我们在 Infrastructure 文件夹中添加一个实现了该接口的名为 AddressBinder 类,代码如下:
public class AddressBinder : IModelBinder { public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { Address model = (Address)bindingContext.Model ?? new Address(); model.City = GetValue(bindingContext, "City"); model.Country = GetValue(bindingContext, "Country"); return model; } private string GetValue(ModelBindingContext context, string name) { name = (context.ModelName == "" ? "" : context.ModelName + ".") + name; ValueProviderResult result = context.ValueProvider.GetValue(name); if (result == null || result.AttemptedValue == "") return "<Not Specified>"; else return (string)result.AttemptedValue; } }
当 MVC 框架需要一个 model 类型的实现时,则调用 BindModel 方法。它的 ControllerContext 类型参数提供请求相关的上下文信息,ModelBindingContext 类型参数提供 model 对象相关的上下文信息。ModelBindingContext 常用的属性有Model、ModelName、ModelType 和 ValueProvider。这里的 GetValue 方法用到的 context.ModelName 属性可以告诉我们,如果有前缀(一般指复合类型名),则需要把它加在属性名的前面,这样 MVC 才能获取到以 [0].City、[0].Country 名称传递的值。
然后我们需要在 Global.asax 的 Application_Start 方法中对自定义的 Model Binder 进行注册,如下所示:
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); //ValueProviderFactories.Factories.Insert(0, new CustomValueProviderFactory()); ModelBinders.Binders.Add(typeof(Address), new AddressBinder()); ...