ASP.NET MVC 中可能出现表单值被替换的错误
-
09-09-2019 - |
题
我似乎在使用 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 帮助器时,这似乎会产生影响。我已经联系过 菲尔·哈克 关于这一点,因为我不知道还能去哪里,而且我不认为这应该是大卫指定的预期行为。
解决方案
是的,这种行为目前是设计使然。即使您显式设置值,如果您回发到同一 URL,我们也会查看模型状态并使用那里的值。一般来说,这使我们能够显示您在回发时提交的值,而不是原始值。
有两种可能的解决方案:
解决方案1
为每个字段使用唯一的名称。请注意,默认情况下我们使用您指定的名称作为 HTML 元素的 id。多个元素具有相同的 id 是无效的 HTML。因此,使用唯一的名称是一个很好的做法。
解决方案2
不要使用隐藏助手。看来你真的不需要它。相反,你可以这样做:
<input type="hidden" name="the-name"
value="<%= Html.AttributeEncode(Model.Value) %>" />
当然,当我更多地考虑这一点时,基于回发更改值对于文本框来说是有意义的,但对于隐藏输入来说意义不大。我们无法在 v1.0 中更改此设置,但我会在 v2 中考虑它。但我们需要仔细思考这种变化的影响。
其他提示
与其他人一样,我希望使用 ModelState 来填充模型,并且当我们在视图的表达式中显式使用模型时,它应该使用模型而不是 ModelState。
这是一个设计选择,我确实明白为什么:如果验证失败,输入值可能无法解析为模型中的数据类型,并且您仍然希望呈现用户输入的任何错误值,因此很容易纠正它。
我唯一不明白的是: 为什么设计上不使用 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 而被迫应用此模式。
问题
所以问题是模型是从 ModelState 填充的,并且在视图中我们明确设置为使用模型。每个人都希望使用模型值(如果它发生变化),除非存在验证错误;然后就可以使用 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);
}
}
然后我们创建自己的 HTML Helper 扩展来在调用 MVC 扩展之前执行此操作:
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 的意图。
我刚刚遇到了同样的问题。Html 帮助器(例如 TextBox() 传递值的优先级)的行为似乎与我从以下内容中推断出的完全相反 文档 它说:
文本输入元素的值。如果此值是null引用(在Visual Basic中没有任何内容),则从ViewDatationaly对象中检索元素的值。如果那里不存在值,则该值将从ModelStateDictionary对象中检索。
对我来说,我读到该值如果通过则被使用。但是阅读 TextBox() 源代码:
string attemptedValue = (string)htmlHelper.GetModelStateValue(name, typeof(string));
tagBuilder.MergeAttribute("value", attemptedValue ?? ((useViewData) ? htmlHelper.EvalString(name) : valueParameter), isExplicitValue);
似乎表明实际顺序与记录的完全相反。实际顺序似乎是:
- 模型状态
- 查看数据
- 值(由调用者传递到 TextBox() 中)
注意 - 这个错误在 MVC 3 中仍然存在。我正在使用 Razor 标记语法(这确实很重要),但我在 foreach 循环中遇到了相同的错误,每次都会为对象属性生成相同的值。
这将是预期的behavoir - MVC不使用背着你名堂视图状态或其他传递额外信息的形式,所以它不知道构成您提交(窗体名称不是数据的一部分提交,只有名称/值对的列表)。
在MVC呈现形式回来,它只是检查是否具有相同的名称提交的值存在 - 再次,它没有办法知道哪些形式的命名值从何而来,甚至是什么类型的控制(无论您使用一台收音机,文本或隐藏,这是当其通过HTTP提交的所有仅举=值)。
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);
实施例重现“设计问题”,和可能的workaroud。 有没有解决方法失去试图找到“错误”,虽然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 ) %>
如果“名称”(当形式呈现)的原始值是“伪”,提交表单后,您会看到“虚拟(用户设置)”渲染。
没有ModelState.Clear()
你仍然会看到“虚拟” !!!!!!
正确的解决方法:
<input type="hidden" name="Name" value="<%= Html.AttributeEncode(Model.Name) %>" />
我觉得这不是一个好的设计可言,因为每个MVC形式,开发者需要牢记这一点。
这个问题在 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));
}
这可能是“设计”,但它不是什么记载:
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帮手结果。 名称:用于查找数值的表单字段名和System.Web.Mvc.ViewDataDictionary键点击。 值:隐藏的输入的值。如果为null,着眼于System.Web.Mvc.ViewDataDictionary然后System.Web.Mvc.ModelStateDictionary为值
这似乎表明,只有当值参数为空(或不指定)将所述的HtmlHelper别处寻找的值。
在我的应用程序,我有一个形式,其中:html.Hidden(“远程”,真)是呈现为
<input id="remote" name="remote" type="hidden" value="False" />
请注意值被获得由什么是在ViewData.ModelState字典过骑。
还是我失去了一些东西?
因此,在MVC 4“设计问题”仍然存在。下面是我为了收集,因为无论我在做控制器中设置正确的隐藏价值使用的代码,视图总是显示不正确的值。
<强> OLD代码强>
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固定此?
有解决方法是:
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());
}
}
正如其他人的建议,我去与使用直接的HTML代码,而不是使用HtmlHelpers(TextBoxFor,CheckBoxFor,HiddenFor等)。
但这种方法的问题是,你需要把这个名字和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)