ASP.NET MVC中表单值被replace的可能的错误
我似乎有一个与ASP.NET MVC的问题在于,如果我有不止一个表单在每个使用相同的名称,但作为不同的types(收音机/隐藏/等),然后,当第一种forms的post(例如,我select“Date”单选button),如果表单被重新渲染(例如作为结果页面的一部分),我似乎有问题,其他forms的SearchType的隐藏价值被更改为最后一个单选button值(在本例中为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的其他人可以证实这一点?
也许这是因为我使用枚举。 我不知道。 我应该补充一点,我可以通过对隐藏字段使用'manual'input()标签来避开这个问题,但是如果我使用MVC标签(<%= Html.Hidden(…)%>),.NET MVC将replace它们每次。
非常感谢。
更新:
我今天又看到了这个bug。 看起来,当你返回一个发布的页面时,它会头一回,并且使用MVC设置Html帮助器的隐藏表单标签。 我已经联系了菲尔·哈克(Phil Haack)这件事,因为我不知道还有什么可以转的,我不认为这应该是大卫指定的行为。
是的,这种行为目前是由devise。 即使你明确地设置了值,如果你回到相同的URL,我们看看模型的状态,并在那里使用的值。 一般来说,这允许我们显示您在回发中提交的值,而不是原始值。
有两种可能的解决scheme:
解决scheme1
为每个字段使用唯一的名称。 请注意,默认情况下,我们使用您指定的名称作为HTML元素的ID。 多个元素具有相同的ID是无效的HTML。 所以使用独特的名字是好的做法。
解决scheme2
不要使用隐藏的帮手。 看来你真的不需要它。 相反,你可以这样做:
<input type="hidden" name="the-name" value="<%= Html.AttributeEncode(Model.Value) %>" />
当然,当我想到更多的时候,基于回发来改变价值对于文本框来说是有意义的,但是对于隐藏的input来说却不太合适。 我们不能改变这个v1.0,但我会考虑v2。 但是我们需要仔细考虑这种变化的影响。
和其他人一样,我会期望ModelState被用来填充模型,当我们在视图的expression式中明确使用Model时,它应该使用Model而不是ModelState。
这是一个deviseselect,我得到了为什么:如果validation失败,input值可能无法parsing为模型中的数据types,并且您仍然想渲染用户input的任何错误值,所以很容易纠正它。
我唯一不明白的是: 为什么不是在devise中使用Model,而是由开发人员明确设置的,如果发生validation错误,则使用ModelState。
我看到很多人使用类似的解决方法
- ModelState.Clear():清除所有的ModelState值,但基本上禁止在MVC中使用默认validation
- ModelState.Remove(“SomeKey”):与ModelState.Clear()相同,但需要对ModelState键进行微操作,工作量太大,而且MVC的自动绑定function不太适合。 感觉像20年前,当我们还pipe理表单和QueryString键。
- 渲染HTML自己:太多的工作,细节,并抛出HTML辅助方法与附加function。 一个例子:replace@ Html.HiddenFor由m.Name)“id =”@ Html.IdFor(m => m.Name)“value =”@ Html.AttributeEncode(Model.Name)“>或replace@Html。 DropDownListFor …
- 创build自定义HTML助手来replace默认MVC HTML助手,以避免按devise问题。 这是一个更通用的方法,然后呈现您的HTML,但仍需要更多的HTML + MVC知识或反编译System.Web.MVC仍然保留所有其他function,但禁用ModelState优先于模型。
- 应用POST-REDIRECT-GET模式:这在一些环境中很容易,但是在具有更多交互/复杂性的情况下更难。 这种模式有其优点和缺点,你不应该被迫应用这种模式,因为ModelState over Model的deviseselect。
问题
所以问题是模型是从ModelState填充的,并且在视图中我们明确地设置了使用模型。 每个人都期望Model值(如果它改变了)被使用,除非有validation错误。 那么可以使用ModelState。
目前在MVC Helper扩展中,ModelState的值优先于Model的值。
解
所以这个问题的实际解决方法应该是:对于每个expression式来拉取Model值,如果没有该值的validation错误,则应删除ModelState值。 如果input控件存在validation错误,则不应删除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扩展之前,我们创build自己的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); }
这个解决scheme消除了这个问题,但是并不要求你反编译,分析和重buildMVC通常提供给你的任何东西(不要忘记同时pipe理更改,浏览器差异等)。
我认为“模型的价值,除非validation错误,然后ModelState”的逻辑应该是devise。 如果是这样,它不会咬这么多人,但仍然涵盖了什么MVC打算待办事项。
这将是预期的行为 – MVC不使用视图状态或其他背后的技巧来传递表单中的额外信息,所以它不知道你提交了哪个表单(表单名称不是提交的数据的一部分,名称/值对的列表)。
当MVC呈现表单时,它只是简单地检查是否存在同名的提交值 – 同样,它无法知道指定值来自哪个表单,甚至是什么types的控制(无论是使用收音机,文本或隐藏,当它通过HTTP提交时,它们都只是name = value)。
我刚刚遇到同样的问题。 Html助手像TextBox()优先传递值似乎performance完全相反,我从文档推断,它说:
文本input元素的值。 如果此值为空引用(在Visual Basic中为Nothing),则元素的值将从ViewDataDictionary对象中检索。 如果这里没有值,则从ModelStateDictionary对象中检索该值。
对我来说,我读过的是,如果传递的价值被使用。 但是阅读TextBox()来源:
string attemptedValue = (string)htmlHelper.GetModelStateValue(name, typeof(string)); tagBuilder.MergeAttribute("value", attemptedValue ?? ((useViewData) ? htmlHelper.EvalString(name) : valueParameter), isExplicitValue);
似乎表明实际的顺序与logging的完全相反。 实际的顺序似乎是:
- 的ModelState
- ViewData的
- 值(由调用者传入TextBox())
单挑 – 这个bug在MVC 3中仍然存在。我使用Razor标记语法(就像那真的很重要),但是我遇到了一个foreach循环,每次都为对象属性产生相同的值。
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);
重现“devise问题”的例子,以及一个可能的解决方法。 没有解决方法3小时试图find“错误”,虽然…注意,这个“devise”仍然在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) %>" />
我觉得这不是一个好的devise,因为每个mvc表单开发者都需要牢记这一点。
这可能是“devise上”,但不是logging的内容:
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的成员
摘要:返回一个隐藏的input标签。
参数:
htmlHelper:HTML助手。
名称:用于查找值的表单字段名称和System.Web.Mvc.ViewDataDictionary键。
值:隐藏input的值。 如果为null,则查看System.Web.Mvc.ViewDataDictionary,然后查看System.Web.Mvc.ModelStateDictionary的值。
这似乎表明,只有当value参数为null(或未指定)时,HtmlHelper才会在其他位置查找值。
在我的应用程序中,我有一个表单,其中:html.Hidden(“remote”,True)呈现为<input id="remote" name="remote" type="hidden" value="False" />
请注意,该值正在被ViewData.ModelState字典中的内容覆盖。
还是我错过了什么?
这个问题在MVC 5中仍然存在,显然它不被认为是一个错误。
我们发现,尽pipe通过devise,这不是我们预期的行为。 相反,我们总是希望隐藏字段的值与其他types的字段类似,不要被视为特殊的,或者从一些隐藏的集合中提取它的值(这让我们想起ViewState!)。
一些发现(我们的正确值是模型值,不正确的是ModelState值):
-
Html.DisplayFor()
显示正确的值(它从模型拉) -
Html.ValueFor
不(它从ModelState拉) -
ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData).Model
拉取正确的值
我们的解决scheme是简单地实现我们自己的扩展
/// <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)); }
所以在MVC 4中,“devise问题”仍然存在。 这是我必须使用的代码,以便在集合中设置正确的隐藏值,因为无论我在控制器中做什么,视图总是显示不正确的值。
旧代码
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中解决了这个问题?
正如其他人所build议的,我使用直接的HTML代码,而不是使用HtmlHelpers(TextBoxFor,CheckBoxFor,HiddenFor等)。
这个方法的问题是,你需要把名字和ID属性作为string。 我想保持我的模型属性强types,所以我用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)
有解决办法:
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()); } }