Friday, July 20, 2012

Fun with Asp.net MVC 3 Custom Json Model Binder


If you have been using Asp.net MVC for quite a while then your are probably familiar with the concept of model binding and more specifically Json Model Binding. Starting from MVC3, Asp.net MVC is elegantly capable of binding json string passed from client into a full blown model. Asp.net MVC provides a new JsonValueProviderFactory that is capable of supplying model values from the json string. To be able to use this feature your client code(javascript) needs to change the content header to application/json and that’s it, your model parameter in the action is automatically set with required/passed values.
Before digging more into Custom Json Model Binder I’d like to show you how this so called JsonValueProviderFactory works. (I hope nobody would sue me for pasting this code here.)

private static object GetDeserializedObject(ControllerContext controllerContext) {
           if (!controllerContext.HttpContext.Request.ContentType.StartsWith(
"application/json", StringComparison.OrdinalIgnoreCase)) {
               // not JSON request
               return null;
           }

           
StreamReader reader = new StreamReader(controllerContext.HttpContext.Request.InputStream);
           string bodyText = reader.ReadToEnd();
           if (
String.IsNullOrEmpty(bodyText)) {
               // no JSON data
               return null;
           }

           JavaScriptSerializer serializer = new JavaScriptSerializer();
           object jsonData = serializer.DeserializeObject(bodyText);
           return jsonData;
       }


The code is quite neat and clean and it’s clear that you don’t need any magic or complex code to handle the json string. Like I said earlier to use the JsonValueProviderFactory your request header should have content-type specified as application/json and to add to it the whole request stream should be a json string. JsonValueProviderFactory then reads this whole input stream and deserializes this string into an object.


Let me take a moment to state that my intention here is not to question the existing Json binding capability of Asp.net MVC or JsonValueProviderFactory. They are self-sufficient and works pretty well in many scenarios. The purpose of my post here is to explain how to create a Custom Model Binder, how to plug them into MVC framework, overcome a scenario when your whole request stream may not be the json string and show how extensible Asp.net MVC is and how easy it really is.  Allow me to further clarify this topic.


Asp.net MVC has pretty neat request pipeline, starting from the UrlRouteModule that intercepts every request, finds the most suitable route, hand over the request to the MvcHandler in the particular route, request the controller from the controller factory, hand over the request then to the controller, load tempdata,  find the action name, hand over the execution of action to action invoker, Action Invoker then finds the action method, detects the action filters applicable to the action method, run authorization filter if any, validate request for XSS attack, build parameter values by finding the appropriate model binder and value providers(model binders request value from value provider), model binder in the course performs validation, execute the action method by supplying the required parameter values then boooom, your action method gets called. There’s a whole lot of thing going on between the browser’s request and your controller’s action method. But the architecture is fairly simple and far more extensible that Asp.net webform. At any point in the request pipeline you can plug in your own implementation from your own MvcHandler to your own value provider. If you want more details on this I suggest you do some googling or binging and of course check out the Asp.net MVC source code itself. It’s open source. It's one of the reason why I love it.

In this post I’m just going to focus myself and you in plugging in our custom model binder. Please note that you probably won’t need to create your own model binder unless you have your own custom model binding functionality to be implemented for specific scenario or type. Asp.net MVC 3 already has 3 custom model binders for System.Web.HttpPostedFileBase, Byte array and System.Data.Linq.Binary and a default model binder which is capable of binding a single value type parameter to any complex type. In our case we do have a specific requrement, that is, we do not want to send the whole request as json string and we may not always send the content type as json, which is what Asp.net MVC 3 doesn’t want to handle. But it’s cool, coz’ it’s not that hard anyway.

Any model binder that you create needs to implement IModelBinder and implement BindModel() function.

public interface IModelBinder {
       object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext);
   }


If your custom model binder is unable to bind the model, then simply return null, MVC will do the rest.

Now lets create a tiny sample MVC project to see how and where can we plug in this custom model binder so that MVC framework can call this binder to build our model class and more over, so that you can understand why did we create this model binder in first place when MVC 3 already has a JsonValueProviderFactory which does pretty much what we did and a powerful default model binder.

I do not want to go into detail how to creating MVC project. This is something I assume that you already know of, otherwise there’s no point in learning how to create a custom model binder.

But before doing our thing let me demonstrate how we can leverage the exising JsonValueProviderFactory to bind our models automatically, let’s create a GuestBookController

public class GuestBookController : Controller
   {
       //
       // GET: /GuestBook/

       public
ActionResult Index()
       {
           return View();
       }


       public
JsonResult Save(string key,GuestBook guest)
       {
           if (guest==null )
           {
               return Json(false);
           }

           return Json(true);

       }

   }


The application we are going to create is quite simple. We have a simple Guest Book data entry form with three fields-GuestName, GuestAddress, GuestAge. When a user clicks on Save Button we will post the data to Save Action using jquery ajax. The Save Action method takes two parameters- “key” of type string(which doesn’t have any significance, but is here to demonstrate how multiple parameters in action can be supplied as json string) and our model “guest” of type GuestBook. The Save Action simply checks to see if guest is null or not and returns json value true or false accordingly.

