4.5 模型绑定

      在ASP.NET MVC中是通过模型绑定(Model Binding)解析客户端传来的数据,用浏览器以HTTP请求方式发送的数据来创建.NET对象的过程。每当定义具有参数的动作方法时,一直是在依赖着这种模型绑定过程。这个参数对象是通过模型绑定来自请求中的数据创建的。

      模型绑定是利用用户在表单中输入的数据(实际上是整个HTTP请求所携带的数据,而不仅仅是表单数据),构造动作方法所需要的参数对象的过程,数据的流向是从客户端的HTML表单到动作方法。

一、使用默认模型绑定器

     模型绑定是HTTP请求与C#方法之间的一个巧妙的桥梁。虽然应用程序可以定义自定义的模型绑定器,但大多数都是依靠内建的绑定器类DefaultModelBinder。默认情况下,这个模型绑定器会搜素偶四个位置,如下表所示,以获取与被绑定的参数名匹配的数据。

描述
Request.Form 由用户在HTML(表单)元素中提供的值
RouteData.Values 用应用程序路由获得的值
Request.QueryString 包含在请求URL中的查询字符串部分的数据
Request.Files 请求中上传的文件

     例如,public ActionResult Index(int id),DefaultModelBinder会为id参数查找以下的一个值:

  • Request.Form["id"]
  • RouteData.Values["id"]
  • Request.QueryString["id"]
  • Request.Files["id"]

     只要找到一个值,搜索便停止。就上述示例而言,对表单数据的搜索不会成功,但会找到具有正确名称的路由变量。并且,不会搜索查询字符串和上传文件的名称。

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();
        }

       以下是这个Action相对应的视图页面,这里只是一个非常简单的HTML窗体,并且在窗体内有一个Username字段。同时,如果从Action有传入ViewData.Model信息的话,也会在这个View里通过@Model显示在界面上:

 
<h2>TestForm</h2>

@using(Html.BeginForm()){
@Html.AntiForgeryToken()
@Html.ValidationSummary(true)

<fieldset>
<legend>输入用户名字</legend>
<p> 使用者名称: <input type="text" name="Username" /> </p> <p> 您输入的使用者名称为:@Model </p> <input type="submit" value="送出查询"/>
</fieldset> }

      该页面第一次被运行时,会出现一个简单的窗体,接着在窗体上的Username字段输入一段文字,运行结果如下图所示。

     

      当窗体送出后,你会发现窗体上的Username字段已经被成功传送到TestForm这个Action里,并且在Action里也成功接收到Username参数的信息,所以ViewData.Model才会有值,且View上的@Model才会正确显示文字在页面上,如下图所示。


    如果在VS2012中利用断点功能检查Action运行时是否真正接收到客户端表单传来的数据,应该可以发现表单信息的确已经被填入TestForm动作方法的Username参数里。

2.使用FormCollection取得窗体信息

      除了通过简单模型绑定取得窗体传来的单栏信息外,还可以通过FormCollection一次取得整份窗体传来的信息。如下程序演示,只要设置一个FormCollection类型的参数,就可以取得所有从窗体传来的信息,这种用法如同使用以前的Request.Form一样。不过,在ASP.NET MVC里还是建议尽量不要使用Request.Form来取得窗体信息。

      这个FormCollection类型实际上是继承自NameValueCollection类型,因此,取用窗体信息的方式就如同字典集合的方式一样,差别只在于所有key与value的类型都必须是字符串。     

      我们将上一小节的演示重新改写Action的部分,原本通过简单模型绑定来接收窗体信息,这次利用FormCollection类型取得上一页传来的所有字段信息,因此,你可以在程序代码中利用这个接收到改写完成后的程序代码如下,其结果将会完全一样。

        [HttpPost]
        public ActionResult TestForm(FormCollection form)
        {
            ViewData.Model = form["Username"];
            return View();
        }

3.复杂模型绑定

      在ASP.NET MVC中,可以通过DefaultModelBinder将窗体信息映射到非常复杂的.NET类型,称之为“复杂模型”,或简称“模型”。该模型可能是一个List<T>或一个含有多个属性的自定义类型。     

      我们一样延续上一小节的演示,另外自定义一个名为UserForm的类别,且定义了三个属性(Properties),此时,Action若直接以UserForm类型来接收窗体信息也是没有问题的,只要表单域名称与UserForm类型中的属性名称一样,同样可以将客户端窗体信息自动绑定到form参数的同名属性上,如下程序演示运行结果也会完全一样。

public class UserForm
    {
        public string Username { get; set; }
        public string Password { get; set; }
        public string Name { get; set; }
    }

[HttpPost] public ActionResult TestForm(UserForm form) { ViewData.Model = form.Username; return View(); }

      通过这种方式做模型绑定还有个好处,那就是我们可以利用VS2012的Intellisense快捷提示功能,帮助我们快速完成属性名称的输入。

      再举一个例子接收复杂模型绑定。假设窗体中有四个字段,分别为Type、Name、Email和Body,代码如下。

