zoukankan      html  css  js  c++  java
  • [转]Asp.Net MVC使用HtmlHelper渲染,并传递FormCollection参数的陷阱 【转】

    在Asp.Net MVC
    1.0编程中,我们经常遇见这样的场景,在新建一个对象时候,通过HtmlHelper
    的方式在View模型中渲染Html控件,当填写完相关内容后,通过Form把需要新建的内容Post回View对应Controller的Action(例如:Create),指定的Action可以通过接受FormCollection参数、值参数或者某个类的实例参数(比如:Movie类),完成新建的操作。(主要指HtmlHelper.TextBox)

    当我们通过传递FormCollection参数进行操作时,如果不使用UpdateModel方法,而利用ModelState.IsValid及ModelState.AddModelError实现错误校验提示等操作。这个时候,小心陷阱。
    【注:本文章源代码通过VS2008创建】



    1
    、View模型中HtmlHelper绑定数据的顺序

    开始前,让我们先了解下View模型中HtmlHelper绑定数据的顺序(主要指HtmlHelper.TextBox,其它还未研究)

    我们知道,当View使用了HtmlHelper进行控件渲染的时候,HtmlHelper会通过键值尝试填充我们曾经填写过的数据,以防止用户从头填写。(比如:我们填写表单,提交,当出现验证错误的时候,我们希望表单刷新后曾经填写的内容依然存在,而不是全部要重新填写。而HtmlHelper就是这样帮助我们的)。HtmlHelper填充数据的顺序如下:

    (1)通过键值调用ModelState集合对应的System.Web.Mvc.ModelState实例的Value属性获取

    (2)通过HtmlHelper指定的值填充(Html.TextBox("Title",指定值))

    (3)通过键值获取ViewData内的对应数据

    (4)通过键值获取View中强类型的Model对象对应属性的数据

    (5)不填充

    2、 传递FormCollection参数,不使用UpdateModel引起的异常

    先看一个简单的例子源代码下载)。View通过Post传递FormCollection参数到对应Controller的Create
    Action,Create
    Action检验参数是否合法。如果合法,暂时什么都不做;如果不合法,则通过ModelState的AddModelError添加错误信息,并通过ModelState.
    IsValid判断,如果无效,重新返回该View。

    (1) View代码(没有任何特殊的地方,HtmlHelper使用Html.TextBox("Title")的方式):

    <%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<ValidationTest.Models.Movie>" %>
    <asp:Content
    ID="Content1"
    ContentPlaceHolderID
    ="TitleContent"
    runat
    ="server">
       
    Create
    </asp:Content>
    <asp:Content
    ID="Content2"
    ContentPlaceHolderID
    ="MainContent"
    runat
    ="server">
       
    <h2>Create</h2>
       
    <%=
    Html.ValidationSummary(
    "Create was unsuccessful.
    Please correct the errors and try again.
    ") %>
       
    <% using (Html.BeginForm())
    {
    %>
           
    <fieldset>
               
    <legend>Fields</legend>
               
    <p>
                   
    <label for="Title">Title:</label>
                   
    <%= Html.TextBox("Title") %>
                   
    <%=
    Html.ValidationMessage(
    "Title", "*") %>
               
    </p>
               
    <p>
                   
    <label for="Director">Director:</label>
                   
    <%= Html.TextBox("Director") %>
                   
    <%=
    Html.ValidationMessage(
    "Director", "*") %>
               
    </p>
               
    <p>
                   
    <label for="Remark">Remark:</label>
                   
    <%= Html.TextBox("Remark") %>
                   
    <%=
    Html.ValidationMessage(
    "Remark", "*") %>
               
    </p>
               
    <p>
                   
    <input type="submit"
    value
    ="Create" />
               
    </p>
           
    </fieldset>
       
    <% } %>
       
    <div>
           
    <%=Html.ActionLink("Back to List", "Index") %>
       
    </div>
    </asp:Content>

     (2) Controller中Create Action代码

    对应的Create
    Action代码如下,我们通过UpdateModel来进行Movie的填充,而是直接创建了一个Movie的实例(我直接在Controller的Action中验证参数,虽然我知道这样做不对,这里只是个例子。):

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Create(FormCollection collection)
           
    {
              
    //手动实例化
                Movie m = new Movie()
    {
                   
    Title
    =
    collection[
    "Title"],
                   
    Director
    = collection["Director"],
                   
    Remark
    =
    collection[
    "Remark"]}
    ;
               
    if
    (m.Title.Trim().Length
    == 0)
               
    {
                   
    ModelState.AddModelError(
    "Title", "Title 不能为空!");
               
    }

               
    if
    (m.Director.Trim().Length
    == 0)
               
    {
                   
    ModelState.AddModelError(
    "Director", "Director 不能为空!");
               
    }

               
    if
    (
    !ModelState.IsValid)
               
    {
                   
    return
    View();
               
    }

               
    try
               
    {
                   
    //TODO
    SaveToDB

    return
    Content(
    "OK");
               
    }

               
    catch
               
    {
                   
    return
    View();
               
    }



     (3) 运行结果

    大家可以下载代码运行,结果如下:不输入参数,提交表单时,我们希望这个时候能够提示“Title 不能为空!”和“Director
    不能为空!”。但是,很不幸,报错了。

     
    3、传递FormCollection,使用UpdateModel

    现在,View的代码不变,我们在Create
    Action中使用UpdateModel方法,代码如下源代码下载

    [AcceptVerbs(HttpVerbs.Post)]     
    public ActionResult Create(FormCollection collection)
           
    {
              
    Movie m
    = new Movie();
              
    //使用UpdateModel方法
                UpdateModel<Movie>(m);

               
    if
    (m.Title.Trim().Length
    == 0)
               
    {
                   
    ModelState.AddModelError(
    "Title", "Title 不能为空!");
               
    }

               
    if
    (m.Director.Trim().Length
    == 0)
               
    {
                   
    ModelState.AddModelError(
    "Director", "Director 不能为空!");
               
    }

               
    if
    (
    !ModelState.IsValid)
               
    {
                   
    return
    View();
               
    }

               
    try
               
    {
                   
    //TODO
    SaveToDB

    return
    Content(
    "OK");
               
    }

               
    catch
               
    {
                   
    return
    View();
               
    }

           
    }



     大家可以下载代码,运行:当不输入参数时,提示“Title 不能为空!”和“Director
    不能为空!”,一切正常。

    4、 原因分析

    下面我们来分析下造成这个问题的原因。

    (1)认识一下System.Web.Mvc.ModelStateDictionarySystem.Web.Mvc.ModelState

    我们知道,每个Controller都有一个类型为System.Web.Mvc.ModelStateDictionary的ModelState集合(后文中称为ModelState集合),该集合是一个System.Web.Mvc.ModelState对象的集合(MVC在这里取名存在严重的问题,Controller里面的ModelState既然是个集合,应该命名为ModelStates或者ModelStateCollection,以免被误会)。System.Web.Mvc.ModelState这个对象包含两个属性:

    l Errors类型为System.Web.Mvc.ModelErrorCollection的属性。

    l Value类型为System.Web.Mvc.ValueProviderResult的属性。

    (2)UpdateModel方法与System.Web.Mvc.ModelStateDictionarySystem.Web.Mvc.ModelState的关系

    当调用UpdateModel方法时,它至少做了两件事情。

    A把提交的数据(FormCollection中的数据)与Movie实例的属性匹配并自动更新(参考:有一天,WebForm MVC 说:能否借你的UpdateModel方法来用用?

    B、 将每个匹配的FormCollection中的数据实例化为System.Web.Mvc.ModelState类,并根据键值分别加入ModelState集合中。

    通过调试发现,在调用UpdateModel方法前,ModelState集合没有数据;调用后,集合内是有数据的

          l 调用UpdateModel                                 l 调用UpdateModel

          

    (3)不使用UpdateModel方法,AddModelErrorSystem.Web.Mvc.ModelStateDictionarySystem.Web.Mvc.ModelState的关系

    当不使用UpdateModel方法,而在验证不通过时候调用ModelState.AddModelError法时。通过调试发现,ModelState集合也是有数据的。

    也就是说,AddModelError方法同样实例化了System.Web.Mvc.ModelState类,并根据键值将它加入ModelState集合。


               通过图可以看到,集合内有两个System.Web.Mvc.ModelState对象的实例。

    (4)UpdateModel方法与ModelState.AddModelError的PK

    既然UpdateModelModelState.AddModelError都实例化了System.Web.Mvc.ModelState,并加入了ModelState集合,那有什么区别呢?

    l UpdateModel方法通过调试发现,当使用UpdateModel方法后,ModelState集合内的System.Web.Mvc.ModelState类的实例的Value属性是不为空的。


    l ModelState.AddModelError方法通过调试发现,当不使用UpdateModel而调用ModelState.AddModelError 方法后,ModelState集合的System.Web.Mvc.ModelState类的实例的Value属性是空的。


     就是说,当传递FormCollection参数时,如果不使用UpdateModel方法,而只使用ModelState.AddModelError方法,ModelState集合中System.Web.Mvc.ModelState类的实例的Value属性并不会被赋值

     (5)不使用UpdateModel方法,手动向ModelState集合的System.Web.Mvc.ModelState实例的Value属性赋值。

    通过上面的分析,我们知道,当传递FormCollection参数时,如果不使用UpdateModel方法,当我们调用ModelState.AddModelError方法时,System.Web.Mvc.ModelState对象会被创建,并根据键值被加入到ModelState集合中了,但它的Value属性是空的。那我们就需要手动执行赋值这个操作。通过使用ModelState集合的Add(string key, ModelState
    value)”
    方法可以搞定。现在,一切OK!(
    代码下载

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Create(FormCollection collection)
           
    {
               
    Movie m
    = new Movie() {
                   
    Title
    =
    collection[
    "Title"],
                   
    Director
    = collection["Director"],
                   
    Remark
    =
    collection[
    "Remark"] }
    ;

               
    //手动添加数据到ModelState集合
                ModelState.Add("Title", new ModelState()
    {
                   
    Value
    =
    collection.ToValueProvider()[
    "Title"] }
    );
               

               
    ModelState.Add(
    "Director", new ModelState() {
                   
    Value
    =
    collection.ToValueProvider()[
    "Director"] }
    );
               

               
    ModelState.Add(
    "Remark", new ModelState() {
                   
    Value
    =
    collection.ToValueProvider()[
    "Remark"] }
    );

               
    if
    (m.Title.Trim().Length
    == 0)
               
    {
                   
    ModelState.AddModelError(
    "Title", "Title 不能为空!");
               
    }

               
    if
    (m.Director.Trim().Length
    == 0)
               
    {
                   
    ModelState.AddModelError(
    "Director", "Director 不能为空!");
               
    }

               
    if
    (
    !ModelState.IsValid)
               
    {
                   
    return
    View();
               
    }

               
    try
               
    {
                   
    //TODO
    SaveToDB

    return
    Content(
    "OK");
               
    }

               
    catch
               
    {
                   
    return
    View();
               
    }

           
    }


     现在,让我们再来分析下异常的原因:

    当传递FormCollection参数时,不使用UpdateModel方法,但在验证失败后调用ModelState.AddModelErro方法时,System.Web.Mvc.ModelState被实例化,并通过某个键值(比如“Title”)加入到了ModelState集合中。但是,该System.Web.Mvc.ModelState实例的Value属性是NULL的。

    当在View中使用HtmlHelper.TextBox("Title")进行渲染的时候,HtmlHelper试图通过键值(“Title”)重新将输入值与控件绑定(例如:TextBox)时,由于ModelState集合的优先级最高,因此HtmlHelper试图通过这个键值(“Title”)从ModelState集合中获取数据(通过调用GetModelStateValue()方法)。由于AddModelErro方法的“功劳”,HtmlHelper获取到了这个键值(“Title”)对应的System.Web.Mvc.ModelState类的实例,但该实例的Value属性是Null。因此,出现了开篇的问题:“未将对象应用设置到对象值的实例”

     

    5、直接传递类参数、值参数

    如果我们在Post的时候不传递FormatCollection,而是直接传递类或者值参数。

     

    传递类

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult
    Create(Movie m)
    {}

    传递值参数

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult
    Create(
    string Title,string
    Director,
    string Remark){}
     

     那不会出现问题。因为当传递的是类或者参数时,默认的ModelBinder除了会实例化Movie类并匹配属性或给参数赋值外,还会根据键值填充ModelState集合,就像UpdateModel会帮你做这件事情一样。

    6、 小结

    (1)   Controller中的ModelState集合是个很重要的东西,它是System.Web.Mvc.ModelState类的集合,System.Web.Mvc.ModelState的实例会负责保存键值匹配的输入值(Value属性)、以及验证错误信息(Errors属性)。

    2)   Post方式传递类参数、值参数时,会通过默认的ModelBinder来填充ModelState集合。

    3)   UpdateModel方法也会填充ModelState集合。

    4)   如果使用HtmlHelper,并传递FormCollection参数,又需要通过ModelState.AddModelError添加错误验证信息,则需要调用UpdateModel方法或通过ModelState.Add方法来填充ModelState集合。

    5)   使用HtmlHelper渲染View中的控件数据的时候(主要指HtmlHelper.TextBox,其它还未研究),绑定顺序为:ModelState集合、指定值、ViewData内的数据、View中强类型Model对象对应属性的数据。

     

    7、PS:

    如果通过Asp.Net MVC 1.0做数据验证的时候,我们通常不会直接在Controller中的Action里面做,提供几个开源的工具和几篇文章:

    n FluentValidation

    下载地址:http://www.codeplex.com/FluentValidation

    文章:http://www.cnblogs.com/wintersun/archive/2009/02/15/1390990.html

     

    n Data Annotation Model Binder

    下载地址:http://aspnet.codeplex.com/Release/ProjectReleases.aspx?ReleaseId=24471

    文章:http://www.asp.net/learn/mvc/tutorial-39-cs.aspx

     

    或者Google 4 : Asp.Net MVC 数据验证



    8、补充:

    根据回复补充:
    一、View模型中采用了HtmlHelper("Title",Model.Title)的方式
        
    如果View模型中采用了HtmlHelper("Title",Model.Title)的方式,在第一次进入Create Action的时候,需要给ViewData.Model赋值,如果是Post回的Create Action,如果还需要显示这个View,也需要给ViewData.Model赋值,否则View模型中的Model为NULL,也会提示
    未将对象应用设置到对象值的实例”给ViewData.Model赋值有两种方法(二选一):
    1、在Create Action中给ViewData.Model赋值
        ViewData.Model = new Movie() (第一次进入Create Action调用)
        ViewData.Model = m(Post回Create Action时候调用,m为手动、自动或者传递参数过来的Movie对象实例)

    2、返回使用带TModel参数的重载函数View(TModel)
       
    Return View(new Movie())(说明同上)
       
    Return View(m)(说明同上)

    对的就做,做的就对

  • 相关阅读:
    UVA10163 Storage Keepers (动态规划)
    Uva12174 Shuffle(滑动窗口)
    上决╇ф人员分配问题 (背包问题)
    UVA
    UVALive
    poj1151 Atlantis (线段树+扫描线+离散化)
    poj2528 Mayor's posters (线段树+离散化)
    php 调用微信虚拟支付
    ptoto文件转换的java文件,报错 UnusedPrivateParameter
    redis 安装及启动时警告处理
  • 原文地址:https://www.cnblogs.com/zhouyunbaosujina/p/3299369.html
Copyright © 2011-2022 走看看