136

I'm using MVC 3 in my project, and I'm seeing a very strange behavior.

I'm trying to create a hidden field for a particular value on my Model, the problem is that for some reason the value set on the field does not correspond to the value in the Model.

e.g.

I have this code, just as a test:

<%:Html.Hidden("Step2", Model.Step) %>
<%:Html.HiddenFor(m => m.Step) %>

I would think that both hidden fields would have the same value. What I do is, set the value to 1 the first time I display the View, and then after the submission I increase the value of the Model field by 1.

So, the first time I render the page both controls have the value 1, but the second time the values rendered are these:

<input id="Step2" name="Step2" type="hidden" value="2" />
<input id="Step" name="Step" type="hidden" value="1" />

As you can see, the first value is correct, but the second value seems to be the same as the first time I display the View.

What am I missing? Are the *For Html helpers caching the values in some way? If so, how can I disable this caching?.

Thanks for your help.

tereško
  • 58,060
  • 25
  • 98
  • 150
willvv
  • 8,439
  • 16
  • 66
  • 101
  • I just tested something else. If I remove the HiddenFor call and let only the Hidden call, but using the "Step" name, it also renders only the first value (1). – willvv Jan 17 '11 at 06:17
  • 1
    happens in get as well – Oren A Feb 11 '13 at 10:47

4 Answers4

197

That's normal and it is how HTML helpers work. They first use the value of the POST request and after that the value in the model. This means that even if you modify the value of the model in your controller action if there is the same variable in the POST request your modification will be ignored and the POSTed value will be used.

One possible workaround is to remove this value from the model state in the controller action which is trying to modify the value:

// remove the Step variable from the model state 
// if you want the changes in the model to be
// taken into account
ModelState.Remove("Step");
model.Step = 2;

Another possibility is to write a custom HTML helper which will always use the value of the model and ignore POST values.

And yet another possibility:

<input type="hidden" name="Step" value="<%: Model.Step %>" />
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • 5
    I really appreciated Simon Ince's blog post about this. The conclusion I take from it is to ensure your workflow is correct. So if you have accepted a valid view model and done something with it then redirect to a confirmation action, even if this also simply retreives and displays an equivalent model. This means you have a fresh ModelState. http://blogs.msdn.com/b/simonince/archive/2010/05/05/asp-net-mvc-s-html-helpers-render-the-wrong-value.aspx (linked from a post I wrote on this today: http://oceanbites.blogspot.com/2011/02/mvc-renders-wrong-value.html ) – Lisa Mar 01 '11 at 06:40
  • This now sort of makes sense, but why in EditorForModel would they look to the request values over the model's values? – ehdv May 02 '12 at 20:24
  • @ehdv I'm very interested by your question but I'm not sure what exactly you mean. Can you elaborate a little bit more? – Samuel Aug 17 '12 at 22:15
  • Yesterday, I figured out that it's not just for POST request but immeditatly when you have at least one parameter in the action that render the view. The exception is when you just have one parameter and the type of this parameter is FormCollection. In all other cases, the modelstate is filled with a key-pair value with each parameter of the action. It's important in this case that your parameters name are not in conflict with xxxFor helpers with different values. – Samuel Aug 20 '12 at 17:43
  • 2
    I really like MVC3, but this bit is really clunky. I hope they fix it in MVC4. – KennyZ Aug 29 '12 at 13:53
  • @Samuel Re-reading that it's definitely unclear. I meant, in the helper function that generates an editor for the model, when there are conflicting values for the same key defined in the model and in the original request, why does it resolve to the request's value? – ehdv Sep 07 '12 at 18:37
  • Ok, I know that it's weird but here is the case: an action GetPerson(int id) but the id is in fact the 'teamId' of the person. You load the person object where the teamId is equal to the specified id in parameter but in the HiddenFor(x=>x.Id) statement of the view, the id of the team will be used instead of the id of the person. – Samuel Sep 08 '12 at 01:08
  • 9
    Wow, this one had me going for quite a while. I basically used the first suggestion but just called ModelState.Clear() before returning. This seems to work great, is there any reason not to use Clear? – Jason Jun 20 '13 at 19:13
  • 1
    The ".Remove" didn't work for me. But ModelState.Clear() did right before the return in Controller. Custom-writing your Hidden would also work well. This all happens because developers don't want to lose their "form values" if they press "submit" and the DB doesn't save correctly. Best solution: don't name different fields on same page the same name/id. – Dexter Sep 15 '14 at 16:10
  • I think I had this same problem when displaying a view with Model.ID, which in turn included a Partial with a different Model.ID. In my case, the Partial's Model.ID was overwritten by the main page's Model.ID. The third of the three solutions posted above solved this problem for me. – ChrisFox Apr 27 '16 at 13:01
  • A fourth option is the PRG (Post-Redirect-Get) approach. – Jason Beck Oct 11 '16 at 20:11
  • The accepted answer says `That's normal and it is how HTML helpers work......bla bla` So is that true for ALL the html helpers like `TextBoxFor`, `DisplayFor` or Is it true only for `HiddenFor` html helper? – LP13 Jun 19 '17 at 20:54
  • 5
    FYI this annoying behavior was graciously carried over to ASP.NET Core in case anyone was worried things would get better – John Hargrove Mar 29 '18 at 14:39
  • 1
    This also applies to GET request parameters. Clearing ModelState seems to be the safest thing to do, unless you specifically want it's state to be used. – yourpublicdisplayname Aug 07 '18 at 02:19