@using(Html.BeginForm()){
    @Html.AntiForgeryToken()
    @Html.ValidationSummary(true)

    <fieldset>
        <legend>ModelBinder</legend>
请选择类型:
@Html.RadioButtonFor(model=>model.Type, 1)THtml.RadioButtonForype1
@Html.RadioButtonFor(model=>model.Type, 2)THtml.RadioButtonForype2 <br />
请输入名字: @Html.EditorFor(model=>model.Name) <br />
请输入Email: @Html.EditorFor(model=>model.Email)
<br /> 请输入留言: @Html.EditorFor(model=>model.Body)
<br /> <input type="submit" value="提交查询内容"/>
</fieldset>
}

      而相应的数据模型与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.多个复杂模型绑定

      下面示范一个更复杂的例子,如果你在一个窗体内要送出两个复杂模型的数据到Action。也就是你希望在一个窗体内一次送出两条数据到Action里,就可以参考一下例子进行开发,假设窗体的HTML如下:

@using(Html.BeginForm()){
    <fieldset>
        <legend>Form1</legend>
        请选择类型:
        <input type="radio" name="form1.Type" value="1" checked="checked" />Type1
        <input type="radio" name="form1.Type" value="2" checked="checked" />Type2
        <br />
        请输入名字:
        <input id="Name1" name="form1.Name" type="text" value="" />
        <br />
        请输入Email:
        <input id="Email1" name="form1.Email" type="text" value="" />
        <br />
        请输入留言:
        <textarea cols="20" id="form1.Body" name="Body" rows="2"></textarea>
        <br />
    </fieldset>
  
    <fieldset>
        <legend>Form2</legend>
        请选择类型:
        <input type="radio" name="form2.Type" value="1" checked="checked" />Type1
        <input type="radio" name="form2.Type" value="2" checked="checked" />Type2
        <br />
        请输入名字:
        <input id="Name2" name="form2.Name" type="text" value="" />
        <br />
        请输入Email:
        <input id="Email2" name="form2.Email" type="text" value="" />
        <br />
        请输入留言:
        <textarea cols="20" id="form2.Body" name="Body" rows="2"></textarea>
        <br />
        <input type="submit" value="提交查询内容" />
    </fieldset>
}

      上述HTML只有一个<form>窗体,但是窗体内却又两组字段,界面显示如下图所示。

      此时,Action会这样写,在ComplexModelBinding动作里设置两组参数,参数名称分别为form1与form2,这两个参数的类型都是GuestbookForm。

public ActionResult ComplexModelBinding(GuestbookFrom form1, GuestbookForm form2)
        {
            InsetIntoDB(form1);
            InsertIntoDB(form2);

            return Redirect("/");
        }

      DefaultModelBinder通过巧妙的名称映射,让窗体传来信息一一映射到Action的两个复杂模型参数中。

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内建的验证失败错误消息。

(1)若要取得在模型绑定的过程中总共有多少属性会被绑定,可以通过以下程序取得:

ModelState.Count

(2)若要取得特定属性在绑定过程中是否出现错误,可用以下程序取得:

if(ModelState["Email"].Errors.Count>0)
{
    //...
}

(3)若要取得特定属性在绑定过程中出现的第一个错误,以及其错误消息或Exception对象,可用以下程序取得:

if(ModelState["Email"].Errors.Count>0)
{
    ModelError err=ModelState["Email"].Errors[0];
    var errMsg=err.ErrorMessage;
    var errExp=err.Exception;
}

(4)除了可以取得模型绑定过程中内建的验证失败信息外,还可以自行增加模型绑定验证失败的信息。

复制代码
        [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.指定自定义前缀

     有些时候,你生成的HTML与一种类型的对象有关,但你希望将其绑定到另一个对象。这意味着,视图包含的前缀与模型绑定器期望的结构不对应,于是对你的数据不能做适当的处理。例如下面的情况,已知模型Person和视图模型AddressSummary。

namespace Ch24.Models
{
    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 }
}
namespace Ch24.Models
{
    public class AddressSummary
    {
        public string City { get; set; }
        public string Country { get; set; }
    }
}

     处理AddressSummary的控制器动作DisplaySummary,代码如下:

 public ActionResult DisplaySummary([Bind(Prefix="HomeAddress")]AddressSummary summary)
        {
            return View(summary);
        }
@model Ch24.Models.AddressSummary
@{
    ViewBag.Title = "DisplaySummary";
}

<h2>Address Summary</h2>
<div>
    <label>City:</label>@Html.DisplayFor(m=>m.City)
</div>
<div>
    <label>Country:</label>@Html.DisplayFor(m => m.Country)
</div>
        public ActionResult CreatePerson()
        {
            return View(new Person());
        }
@model Ch24.Models.Person
@{
    ViewBag.Title = "CreatePerson";
}

