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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/594600/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-07 23:21:52  来源:igfitidea点击:

Possible bug in ASP.NET MVC with form values being replaced

asp.net-mvcformsasp.net-mvc-3

提问by Dan Atkinson

I appear to be having a problem with ASP.NET MVC in that, if I have more than one form on a page which uses the same name in each one, but as different types (radio/hidden/etc), then, when the first form posts (I choose the 'Date' radio button for instance), if the form is re-rendered (say as part of the results page), I seem to have the issue that the hidden value of the SearchType on the other forms is changed to the last radio button value (in this case, SearchType.Name).

我似乎在使用 ASP.NET MVC 时遇到了问题,如果页面上有多个表单,每个表单都使用相同的名称,但类型不同(广播/隐藏/等),那么,当第一个表单发布(例如,我选择“日期”单选按钮),如果表单被重新呈现(比如作为结果页面的一部分),我似乎遇到了其他表单上 SearchType 的隐藏值的问题更改为最后一个单选按钮值(在本例中为 SearchType.Name)。

Below is an example form for reduction purposes.

以下是用于减少目的的示例表格。

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

Resulting page source (this would be part of the results page)

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

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

Please can anyone else with RC1 confirm this?

请有RC1的其他人确认这一点吗?

Maybe it's because I'm using an enum. I don't know. I should add that I can circumvent this issue by using 'manual' input () tags for the hidden fields, but if I use MVC tags (<%= Html.Hidden(...) %>), .NET MVC replaces them every time.

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

Many thanks.

非常感谢。

Update:

更新:

I've seen this bug again today. It seems that this crops its head when you return a posted page and use MVC set hidden form tags with the Html helper. I've contacted Phil Haackabout this, because I don't know where else to turn, and I don't believe that this should be expected behaviour as specified by David.

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

采纳答案by Haacked

Yes, this behavior is currently by design. Even though you're explicitly setting values, if you post back to the same URL, we look in model state and use the value there. In general, this allows us to display the value you submitted on postback, rather than the original value.

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

There are two possible solutions:

有两种可能的解决方案:

Solution 1

解决方案1

Use unique names for each of the fields. Note that by default we use the name you specify as the id of the HTML element. It's invalid HTML to have multiple elements have the same id. So using unique names is good practice.

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

Solution 2

解决方案2

Do not use the Hidden helper. It seems like you really don't need it. Instead, you could do this:

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

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

Of course, as I think about this more, changing the value based on a postback makes sense for Textboxes, but makes less sense for hidden inputs. We can't change this for v1.0, but I'll consider it for v2. But we need to think through carefully the implications of such a change.

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

回答by Dacker

Same as others I would have expected the ModelState to be used to fill the Model and as we explicitly use the Model in expressions in the view, it should use the Model and not ModelState.

与其他人一样,我希望使用 ModelState 来填充模型,因为我们在视图中的表达式中显式使用模型,它应该使用模型而不是模型状态。

It's a design choice and I do get why: if validations fail, the input value might not be parseable to the datatype in the model and you still want to render whatever wrong value the user typed, so it's easy to correct it.

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

The only thing I don't understand is: why isn't it by design that the Model is used, which is set explicitly by the developer and if a validation error occurred, the ModelState is used.

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

I have seen many people using workarounds like

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

  • ModelState.Clear(): Clears all ModelState values, but basically disables usage of default validation in MVC
  • ModelState.Remove("SomeKey"): Same as ModelState.Clear() but needs micromanagement of ModelState keys, which is too much work and it doesn't feel right with the auto binding feature from MVC. Feels like 20 years back when we were also managing Form and QueryString keys.
  • Rendering HTMLthemselves: too much work, detail and throws away the HTML Helper methods with the additional features. An example: Replace @Html.HiddenFor by m.Name)" id="@Html.IdFor(m=>m.Name)" value="@Html.AttributeEncode(Model.Name)">. Or replace @Html.DropDownListFor by ...
  • Create custom HTML Helpers to replace default MVC HTML Helpers to avoid the by-design issue. This is a more generic approach then rendering your HTML, but still requires more HTML+MVC knowledge or decompiling System.Web.MVC to still keep all other features but disable ModelState precedence over Model.
  • Apply the POST-REDIRECT-GET Pattern: this is easy in some environments, but harder in the ones with more interaction/complexity. This pattern has it's pros and cons and you shouldn't be forced to apply this pattern because of a by-design choice of ModelState over Model.
  • ModelState.Clear():清除所有 ModelState 值,但基本上禁用了 MVC 中默认验证的使用
  • ModelState.Remove("SomeKey"):与 ModelState.Clear() 相同,但需要对 ModelState 键进行微管理,这工作量太大,而且感觉不适合 MVC 的自动绑定功能。感觉就像 20 年前我们还管理 Form 和 QueryString 键。
  • 渲染 HTML 本身:太多的工作、细节和附加功能的 HTML Helper 方法。一个例子:将@Html.HiddenFor 替换为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 的设计选择而被迫应用这种模式。

Issue

问题

So the issue is that the Model is filled from ModelState and in the view we set explicitly to use the Model. Everybody expects the Model value (in case it changed) to be used, unless there's a validation error; then the ModelState can be used.

所以问题是模型是从 ModelState 填充的,并且在我们明确设置为使用模型的视图中。除非出现验证错误,否则每个人都希望使用模型值(万一更改);那么 ModelState 就可以使用了。

Currently in the MVC Helper extensions the ModelState value gets precedence over the Model value.

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

