从本章开始,将介绍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