ASP.NET MVC中可能存在的错误,表单值被替换

Dan*_*son 41 forms asp.net-mvc asp.net-mvc-3

我似乎遇到了ASP.NET MVC的问题,如果我在一个页面上有多个表单,每个表单在每个表单中使用相同的名称,但是作为不同的类型(radio/hidden/etc),那么,当第一个表单帖子(我选择'Date'单选按钮),如果表单被重新呈现(比如作为结果页面的一部分),我似乎有问题,SearchType的隐藏值在其他表单上更改为最后一个单选按钮值(在本例中为SearchType.Name).

以下是用于减少目的的示例表单.

<% Html.BeginForm("Search", "Search", FormMethod.Post); %>
  <%= Html.RadioButton("SearchType", SearchType.Date, true) %>
  <%= Html.RadioButton("SearchType", SearchType.Name) %>
  <input type="submit" name="submitForm" value="Submit" />
<% Html.EndForm(); %>

<% Html.BeginForm("Search", "Search", FormMethod.Post); %>
  <%= Html.Hidden("SearchType", SearchType.Colour) %>
  <input type="submit" name="submitForm" value="Submit" />
<% Html.EndForm(); %>

<% Html.BeginForm("Search", "Search", FormMethod.Post); %>
  <%= Html.Hidden("SearchType", SearchType.Reference) %>
  <input type="submit" name="submitForm" value="Submit" />
<% Html.EndForm(); %>
Run Code Online (Sandbox Code Playgroud)

结果页面源(这将是结果页面的一部分)

<form action="/Search/Search" method="post">
  <input type="radio" name="SearchType" value="Date" />
  <input type="radio" name="SearchType" value="Name" />
  <input type="submit" name="submitForm" value="Submit" />
</form>

<form action="/Search/Search" method="post">
  <input type="hidden" name="SearchType" value="Name" /> <!-- Should be Colour -->
  <input type="submit" name="submitForm" value="Submit" />
</form>

<form action="/Search/Search" method="post">
  <input type="hidden" name="SearchType" value="Name" /> <!-- Should be Reference -->
  <input type="submit" name="submitForm" value="Submit" />
</form>
Run Code Online (Sandbox Code Playgroud)

请RC1的其他人确认一下吗?

也许是因为我正在使用枚举.我不知道.我应该补充一点,我可以通过对隐藏字段使用'manual'input()标记来规避这个问题,但如果我使用MVC标记(<%= Html.Hidden(...)%>),.NET MVC会替换它们每次.

非常感谢.

更新:

我今天再次看到这个bug.当你返回一个发布的页面并使用MVC设置隐藏的表单标签和Html帮助程序时,这似乎就会产生影响.我已经联系过Phil Haack,因为我不知道还有什么地方可以转,我不相信这应该是大卫所规定的预期行为.

Haa*_*ked 36

是的,这种行为目前是设计使然.即使您明确设置了值,如果您回发到同一个URL,我们也会查看模型状态并在那里使用值.通常,这允许我们显示您在回发时提交的值,而不是原始值.

有两种可能的解决方案:

解决方案1

为每个字段使用唯一的名称.请注意,默认情况下,我们使用您指定的名称作为HTML元素的ID.多个元素具有相同的ID是无效的HTML.所以使用唯一的名称是很好的做法.

解决方案2

不要使用隐藏的助手.看起来你真的不需要它.相反,你可以这样做:

<input type="hidden" name="the-name" 
  value="<%= Html.AttributeEncode(Model.Value) %>" />
Run Code Online (Sandbox Code Playgroud)

当然,正如我对此的考虑更多,基于回发更改值对于Textboxes是有意义的,但对隐藏的输入没有多大意义.我们无法为v1.0更改此内容,但我会将其视为v2.但我们需要仔细思考这种变化的含义.

  • 我有一段时间没有在猪身上看到这么多的口红.它不是设计行为,它是一个简单明了的bug. (7认同)
  • 由于这个"功能",我花了24小时试图调试我们的应用程序. (4认同)
  • @Haacked,这是非常糟糕的设计决定而没有记录在案! (3认同)
  • 下面的解决方案使用`ModelState.Clear()`解决了绑定回局部视图时的问题. (3认同)
  • 随着v2开发(和预览版)你知道这个'错误'是否有任何更新? (2认同)
  • @Haacked逻辑是否应该"除非存在验证错误,否则使用模型值"?总的来说,人们一致认为这种设计选择是出乎意料的.只有在必要时才应使用ModelState值,否则应使用Model值,因为它在代码中显式设置. (2认同)

Dac*_*ker 11

与其他人一样,我原本期望ModelState用于填充模型,因为我们在视图中的表达式中明确使用Model,它应该使用Model而不是ModelState.

这是一个设计选择,我明白为什么:如果验证失败,输入值可能无法解析模型中的数据类型,并且您仍然希望呈现用户键入的任何错误值,因此很容易纠正它.

我唯一不明白的是:为什么不使用模型,开发人员明确设置模型,如果发生验证错误,则使用ModelState.