<h2>Create Person</h2>
@using(Html.BeginForm("DisplaySummary","Home"))
{
    <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>
    <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>
    <button type="submit">Submit</button>

     如果启动应用程序,并导航到Home/CreatePerson,便可以看出这种问题。表单递交后,为city和country属性输入的值并没有显示在DisplaySummary视图生成的HTML中。

     问题在于,表单中的name属性具有HomeAddress前缀,这不是模型绑定器在视图绑定AddressSummary类型时要查找的前缀。修复时,可以对动作方法的参数运用Bind注解属性即可,目的是用它来告诉绑定器,应该查找哪一个前缀。

 public ActionResult DisplaySummary([Bind(Prefix="HomeAddress")]AddressSummary summary)
        {
            return View(summary);
        }

   

9.使用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才对。程序代码如下:

        [HttpPost]
public ActionResult UpdateProfile(Member member) { //TODO:更新数据库中的Member信息 return View(); }

      但如果黑客这时窜改了客户端窗体,多塞一个LastLoginTime字段上去,并设置任意时间,那么你数据库中的这条信息,其LastLoginTime字段可能就会被用户任意窜改,如此一来,ASP.NET MVC程序就会有风险,因此不得不小心。
     此时,可通过ASP.NET MVC内建的Bind属性(Attribute)并套用在该数据模型的参数上,明确声明有哪些字段可以被自动绑定进来,或是哪些字段该被排除在自动绑定的名单外。以下演示程序就是声明Member参数在自动绑定时要排除LastLoginTime字段的信息:

        [HttpPost]
public ActionResult UpdateProfile([Bind(Exclude="LastLoginTime")]Member member) { //TODO:更新数据库中的Member信息 return View(); }

      如果你想明确指明“只有”哪些字段需要绑定,可以使用Include具名参数。

        [HttpPost]
public ActionResult UpdateProfile([Bind(Include="Password")]Member member) { //TODO:更新数据库中的Member信息 return View(); }

      如果你不希望在每个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; }
    }

      如果在模型类上排除了Country,但在动作方法参数中包含了Country,该属性仍然是被排除的。

9.绑定到数组和集合

(1)绑定成数组

        public ActionResult Names(string[] names)
        {
            names = names ?? new string[0];
            return View(names);
        }
@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");
}

     

(2)绑定到集合

        public ActionResult Names(IList<string> names)
        {
            names = names ?? new List<string>();
            return View(names);
        }
@model IList<string>
@{
    ViewBag.Title = "Names";
}

<h2>Names</h2>
@if( Model.Count() == 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");
}

(3)绑定到自定义模型类型集合

        public ActionResult Address(IList<AddressSummary> addresses)
        {
            addresses = addresses ?? new List<AddressSummary>();
            return View(addresses);
        }
@using Ch24.Models
@model IList<AddressSummary>
@{
    ViewBag.Title = "Address";
}

<h2>Address</h2>
@if(Model.Count()==0)
{
    using (Html.BeginForm())
    {
        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");
}

                    

二、手工调用模型绑定

      当动作方法定义了参数时,模型绑定过程是自动执行的,但是我们通过手工也可以直接控制这一过程。下面的代码演示了如何将Home控制器的Address动作方法修改成手工调用绑定过程。

        public ActionResult Address()
        {
            IList<AddressSummary> addresses = new List<AddressSummary>();
            UpdateModel(addresses);
            return View(addresses);
        }    

  UpdateModel方法将表单数据绑定到addresses上。

      当手工调用绑定过程时,可以将绑定过程限制到一个单一的数据源。默认情况下,绑定器会查找四个地方:表单数据、路由数据、查询字符串,以及上传文件。下面的代码演示了如何将绑定器限制到搜索单一位置的数据——表单数据。

        public ActionResult Address()
        {
            IList<AddressSummary> addresses = new List<AddressSummary>();
            UpdateModel(addresses, new FormValueProvider(ControllerContext));
            return View(addresses);
        }    

      UpdateModel方法以IValueProvider接口的一个实现为参数,该实现也称为绑定过程的唯一数据源。四个默认数据位置中的每一个都是由一个IValueProvider实现表示,如下表所示。

IValueProvider实现
Request.Form FormValueProvider
 RouteData.Values RouteDataValueProvider
 Request.QueryString  QueryStringValueProvider
 Request.Files  HttpFileCollectionValueProvider

      上表列出的每一个类都以ControllerContext为构造函数。如果只是查找表单值,还可以如下改进。

      public ActionResult Address(FormCollection formData)
        {
            IList<AddressSummary> addresses = new List<AddressSummary>();
            UpdateModel(addresses, formData);
            return View(addresses);
        }    

 

      FormCollection类也实现了IValueProvider接口。

三、定制模型绑定系统

1.创建自定义值提供器

2.创建自定义模型绑定器

3.注册自定义模型绑定器

posted @ 2015-11-10 09:47  RunningYY  阅读(602)  评论(0编辑  收藏  举报