42

我似乎遇到了 ASP.NET MVC 的问题,如果我在一个页面上有多个表单,每个表单都使用相同的名称,但类型不同(无线电/隐藏/等),那么,当第一个表单帖子(例如,我选择“日期”单选按钮),如果重新呈现表单(例如作为结果页面的一部分),我似乎遇到了其他表单上 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(); %>

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

<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>

请问有RC1的其他人可以确认吗?

也许是因为我使用的是枚举。我不知道。我应该补充一点,我可以通过对隐藏字段使用“手动”输入()标签来规避这个问题,但是如果我使用 MVC 标签(<%= Html.Hidden(...) %>),.NET MVC 会替换它们每次。

非常感谢。

更新:

今天又看到了这个bug。当您返回已发布的页面并使用 MVC 设置隐藏表单标签和 Html 帮助程序时,这似乎会引起人们的注意。我已经就此事联系了Phil Haack,因为我不知道还能去哪里,而且我不认为这应该是 David 指定的预期行为。

4

12 回答 12

36

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

有两种可能的解决方案:

解决方案 1

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

解决方案 2

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

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

当然,当我更多地考虑这一点时,基于回发更改值对文本框有意义,但对隐藏输入意义不大。我们无法为 v1.0 更改此设置,但我会考虑为 v2 更改此设置。但我们需要仔细考虑这种变化的含义。

于 2009-03-06T17:47:09.033 回答
11

与其他人一样,我希望 ModelState 用于填充模型,并且由于我们在视图的表达式中明确使用模型,它应该使用模型而不是模型状态。

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

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

我见过很多人使用诸如

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

问题

所以问题是模型是从模型状态填充的,在视图中,我们明确设置为使用模型。除非出现验证错误,否则每个人都希望使用 Model 值(以防它发生变化);然后可以使用 ModelState。

目前,在 MVC Helper 扩展中,ModelState 值优先于 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);
        }
    }

然后我们在调用 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);
    }

此解决方案消除了该问题,但不需要您反编译、分析和重建 MVC 通常为您提供的任何内容(不要忘记管理随时间的变化、浏览器差异等)。

我认为“模型值,除非验证错误然后是 ModelState”的逻辑应该是设计使然。如果是这样,它就不会咬那么多人,但仍然涵盖了 MVC 的意图。

于 2015-06-07T21:49:27.007 回答
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);

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

  1. 模型状态
  2. 查看数据
  3. 值(由调用者传递给 TextBox())
于 2009-10-03T03:05:30.227 回答
6

注意——这个错误仍然存​​在于 MVC 3 中。我使用的是 Razor 标记语法(这很重要),但我遇到了与 foreach 循环相同的错误,它每次都为对象属性生成相同的值。

于 2011-10-13T13:56:22.683 回答
5

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

当 MVC 将表单返回时,它只是检查是否存在具有相同名称的提交值 - 再次,它无法知道命名值来自哪个表单,甚至它是什么类型的控件(无论您使用单选、文本或隐藏,通过 HTTP 提交时都只是名称=值)。

于 2009-02-27T13:25:45.133 回答
4
foreach (var s in ModelState.Keys.ToList())
                if (s.StartsWith("detalleProductos"))
                    ModelState.Remove(s);

ModelState.Remove("TimeStamp");
ModelState.Remove("OtherOfendingHiddenFieldNamePostedToSamePage1");
ModelState.Remove("OtherOfendingHiddenFieldNamePostedToSamePage2");

return View(model);
于 2011-02-15T01:00:18.413 回答
4

这个问题在 MVC 5 中仍然存在,显然它不被认为是一个很好的错误。

我们发现,尽管按照设计,这不是我们的预期行为。相反,我们总是希望隐藏字段的值与其他类型的字段类似地操作,而不是被特殊对待,或者从一些不起眼的集合中提取它的值(这让我们想起了 ViewState!)。

一些发现(对我们来说正确的值是模型值,不正确的是 ModelState 值):

  • Html.DisplayFor()显示正确的值(它从模型中提取)
  • Html.ValueFor没有(它从 ModelState 中提取)
  • ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData).Model提取正确的值

我们的解决方案是简单地实现我们自己的扩展:

        /// <summary>
        /// Custom HiddenFor that addresses the issues noted here:
        /// http://stackoverflow.com/questions/594600/possible-bug-in-asp-net-mvc-with-form-values-being-replaced
        /// We will only ever want values pulled from the model passed to the page instead of 
        /// pulling from modelstate.  
        /// Note, do not use 'ValueFor' in this method for these reasons.
        /// </summary>
        public static IHtmlString HiddenTheWayWeWantItFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
                                                    Expression<Func<TModel, TProperty>> expression,
                                                    object value = null,
                                                    bool withValidation = false)
        {
            if (value == null)
            {
                value = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData).Model;
            }

            return new HtmlString(String.Format("<input type='hidden' id='{0}' name='{1}' value='{2}' />",
                                    htmlHelper.IdFor(expression),
                                    htmlHelper.NameFor(expression),
                                    value));
        }
于 2014-02-18T15:39:40.153 回答
3

