Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Updating ModelState with model object

Tags:

asp.net-mvc

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)?

like image 729
user87338 Avatar asked Apr 05 '09 18:04

user87338


People also ask

How do you make a ModelState IsValid false?

AddModelError("Region", "Region is mandatory"); ModelState. IsValid will then return false.

What does ModelState IsValid mean?

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.

Why ModelState IsValid is false in MVC?

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.


2 Answers

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!

like image 158
Kieran Avatar answered Sep 27 '22 21:09

Kieran


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.

like image 36
Çağdaş Tekin Avatar answered Sep 27 '22 20:09

Çağdaş Tekin