Now lets see how does our GuestBook model looks like.

public class GuestBook
   {
       public string GuestName { get; set; }
       public string GuestAddress { get; set; }
       public int GuestAge { get; set; }

   }


Now let’s create our Data entry view(index.cshtml)

@{
   ViewBag.Title =
"Index";
}

<br/>
@using(Html.BeginForm(
"Save", "GuestBook", FormMethod.Post, new { @class = "form-horizontal" })){

 <fieldset>
   <legend>Add Guest Entry</legend>
     <div class="control-group">
         <label class="control-label" for="guestName">Guest Name</label>
         <div class="controls">
             <input type="text" class="input-xlarge" id="guestName">
             
         </div>
     </div>
     <div class="control-group">
         <label class="control-label" for="guestAddress">Address</label>
         <div class="controls">
             <input type="text" class="input-xlarge" id="guestAddress">
             
         </div>
     </div>
     <div class="control-group">
         <label class="control-label" for="guestAge">Age</label>
         <div class="controls">
             <input type="text" class="input-xlarge" id="guestAge">
             
         </div>
     </div>
     <div class="form-actions">
           <button type="submit" id="save" class="btn btn-primary">Save changes</button>
           <button class="btn" id="cancel">Cancel</button>
         </div>
 </fieldset>


}


<script type="text/javascript">

   $(document).ready(function () {
       $("#save").click(function (e) {
           var act = {};
           var guestBook = {};
           guestBook.GuestName = $('#guestName').val();
           guestBook.GuestAddress = $('#guestAddress').val();
           guestBook.GuestAge = $('#guestAge').val();
           act.guest = guestBook;
           act.key= "my key" ;

           var guestBookString = JSON.stringify(act);

           $.ajax({
               url: '@Url.Action(
"Save")',
               dataType: 'json',
               contentType: 'application/json',
               type: 'POST',
               data: guestBookString,
               success: function (result) {
                   alert(result);
               },
               error: function (xhr) {
                   alert(xhr.responseText);
               }
           });

           return false;
       });

       $('#cancel').click(function (e) {
           return false;
       })
   });
</script>


Which looks something like this.


Please note that I’ve used Twitter bootstrap to customize the look. It’s a cool project with a lot of goodness in it. You can download it yourself from here. If you don’t want to use it then it’s ok, just have three input text fields, three labels, and two buttons. I’ve also used jquery 1.7.2 for ajax call.

The code is pretty straight forward. When the save button is clicked you build a guest object with properties similar to your c# GuestBook model. Then create an object named act with guest and key property, which are the parameter names in your Save action. The code then serializes the act object as string and ajax calls the Save action method, by supplying necessary parameters to $.ajax() function. There are two things you should notice. First is the data option which is the json string and second is the contentType, which is set as “application/json”. You are required to set the contentType to “application/json” so that JsonValueProviderFactory to come into play(Please check the source code that I’ve posted at the top of this post). This is all you need to leverage the existing capability of Asp.net MVC 3 to bind the json string. Now run the application, input the desired values and click on save button. Before that don’t forget to place breakpoint somewhere in Save action method to check if the guest parameter of type Guest has been properly bound with proper values. And I’m pretty sure it does, if everything is in place.

This is how you can bind json string in Asp.net MVC 3. Now let’s dig into our problem. Let me rephrase it again, as JsonValueProvider factory expects we are not going to change the contentType to “application/json” and our whole request stream(data) won’t be json string. Only the guest parameter will be supplied as json string. Now keeping everything as they are, let’s change your javascript function as.

<script type="text/javascript">

   $(document).ready(function () {
       $("#save").click(function (e) {
           //var act = {};
           var guestBook = {};
           guestBook.GuestName = $('#guestName').val();
           guestBook.GuestAddress = $('#guestAddress').val();
           guestBook.GuestAge = $('#guestAge').val();
           //act.guest = guestBook;
           //act.key= "my key" ;

           var guestBookString = JSON.stringify(guestBook);

           $.ajax({
               url: '@Url.Action(
"Save")',
               dataType: 'json',
               //contentType: 'application/json',
               type: 'POST',
               data: {key:"my key",guest:guestBookString},
               success: function (result) {
                   alert(result);
               },
               error: function (xhr) {
                   alert(xhr.responseText);
               }
           });

           return false;
       });

       $('#cancel').click(function (e) {
           return false;
       })
   });
</script>


Here I’ve commented the contentType parameter in $.ajax call and now the data parameter is not the json string anymore. It’s the key value pair of “key” and “guest” as params(which is similar to the parameter names in our Save action method) and their corresponding values. Now start debugging your application and check to see if the guest parameter in the action has been bound with proper values. You will probably notice that the guest parameter will be null. Shame on us. We just acted like a 4 year old stubborn child and didn’t do what somebody else expected us to do. Now if we want things to work properly then let’s add few more things.

Now let’s add our custom json binder into our project. The JsonBinder looks something like this.

