tags:

views:

580

answers:

1

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

+1  A: 

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.

çağdaş
But I want to get the default binding. I want it because I want to make use of ModelState. I just want to update the ModelState to reflect changes in my model object.
Please see my edit.
çağdaş
Your comment is exactly what I'm doing, but you do it by yourself and I'm using te deault binder so I have something bit more "generic". The change of value ("a") happens in a lower tier so I don't actualy know what props have changed. And also you wouldn't want to ModelState["m.Value"].Value = new ValueProviderResult("a", m.Name, CultureInfo.CurrentCulture);for every single object's property, do you :).
:) You're right. And I just took a look at ModelState object again and it appears there's no easy way to do this. I tried ModelState["m.Value"].Value.AttemptedValue = "a"; but it turned out that property is readonly.
çağdaş