Introduction
I want to be short as much as I can by stating the problem, possible solutions and cleanest solution that I am sharing with you here, so I will be descriptive and straight forward to the point as much as I can.
Problem
Simply what I wanted is to submit a JSON object from one page to another as a POST and be redirected to that page, and I wanted that to happen by having the JSON object as a parameter for my controller post action. Please note that we are doing this from one page to another (from one controller action to another controller action).
Below is my destination page or controller action:
[HttpPost]
public ActionResult SubmitOrder(OrderViewModel vmOrder)
{
return View(vmOrder);
}
Now imagine that my source page or controller action has the following JavaScript snippet of which we want to submit the vmOrder
to the destination.
function CollectOrderDetails(){
var order = new Object();
(e.g. ../MyApplication/SubmitOrder)
}
Possible Solutions
Now to do a submit here (POST
), I have two options:
- Use Ajax: Simply I can just use
jquery.post
or jquery.ajax
(with the verb POST
of course) and content-type "application/json
", and here the serialized order object will be sent to the destination controller action and ASP.NET MVC will automatically de-serialize the json object into OrderViewModel
.
The problem here is that it's AJAX !! which means the response will be back in the xhr
object of the Ajax call, and therefore no redirect has occurred.
So we managed to send the object, serialize it with no effort but we couldn't get a response that represents the new page with a redirect.
- Use ajax and inject the response into the document: It's the same as the previous one, except that when the response gets back from the
xhr
object (which in this case will be a full HTML response that represents the destination page), we inject it in the document.
Of course this option is bad, why? Because first of all, it will return the HTML and JavaScript yes but it will not be parsed, and actually we are still on the source page (URL will remain on the source) and we displayed the destination page only.
This option is best fit when you want to deal with partial views, or when you deal with pages that return static content with no script
tags included to be parsed.
- Use a dynamic form submission: This method is known for us, and it's the best of all, you create a form element in JavaScript, and create a hidden HTML
input
element, serialize the order object and store it in the hidden field, append the hidden input element to the form and submit it.
var form = document.createElement("form");
<pre>var input = document.createElement("input");
input.setAttribute("type", "hidden") ;
input.setAttribute("name", "XOrder") ;
input.setAttribute("value", JSON.stringify(order));
form.appendChild(input);
document.body.appendChild(form);
form.submit();
Now here what we did is that we serialized the object into JSON and sent it to the destination action controller BUT the action controller parameter (OrderViewModel vmOrder
) will be NULL
, why? Simply because MVC received the HTTP request as an html/text request not as JSON, remember that when we used Ajax we specified that the content type is application/json and that helped us to send the data (payload) to the destination and MVC was able to recognize the payload from the content-type as JSON and therefore de-serialize it to OrderViewModel
. The case here is different, this is a normal form submission and you can read data in ASP.NET (server side) by iterating over this.Request.Form
, which will hold all inputs (hidden or non-hidden of course). Remember too that using form submission, you can't specify JSON as the content type as it's not supported by the browsers.
Best Solution
Ok .. Now what I wanted is: NOT AJAX, normal form submission (POST of course) and I wanted the parameter of the controller action to automatically deserialize to my view model parameter (OrderViewModel
).
To do this, I knew I had to understand more of Model Binders in ASP.NET MVC, model binders simply translate HTTP requests coming to MVC controllers into objects and terms the controller can understand and deal with easily, and in many times it uses conventions to accomplish that.
So what I wanted is a custom binder that will receive my serialized JSON object from the hidden field and automatically convert it to my parameter in the controller action without worrying about that nasty workload.
Below is the code:
public class JsonModelBinder : DefaultModelBinder
{
public override BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
try
{
var strJson = controllerContext.HttpContext.Request.Form[bindingContext.ModelName];
if(string.IsNullOrEmpty(strJson))
{
return null;
}
else
{
JavaScriptSerializer serializer = new JavasScriptSerializer();
var model = serializer.Deserialize(strJson, bindingContex.ModelType);
var modelMetaData = ModelMetadataProviders.Current
.GetMetadataForType(()=>model, bindingContext.ModelType);
var validator= ModelValidator
.GetModelValidator(modelMetaData, controllerContext);
var validationResult = validator.Validate(null);
foreach(var item in validationResult)
{
bindingContext.ModelState
.AddModelError(itrem.MemberName, item.Message);
}
return model;
}
}
catch(Exception ex)
{
bindingContext.ModelState.AddModelError(bindingContext.ModelType.Name, ex.Message);
}
}
Now the above is a model binder that inherits the default model binder of MVC, without going into details we need to use that in the correct place in order to get the JSON object as a parameter for the controller action.
MVC already has a support for that in several ways and I think the cleanest one without changing the default behaviour you have in your application is through .NET Attributes. MVC already has a class CustomModelBinderAttribute
which we can inherit from and use our custom model binders.
public class JsonBinderAttribute : CustomModelBinderAttribute
{
public overried IModelBinder GetBinder()
{
return new JsonModelBinder();
}
}
And now what you will be doing is two things:
First: Add JsonBinder
attribute to the parameter on the destination controller action:
[HttpPost]
public ActionResult SubmitOrder([JsonBinder]OrderViewModel vmOrder)
{
return View(vmOrder);
}
Second: When you send the data from JavaScript, set the name of the hidden field as the name of the parameter:
var form = document.createElement("form");
var input = document.createElement("input");
input.setAttribute("type", "hidden") ;
input.setAttribute("name", "vmOrder") ;
input.setAttribute("value", JSON.stringify(order));
form.appendChild(input);
document.body.appendChild(form);
form.submit();
Points of Interest
Why to set the name of the hidden field as the name of the parameter? Because we want to have some kind of convention based mapping in between the sender and the receiver, name of the parameter is the most obvious clear choice.
Summary
Hopefully that was helpful, I believe this is the second time I want something almost similar in concept of posting data and redirecting in the same time, and the first time I posted an article about 4 years ago .. here is the link to it: http://www.codeproject.com/Articles/37539/Redirect-and-POST-in-ASP-NET.