七、模型绑定
在ASP.NET MVC中是通过模型绑定(Model Binding)达到解析客户端传来的数据。
1.简单模型绑定
当网页上有个窗体,且窗体内有个名为Username的输入字段,而Action的参数也定义了一个名为Username的参数,只要窗体的域名与Action方法上的参数名称一样,那么Action在被运行的时候,就会通过DefaultModelBinder类型将窗体或QueryString传来的数据进行处理,将原本传来的字符串数据转换成对应的.NET类型并传给Action方法的同名参数里。
我们用个简单的例子来描述“简单模型绑定”的过程,请先参考以下动作方法的程序代码,Action名称为TestForm,它会通过简单模型绑定取得从客户端窗体传来的Username参数,最后会将该参数传入ViewData.Model让View使用。
[HttpPost] public ActionResult TestForm(string Username) { ViewData.Model = Username; return View(); }
以下是相关的视图页面。
<h2>TestForm</h2> <form method="post"> <p> 使用者名称: <input type="text" name="Username" /> </p> <p> 您输入的使用者名称为:@Model </p> <input type="submit" /> </form>
当表单提交后,你会发现窗体上的Username字段已经被成功传送到TestForm这个Action里,并且在Action里也成功接收到Username参数的信息,所以ViewData.Model才会有值,且View上的@Model才会正确显示文字在页面上。
如果在VS2012中利用断点功能检查Action运行时是否真正接收到客户端表单传来的数据,应该可以发现表单信息的确已经被填入TestForm动作方法的Username参数里。
2.使用FormCollection取得窗体信息
除了通过简单模型绑定取得窗体传来的单栏信息外,还可以通过FormCollection一次取得整份窗体传来的信息。如下程序演示,只要设置一个FormCollection类型的参数,就可以取得所有从窗体传来的信息,这种用法如同使用以前的Request.Form一样。不过,在ASP.NET MVC里还是建议尽量不要使用Request.Form来取得窗体信息。
除了通过简单模型绑定取得窗体传来的单个表单属性外,还可以通过( )一次取得整份窗体传来的信息。
A.ViewBag B.ViewData C.TempData D.FormCollection
我们将上一小节的演示重新改写Action的部分,代码如下。
[HttpPost] public ActionResult TestForm(FormCollection form) { ViewData.Model = form["Username"]; return View(); }
3.复杂模型绑定
我们一样延续上一小节的演示,另外自定义一个名为UserForm的类别,且定义了三个属性(Properties),此时,Action若直接以UserForm类型来接收窗体信息也是没有问题的,只要表单域名称与UserForm类型中的属性名称一样,同样可以将客户端窗体信息自动绑定到form参数的同名属性上,代码如下。
[HttpPost] public ActionResult TestForm(UserForm form) { ViewData.Model = form.Username; return View(); }
通过这种方式做模型绑定还有个好处,那就是我们可以利用VS2012的Intellisense快捷提示功能,帮助我们快速完成属性名称的输入。
再举一个例子接收复杂模型绑定。假设窗体中有四个字段,分别为Type、Name、Email和Body,代码如下。
<form method="post"> Type <input type="radio" name="Type" value="1" checked="checked" /> Type1 <input type="radio" name="Type" value="2" checked="checked" /> Type2 <br /> Name <input id="Name" name="Name" type="text" value="" /> <br /> Email <input id="Email" name="Email" type="text" value="" /> <br /> Body <textarea cols="20" id="Body" name="Body" rows="2"></textarea> <br /> <input type="submit" /> </form>
数据模型与Action定义如下。
public class GuestbookForm { public int Type { get; set; } public string Name { get; set; } public string Email { get; set; } public string Body { get; set; } }
[HttpPost] public ActionResult TestForm(GuestbookForm gbook) { return View(); }
当客户端送出窗体到Save动作,ASP.NET MVC的DefaultModelBinder会很神奇地自动将字段信息映射到Action的gbook参数中。
4.多个复杂模型绑定
5.判断模型绑定的验证结果
当Controller在模型绑定完成后,会得到一个完整的ModelState对象,这个对象将包括模型绑定的过程中收集到的各种信息,其中有模型绑定在输入验证后的状态、模型绑定过程中发生的异常、以及模型绑定时发生的异常,因此,当模型绑定发生输入验证失败时,会在Action里得到一个ModelState.IsValid为false的属性,此时,你就可以判断程序是否要继续运行下去,例如,原本想要通过模型绑定取得的信息新增至数据库,就可以改成新增错误消息到页面上。
我们延续之前的演示,试着判断模型绑定成功与否。首先,声明一个含有模型验证属性的数据模型,并定义一个含有ModelState.IsValid判断条件的Action方法:
public class GuestbookForm { [Required] public int Type { get; set; } [Required] public string Name { get; set; } [Required] public string Email { get; set; } [Required] public string Body { get; set; } }
[HttpPost] public ActionResult TestForm(GuestbookForm gbook) { if (!ModelState.IsValid) { //已验证出无效的模型绑定,有些字段不符合格式要求 return View(); } //验证成功,此时可以将信息写入数据库 //InsertIntoDB(gbook); return Redirect("/"); }
在视图的部分完全不用改写,在ModelState.IsValid这行设置一个断点,试着在只输入Type与Name字段的情况下输出窗体,当窗体接收信息时,你会发现ModelState.IsValid的值为false,如下图。
6.模型绑定验证失败的错误详细信息
除了可以在Action中验证模型绑定的验证状态外,在Action中还可以通过ModelState属性取得ASP.NET MVC内建的验证失败错误消息。
若要取得在模型绑定的过程中总共有多少属性会被绑定,可以通过以下程序取得:
ModelState.Count
若要取得特定属性在绑定过程中是否出现错误,可用以下程序取得:
if(ModelState["Email"].Errors.Count>0)
{
//...
}
若要取得特定属性在绑定过程中出现的第一个错误,以及其错误消息或Exception对象,可用以下程序取得:
if(ModelState["Email"].Errors.Count>0) { ModelError err=ModelState["Email"].Errors[0]; var errMsg=err.ErrorMessage; var errExp=err.Exception; }
除了可以取得模型绑定过程中内建的验证失败信息外,还可以自行增加模型绑定验证失败的信息。
[HttpPost] public ActionResult TestForm(GuestbookForm gbook) { if (!ModelState.IsValid) { //已验证出无效的模型绑定,有些字段不符合格式要求 if (gbook.Email == null) ModelState.AddModelError("Email", "请输入Email字段"); return View(); } //验证成功,此时可以将信息写入数据库 //InsertIntoDB(gbook); return Redirect("/"); }
7.清空模型绑定状态
在Action里除了得到这些模型绑定的详细信息外,ModelState对象里的信息也一样会传送到View里,如果希望模型绑定状态(ModelState)不要传送到View里,还可以将模型绑定的所有状态清空,让View页面上的强类型信息不受模型绑定状态的影响,代码如下。
[HttpPost] public ActionResult TestForm(GuestbookForm gbook) { if (!ModelState.IsValid) { //已验证出无效的模型绑定,有些字段不符合格式要求 //清空模型绑定状态 ModelState.Clear(); return View(); } //验证成功,此时可以将信息写入数据库 //InsertIntoDB(gbook); return Redirect("/"); }
8.使用Bind属性限制可被更新的数据模型属性
复杂模型绑定的验证技巧在实际中经常使用也非常方便,但有一个很明显的限制,那就是模型在做绑定的时候,是在Action运行时就完成了,而且不管Model有多少字段,只要客户端有窗体过来就会自动绑定,看来方便,但实际上是有安全风险的。
因为客户端的表单域非常容易被窜改,如果黑客企图从窗体塞如一些额外的表单域,只要猜到正确的属性名称,就可以通过ASP.NET MVC的模型绑定功能自动将数据绑定到特定对象的同名属性里。
举个实际的例子来说,假设你有个数据模型名为Member,其属性定义如下,其中LastLoginTime属性代表的是“上次登录时间”。
public class Member { public int Id { get; set; } public string Username { get; set; } public string Password { get; set; } public DateTime? LastLoginTime { get; set; } }
而你的客户端窗体上只有让用户输入Username与Password而已,所以当你使用模型绑定的方式传入Member信息后,会预期LastLoginTime字段应该不会绑定到任何信息,而且该字段传入之后的同名属性值应该为null才对。程序代码如下:
public ActionResult UpdateProfile(Member member) { //TODO:更新数据库中的Member信息 return View(); }
但如果黑客这时窜改了客户端窗体,多塞一个LastLoginTime字段上去,并设置任意时间,那么你数据库中的这条信息,其LastLoginTime字段可能就会被用户任意窜改,如此一来,ASP.NET MVC程序就会有风险,因此不得不小心。
此时,可通过ASP.NET MVC内建的Bind属性(Attribute)并套用在该数据模型的参数上,明确声明有哪些字段可以被自动绑定进来,或是哪些字段该被排除在自动绑定的名单外。以下演示程序就是声明Member参数在自动绑定时要排除LastLoginTime字段的信息:
public ActionResult UpdateProfile([Bind(Exclude="LastLoginTime")]Member member) { //TODO:更新数据库中的Member信息 return View(); }
如果你想明确指明“只有”哪些字段需要绑定,可以使用Include具名参数。
public ActionResult UpdateProfile([Bind(Include="Password")]Member member) { //TODO:更新数据库中的Member信息 return View(); }
通过ASP.NET MVC内建的( )属性,可以明确声明哪些字段可以被自动绑定进来,或是哪些字段该被排除在自动绑定的名单外。
A.Include B.Exclude C.Bind D.Filter
如果你不希望在每个Action的参数都套用Bind属性的话,也可以套用在数据模型声明定义的地方,这样一来,整个项目的模型都不需要额外的声明了。
[Bind(Include="Username,Password")] public class Member { public int Id { get; set; } public string Username { get; set; } public string Password { get; set; } public DateTime? LastLoginTime { get; set; } }
9.使用UpdataeModel与TryUpdateModel