public class JsonBinder : IModelBinder
   {
       public object BindModel(
ControllerContext controllerContext, ModelBindingContext bindingContext)
       {
           //return base.BindModel(controllerContext, bindingContext);
           if (controllerContext == null)
           {
               throw new
ArgumentNullException("controllerContext");
           }

           if (bindingContext == null)
           {
               throw new
ArgumentNullException("bindingContext");
           }

           var prefix = bindingContext.ModelName;
           string jsonString = controllerContext.RequestContext.HttpContext.Request.Params[prefix];
           if (jsonString != null)
           {
               var serializer = new
JavaScriptSerializer();
               var result = serializer.Deserialize(jsonString, bindingContext.ModelType);
               return result;

           }
           else
           {
               return null;
           }


       }
   }



The code is self explanatory and more or less similar to what JsonValueProviderFactory does- get the prefix or parameter name, try to find the request value for that prefix and deserialize the string into an object using JavaScriptSerializer. That’s all you need to do. For complex model binding logic you can use the controllerContext and bindingContext parameters to find out more on the request parameters and model metadata.

Adding JsonBinder class is not just enough. MVC let’s you add a model binder for a specific type. This is usually done in Global.asax.cs file. Let’s register our custom model binder JsonBinder for GuestBook type. My Application_Start() in Global.asax.cs file looks something like this.

protected void Application_Start()
       {
           
AreaRegistration.RegisterAllAreas();

           RegisterGlobalFilters(
GlobalFilters.Filters);
           RegisterRoutes(
RouteTable.Routes);
           
ModelBinders.Binders.Add(typeof(GuestBook),new JsonBinder()); //register
           
       }


You just noticed that I’ve added a new JsonBinder for type GuestBook into Binders collection. When time comes for model binders to bind the parameter in action, MVC checks to see if there’s any custom binder that has been registered for the type of the parameter it’s trying to bind. And in our case we did for GuestBook. Now let’s rebuild and start debugging our application and see if the guest parameter in the Save action has proper values or not. The guest parameter should be properly bound with values supplied from the browser. If not you probably missed something somewhere. Double check your javascript, and Global.asax.cs to see you did exactly what I explained above.
You just learnt one approach of binding your model using custom model binder. But the problem with this approach is that whenever MVC finds any action with a parameter of GuestBook type then it always calls and expects  JsonBinder to bind the model. Let’s assume you are not doing ajax call and simply letting the form to submit. In this case the values being submitted by the browsers are not json string but rather key value pair, which in our case is better handled by DefaultModelBinder. JsonBinder won’t be able to handle this as the request stream is no more a json string. There are few work arounds for this problem. One way is, you can add a constructor in JsonBinder to take the parameter name to bind to and try to deserialize only when the current prefix to bind is equal to the key name with which the binder was registered for a specific type. And if the key and prefix doesn’t match then that means the binding is better handled by DefaultModelBinder. So create an instance of DefaultModelBinder and call the BindModel function by supplying the required parameters and return the returned value.

So instead of registering our model binder globally for specific type let’s do it in a different way. Asp.net MVC 3 also checks to see if the parameter in our Action method has been decorated with an attribute derived from CustomModelBinderAttribute . CustomModelBinderAttribute is an Abstract attribute class which looks something like this.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Enum | AttributeTargets.Interface | AttributeTargets.Parameter | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
   public abstract class
CustomModelBinderAttribute : Attribute
   {
       public abstract IModelBinder GetBinder();
   }



If Mvc finds that the action parameter is decorated with any attribute derived from CustomModelBinderAttribute then it calls the GetBinder function of the attribute, which is supposed to return a custom binder type which implements IModelBinder. Mvc then calls the BindModel function of the binder returned by the Custom Model Binder Attribute.

Rather than trying to explain this theoretically, let’s dive into the code. Now let’s create our JsonModelBinderAttribute which is derived from CustomModelBinderAttribute and return our previous JsonBinder. Our JsonModelBinderAttribute looks something like this.

public class JsonModelBinderAttribute : CustomModelBinderAttribute
   {
       public override
IModelBinder GetBinder()
       {
           return new
JsonBinder();
       }
   }



Now let’s remove the code from Application_Start() function in Global.asax.cs file that we added earlier, i.e

ModelBinders.Binders.Add(typeof(GuestBook),new JsonBinder());
We don’t need this any more.
Now let’s change your action method Save and decorate the guest parameter with JsonModelBinder attribute. Your Save Action method must look something like this.

public JsonResult Save(string key,[JsonModelBinder]GuestBook guest)
       {
           if (guest==null )
           {
               return Json(false);
           }

           return Json(true);

       }



And that’s it. Compile your application. Run in debug mode and check to see if the guest parameter is properly built with proper values. If things are right then your guest object should have all the properties set with proper values sent from browser


Phewwww!!!!!!!!!!!..... I hope that’s enough for this post and really really hope that I got everything right and everything on your side worked perfectly as expected. Please forgive me for not going in depth on everything, which doesn’t seem possible in one blog post.

Please find the final source code here.


No comments:

Post a Comment