Solution

解决方案

So the actual fix for this issue should be: for each expression to pull the Model value the ModelState value should be removed if there is no validation error for that value. If there's a validation error for that input control the ModelState value shouldn't be removed and it will be used like normal. I think this solves the issue exactly, which is better then most workarounds.

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

The code is here:

代码在这里:

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

And then we create our own HTML Helper extensions todo this before calling the MVC extensions:

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

This solution removes the issue, but doesn't require you to decompile, analyse and rebuild whatever MVC is offering you normally (don't forget also managing changes over-time, browser differences, etc.).

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

I think the logic of "Model value, unless validation error then ModelState"should have been by-design. If it was, it wouldn't have bitten so many people, but still covered what MVC was intended todo.

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

回答by user183460

I just ran into same issue. Html helpers like TextBox() precedence for passed values appear to behave exactly opposite what I inferred from the Documentationwhere it says:

我刚刚遇到了同样的问题。像 TextBox() 传递值的优先级这样的 Html 助手的行为似乎与我从文档中推断出的完全相反,它说:

The value of the text input element. If this value is null reference (Nothing in Visual Basic), the value of the element is retrieved from the ViewDataDictionary object. If no value exists there, the value is retrieved from the ModelStateDictionary object.

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

To me, I read that the value, if passed is used. But reading TextBox() source:

对我来说,我读到该值,如果传递被使用。但是阅读 TextBox() 源代码:

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

seems to indicate that the actual order is the exact opposite of what is documented. Actual order seems to be:

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

  1. ModelState
  2. ViewData
  3. Value (passed into TextBox() by caller)
  1. 模型状态
  2. 查看数据
  3. 值(由调用者传入 TextBox())

回答by Mark S

Heads-up - this bug still exists in MVC 3. I'm using the Razor markup syntax (like that really matters), but I encountered the same bug with a foreach loop that produced the same value for an object property every single time.

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

回答by David

This would be the expected behavtheitroad - MVC doesn't use a viewstate or other behind your back tricks to pass extra information in the form, so it has no idea which form you submitted (the form name is not part of the data submitted, only a list of name/value pairs).

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

When MVC renders the form back, it is simply checking to see if a submitted value with the same name exists - again, it has no way of knowing which form a named value came from, or even what type of control it was (whether you use a radio, text or hidden, it's all just name=value when its submitted through HTTP).

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

回答by BlackHymanetMack

This issue still exists in MVC 5, and clearly it's not considered a bug which is fine.

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

We're finding that, although by design, this is not the expected behavior for us. Rather we always want the value of the hidden field to operate similarly to other types of fields and not be treated special, or pull its value from some obscure collection (which reminds us of ViewState!).

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

A few findings (correct value for us is the model value, incorrect is the ModelState value):

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

  • Html.DisplayFor()displays the correct value (it pulls from Model)
  • Html.ValueFordoes not (it pulls from ModelState)
  • ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData).Modelpulls the correct value
  • Html.DisplayFor()显示正确的值(它从模型中提取)
  • Html.ValueFor没有(它从 ModelState 中提取)
  • ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData).Model拉取正确的值

Our solution is to simply implement our own Extension:

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

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

回答by jorge

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

回答by Softlion

Example to reproduce the "design problem", and a possible workaroud. There is no workaround for the 3 hours lost trying to find the "bug" though ... Note that this "design" is still in ASP.NET MVC 2.0 RTM.

重现“设计问题”的示例,以及可能的解决方法。虽然在试图找到“错误”的过程中浪费了 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);
    }

If your form originally contains this: <%= Html.HiddenFor( m => m.ProductId ) %>

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

If the original value of "Name" (when the form was rendered) is "dummy", after the form is submitted you expect to see "dummy (user set)" rendered. Without ModelState.Clear()you'll still see "dummy" !!!!!!

如果“Name”的原始值(呈现表单时)为“dummy”,则在提交表单后,您希望看到呈现“dummy(用户设置)”。没有ModelState.Clear()你仍然会看到“虚拟”!!!!!!

Correct workaround:

正确的解决方法:

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

I feel this is not a good design at all, as every mvc form developer needs to keep that in mind.

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

回答by Juan Carlos Puerto

So in MVC 4 the "design problem" still there. Here's the code I had to use in order to set the correct hidden values in a collection since regardless of what I do in the controller, the view always showed incorrect values.

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

OLD code

旧代码

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
}

UPDATED code

更新代码

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

Did they fixed this in MVC 5?

他们是否在 MVC 5 中修复了这个问题?

回答by Darragh

This may be 'by design' but it's not what is documented:

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

Public Shared Function Hidden(  

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

Member of System.Web.Mvc.Html.InputExtensions

Summary: Returns a hidden input tag.

Parameters:
htmlHelper: The HTML helper.
name: The form field name and System.Web.Mvc.ViewDataDictionary key used to look up the value.
value: The value of the hidden input. If null, looks at the System.Web.Mvc.ViewDataDictionary and then System.Web.Mvc.ModelStateDictionary for the value.

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 以获取值。

This would seem to suggest that ONLY when the value parameter is null (or not specified) would the HtmlHelper look elsewhere for a value.

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

In my app, I've got a form where: html.Hidden("remote", True) is rendering as <input id="remote" name="remote" type="hidden" value="False" />

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

Note the value is getting over-ridden by what is in the ViewData.ModelState dictionary.

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

Or am I missing something?

或者我错过了什么?