模型綁定指的是MVC從浏覽器發送的HTTP請求中為我們建立.NET對象,在HTTP請求和C#間起着橋梁的作用。模型綁定的一個最簡單的例子是帶參數的控制器action方法,比如我們注冊這樣的路徑映射:
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index",
id = UrlParameter.Optional }
);
控制器Home的Index action帶有名為id的參數:
public ActionResult Index(int id) {
Person dataItem = personData.Where(p => p.PersonId == id).First();
return View(dataItem);
}
在我們請求URL“/Home/Index/1”時,預設action調用器ControllerActionInvoker使用模型綁定器為參數id指派“1”。
預設模型綁定器
模型綁定器實作IModelBinder接口,MVC預設的模型綁定器類名為DefaultModelBinder。它從Request.form、RouteData.Values 、Request.QueryString、Request.Files查找參數值,比如上面例子中的參數id,它在下面路徑中搜尋:
- Request.Form["id"]
- RouteData.Values["id"]
- Request.QueryString["id"]
- Request.Files["id"]
模型綁定器使用參數的名稱搜尋可用值,一旦找到一個可以結果搜尋即停止。
DefaultModelBinder在參數綁定中同時做類型變換,如果類型轉換失敗,參數綁定也失敗,比如我們請求URL “/Home/Index/apple”會得到int類型不能null的錯誤,模型綁定器無法将apple轉換成整數,視圖将null指派給id引發此錯誤。我們可以定義id參數為int?,這也隻能解決部分問題,在Index方法内我們沒有檢查id為null的情況,我們可以使用預設參數來徹底解決:
...
public ActionResult Index(int id = 1) {
Person dataItem = personData.Where(p => p.PersonId == id).First();
return View(dataItem);
}
...
實際的應用中我們還需要驗證綁定的參數值,比如URL /Home/Index/-1和 /Home/Index/500都可以成功綁定數值到id,但他們超過了集合的上下限。在類型轉換時還必須注意文化語言差異,比如日期格式,我們可以使用語言無關的通用格式yyyy-mm-dd。
複雜類型的綁定
上面我們看到的都是綁定到簡單c#類型的例子,如果要綁定的模型是類則要複雜的多。以下面的Model類為例:
public class Person {
public int PersonId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BirthDate { get; set; }
public Address HomeAddress { get; set; }
public bool IsApproved { get; set; }
public Role Role { get; set; }
}
public class Address {
public string Line1 { get; set; }
public string Line2 { get; set; }
public string City { get; set; }
public string PostalCode { get; set; }
public string Country { get; set; }
}
public enum Role {
Admin,
User,
Guest
}
建立兩個CreatePerson控制器action來擷取資料:
public ActionResult CreatePerson() {
return View(new Person());
}
[HttpPost]
public ActionResult CreatePerson(Person model) {
return View("Index", model);
}
這裡的action方法參數為複雜類型Person,我們使用Html.EditorFor()幫助函數在視圖中建立輸入資料的HTML:
@model MvcModels.Models.Person
@{
ViewBag.Title = "CreatePerson";
}
<h2>Create Person</h2>
@using (Html.BeginForm())
{
<div>@Html.LabelFor(m => m.PersonId)@Html.EditorFor(m => m.PersonId)</div>
<div>@Html.LabelFor(m => m.FirstName)@Html.EditorFor(m => m.FirstName)</div>
<div>@Html.LabelFor(m => m.LastName)@Html.EditorFor(m => m.LastName)</div>
<div>@Html.LabelFor(m => m.Role)@Html.EditorFor(m => m.Role)</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>
}
使用強類型的EditFor函數能保證生成的HTML元素Name包含模型綁定需要的嵌套字首,比如HomeAddress.Country,生成的HTML為:
...
<input class="text-box single-line" id="HomeAddress_Country" name="HomeAddress.Country" type="text" value="" />
...
自定義綁定名稱字首
有這樣一種情況,我們根據一個對象類型生成HTML,但是希望結果綁定到另外一個對象類型,我們可以通過自定義綁定字首來實作。比如我們的Model類:
public class AddressSummary {
public string City { get; set; }
public string Country { get; set; }
}
定義一個控制器方法來使用這個Model:
public ActionResult DisplaySummary(AddressSummary summary) {
return View(summary);
}
對應的DisplaySummary.cshtml視圖也使用這個Model類:
@model MvcModels.Models.AddressSummary
@{
ViewBag.Title = "DisplaySummary";
}
<h2>Address Summary</h2>
<div><label>City:</label>@Html.DisplayFor(m => m.City)</div>
<div><label>Country:</label>@Html.DisplayFor(m => m.Country)</div>
如果我們從上面編輯Person的視圖CreatePerson.cshtml送出到DisplaySummary action:
@model MvcModels.Models.Person
@{
ViewBag.Title = "CreatePerson";
}
<h2>Create Person</h2>
@using(Html.BeginForm("DisplaySummary", "Home")) {
<div>@Html.LabelFor(m => m.PersonId)@Html.EditorFor(m=>m.PersonId)</div>
<div>@Html.LabelFor(m => m.FirstName)@Html.EditorFor(m=>m.FirstName)</div>
<div>@Html.LabelFor(m => m.LastName)@Html.EditorFor(m=>m.LastName)</div>
<div>@Html.LabelFor(m => m.Role)@Html.EditorFor(m=>m.Role)</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>
}
DisplaySummary視圖中将無法正确綁定City和Country,因為CreatePerson中City和Country的input元素名稱包含HomeAddress字首,送出的資料是HomeAddress.City和HomeAddress.Country,而DisplaySummary視圖中是不需要這個字首的。我們可以在控制器方法上通過Bind特性指定綁定字首來修正:
public ActionResult DisplaySummary([Bind(Prefix="HomeAddress")]AddressSummary summary) {
return View(summary);
}
在Bind特性中我們還可以指定哪個屬性不要綁定,比如:
public ActionResult DisplaySummary([Bind(Prefix="HomeAddress", Exclude="Country")]AddressSummary summary) {
return View(summary);
}
這裡通過Exclude="Country"禁止Country屬性的綁定,與此相對,可以通過Include來指定需要綁定的屬性。Bind可以應用在單個action方法上,如果需要更大範圍的效果,我們可以直接應用在模型類上:
[Bind(Include="City")]
public class AddressSummary {
public string City { get; set; }
public string Country { get; set; }
}
Bind可以同時應用在Model類和action方法上,一個屬性隻有在兩個地方都沒有被排除才會包含在綁定結果中。
綁定到數組和集合
DefaultModelBinder支援數組集合的綁定,比如下面的action方法使用數組作為參數:
public ActionResult Names(string[] names) {
names = names ?? new string[0];
return View(names);
}
視圖中我們建立一組同名的input元素:
@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");
}
生成的HTML:
...
<form action="/Home/Names" method="post">
<div><label>1:</label><input id="names" name="names"type="text" value="" /></div>
<div><label>2:</label><input id="names" name="names"type="text" value="" /></div>
<div><label>3:</label><input id="names" name="names"type="text" value="" /></div>
<button type="submit">Submit</button>
</form>
...
送出資料時綁定器從多個names建構一個數組。
上面的例子換成集合是這樣的:
public ActionResult Names(IList<string> names) {
names = names ?? new List<string>();
return View(names);
}
視圖:
@model IList<string>
@{
ViewBag.Title = "Names";
}
<h2>Names</h2>
@if (Model.Count == 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");
}
如果是要綁定到一個自定義Model類型的集合:
public ActionResult Address(IList<AddressSummary> addresses) {
addresses = addresses ?? new List<AddressSummary>();
return View(addresses);
}
@using MvcModels.Models
@model IList<AddressSummary>
@{
ViewBag.Title = "Address";
}
<h2>Addresses</h2>
@if (Model.Count() == 0) {
using (Html.BeginForm()) {
for (int i = 0; i < 3; 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 (AddressSummary str in Model) {
<p>@str.City, @str.Country</p>
}
@Html.ActionLink("Back", "Address");
}
生成的HTML表單:
...
<fieldset>
<legend>Address 1</legend>
<div>
<label>City:</label>
<input class="text-box single-line" name="[0].City"type="text" value="" />
</div>
<div>
<label>Country:</label>
<input class="text-box single-line" name="[0].Country"type="text" value="" />
</div>
</fieldset>
<fieldset>
<legend>Address 2</legend>
<div>
<label>City:</label>
<input class="text-box single-line" name="[1].City"type="text" value="" />
</div>
<div>
<label>Country:</label>
<input class="text-box single-line" name="[1].Country"type="text" value="" />
</div>
</fieldset>
...
使用[0]、[1]作為輸入元素的名稱字首,綁定器知道需要建立一個集合。
手工調用模型綁定
在請求action方法時MVC自動為我們處理模型綁定,但是我們也可以在代碼中手工綁定,這提供了額外的靈活性。我們調用控制器方法UpdateModel手工綁定:
public ActionResult Address() {
IList<AddressSummary> addresses = new List<AddressSummary>();
UpdateModel(addresses);
return View(addresses);
}
我們可以提供UpdateModel額外的參數指定要資料提供者:
public ActionResult Address() {
IList<AddressSummary> addresses = new List<AddressSummary>();
UpdateModel(addresses, new FormValueProvider(ControllerContext));
return View(addresses);
}
參數FormValueProvider指定從Request.Form綁定資料,其他可用的Provider的還有RouteDataValueProvider(RouteData.Values)、QueryStringValueProvider(Request.QueryString)、HttpFileCollectionValueProvider(Request.Files),它們都實作IValueProvider接口,使用控制器類提供的ControllerContext作為構造函數參數。
實際上最常用的限制綁定源的方式是:
public ActionResult Address(FormCollection formData) {
IList<AddressSummary> addresses = new List<AddressSummary>();
UpdateModel(addresses, formData);
return View(addresses);
}
FormCollection為表單資料的鍵值集合,這是UpdateModel衆多重載形式中的一種。
手工資料綁定的另外一個好處是友善我們處理綁定錯誤:
public ActionResult Address(FormCollection formData) {
IList<AddressSummary> addresses = new List<AddressSummary>();
try {
UpdateModel(addresses, formData);
} catch (InvalidOperationException ex) {
// provide feedback to user
}
return View(addresses);
}
另外一種處理錯誤的方式是使用TryUpdateModel:
public ActionResult Address(FormCollection formData) {
IList<AddressSummary> addresses = new List<AddressSummary>();
if (TryUpdateModel(addresses, formData)) {
// proceed as normal
} else {
// provide feedback to user
}
return View(addresses);
}
自定義Value Provider
除了上面看到的内建Value provider,我們可以從IValueProvider接口實作自定義的Value provider:
namespace System.Web.Mvc {
public interface IValueProvider {
bool ContainsPrefix(string prefix);
ValueProviderResult GetValue(string key);
}
}
模型綁定器調用ContainsPrefix方法确定value provider是否可以處理提供的名稱字首,GetValue根據傳入的鍵傳回可用的參數值,如果沒有可用的資料傳回null。下面用執行個體示範如何使用自定義value provider:
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("USA", "USA", CultureInfo.InvariantCulture);
} else {
return null;
}
}
}
CountryValueProvider處理任何包含country的屬性,對所有包含country名稱的屬性總是傳回“USA”。使用自定義value provider之前還需要建立一個工廠類來建立自動那個有value provider的執行個體:
public class CustomValueProviderFactory : ValueProviderFactory {
public override IValueProvider GetValueProvider(ControllerContext controllerContext) {
return new CountryValueProvider();
}
}
最後把我們的類工廠在global.asax的application_start中添加到value provider工廠清單中:
public class MvcApplication : System.Web.HttpApplication {
protected void Application_Start() {
AreaRegistration.RegisterAllAreas();
ValueProviderFactories.Factories.Insert(0, new CustomValueProviderFactory());
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
}
這裡使用ValueProviderFactories.Factories.Insert()将自定義的value provider工廠添加到清單首位以優先使用,當然也可以ValueProviderFactories.Factories.Add()添加到清單末尾。在注冊使用這個value provider後,任何對country屬性的綁定都會得到值USA。
自定義模型綁定器
除了自定義value provider,我們還可以從IModelBinder接口建立自定義的模型綁定器:
public class AddressSummaryBinder : IModelBinder {
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
AddressSummary model = (AddressSummary)bindingContext.Model ?? new AddressSummary();
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調用AddressSummaryBinder的BindModel()方法擷取模型類型的執行個體,這裡簡單的初始化一個AddressSummary執行個體,調用value provider擷取對象屬性值,在從value provider擷取屬性值時我們把添加模型名稱ModelBindingContext.ModelName作為屬性的字首。同樣,必須在application_start中注冊自定義模型綁定器後才能使用:
...
ModelBinders.Binders.Add(typeof(AddressSummary), new AddressSummaryBinder());
...
Dependency Injection和依賴解決器
C#中使用接口可以幫助我們解耦構件, 擷取接口的實作我們通常是直接初始化接口的一個實作類:
public class PasswordResetHelper {
public void ResetPassword() {
IEmailSender mySender = new MyEmailSender();
//...call interface methods to configure e-mail details...
mySender.SendEmail();
}
}
使用IEmailSender接口在一定程度上PasswordResetHelper不再要求發送郵件時需要一個具體的郵件發送類,但是直接初始化MyEmailSender使得PasswordResetHelper并沒有和MyEmailSender解耦開。我們可以把IEmailSender接口的初始化放到PasswordResetHelper的構造函數上來解決:
public class PasswordResetHelper {
private IEmailSender emailSender;
public PasswordResetHelper(IEmailSender emailSenderParam) {
emailSender = emailSenderParam;
}
public void ResetPassword() {
// ...call interface methods to configure e-mail details...
emailSender.SendEmail();
}
}
但這樣帶來的問題是如何擷取IEmailSender的實作呢?這可以通過運作時Dependency Injection機制來解決,在建立PasswordResetHelper執行個體時依賴解決器提供一個IEmailSender的執行個體給PasswordResetHelper構造函數,這種注入方式又稱為構造注入。依賴解決器又是怎麼知道如何初始化接口的固實實作呢?答案是DI容器,通過在DI容器中注冊接口/虛類和對應的實作類将兩者聯系起來。當然DI不隻是DI容器這麼簡單,還必須考慮類型依賴鍊條、對象生命周期管理、構造函數參數配置等等問題,好在我們不需要編寫自己的容器,微軟提供自己的DI容器名為Unity(在nity.codeplex.com擷取),而開源的Ninject是個不錯的選擇。Ninject可以在visual studio中使用nuget包管理器擷取并安裝,下面就以執行個體示範如何使用Ninject,我們從接口的定義開始:
using System.Collections.Generic;
namespace EssentialTools.Models {
public interface IValueCalculator {
decimal ValueProducts(IEnumerable<Product> products);
}
}
接口的一個類實作:
using System.Collections.Generic;
using System.Linq;
namespace EssentialTools.Models {
public class LinqValueCalculator : IValueCalculator {
private IDiscountHelper discounter;
public LinqValueCalculator(IDiscountHelper discounterParam) {
discounter = discounterParam;
}
public decimal ValueProducts(IEnumerable<Product> products) {
return discounter.ApplyDiscount(products.Sum(p => p.Price));
}
}
}
我們建立一個使用Ninject的自定義依賴解決器:
using System;
using System.Collections.Generic;
using System.Web.Mvc;
using Ninject;
using EssentialTools.Models;
namespace EssentialTools.Infrastructure {
public class NinjectDependencyResolver : IDependencyResolver {
private IKernel kernel;
public NinjectDependencyResolver() {
kernel = new StandardKernel();
AddBindings();
}
public object GetService(Type serviceType) {
return kernel.TryGet(serviceType);
}
public IEnumerable<object> GetServices(Type serviceType) {
return kernel.GetAll(serviceType);
}
private void AddBindings() {
kernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
}
}
}
這裡最重要的是AddBindings方法中的kernel.Bind<IValueCalculator>().To<LinqValueCalculator>(),它将接口IValueCalculator和類實作LinqValueCalculator結合起來,在我們需要接口IValueCalculator的一個執行個體時,會調用NinjectDependencyResolver的GetService擷取到LinqValueCalculator的一個執行個體。要使NinjectDependencyResolver起作用還必須注冊它為應用預設的依賴解決器,這是在application_start中操作:
public class MvcApplication : System.Web.HttpApplication {
protected void Application_Start() {
AreaRegistration.RegisterAllAreas();
DependencyResolver.SetResolver(new NinjectDependencyResolver());
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
}
}
控制器的構造函數中我們傳入接口IValueCalculator,依賴解決器會自動為我們建立一個LinqValueCalculator的執行個體:
public class HomeController : Controller {
private Product[] products = {
new Product {Name = "Kayak", Category = "Watersports", Price = 275M},
new Product {Name = "Lifejacket", Category = "Watersports", Price = 48.95M},
new Product {Name = "Soccer ball", Category = "Soccer", Price = 19.50M},
new Product {Name = "Corner flag", Category = "Soccer", Price = 34.95M}
};
private IValueCalculator calc;
public HomeController(IValueCalculator calcParam) {
calc = calcParam;
}
public ActionResult Index() {
ShoppingCart cart = new ShoppingCart(calc) { Products = products };
decimal totalValue = cart.CalculateProductTotal();
return View(totalValue);
}
}
Ninject的綁定方法非常的靈活:
kernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>().WithPropertyValue("DiscountSize", 50M); //綁定時指定DefaultDiscountHelper的屬性DiscountSize=50
kernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>().WithConstructorArgument("discountParam", 50M);//綁定時指定DefaultDiscountHelper的構造函數參數discountParam=50
kernel.Bind<IDiscountHelper>().To<FlexibleDiscountHelper>().WhenInjectedInto<LinqValueCalculator>();//條件綁定,在注入到LinqValueCalculator時綁定接口LinqValueCalculator到FlexibleDiscountHelper
除了使用自定義的依賴解決器,我們可以從預設控制器工廠擴充控制器工廠,在自定義控制器工廠中使用Ninject依賴注入:
public class NinjectControllerFactory : DefaultControllerFactory {
private IKernel ninjectKernel;
public NinjectControllerFactory() {
ninjectKernel = new StandardKernel();
AddBindings();
}
protected override IController GetControllerInstance(RequestContext
requestContext, Type controllerType) {
return controllerType == null
? null
: (IController)ninjectKernel.Get(controllerType);
}
private void AddBindings() {
ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
}
MVC在擷取控制器時調用GetControllerInstance,它使用ninjectKernel.Get(controllerType)來擷取相應的控制類執行個體,同時解決構造注入的問題,比如HomeController的構造函數參數IValueCalculator calcParam,使用這種方式可以限制僅在控制器内注入,控制器外整個應用範圍内我們仍然可以使用自定義依賴解決器注入。
需要注意的是依賴解決和注入不是模型綁定的一部分,但它們有一定的相似性,後者解決的action方法上的參數綁定,前者可以說是整個控制器類(構造函數)上的參數綁定(當然不隻是用在控制器類上)。
以上為對《Apress Pro ASP.NET MVC 4》第四版相關内容的總結,不詳之處參見原版 http://www.apress.com/9781430242369。