Redirect and Post JSON object in ASP.NET MVC
2014-05-21 09:14 朱峰(Peter.zhu) 阅读(751) 评论(0) 编辑 收藏 举报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) { //Process the vmOrder if wanted return View(vmOrder); }
Now imagine that my source page or contoller action has the following javascript snippet of which we want to submit the vmOrder to the destination.
function CollectOrderDetails(){ var order = new Object(); //Fill order data //Submit the order object to destination page after serializing it (e.g. ../MyApplication/SubmitOrder) //====>JSON.stringify(order) }
Possible Solutions
Now to do a submit here (POST) I have two options:
1. 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 its AJAX !! which means the response will be back in the xhr object of the ajax call, and therefore there is no redirect has occurred.
So we managed to send the object, serialize it with no effort but we couldn't get a response that represent the new page with a redirect.
2. Use ajax and inject the response into the document: its 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 represent 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.
3. Use a dynamic form submission: This method is known for us, and its 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 submittion 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 cant specify JSON as the content type as its 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 classCustomModelBinderAttribute 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: set the property for the parameter on the destination controller action
[HttpPost] public ActionResult SubmitOrder([JsonBinder]OrderViewModel vmOrder) { //Process the vmOrder if wanted 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.