22

I encountered the same problem when writing a Wizard that shows different parts of a larger model at every step.
Data and/or Errors from "Step 1" would become mixed up with "Step 2", etc, until I finally realized that ModelState was to 'blame'.

This was my simple solution:

if (oldPageIndex != newPageIndex)
{
    ModelState.Clear(); // <-- solution
}

return View(model[newPageIndex]);
Peter B
  • 22,460
  • 5
  • 32
  • 69
  • 10
    `ModelState.Clear()` solved my issue with sequential POST requests in a similar situation. – Evan Mulawski May 07 '14 at 14:04
  • Thanks for the ModelState.Clear() tip Evan. This was an anomaly I have never encountered before. I had several sequential ajax.beginform posts and one of them was retaining values from a previous post. Debugging black hole. Anyone know why this gets cached? – Rob Jun 01 '18 at 06:19
1

This code will not work

// remove the Step variable from the model state
// if you want the changes in the model to be
// taken into account
ModelState.Remove("Step");
model.Step = 2;

...because HiddenFor always (!) reads from ModelState not the model itself. And if it doesn't find the "Step" key it will produce the default for that variable type which will be 0 in this case

Here is the solution. I wrote it for myself but don't mind sharing it cause I see many people are struggling with this naughty HiddenFor helper.

public static class CustomExtensions
{
    public static MvcHtmlString HiddenFor2<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression)
    {
        ReplacePropertyState(htmlHelper, expression);
        return htmlHelper.HiddenFor(expression);
    }

    public static MvcHtmlString HiddenFor2<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object htmlAttributes)
    {
        ReplacePropertyState(htmlHelper, expression);
        return htmlHelper.HiddenFor(expression, htmlAttributes);
    }

    public static MvcHtmlString HiddenFor2<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IDictionary<string, object> htmlAttributes)
    {
        ReplacePropertyState(htmlHelper, expression);
        return htmlHelper.HiddenFor(expression, htmlAttributes);
    }

    private static void ReplacePropertyState<TModel, TProperty>(HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression)
    {
        string text = ExpressionHelper.GetExpressionText(expression);
        string fullName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(text);
        ModelStateDictionary modelState = htmlHelper.ViewContext.ViewData.ModelState;
        ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);

        if (modelState.ContainsKey(fullName))
        {                
            ValueProviderResult currentValue = modelState[fullName].Value;
            modelState[fullName].Value = new ValueProviderResult(metadata.Model, Convert.ToString(metadata.Model), currentValue.Culture);
        }
        else
        {
            modelState[fullName] = new ModelState
            {
                Value = new ValueProviderResult(metadata.Model, Convert.ToString(metadata.Model), CultureInfo.CurrentUICulture)
            };
        }
    }
}

Then you just use it as usual from within you view:

@Html.HiddenFor2(m => m.Id)

It worth to mention it works with collections too.

  • this solution not fully worked. After next post back property is null in Action – user576510 Nov 17 '14 at 05:40
  • Well, this is the code from production where it works fine. I cannot tell why it doesn't work for you but if you see the hidden field with correct value rendered on the page I see no obvious reason why it wouldn't be restored into the model's property. If you see wrong hidden field value on the page though - that's another story, I would be very keen to know under what circumstances this happens before the same happens on my production :-) Thank you. – Ruslan Georgievskiy Nov 18 '14 at 13:52
0

I am too struggling with the same situation I think, where I use the same model state between calls, and when I alter a model property on backend. Though, it does not matter for me, if I use textboxfor or hiddenfor.

I just bypass the situation by using page scripts to store the model value as a js variable, because I need the hiddenfield for that purpose in the beginning.

Not sure if this helps but just consider..