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

Asp.Net MVC4 系列--进阶篇之Model(1)

 

Scenario 2 : 传递一个正确类型的id

Asp.Net MVC4 系列--进阶篇之Model(1)

 

Scenario 3: 传递一个错误类型的id

Asp.Net MVC4 系列--进阶篇之Model(1)

结果:只有Scenario 2代码是正确工作的,也就是对于valuetype来说,不给参数或者错误类型,model都无法完成参数解析。

 

从Request到Render

Asp.Net MVC4 系列--进阶篇之Model(1)

本章重点在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>
}


运行查看结果:

Asp.Net MVC4 系列--进阶篇之Model(1)

点击submit

Asp.Net MVC4 系列--进阶篇之Model(1)

可以看到,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,流程图:

Asp.Net MVC4 系列--进阶篇之Model(1)                                                 

嵌套类型的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。

查看结果:

Asp.Net MVC4 系列--进阶篇之Model(1)

点击Submit

Asp.Net MVC4 系列--进阶篇之Model(1)

 

可以看到,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);
        }


 

再次运行:

Asp.Net MVC4 系列--进阶篇之Model(1)

可以看到,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");
}



运行

Asp.Net MVC4 系列--进阶篇之Model(1)

Click Submit

Asp.Net MVC4 系列--进阶篇之Model(1)

可以看到,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");
}


 

运行

Asp.Net MVC4 系列--进阶篇之Model(1)

Submit

Asp.Net MVC4 系列--进阶篇之Model(1)

 

可以看到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);
        }


测试:

 

Asp.Net MVC4 系列--进阶篇之Model(1)

可以看到,即便没给参数,我们调用了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.      运行

Asp.Net MVC4 系列--进阶篇之Model(1)

Submit

Asp.Net MVC4 系列--进阶篇之Model(1)

可以看到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());


 

运行:

Asp.Net MVC4 系列--进阶篇之Model(1)

Submit

 

Asp.Net MVC4 系列--进阶篇之Model(1)

 

Asp.Net MVC4 系列--进阶篇之Model(1),布布扣,bubuko.com

Asp.Net MVC4 系列--进阶篇之Model(1)

上一篇:深入浅出 - Android系统移植与平台开发(五)- 定制手机模拟器ROM


下一篇:ASP.NET Ajax学习笔记(一)异步通信层