可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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).
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?
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.
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 Haack about 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.
回答1:
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.
There are two possible solutions:
Solution 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.
Solution 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.
回答2:
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.
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.
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.
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.
Currently in the MVC Helper extensions the ModelState value gets precedence over the Model value.
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.
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:
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.).
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.
回答3:
I just ran into same issue. Html helpers like TextBox() precedence for passed values appear to behave exactly opposite what I inferred from the Documentation where it says:
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.
To me, I read that the value, if passed is used. But reading TextBox() source:
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:
- ModelState
- ViewData
- Value (passed into TextBox() by caller)
回答4:
This would be the expected behavoir - 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).
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).
回答5:
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.
回答6:
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);
回答7:
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.
[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 ) %>
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" !!!!!!
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.
回答8:
This issue still exists in MVC 5, and clearly it's not considered a bug which is fine.
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!).
A few findings (correct value for us is the model value, incorrect is the ModelState value):
Html.DisplayFor()
displays the correct value (it pulls from Model)
Html.ValueFor
does not (it pulls from ModelState)
ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData).Model
pulls the correct value
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));
}
回答9:
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.
This would seem to suggest that ONLY when the value parameter is null (or not specified) would the HtmlHelper look elsewhere for a value.
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" />
Note the value is getting over-ridden by what is in the ViewData.ModelState dictionary.
Or am I missing something?
回答10:
There is workaround:
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());
}
}
回答11:
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.
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?
回答12:
As others have suggested, I went with using direct html code instead of using the HtmlHelpers (TextBoxFor, CheckBoxFor, HiddenFor etc.).
The problem though with this approach is that you need to put the name and id attributes as strings. I wanted to keep my model properties strongly-typed so I used the NameFor and IdFor HtmlHelpers.
<input type="hidden" name="@Html.NameFor(m => m.Name)" id="@Html.IdFor(m=>m.Name)" value="@Html.AttributeEncode(Model.Name)">
Update:
Here's a handy HtmlHelper extension
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;
}
You can then use it like
@Html.MyHiddenFor(m => m.Name)