Asp.Net MVC4 系列--進階篇之Model(1)

mybwu_com發表於2014-04-19

從本章開始,將介紹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


相關文章