Condividi tramite


ASP.NET MVC’s Html Helpers Render the Wrong Value!

First things first – oh no they don’t J

But it can look like a bug if you’re not used to MVC, so I thought it worth calling out.

Scenario

Imagine we have a pair of controller actions like this;

[HttpGet]

public ActionResult Index()

{

    var model = new MyModel

    {

        Count = 1

    };

    return View(model);

}

 

 

[HttpPost]

public ActionResult Index(MyModel model)

{

    if (!model.Name.StartsWith("Mr"))

        model.Name = "Mr " + model.Name;

 

  model.Count++;

 

    return View(model);

}

 

What this is doing should be obvious – we want to increment a counter every time a POST to the Index action occurs, and if they didn’t specify a prefix of “Mr” for their name we add it for them. Easy. The “Index” View to render this looks like this;

<% using (Html.BeginForm())

    { %>

    <%= Html.HiddenFor(m => m.Count)%>

    Name: <%= Html.TextBoxFor(m => m.Name)%>

    <input type="submit" value="Submit" />

<% } %>

 

This is pretty simple – we’re outputting the Counter as a hidden form field, and letting them type a Name into a Text Box.

The Problem

If you paste these into a solution and give it a try you’ll notice a problem – Name never gets the “Mr” prefix, and our Counter never increments! Instead, they just display exactly as they were POST-ed to the server. Set a breakpoint in the action and inspect the model – you’ll see it is being updated as expected within the action, yet still the changes are not rendered. So what’s going on?

Why?

ASP.NET MVC assumes that if you’re rendering a View in response to an HTTP POST, and you’re using the Html Helpers, then you are most likely to be redisplaying a form that has failed validation. Therefore, the Html Helpers actually check in ModelState for the value to display in a field before they look in the Model. This enables them to redisplay erroneous data that was entered by the user, and a matching error message if needed.

Since our [HttpPost] overload of Index relies on Model Binding to parse the POST data, ModelState has automatically been populated with the values of the fields. In our action we change the Model data (not the ModelState), but the Html Helpers (i.e. Html.Hidden and Html.TextBox) check ModelState first… and so display the values that were received by the action, not those we modified.

To prove this, make a tiny temporary change to the second action;

[HttpPost]

public ActionResult Index(MyModel model)

{

    if (!model.Name.StartsWith("Mr"))

        model.Name = "Mr " + model.Name;

  model.Count++;

  ModelState.Clear();

    return View(model);

}

This call to clear the ModelState will mean that the View now works as we expected. However, I wouldn’t recommend this as a long term solution. In reality, there are a few possible solutions;

1. If we want to display a confirmation page, we should be using the Post-Redirect-Get pattern, and not displaying this content in response to a POST. This means a view render during a POST is always a validation failure, which is what the MVC framework expects.

2. If we don’t want to do that, but we don’t want to perform any validation of the fields, we shouldn’t be using the Html Helpers. They only exist to assist with MVC framework functionality – if all you want to do is render simple HTML, guess what you should use? HTML! Just render data using <%= Model.Count %> etc.

3. If you don’t like that, you could consider avoiding Model Binding, and therefore avoiding ModelState from being populated. If we removed MyModel from the Index parameter list and instead accessed POST data using Request.Form this would work… but this fails to take advantage of MVC programming constructs, and complicates testability, so I would avoid it again.

4. If you have a more complex scenario (perhaps validation on some POSTs, and manual manipulation of posted data on others) you may need to consider calling ModelState.Remove for a specific field – but exercise caution; I do not recommend this approach. Complex action interactions that don’t quite fit with the framework are much more likely to cause you problems later.

There may be other solutions – such as writing your own Model Binder, but fundamentally they’re likely to change how MVC works, so I’d recommend avoiding them for this particular problem. If you’ve come up with something that fits well do let me know though.

So to go with my recommendation of taking control of our HTML, the revised Index view looks like this;

<% using (Html.BeginForm())

    { %>

    <input type="hidden" name="Count" value="<%: Model.Count %>" />

    Name: <input type="text" name="Name" value="<%: Model.Name %>" />

    <input type="submit" value="Submit" />

<% } %>

Give it a whirl and you’ll see that it works this time!

* if you’re not using Visual Studio 2010, note that you must use <%= Html.Encode(field) %> instead of <%: field %>

Originally posted by Simon Ince on 5th May 2010 here https://blogs.msdn.com/b/simonince/archive/2010/05/05/asp-net-mvc-s-html-helpers-render-the-wrong-value.aspx