Asp.Net MVC4 系列--進階篇之Model(1)
從本章開始,將介紹Asp.NetMVC4中的model部分
model binding
從sample開始
1.準備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
}
2.準備Controller
public class PersonController : Controller
{
//
// GET: /Person/
private readonly Person[] _personData = {
new Person {FirstName = "Iori",LastName = "Lan", Role = Role.Admin,PersonId = 1},
new Person {FirstName = "Edwin",LastName = "Sanderson", Role = Role.Admin,PersonId = 2},
new Person {FirstName = "John",LastName = "Griffyth", Role = Role.User,PersonId = 3},
new Person {FirstName = "Tik",LastName = "Smith", Role = Role.User,PersonId = 4},
new Person {FirstName = "Anne",LastName = "Jones", Role = Role.Guest,PersonId = 5}
};
public ActionResult PersonInfo(int id)
{
Person dataItem = _personData.Where(p => p.PersonId == id).First();
return View("PersonInfo", dataItem);
}
}
3.View(PersonInfo.cshtml)
@model MVCModel.Models.Person
@{
ViewBag.Title = "Index";
}
<h2>Person</h2>
<div><label>ID:</label>@Html.DisplayFor(m=> m.PersonId)</div>
<div><label>FirstName:</label>@Html.DisplayFor(m => m.FirstName)</div>
<div><label>LastName:</label>@Html.DisplayFor(m => m.LastName)</div>
<div><label>Role:</label>@Html.DisplayFor(m=> m.Role)</div>
4.執行
Scenario 1 : 不傳遞id
Scenario 2 : 傳遞一個正確型別的id
Scenario 3: 傳遞一個錯誤型別的id
結果:只有Scenario 2程式碼是正確工作的,也就是對於valuetype來說,不給引數或者錯誤型別,model都無法完成引數解析。
從Request到Render
本章重點在Action Invoker到GetParameter這個過程,Modelbinding的工作就是負責給Action搞定Parameter,而資訊Request中都有。
Model binder的搜尋範圍和順序:
Request.Form |
RouteData.Values |
Request.QueryString |
Request.Files |
以剛才的Scenario 為例,當id被解析為引數時,modelbinder會從Form,RouteData.Values,QueryString,Files中找到key為id的值,找到了就返回。
對於值型別,為了避免傳參型別不對或者為空model直接拋異常,可以給一個預設值引數,或者給一個Nullable型別(int?)
對於類,如果沒有傳參,model會給一個空進來,但是也可以給一個預設引數,或者每次Assert引數不為空也可以。
Binding到類型別
Controller新增Action:
public ActionResult CreatePerson()
{
return View(new Person());
}
[HttpPost]
public ActionResult CreatePerson(Personmodel)
{
////Repository operation
return View("PersonInfo",model);
}
新增 View(CreatePerson.cshtml) :
@model MVCModel.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>
<button type="submit">Submit</button>
}
執行檢視結果:
點選submit
可以看到,model binding幫我們“翻譯”出了Person物件,傳入了PersonInfo,顯示了出來。
檢視CreatePerson View的Formhtml:
<form action="/Person/CreatePerson" method="post"><div><label for="PersonId">PersonId</label><input class="text-box single-line" data-val="true" data-val-number="Thefield PersonId must be a number." data-val-required="The PersonId field is required." id="PersonId" name="PersonId" type="number" value="0" /></div>
<div><label for="FirstName">FirstName</label><input class="text-box single-line" id="FirstName" name="FirstName" type="text" value=""/></div>
<div><label for="LastName">LastName</label><input class="text-box single-line" id="LastName" name="LastName" type="text" value=""/></div>
<div><label for="Role">Role</label><input class="text-box single-line" data-val="true" data-val-required="The Role field isrequired." id="Role" name="Role" type="text" value="Admin" /></div>
<button type="submit">Submit</button>
</form>
Model binder找到了controller,會遍歷action,發現引數需要一個Person物件,為了構造這個Person物件,於是反射出Person需要的Member,於是從Request.Form中找匹配PersonMember名稱的name,發現了PersonId,FirstName,LastName,還有Role,然後拿出value(根據control型別拿不同的屬性),依次賦值給Person的Member,流程圖:
巢狀型別的binding
1. 為了學習巢狀型別的binding,在View(CreatePerson)中新增:
<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>
看一下Address部分生成的html
<div>
<label for="HomeAddress_City">City</label>
<input class="text-boxsingle-line" id="HomeAddress_City" name="HomeAddress.City" type="text" value=""/>
</div>
<div>
<label for="HomeAddress_Country">Country</label>
<input class="text-boxsingle-line" id="HomeAddress_Country" name="HomeAddress.Country" type="text" value=""/>
</div>
可以看到name分別為:HomeAddress.City和HomeAddress.Country。modelbinder在Person中查詢到HomeAddress是個類,就會反射出裡面的基本型別(如果又是類,那麼繼續遞迴執行,但是名字會累加),執行上面演示的順序進行匹配(匹配時名稱用的是累加之後的),因此,巢狀型別的關鍵在於,匹配時使用的名字和html要對上,html為HomeAddress.City,那麼model反射時找到的是類,就也要把這個欄位名累加。
指定Prefix
Scenario : 可能會有一些場景,比如View1需要Submit請求到Action2,Submit的是Model1,而Action2接收的是Model2,期望Action2可以識別出兩個Model公共欄位,賦值然後生成Model2.
具體例子:
新增一個Model:
public class AddressSummary
{
public string City { get; set; }
public string Country { get; set; }
}
新增一個Action:
public ActionResult DisplaySummary(AddressSummary summary)
{
return View("AddressSummary",summary);
}
新增View:
@model MVCModel.Models.AddressSummary
@{
ViewBag.Title ="DisplaySummary";
}
<h2>AddressSummary</h2>
<div><label>City:</label>@Html.DisplayFor(m=> m.City)</div>
<div><label>Country:</label>@Html.DisplayFor(m=> m.Country)</div>
然後把CreatePersonView改一下,Form指向DisplaySummaryAction
@using(Html.BeginForm("DisplaySummary","Person")){
期望結果:進入CreatePerson,點Submit,Form被提交到DisplaySummary的Action,然後ModelBinder把HomeAddress的City和Country拿出來構造為AddressSummary物件傳給AddressSummaryView,顯示出City和Country。
檢視結果:
點選Submit
可以看到,Form指向了Person/DisplaySummary,可是ModelBinder並沒有成功的把Action需要的AddressSummary解析正確,以致於傳給AddressSummaryView的物件是空的,什麼也沒有顯示。
檢視CreatePerson生成的Html(Address部分):
<div>
<label for="HomeAddress_City">City</label>
<input class="text-box single-line" id="HomeAddress_City" name="HomeAddress.City" type="text" value=""/>
</div>
<div>
<label for="HomeAddress_Country">Country</label>
<input class="text-boxsingle-line" id="HomeAddress_Country" name="HomeAddress.Country" type="text" value=""/>
</div>
可以看到,字首為HomeAddress,可是Model工作時看到Action需要的是AddressSummary物件,需要的是City和Country,因此並不認為它需要任何字首,因此我們要manually的告訴model我們的字首:
public ActionResult DisplaySummary([Bind(Prefix = "HomeAddress")] AddressSummary summary)
{
return View("AddressSummary",summary);
}
再次執行:
可以看到,modelbinder這次成功的拿著我們給它的字首,正確的找到了Form裡面的value,取出來賦值給了AddressSummary View,View中正確的render出了我們希望的html。
除了Prefix,我們還可以設定:
Include:告訴binder,只有這些欄位需要找,賦值,其他的都不管。語法:
[Bind(Include="City")]
public class AddressSummary {
public string City { get; set; }
public string Country { get; set; }
}
注:這個attribute除了加到引數上,還可以加在model上。
Exclude : 告訴binder,binding時不需要哪些欄位,這樣binder就不管了,語法:[Bind(Prefix="HomeAddress",Exclude="Country")] 。
Binding到陣列
新增Action:
public ActionResult Names(string[] names)
{
names = names ?? new string[0];
return View("Names",names);
}
新增View:
@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");
}
執行
Click Submit
可以看到,ModelBinder成功的解析除了View給它的陣列,解析出來給回了View,View中Razor判斷Model有資料,foreach出了每一個Name。
分析Person/Names的html(Form部分):
<form action="/person/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>
可以看到,我們給modelbinder的是name為”names”的三個input,放在了Request.Form裡;而對Model而言,它發現Action要的是String陣列,於是從Form,QueryString,RouteData.Values,Files找,name為names的所有匹配,拿到值放在陣列中給action。
類的陣列
Controller 新增
public ActionResult Address(IList<AddressSummary> addresses)
{
addresses = addresses ?? new List<AddressSummary>();
return View("AddressList",addresses);
}
新增View
@using MVCModel.Models
@model IList<AddressSummary>
@{
ViewBag.Title = "Address";
}
<h2>Addresses</h2>
@if (Model.Count() == 0) {
using (Html.BeginForm("Address"))
{
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");
}
執行
Submit
可以看到Model解析並給類陣列正確的賦值傳給了View。
檢視html(Form部分)
<form action="/person/Address?Length=7" method="post"><fieldset>
<legend>Address1</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>Address2</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>
<fieldset>
<legend>Address3</legend>
<div><label>City:</label><input class="text-box single-line" name="[2].City" type="text" value="" /></div>
<div><label>Country:</label><input class="text-box single-line" name="[2].Country" type="text" value="" /></div>
</fieldset>
<button type="submit">Submit</button>
</form>
分析:ModelBinder會foreach型別的每一個欄位,拿著欄位名字去執行上述的查詢過程,一個一個物件來construct,可是Construct物件時候,我們需要給model一個索引,這樣model能夠在構造物件時,知道把哪個屬性放在哪個物件裡,最後給action。
UpdateModel
有時需要手動來呼叫updateModel,從Request中拿到value把值給到要賦值的物件中,改動上例的Action:
public ActionResult Address()
{
IList<AddressSummary> addresses = new List<AddressSummary>();
UpdateModel(addresses);
return View("AddressList",addresses);
}
測試:
可以看到,即便沒給引數,我們呼叫了UpdateModel,給它了一個Address陣列,它還是替我們工作了,把我們要的值放在了我們給的address陣列中,我們給了View,View顯示了出來。
顯示Model Binder的查詢範圍
預設的,ModelBinder會去Form,QueryString,RouteData.Values,Request.Files中找,但是如果要限制ModelBinder的搜尋範圍,可以給它一個Provider,告訴它在指定的provider裡面找:
例如:UpdateModel(addresses,new FormValueProvider(ControllerContext))
每個查詢物件,都有對應的provider:
Form |
FormValueProvider |
RouteData.Values |
RouteDataValueProvider |
QueryString |
QueryStringValueProvider |
Files |
HttpFileCollectionValueProvider |
我們可以根據不同的需要告訴modelbinder,給一個provider,在指定的裡面找。
對於Form,我們可以方便的給一個FormCollection(因為它實現了IValueProvider介面),這種用法更common。
處理Model binding過程中的錯誤
出了像這樣加try-catch:
try {
UpdateModel(addresses, formData);
} catch (InvalidOperationException ex) {
// provide feedback to user
}
我們可以使用TryUpdateModel:
if (TryUpdateModel(addresses, formData)){
// proceed as normal
} else {
// provide feedback to user
}
方法類似大家熟悉的TryParse。
如果不是手動updatemodel,像最開始例子的那樣希望model自動獲取action的引數進行binding,那麼binding出錯不會有異常,我們需要判斷ModelState.IsValid.
Customize Model binding
我們有兩個入口來customizemodel binding system,一個是ValueProvider,一個是ModelBinder,我們先來看Value Provider。
Customize Value Provider
需要實現的介面
public interface IValueProvider {
bool ContainsPrefix(string prefix);
ValueProviderResult GetValue(stringkey);
}
ContainsPrefix: 會被mode binder呼叫,當前段給一個prefix的時候,判斷是否滿足當前bind的 attribute。
ValueProviderResult: model binder會給一個key,我們需要拿著這個key去當前請求物件中找匹配的value,返回一個ValueProviderResult。
示例實現:
1.準備一個ValueProvider
public class CountryValueProvider :IValueProvider
{
public bool ContainsPrefix(stringprefix)
{
return prefix.ToLower().IndexOf("country") > -1;
}
public ValueProviderResult GetValue(string key)
{
if (ContainsPrefix(key))
{
return new ValueProviderResult("USA", "USA",
CultureInfo.InvariantCulture);
}
return null;
}
}
程式碼說明:
示例的實現目的很顯然,我們判斷prefix是否包含”country”,如果包含return true,GetValue方法判斷如果prefix滿足條件,總是返回”USA”,其他情況,總返回null。
2.準備一個Customize ValueProviderFactory
public class CustomValueProviderFactory : ValueProviderFactory
{
public override IValueProvider GetValueProvider(ControllerContext
controllerContext)
{
return new CountryValueProvider();
}
}
3.在Application_Start中註冊工廠
ValueProviderFactories.Factories.Insert(0, new CustomValueProviderFactory());
4.執行
Submit
可以看到Customize的ValueProvider找到了Prefix包含Country的請求key,直接返回了USA作為value。
ValueProviderResult的三個引數:
RawValue : Value Provider 給回的value
Attempted Value :raw value的字串顯示
Culture Info:當前使用的culture
Customize model binder
示例實現:
public class AddressSummaryBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext,ModelBindingContext bindingContext)
{
var 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 "NULL";
}
return result.AttemptedValue;
}
}
程式碼說明:從bindingContext中拿到Model,如果為空new一個AddressSummary物件,從modelBindingContext拿到相應欄位的值,賦值返回。如果沒拿到值,返回“Null”
關於ModelBindingContext物件:
Model |
當前的model物件,從action引數獲得(自動binding)或者是updateModel的引數(手動呼叫) |
ModelName |
Model的名字 |
ModelType |
Model型別 |
ValueProvider |
當前的ValueProvider |
註冊customize的model binder
在Global.asax.Application_Start中,把剛才註冊的測試的ValueProvider拿掉,新增:
//ValueProviderFactories.Factories.Insert(0, newCustomValueProviderFactory());
ModelBinders.Binders.Add(typeof(AddressSummary), new AddressSummaryBinder());
執行:
Submit
相關文章
- Asp.Net MVC4 系列--進階篇之Model(2)ASP.NETMVC
- Asp.Net MVC4系列--進階篇之Helper(1)ASP.NETMVC
- Asp.Net MVC4系列--進階篇之AJAXASP.NETMVC
- Asp.Net MVC4 系列--進階篇之ViewASP.NETMVCView
- Asp.Net MVC4 系列-- 進階篇之路由(1)ASP.NETMVC路由
- Asp.Net MVC4 系列--進階篇之Helper(2)ASP.NETMVC
- Asp.Net MVC4 系列--進階篇之Controller(2)ASP.NETMVCController
- Asp.Net MVC4 系列--進階篇之路由 (2)ASP.NETMVC路由
- Asp.Net MVC系列--進階篇之controller(1)ASP.NETMVCController
- Asp.Net MVC4 系列--基礎篇(1)ASP.NETMVC
- Asp.Net MVC 系列--進階篇之FilterASP.NETMVCFilter
- Asp.Net MVC4系列---基礎篇(5)ASP.NETMVC
- Asp.Net MVC4系列---基礎篇(4)ASP.NETMVC
- 【webpack 系列】進階篇Web
- React進階篇1React
- ASP.NET MVC系列:ModelASP.NETMVC
- QlikView Script – 進階篇1 Script呼叫Macro之變化ViewMac
- 測開之函式進階· 第1篇《遞迴函式》函式遞迴
- .NET進階系列之四:深入DataTable
- 帶你深度解鎖Webpack系列(進階篇)Web
- 正規表示式系列之中級進階篇
- asp.net core 系列之Response caching(1)ASP.NET
- Java多執行緒之進階篇Java執行緒
- Membership三步曲之進階篇
- Java進階篇 設計模式之十四 ----- 總結篇Java設計模式
- to debug asp.net mvc4ASP.NETMVC
- ASP.NET Core MVC 之模型(Model)ASP.NETMVC模型
- 你所不知道的ASP.NET Core進階系列(三)ASP.NET
- create table進階學習系列(十一)之cluster
- Linux ACL 許可權之進階篇Linux
- Dagger 2 系列(五) -- 進階篇:@Scope 和 @Singleton
- asp.net mvc原始碼分析-Action篇 DefaultModelBinderASP.NETMVC原始碼
- ASP.NET進階:認清控制元件 之 Button (轉)ASP.NET控制元件
- 高階前端進階系列 - webview前端WebView
- 持續改進之技術篇#1
- [一天一個進階系列] - MyBatis基礎篇MyBatis
- Three.js進階篇之6 - 碰撞檢測JS
- Three.js進階篇之5 - 粒子系統JS