Search code examples
asp.net-mvcpostknockout.jsobject-serialization

How to do a form POST to an MVC 3 application and obtain the deserialized class?


I have the following form:

<form id="MakeDocumentForm" name="MakeDocumentForm" 
      action="Document/GetWordDocument" method="post" 
      enctype="application/json">

    <button type="submit" style="float:right;">Make Word Document</button> 
    <textarea id="hiddenJson" 
              name="hiddenJson" 
              data-bind="text: ko.toJSON(viewModel.selectedDocument)" 
              rows="5" cols="100" 
              style="visibility:hidden;" >
    </textarea>

</form>

The data-bind attribute is knockoutjs - but this isn't important, the textarea correctly contains the JSON that is the serialized object.

[HttpPost]
public void GetWordDocument(DocumentModel hiddenJson)
{
   //hiddenJson is not a correctly populated instance of my DocumentModel class
   //any MVC experts know what I am doing wrong?
}

Now, how do I do a form POST to an MVC 3 application and obtain the deserialized class?


Solution

  • If you are posting it via AJAX with the content type set to JSON, then MVC 3 will be able to bind it properly in your controller action.

    $.ajax({
        url: location.href, 
        type: "POST",
        data: ko.toJSON(viewModel),
        datatype: "json",
        contentType: "application/json charset=utf-8",
        success: function (data) { alert("success"); }, 
        error: function (data) { alert("error"); }
    });
    

    However, if like in your example, you want to do a normal form post that includes JSON, then you need to do some more work as MVC3 won't automatically bind it to your model, as the content type will be application/x-www-form-urlencoded.

    Steve Sanderson has an older sample that demonstrates getting submitted JSON data to be bound properly in your controller action here: http://blog.stevensanderson.com/2010/07/12/editing-a-variable-length-list-knockout-style/

    The gist of it is that he creates an attribute called "FromJson" that looks like:

    public class FromJsonAttribute : CustomModelBinderAttribute
    {
        private readonly static JavaScriptSerializer serializer = new JavaScriptSerializer();
    
        public override IModelBinder GetBinder()
        {
            return new JsonModelBinder();
        }
    
        private class JsonModelBinder : IModelBinder
        {
            public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
            {
                var stringified = controllerContext.HttpContext.Request[bindingContext.ModelName];
                if (string.IsNullOrEmpty(stringified))
                    return null;
                return serializer.Deserialize(stringified, bindingContext.ModelType);
            }
        }
    }
    

    Then, the action looks like:

        [HttpPost]
        public ActionResult Index([FromJson] IEnumerable<GiftModel> gifts)
    

    Also, if you don't like having to use the attribute, then you can actually register a type to always use a certain model binder.

    You could create a model binder that looks like:

    public class JsonModelBinder: IModelBinder
    {
        private readonly static JavaScriptSerializer serializer = new JavaScriptSerializer();
    
        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            var stringified = controllerContext.HttpContext.Request[bindingContext.ModelName];
            if (string.IsNullOrEmpty(stringified))
                return null;
            return serializer.Deserialize(stringified, bindingContext.ModelType);
        }
    }
    

    Then, register it in global.asax.cs like:

    ModelBinders.Binders.Add(typeof(DocumentModel), new JsonModelBinder());
    

    Now, you would not need to use an attribute and your DocumentModel would be bound properly. This would mean that you would always be sending the DocumentModel via JSON though.