The problem: How to update ModelState in posting+validation scenario.
I've got a simple form:
<%= Html.ValidationSummary() %>
<% using(Html.BeginForm())%>
<%{ %>
<%=Html.TextBox("m.Value") %>
<input type="submit" />
<%} %>
When user submits I want to validate input and in some circumstances I want to fix the error for user, letting him know that he made an error that is already fixed:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Index(M m)
{
if (m.Value != "a")
{
ModelState.AddModelError("m.Value", "should be \"a\"");
m.Value = "a";
return View(m);
}
return View("About");
}
Well the problem is, MVC will simply ignore the model passed to the view and will re-render whatever the user typed -- and not my value ("a"). This happens, because the TextBox renderer checkes if there is a ModelState and if it's not null - ModelState's value is used. That value is of course the one user typed before posting.
Since I can't change the behaviour of TextBox renderer the only solution I found would be to update the ModelState by myself. The quick'n'dirty way is to (ab)use the DefaultModelBinder and override the method that assigns the values from forms to model by simply changing the assignment direction ;). Using DefaultModelBinder I don't have to parse the ids. The following code (based on original implementation of DefaultModelBinder) is my solution to this:
/// <summary>
/// Updates ModelState using values from <paramref name="order"/>
/// </summary>
/// <param name="order">Source</param>
/// <param name="prefix">Prefix used by Binder. Argument name in Action (if not explicitly specified).</param>
protected void UpdateModelState(object model, string prefix)
{
new ReversedBinder().BindModel(this.ControllerContext,
new ModelBindingContext()
{
Model = model,
ModelName = prefix,
ModelState = ModelState,
ModelType = model.GetType(),
ValueProvider = ValueProvider
});
}
private class ReversedBinder : DefaultModelBinder
{
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
{
string prefix = CreateSubPropertyName(bindingContext.ModelName, propertyDescriptor.Name);
object val = typeof(Controller)
.Assembly.GetType("System.Web.Mvc.DictionaryHelpers")
.GetMethod("DoesAnyKeyHavePrefix")
.MakeGenericMethod(typeof(ValueProviderResult))
.Invoke(null, new object[] { bindingContext.ValueProvider, prefix });
bool res = (bool)val;
if (res)
{
IModelBinder binder = new ReversedBinder();//this.Binders.GetBinder(propertyDescriptor.PropertyType);
object obj2 = propertyDescriptor.GetValue(bindingContext.Model);
ModelBindingContext context2 = new ModelBindingContext();
context2.Model = obj2;
context2.ModelName = prefix;
context2.ModelState = bindingContext.ModelState;
context2.ModelType = propertyDescriptor.PropertyType;
context2.ValueProvider = bindingContext.ValueProvider;
ModelBindingContext context = context2;
object obj3 = binder.BindModel(controllerContext, context);
if (bindingContext.ModelState.Keys.Contains<string>(prefix))
{
var prefixKey = bindingContext.ModelState.Keys.First<string>(x => x == prefix);
bindingContext.ModelState[prefixKey].Value
= new ValueProviderResult(obj2, obj2.ToString(),
bindingContext.ModelState[prefixKey].Value.Culture);
}
}
}
}
So the question remains: am I doing something extremely uncommon or am I missing something? If the former, then how could I implement such functionality in a better way (using existing MVC infrastructure)?
AddModelError("Region", "Region is mandatory"); ModelState. IsValid will then return false.
ModelState. IsValid indicates if it was possible to bind the incoming values from the request to the model correctly and whether any explicitly specified validation rules were broken during the model binding process.
IsValid is false now. That's because an error exists; ModelState. IsValid is false if any of the properties submitted have any error messages attached to them. What all of this means is that by setting up the validation in this manner, we allow MVC to just work the way it was designed.
I know this post is fairly old but it's a problem I've had before and I just thought of a simple solution that I like - just clear the ModelState after you've got the posted values.
UpdateModel(viewModel);
ModelState.Clear();
viewModel.SomeProperty = "a new value";
return View(viewModel);
and the view has to use the (possibly modified) view model object rather than the ModelState.
Maybe this is really obvious. It seems so in hindsight!
You could accept a form collection as a parameter instead of your model object in your controller, like this : public ActionResult Index(FormCollection Form)
.
Therefor default model binder will not update the model state, and you'll get the behaviour you want.
Edit : Or you can just update the ModelStateDictionary to reflect your changes to the model.
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Index(M m)
{
if (m.Value != "a")
{
ModelState["m.Value"].Value = new ValueProviderResult("a", m.Name,
CultureInfo.CurrentCulture);
ModelState.AddModelError("m.Value", "should be \"a\"");
m.Value = "a";
return View(m);
}
return View("About");
}
Note : I'm not sure if this is the best way. But it seems to work and it should be the behaviour you want.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With