So here's a binding behavior in ASP.NET MVC that I didn't really get until today: HtmlHelpers controls (like .TextBoxFor() etc.) don't bind to model values on Postback, but rather get their
value directly out
of the POST buffer from ModelState. Effectively it looks like you can't change the display
value of a control via model
value updates on a Postback operation. To demonstrate here's an example. I have a small section in a document where I display an editable email address: This is what the form displays on a GET operation and as expected I get the email
value displayed in both the textbox and plain
value display below, which reflects the
value in the mode. I added a plain text
value to demonstrate the model
value compared to what's rendered in the textbox. The relevant markup is the email address which needs to be manipulated via the model in the Controller code. Here's the Razor markup: <div class="fieldcontainer">
<label>
Email: <small>(username and <a href="http://gravatar.com">Gravatar</a> image)</small>
</label>
<div>
@Html.TextBoxFor( mod=> mod.User.Email, new {type="email",@class="inputfield"}) @Model.User.Email
</div>
</div>
So, I have this form and the user can change their email address. On postback the Post controller code then asks the business layer whether the change is allowed. If it's not I want to reset the email address back to the old
value which exists in the database and was previously store. The obvious thing to do would be to modify the model. Here's the Controller logic block that deals with that:// did user change email?
if (!string.IsNullOrEmpty(oldEmail) && user.Email != oldEmail)
{
if (userBus.DoesEmailExist(user.Email))
{
userBus.ValidationErrors.Add("New email address exists already. Please…");
user.Email = oldEmail;
}
else
// allow email change but require verification by forcing a login
user.IsVerified = false;
}… model.user = user;
return View(model);
The logic is straight forward - if the new email address is not valid because it already exists I don't want to display the new email address the user entered, but rather the old one. To do this I change the
value on the model which effectively does this:model.user.Email = oldEmail;
return View(model);
So when I press the Save button after entering in my new email address (
[email protected]) here's what comes back in the rendered view:
Notice that the textbox
value and the raw displayed model
value are different. The TextBox displays the POST
value, the raw
value displays the actual model
value which are different.
This means that MVC renders the textbox
value from the POST data rather than from the view data when an Http POST is active.
Now I don't know about you but this is not the behavior I expected - initially. This behavior effectively means that I cannot modify the contents
of the textbox from the Controller code if using HtmlHelpers for binding. Updating the model for display purposes in a POST has in effect - no effect.
(Apr. 25, 2012 - edited the post heavily based on comments and more experimentation)
What should the behavior be?
After getting quite a few comments on this post I quickly realized that the behavior I described above is actually the behavior you'd want in 99%
of the binding scenarios. You do want to get the POST values back into your input controls at all times, so that the data displayed on a form for the user matches what they typed. So if an error occurs, the error doesn't mysteriously disappear getting replaced either with a default
value or some
value that you changed on the model on your own. Makes sense.
Still it is a little non-obvious because the way you create the UI elements with MVC, it certainly looks like your are binding to the model value:@Html.TextBoxFor( mod=> mod.User.Email, new {type="email",@class="inputfield",required="required" })
and so unless one understands a little bit about how the model binder works this is easy to trip up. At least it was for me. Even though I'm telling the control which model
value to bind to, that model
value is only used initially on GET operations. After that ModelState/POST values provide the display
value.
Workarounds
The default behavior should be fine for 99%
of binding scenarios. But if you do need fix up values based on your model rather than the default POST values, there are a number
of ways that you can work around this.
Initially when I ran into this, I couldn't figure out how to set the
value using code and so the simplest solution to me was simply to not use the MVC Html Helper for the specific control and explicitly bind the model via HTML markup and @Razor expression:
<input type="text" name="User.Email" id="User_Email" value="@Model.User.Email" />
And this produces the right result. This is easy enough to create, but feels a little out
of place when using the @Html helpers for everything else. As you can see by the difference in the name and id values, you also are forced to remember the naming conventions that MVC imposes in order for ModelBinding to work properly which is a pain to remember and set manually (name is the same as the property with . syntax, id replaces dots with underlines).
Use the ModelState
Some
of my original confusion came because I didn't understand how the model binder works. The model binder basically maintains ModelState on a postback, which holds a
value and binding errors for each
of the Post back
value submitted on the page that can be mapped to the model. In other words there's one ModelState entry for each bound property
of the model. Each ModelState entry contains a
value property that holds AttemptedValue and RawValue properties. The AttemptedValue is essentially the POST
value retrieved from the form. The RawValue is the
value that the model holds.
When MVC binds controls like @Html.TextBoxFor() or @Html.TextBox(), it always binds values on a GET operation. On a POST operation however, it'll always used the AttemptedValue to display the control. MVC binds using the ModelState on a POST operation, not the model's
value.
So, if you want the behavior that I was expecting originally you can actually get it by clearing the ModelState in the controller code:ModelState.Clear();
This clears out all the captured ModelState values, and effectively binds to the model. Note this will produce very similar results - in fact if there are no binding errors you see exactly the same behavior as if binding from ModelState, because the model has been updated from the ModelState already and binding to the updated values most likely produces the same values you would get with POST back values.
The big difference though is that any values that couldn't bind - like say putting a string into a numeric field - will now not display back the
value the user typed, but the default field
value or whatever you changed the model
value to.
This is the behavior I was actually expecting previously. But - clearing out all values might be a bit heavy handed. You might want to fix up one or two values in a model but rarely would you want the entire model to update from the model.
So, you can also clear out individual values on an as needed basis:if (userBus.DoesEmailExist(user.Email))
{
userBus.ValidationErrors.Add("New email address exists already. Please…");
user.Email = oldEmail;
ModelState.Remove("User.Email");
}
This allows you to remove a single
value from the ModelState and effectively allows you to replace that
value for display from the model.
Why?
While researching this I came across a post from Microsoft's Brad Wilson who describes the default binding behavior best in a forum post:
The reason we use the posted
value for editors rather than the model
value is that the model may not be able to contain the
value that the user typed. Imagine in your "int" editor the user had typed "dog". You want to display an error message which says "dog is not valid", and leave "dog" in the editor field. However, your model is an int: there's no way it can store "dog". So we keep the old
value.
If you don't want the old values in the editor, clear out the Model State. That's where the old
value is stored and pulled from the HTML helpers.
There you have it. It's not the most intuitive behavior, but in hindsight this behavior does make some sense even if at first glance it looks like you should be able to update values from the model. The solution
of clearing ModelState works and is a reasonable one but you have to know about some
of the innards
of ModelState and how it actually works to figure that out.© Rick Strahl, West Wind Technologies, 2005-2012Posted in ASP.NET MVC
Tweet
!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");
(function() {
var po = document.createElement('script'); po.type = 'text/javascript'; po.async = true;
po.src = 'https://apis.google.com/js/plusone.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(po, s);
})();