模型綁定指的是MVC從瀏覽器發送的HTTP請求中爲咱們建立.NET對象,在HTTP請求和C#間起着橋樑的做用。模型綁定的一個最簡單的例子是帶參數的控制器action方法,好比咱們註冊這樣的路徑映射:html
routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } );
控制器Home的Index action帶有名爲id的參數:c#
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,它在下面路徑中搜索:瀏覽器
模型綁定器使用參數的名稱搜索可用值,一旦找到一個能夠結果搜索即中止。app
DefaultModelBinder在參數綁定中同時作類型變換,若是類型轉換失敗,參數綁定也失敗,好比咱們請求URL 「/Home/Index/apple」會獲得int類型不能null的錯誤,模型綁定器沒法將apple轉換成整數,視圖將null賦值給id引起此錯誤。咱們能夠定義id參數爲int?,這也只能解決部分問題,在Index方法內咱們沒有檢查id爲null的狀況,咱們可使用默認參數來完全解決:ide
... 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類爲例:post
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來獲取數據:url
public ActionResult CreatePerson() { return View(new Person()); } [HttpPost] public ActionResult CreatePerson(Person model) { return View("Index", model); }
這裏的action方法參數爲複雜類型Person,咱們使用Html.EditorFor()幫助函數在視圖中建立輸入數據的HTML:spa
@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,咱們能夠從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()); ...
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。