這篇文章我們來講講模型綁定(Model Binding),其實在初步了解ASP.NET MVC之後,大家可能都會產生一個疑問,為什麼URL片段最後會轉換為例如int型或者其他類型的參數呢?這裡就不得不說模型綁定了。模型綁定是指,用浏覽器以HTTP請求方式發送的數據來創建.NET對象的過程。每當定義具有參數的動作方法時,一直是在依賴著這種模型綁定過程。
准備項目
我們先來創建一個MVC項目,名叫MVCModels,並在Models文件夾中創建一個新的類文件Person。
1 using System; 2 3 namespace MVCModels.Models 4 { 5 public class Person 6 { 7 public int PersonId { get; set; } 8 public string FirstName { get; set; } 9 public string LastName { get; set; } 10 public DateTime BirthDate { get; set; } 11 public Address HomeAddress { get; set; } 12 public Role Role { get; set; } 13 } 14 15 public class Address 16 { 17 public string Line { get; set; } 18 public string City { get; set; } 19 public string PostalCode { get; set; } 20 public string Country { get; set; } 21 } 22 23 public enum Role 24 { 25 Admin, 26 User, 27 Guest 28 } 29 }
另外定義一個Home控制器。
1 using MVCModels.Models; 2 using System.Linq; 3 using System.Web.Mvc; 4 5 namespace MVCModels.Controllers 6 { 7 public class HomeController : Controller 8 { 9 private Person[] personDate = { 10 new Person { PersonId = 1, FirstName = "Adam", LastName = "Freeman", Role = Role.Admin }, 11 new Person { PersonId = 2, FirstName = "Jacqui", LastName = "Griffyth", Role = Role.User }, 12 new Person { PersonId = 1, FirstName = "John", LastName = "Smith", Role = Role.Guest }, 13 }; 14 public ActionResult Index(int id) 15 { 16 Person dataItem = personDate.Where(p => p.PersonId == id).First(); 17 return View(dataItem); 18 } 19 } 20 }
接下來再創建一個視圖文件Index。
@model MVCModels.Models.Person @{ ViewBag.Title = "Index"; Layout = "~/Views/Shared/_Layout.cshtml"; } <h2>Person</h2> <div> <label>ID:</label> @Html.DisplayFor(m => m.PersonId) </div> <div> <label>First Name:</label> @Html.DisplayFor(m => m.FirstName) </div> <div> <label>Last Name:</label> @Html.DisplayFor(m => m.LastName) </div> <div> <label>Role:</label> @Html.DisplayFor(m => m.Role) </div>
理解模型綁定
模型綁定是HTTP請求與C#方法之間的一個橋梁,它根據 Action 方法中的 Model 類型創建 .NET 對象,並將 HTTP 請求數據經過轉換賦給該對象。當我們啟動項目,並導航到/Home/Index/1,我們會看見圖下:
當我們請求 /Home/Index/1 URL 時,路由系統便將最後一個片段值 1 賦給了 id 變量。action invoker 通過路由信息知道當前的請求需要 Index action 方法來處理,但它調用 Index action 方法之前必須先拿到該方法參數的值。 默認的動作調用器ControllerActionInvoker,要依靠模型綁定器來生成調用動作所需要的的數據對象,模型綁定器由IModelBinder接口所定義。在本例中,動作調用器會檢查Index方法,並發現它具有一個int型參數,於是會查找負責int值綁定的綁定器,並調用它的BindModel方法。
使用默認模型綁定器
雖然應用程序可以自定義模型綁定器,但是大多數情況下還是依靠內建的綁定器類DefaultModelBinder。當動作調用器找不到綁定某個類型的自定義綁定器時,就會使用這個默認的模型綁定器,默認情況下,這個模型綁定器會搜索四個位置。如下表所示:
這些位置將被依次搜索,在本例中,DefaultModelBinder會為id參數查找以下的一個值:
1.Request.Form["id"]
2.RouteData.Values["id"]
3.Request.QueryString["id"]
4.Request.Files["id"]
綁定簡單類型
處理簡單參數類型時,DefaultModelBinder會使用System.ComponentModel.TypeDescriptor類,講請求數據獲得的字符參數值轉換為參數類型,如果無法轉換,那麼DefaultModelBinder便不能綁定到Model。比如訪問/Home/Index/apple,便會出現如圖所示:
默認模型綁定器看到需要的是int值,如果視圖將URL中提供的apple值轉換為int型,就會出錯,此時我們可以修改代碼,為參數提供一個默認值,這樣當默認綁定器無法轉換時也不會出錯了。
1 ... 2 public ActionResult Index(int id = 1) 3 { 4 Person dataItem = personDate.Where(p => p.PersonId == id).First(); 5 return View(dataItem); 6 }
綁定復雜類型
當動作方法參數是符合類型時(即不能用TypeConverter類進行轉換的屬性),DefaultModelBinder類將用反射類獲取public屬性集。好的,我們現在來修改代碼。
1 using MVCModels.Models; 2 using System.Linq; 3 using System.Web.Mvc; 4 5 namespace MVCModels.Controllers 6 { 7 public class HomeController : Controller 8 { 9 private Person[] personDate = { 10 new Person { PersonId = 1, FirstName = "Adam", LastName = "Freeman", Role = Role.Admin }, 11 new Person { PersonId = 2, FirstName = "Jacqui", LastName = "Griffyth", Role = Role.User }, 12 new Person { PersonId = 1, FirstName = "John", LastName = "Smith", Role = Role.Guest }, 13 }; 14 public ActionResult Index(int id = 1) 15 { 16 Person dataItem = personDate.Where(p => p.PersonId == id).First(); 17 return View(dataItem); 18 } 19 public ActionResult CreatePerson() 20 { 21 return View(new Person()); 22 } 23 [HttpPost] 24 public ActionResult CreatePerson(Person model) 25 { 26 return View("Index", model); 27 } 28 } 29 }
另外創建一個CreatePerson視圖。
1 @model MVCModels.Models.Person 2 @{ 3 ViewBag.Title = "CreatePerson"; 4 Layout = "~/Views/Shared/_Layout.cshtml"; 5 } 6 7 <h2>CreatePerson</h2> 8 @using (Html.BeginForm()) 9 { 10 <div> 11 <label> 12 @Html.LabelFor(m => m.PersonId) 13 @Html.EditorFor(m => m.PersonId) 14 </label> 15 </div> 16 <div> 17 <label> 18 @Html.LabelFor(m => m.FirstName) 19 @Html.EditorFor(m => m.FirstName) 20 </label> 21 </div> 22 <div> 23 <label> 24 @Html.LabelFor(m => m.LastName) 25 @Html.EditorFor(m => m.LastName) 26 </label> 27 </div> 28 <div> 29 <label> 30 @Html.LabelFor(m => m.Role) 31 @Html.EditorFor(m => m.Role) 32 </label> 33 </div> 34 <button type="submit">Submit</button> 35 }
在表單回遞給CreatePerson方法時,默認綁定器發現動作方法需要一個Person對象,便會依次處理每個屬性。綁定器會從請求中找到每一個值。如果一個屬性需要另一個復合類型時,那麼該過程會重復執行。例如本例中,Person類的HomeAddress屬性是Address類型,在為Line屬性查找值時,模型綁定器查找的是HomeAddress.Line的值,即模型對象的屬性名(HomeAddress)與屬性類型(Address)的屬性名(Line)的組合。
Bind特性
我們還可以用bind特性為 Address 類型的參數綁定 Person 對象中的 HomeAddress 屬性值,例如這樣:
1 public ActionResult DisplayAddress([Bind(Prefix="HomeAddress")]Address address) 2 { 3 return View(address); 4 }
DisplayAddress action 方法的參數類型 Address 不一定必須是 Person 的 HomeAddress 屬性的類型,它可以是其他類型,只要該類型中含有City
或 Country 同名的屬性就都會被綁定到。不過,要注意的是,使用 Bind 特性指定了前綴後,需要提交的表單元素的 name 屬性必須有該前綴才能被綁定。Bind 特性還有兩個屬性,Exclude 和 Include。它們可以指定在 Mdoel 的屬性中,Binder 不查找或只查找某個屬性,即在查找時要麼只包含這個屬性要麼不包含這個屬性。如下面的 action 方法:
1 public ActionResult DisplayAddress([Bind(Prefix = "HomeAddress", Exclude = "Country")]Address address) 2 { 3 return View(address); 4 }
這時 Binder 在綁定時不會對 Address 這個 Model 的 Country 屬性綁定值。