IModelBinder and property key for successful binding at the ModelState + partial form validation

28 באוקטובר 2010

I’m talking about asp.net MVC2

The point:

if you are implementing an IModelBinder for a type, you should add a call to
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueFromRequest);

this is done to make sure that at your controller action you can access ModelState[nameOfThisProperty] and nameOfThisProperty  will be present as a key at ModelState also for successful binding.

 

And for the details: (  Partial form validation including custom modelBinding  for trimming strings)

why am I using ModelState[nameOfThisProperty] ??

usually you don’t need to access ModelState[nameOfThisProperty]  like this, it is ugly and stringly typed Smile

but lets say you have a form  that has two options: “save”,”save draft”

“save” should  trigger the  entire  form’s validation.

”save draft”  should trigger  validation only on a filed named Title.

 

I’m using the DataAnnotations  validation  attributes and no chance in hell that I’m going to create two  separate objects to enforce this validation rules.

so I have two separate action methods and at SaveDraft I have this code:

public ActionResult SaveDraft(ArticleModel articleModel ) 

     if (ModelState[ArticleModel .ARTICLE_TITLE_FIELD_KEY].Errors.Count > 0) 
     { 
         ModelStateDictionary mDic = new ModelStateDictionary(); 
         ModelState titleModelState = ModelState[ArticleModel .ARTICLE_TITLE_FIELD_KEY]; 
         ModelState.Clear();

         //ugly way to preserve this error message if it exists 
         ModelState.Add(ArticleModel .ARTICLE_TITLE_FIELD_KEY, titleModelState); 
         return View(“New", articleModel ); 
      }

      //party with  articleModel  ignoring all other validation errors

      ..

At the client side  I have this function(I use jQuery and jQuery validation):

function BindSaveDraftButton()
{
    var jqBtnSave = $("#btnSaveDraft");
    jqBtnSave.click(function ()
    {
        var jqTxbTitle = $(".txbTitle:first");
        if (!jqTxbTitle.valid()) //trigger only this field’s validation manually
        {
            jqTxbTitle.focus();
            return false;
        }

        //  the button holds the saveDraft action at a specific actionUrl attribute

        // (I know I know I shuld change this to  data-actionUrl
        $("#formPostArticle").attr("action", $(this).attr("actionUrl"));

        //NOTE: I don’t return anything here so the form will submit as a regular form, just to a different action
    });
}

oh.. and the button for  “save draft” has the class “cancel” so it won’t trigger client validations

 

And the connection to custom ModelBinding??

Everything was working great(some bits a it ugly, but this is the case for now) till I decided to implement a TrimmedStringModelBinder so all bound strings will be trimmed during binding.

public class TrimmedStringModelBinder : IModelBinder

{

    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
        {
            throw new ArgumentNullException("bindingContext");
        }
       ValueProviderResult valueFromRequest =

                                                     bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
       bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueFromRequest);  

       if (valueFromRequest == null)
       {
            return null;
       }
       if (!string.IsNullOrEmpty(valueFromRequest.AttemptedValue))
       {
            return valueFromRequest.AttemptedValue.Trim();
       }
       return valueFromRequest.AttemptedValue;

   }

}

registered it as follows at the Global.asax

ModelBinders.Binders.Add(typeof(string), new TrimmedStringModelBinder());

 

Note the row with yellow BG – it was missing  at the first implementation  because I had no idea I need it.

without it the check for  –

ModelState[ArticleModel .ARTICLE_TITLE_FIELD_KEY].Errors.Count

threw  an exception since this property was valid(had value in it) so during the server validation this key wasn’t entered into the ModelState.

when I removed the registration to the TrimmedStringModelBinder  I saw that even if the property is valid – a key would be added the ModelState stating there are no errors here.

 

Looked a bit at Stackoverflow but didn’t find anything useful..

The  solution can  always be found at the source:

So opened the  mvc2-rtm-sources\src\SystemWebMvc\Mvc\DefaultModelBinder.cs

and the solution is right there at the  BindModel method :

            // Simple model = int, string, etc.; determined by calling TypeConverter.CanConvertFrom(typeof(string))
            // or by seeing if a value in the request exactly matches the name of the model we're binding.
            // Complex type = everything else.
            if (!performedFallback) {
                ValueProviderResult vpResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
                if (vpResult != null) {
                   return BindSimpleModel(controllerContext, bindingContext, vpResult);
                }
            }.

went directly to the Simple model condition since this is what I’m binding(string remember?)

that led to the  BindSimpleModel method, where the first line is:

bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);

 

So here it is:

you must add all all the keys to the ModelState , during the binding, the validation only updates faulty ones.

(it is so awesome that we have access to the MVC code,  it is not a huge codebase it is self explanatory, and have some comments where needed) .

 

This post has become too long, and I have more  things to elaborate on the TrimmedStringModelBinder  – so will do it at another  post later.

 

till then  – happy coding

הוסף תגובה
facebook linkedin twitter email

כתיבת תגובה

האימייל לא יוצג באתר. שדות החובה מסומנים *

2 תגובות

  1. vgirard@spektrummedia.com14 בדצמבר 2010 ב 0:11

    When you say "oh.. and the button for “save draft” has the class “cancel” so it won’t trigger client validations" what do you mean?

    הגב
  2. Avi Pinto14 בדצמבר 2010 ב 6:09

    Adding the "cancel" class will disable the jQuery validation of the form on submit(it is a feature of jQuery validation).

    This is the behavior i need for partial validation,
    and i manually validate the fields that need validation at the BindSaveDraftButton JS function, returning false if not valid to stop the form from submitting.

    הגב