我见过很多人使用的解决方法

  • ModelState.Clear():清除所有ModelState值,但基本上禁用MVC中默认验证的使用
  • ModelState.Remove("SomeKey"):与ModelState.Clear()相同,但需要对ModelState键进行微观管理,这是太多的工作,并且使用MVC的自动绑定功能感觉不对.感觉像20年前我们还在管理Form和QueryString键.
  • 渲染HTML本身:过多的工作,细节和抛弃HTML Helper方法以及其他功能.例如:用m.Name替换@ Html.HiddenFor)"id ="@ Html.IdFor(m => m.Name)"value ="@ Html.AttributeEncode(Model.Name)">.或者替换@Html. DropDownListFor by ...
  • 创建自定义HTML帮助程序以替换默认MVC HTML帮助程序以避免设计问题.这是一种更通用的方法,然后呈现您的HTML,但仍然需要更多的HTML + MVC知识或反编译System.Web.MVC仍然保留所有其他功能但禁用ModelState优先于Model.
  • 应用POST-REDIRECT-GET模式:这在某些环境中很容易,但在具有更多交互/复杂性的环境中更难.这种模式有它的优点和缺点,你不应该被迫应用这种模式,因为模型上的ModelState的设计选择.

问题

所以问题是模型是从ModelState填充的,在我们明确设置的视图中使用Model.除非存在验证错误,否则每个人都希望使用Model值(如果它已更改); 然后可以使用ModelState.

目前,在MVC Helper扩展中,ModelState值优先于Model值.

因此,此问题的实际修复应该是:对于每个表达式来拉取Model值,如果该值没有验证错误,则应删除ModelState值.如果该输入控件存在验证错误,则不应删除ModelState值,并且将像平常一样使用它.我认为这完全解决了这个问题,这比大多数解决方法更好.

代码在这里:

    /// <summary>
    /// Removes the ModelState entry corresponding to the specified property on the model if no validation errors exist. 
    /// Call this when changing Model values on the server after a postback, 
    /// to prevent ModelState entries from taking precedence.
    /// </summary>
    public static void RemoveStateFor<TModel, TProperty>(this HtmlHelper helper,  
        Expression<Func<TModel, TProperty>> expression)
    {
        //First get the expected name value. This is equivalent to helper.NameFor(expression)
        string name = ExpressionHelper.GetExpressionText(expression);
        string fullHtmlFieldName = helper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);

        //Now check whether modelstate errors exist for this input control
        ModelState modelState;
        if (!helper.ViewData.ModelState.TryGetValue(fullHtmlFieldName, out modelState) ||
            modelState.Errors.Count == 0)
        {
            //Only remove ModelState value if no modelstate error exists,
            //so the ModelState will not be used over the Model
            helper.ViewData.ModelState.Remove(name);
        }
    }
Run Code Online (Sandbox Code Playgroud)

然后我们在调用MVC扩展之前创建自己的HTML Helper扩展:

    public static MvcHtmlString TextBoxForModel<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
        Expression<Func<TModel, TProperty>> expression,
        string format = "",
        Dictionary<string, object> htmlAttributes = null)
    {
        RemoveStateFor(htmlHelper, expression);
        return htmlHelper.TextBoxFor(expression, format, htmlAttributes);
    }

    public static IHtmlString HiddenForModel<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
        Expression<Func<TModel, TProperty>> expression)
    {
        RemoveStateFor(htmlHelper, expression);
        return htmlHelper.HiddenFor(expression);
    }
Run Code Online (Sandbox Code Playgroud)

此解决方案消除了该问题,但不要求您反编译,分析和重建MVC正常提供的任何内容(不要忘记管理更改的持续时间,浏览器差异等).

我认为"模型值,除非验证错误然后是ModelState"的逻辑应该是按设计的.如果是的话,它就不会被这么多人咬伤,但仍然涵盖了MVC的意图.


小智 6

我刚遇到同样的问题.传递值的TextBox()优先级之类的Html助手似乎与我在文档中推断的行为完全相反:

文本输入元素的值.如果此值为空引用(在Visual Basic中为Nothing),则从ViewDataDictionary对象中检索元素的值.如果那里不存在值,则从ModelStateDictionary对象中检索该值.

对我来说,我读过如果传递了值,则使用该值.但是阅读TextBox()源代码:

string attemptedValue = (string)htmlHelper.GetModelStateValue(name, typeof(string));
tagBuilder.MergeAttribute("value", attemptedValue ?? ((useViewData) ? htmlHelper.EvalString(name) : valueParameter), isExplicitValue);
Run Code Online (Sandbox Code Playgroud)

似乎表明实际的顺序与记录的完全相反.实际订单似乎是:

  1. 的ModelState
  2. ViewData的
  3. 值(由调用者传入TextBox())


Mar*_*k S 6

单挑 - 这个错误仍然存​​在于MVC 3中.我正在使用Razor标记语法(这非常重要),但我遇到了一个foreach循环的错误,它每次都为对象属性生成相同的值.


Dav*_*vid 5

这将是预期的行为 - MVC不使用视图状态或其他背后技巧来传递表单中的额外信息,因此它不知道您提交的表单(表单名称不是提交的数据的一部分,仅名称/值对列表).

当MVC重新呈现表单时,它只是检查是否存在具有相同名称的提交值 - 再次,它无法知道命名值来自哪个表单,甚至不知道它是什么类型的控件(无论你是否使用收音机,文本或隐藏,它通过HTTP提交时只是name = value.