重现“设计问题”的示例,以及可能的解决方法。但是,在试图找到“错误”的过程中浪费了 3 个小时,但没有解决方法……请注意,这种“设计”仍在 ASP.NET MVC 2.0 RTM 中。

    [HttpPost]
    public ActionResult ProductEditSave(ProductModel product)
    {
        //Change product name from what was submitted by the form
        product.Name += " (user set)";

        //MVC Helpers are using, to find the value to render, these dictionnaries in this order: 
        //1) ModelState 2) ViewData 3) Value
        //This means MVC won't render values modified by this code, but the original values posted to this controller.
        //Here we simply don't want to render ModelState values.
        ModelState.Clear(); //Possible workaround which works. You loose binding errors information though...  => Instead you could replace HtmlHelpers by HTML input for the specific inputs you are modifying in this method.
        return View("ProductEditForm", product);
    }

如果您的表单最初包含以下内容:<%= Html.HiddenFor( m => m.ProductId ) %>

如果“Name”的原始值(在呈现表单时)是“dummy”,那么在提交表单后,您希望看到呈现的“dummy (user set)”。没有ModelState.Clear()你仍然会看到“假人”!!!!!!!

正确的解决方法:

<input type="hidden" name="Name" value="<%= Html.AttributeEncode(Model.Name) %>" />

我觉得这根本不是一个好的设计,因为每个 mvc 表单开发人员都需要牢记这一点。

于 2010-03-23T07:08:27.873 回答
1

这可能是“设计使然”,但这不是记录的内容:

Public Shared Function Hidden(  

  ByVal htmlHelper As System.Web.Mvc.HtmlHelper,  
  ByVal name As String, ByVal value As Object)  
As String  

System.Web.Mvc.Html.InputExtensions 的成员

摘要:返回隐藏的输入标签。

参数:
htmlHelper:HTML 帮助器。
name:用于查找值的表单字段名称和 System.Web.Mvc.ViewDataDictionary 键。
value:隐藏输入的值。如果为 null,则查看 System.Web.Mvc.ViewDataDictionary,然后查看 System.Web.Mvc.ModelStateDictionary 的值。

这似乎表明只有当 value 参数为空(或未指定)时,HtmlHelper 才会在别处寻找值。

在我的应用程序中,我有一个表单: html.Hidden("remote", True) 呈现为 <input id="remote" name="remote" type="hidden" value="False" />

请注意,该值被 ViewData.ModelState 字典中的内容覆盖。

还是我错过了什么?

于 2009-09-02T14:52:51.987 回答
1

所以在 MVC 4 中,“设计问题”仍然存在。这是我必须使用的代码,以便在集合中设置正确的隐藏值,因为无论我在控制器中做什么,视图总是显示不正确的值。

旧代码

for (int i = 0; i < Model.MyCollection.Count; i++)
{
    @Html.HiddenFor(m => Model.MyCollection[i].Name) //It doesn't work. Ignores what I changed in the controller
}

更新代码

for (int i = 0; i < Model.MyCollection.Count; i++)
{
    <input type="hidden" name="MyCollection[@(i)].Name" value="@Html.AttributeEncode(Model.MyCollection[i].Name)" /> // Takes the recent value changed in the controller!
}

他们在 MVC 5 中解决了这个问题吗?

于 2013-12-11T21:50:57.627 回答
1

有解决方法:

    public static class HtmlExtensions
    {
        private static readonly String hiddenFomat = @"<input id=""{0}"" type=""hidden"" value=""{1}"" name=""{2}"">";
        public static MvcHtmlString HiddenEx<T>(this HtmlHelper htmlHelper, string name, T[] values)
        {
            var builder = new StringBuilder(values.Length * 100);
            for (Int32 i = 0; i < values.Length; 
                builder.AppendFormat(hiddenFomat,
                                        htmlHelper.Id(name), 
                                        values[i++].ToString(), 
                                        htmlHelper.Name(name)));
            return MvcHtmlString.Create(builder.ToString());
        }
    }
于 2015-12-21T13:49:06.963 回答
0

正如其他人所建议的那样,我使用直接 html 代码而不是使用 HtmlHelpers(TextBoxFor、CheckBoxFor、HiddenFor 等)。

但是这种方法的问题是您需要将 name 和 id 属性作为字符串。我想保持我的模型属性是强类型的,所以我使用了 NameFor 和 IdFor HtmlHelpers。

<input type="hidden" name="@Html.NameFor(m => m.Name)" id="@Html.IdFor(m=>m.Name)" value="@Html.AttributeEncode(Model.Name)">

更新: 这是一个方便的 HtmlHelper 扩展

    public static MvcHtmlString MyHiddenFor<TModel, TValue>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TValue>> expression, object htmlAttributes = null)
    {
        return new MvcHtmlString(
            string.Format(
                @"<input id=""{0}"" type=""hidden"" value=""{1}"" name=""{2}"">",
                helper.IdFor(expression),
                helper.NameFor(expression),
                GetValueFor(helper, expression)
            ));
    }

    /// <summary>
    /// Retrieves value from expression
    /// </summary>
    private static string GetValueFor<TModel, TValue>(HtmlHelper<TModel> helper, Expression<Func<TModel, TValue>> expression)
    {
        object obj = expression.Compile().Invoke(helper.ViewData.Model);
        string val = string.Empty;
        if (obj != null)
            val = obj.ToString();
        return val;
    }

然后你可以像这样使用它

@Html.MyHiddenFor(m => m.Name)
于 2015-06-03T13:39:20.617 回答