Posting JSON Data to ASP.NET MVC
ajax, asp.net-mvc, c#, jquery, json
Solution
Take a look at Phil Haack's post on model binding JSON data. The problem is that the default model binder doesn't serialize JSON properly. You need some sort of ValueProvider OR you could write a custom model binder:
using System.IO;
using System.Web.Script.Serialization;
public class JsonModelBinder : DefaultModelBinder {
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
if(!IsJSONRequest(controllerContext)) {
return base.BindModel(controllerContext, bindingContext);
}
// Get the JSON data that's been posted
var request = controllerContext.HttpContext.Request;
//in some setups there is something that already reads the input stream if content type = 'application/json', so seek to the begining
request.InputStream.Seek(0, SeekOrigin.Begin);
var jsonStringData = new StreamReader(request.InputStream).ReadToEnd();
// Use the built-in serializer to do the work for us
return new JavaScriptSerializer()
.Deserialize(jsonStringData, bindingContext.ModelMetadata.ModelType);
// -- REQUIRES .NET4
// If you want to use the .NET4 version of this, change the target framework and uncomment the line below
// and comment out the above return statement
//return new JavaScriptSerializer().Deserialize(jsonStringData, bindingContext.ModelMetadata.ModelType);
}
private static bool IsJSONRequest(ControllerContext controllerContext) {
var contentType = controllerContext.HttpContext.Request.ContentType;
return contentType.Contains("application/json");
}
}
public static class JavaScriptSerializerExt {
public static object Deserialize(this JavaScriptSerializer serializer, string input, Type objType) {
var deserializerMethod = serializer.GetType().GetMethod("Deserialize", BindingFlags.NonPublic | BindingFlags.Static);
// internal static method to do the work for us
//Deserialize(this, input, null, this.RecursionLimit);
return deserializerMethod.Invoke(serializer,
new object[] { serializer, input, objType, serializer.RecursionLimit });
}
}
And tell MVC to use it in your Global.asax file:
ModelBinders.Binders.DefaultBinder = new JsonModelBinder();
Also, this code makes use of the content type = 'application/json' so make sure you set that in jquery like so:
$.ajax({
dataType: "json",
contentType: "application/json",
type: 'POST',
url: '/Controller/Action',
data: { 'items': JSON.stringify(lineItems), 'id': documentId }
});
Problem
Im trying to get a list of line items to a webpage using JSON, which will then be manipulated and sent back to the server by ajax request using the same JSON structure that arrived (except having had a field values changed). Receiving data from the server is easy, manipulation even easier! but sending that JSON data back to the server for saving... suicide time! PLEASE can someone help! Javascript ``` var lineitems; // get data from server $.ajax({ url: '/Controller/GetData/', success: function(data){ lineitems = data; } }); // post data to server $.ajax({ url: '/Controller/SaveData/', data: { incoming: lineitems } }); ``` C# - Objects ``` public class LineItem{ public string reference; public int quantity; public decimal amount; } ``` C# - Controller ``` public JsonResult GetData() { IEnumerable<LineItem> lineItems = ... ; // a whole bunch of line items return Json(lineItems); } public JsonResult SaveData(IEnumerable<LineItem> incoming){ foreach(LineItem item in incoming){ // save some stuff } return Json(new { success = true, message = "Some message" }); } ``` The data arrives at the server as serialized post data. The automated model binder tries to bind `IEnumerable<LineItem> incoming` and surprisingly gets the resulting `IEnumerable` has the correct number of `LineItems` - it just doesnt populate them with data. SOLUTION Using answers from a number of sources, primarily `djch` on another stackoverflow post and `BeRecursive` below, I solved my problem using two main methods. Server Side The deserialiser below requires reference to `System.Runtime.Serialization` and `using System.Runtime.Serialization.Json` ``` private T Deserialise<T>(string json) { using (var ms = new MemoryStream(Encoding.Unicode.GetBytes(json))) { var serialiser = new DataContractJsonSerializer(typeof(T)); return (T)serialiser.ReadObject(ms); } } public void Action(int id, string items){ IEnumerable<LineItem> lineitems = Deserialise<IEnumerable<LineItem>>(items); // do whatever needs to be done - create, update, delete etc. } ``` Client Side It uses json.org's stringify method, available in this dependecy https://github.com/douglascrockford/JSON-js/blob/master/json2.js (which is 2.5kb when minified) ``` $.ajax({ type: 'POST', url: '/Controller/Action', data: { 'items': JSON.stringify(lineItems), 'id': documentId } }); ```