Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Posting JSON Data to ASP.NET MVC

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 }
});
like image 335
Jimbo Avatar asked Nov 12 '10 11:11

Jimbo


4 Answers

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 }
});
like image 95
BeRecursive Avatar answered Sep 29 '22 05:09

BeRecursive


The simplest way of doing this

I urge you to read this blog post that directly addresses your problem.

Using custom model binders isn't really wise as Phil Haack pointed out (his blog post is linked in the upper blog post as well).

Basically you have three options:

  1. Write a JsonValueProviderFactory and use a client side library like json2.js to communicate wit JSON directly.

  2. Write a JQueryValueProviderFactory that understands the jQuery JSON object transformation that happens in $.ajax or

  3. Use the very simple and quick jQuery plugin outlined in the blog post, that prepares any JSON object (even arrays that will be bound to IList<T> and dates that will correctly parse on the server side as DateTime instances) that will be understood by Asp.net MVC default model binder.

Of all three, the last one is the simplest and doesn't interfere with Asp.net MVC inner workings thus lowering possible bug surface. Using this technique outlined in the blog post will correctly data bind your strong type action parameters and validate them as well. So it is basically a win win situation.

like image 39
Robert Koritnik Avatar answered Sep 29 '22 07:09

Robert Koritnik


In MVC3 they've added this.

But whats even more nice is that since MVC source code is open you can grab the ValueProvider and use it yourself in your own code (if youre not on MVC3 yet).

You will end up with something like this

ValueProviderFactories.Factories.Add(new JsonValueProviderFactory())
like image 9
Kenny Eliasson Avatar answered Sep 29 '22 07:09

Kenny Eliasson


I solved this problem following vestigal's tips here:

Can I set an unlimited length for maxJsonLength in web.config?

When I needed to post a large json to an action in a controller, I would get the famous "Error during deserialization using the JSON JavaScriptSerializer. The length of the string exceeds the value set on the maxJsonLength property.\r\nParameter name: input value provider".

What I did is create a new ValueProviderFactory, LargeJsonValueProviderFactory, and set the MaxJsonLength = Int32.MaxValue in the GetDeserializedObject method

public sealed class LargeJsonValueProviderFactory : ValueProviderFactory
{
    private static void AddToBackingStore(LargeJsonValueProviderFactory.EntryLimitedDictionary backingStore, string prefix, object value)
    {
        IDictionary<string, object> dictionary = value as IDictionary<string, object>;
        if (dictionary != null)
        {
            foreach (KeyValuePair<string, object> keyValuePair in (IEnumerable<KeyValuePair<string, object>>) dictionary)
                LargeJsonValueProviderFactory.AddToBackingStore(backingStore, LargeJsonValueProviderFactory.MakePropertyKey(prefix, keyValuePair.Key), keyValuePair.Value);
        }
        else
        {
            IList list = value as IList;
            if (list != null)
            {
                for (int index = 0; index < list.Count; ++index)
                    LargeJsonValueProviderFactory.AddToBackingStore(backingStore, LargeJsonValueProviderFactory.MakeArrayKey(prefix, index), list[index]);
            }
            else
                backingStore.Add(prefix, value);
        }
    }

    private static object GetDeserializedObject(ControllerContext controllerContext)
    {
        if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
            return (object) null;
        string end = new StreamReader(controllerContext.HttpContext.Request.InputStream).ReadToEnd();
        if (string.IsNullOrEmpty(end))
            return (object) null;

        var serializer = new JavaScriptSerializer {MaxJsonLength = Int32.MaxValue};

        return serializer.DeserializeObject(end);
    }

    /// <summary>Returns a JSON value-provider object for the specified controller context.</summary>
    /// <returns>A JSON value-provider object for the specified controller context.</returns>
    /// <param name="controllerContext">The controller context.</param>
    public override IValueProvider GetValueProvider(ControllerContext controllerContext)
    {
        if (controllerContext == null)
            throw new ArgumentNullException("controllerContext");
        object deserializedObject = LargeJsonValueProviderFactory.GetDeserializedObject(controllerContext);
        if (deserializedObject == null)
            return (IValueProvider) null;
        Dictionary<string, object> dictionary = new Dictionary<string, object>((IEqualityComparer<string>) StringComparer.OrdinalIgnoreCase);
        LargeJsonValueProviderFactory.AddToBackingStore(new LargeJsonValueProviderFactory.EntryLimitedDictionary((IDictionary<string, object>) dictionary), string.Empty, deserializedObject);
        return (IValueProvider) new DictionaryValueProvider<object>((IDictionary<string, object>) dictionary, CultureInfo.CurrentCulture);
    }

    private static string MakeArrayKey(string prefix, int index)
    {
        return prefix + "[" + index.ToString((IFormatProvider) CultureInfo.InvariantCulture) + "]";
    }

    private static string MakePropertyKey(string prefix, string propertyName)
    {
        if (!string.IsNullOrEmpty(prefix))
            return prefix + "." + propertyName;
        return propertyName;
    }

    private class EntryLimitedDictionary
    {
        private static int _maximumDepth = LargeJsonValueProviderFactory.EntryLimitedDictionary.GetMaximumDepth();
        private readonly IDictionary<string, object> _innerDictionary;
        private int _itemCount;

        public EntryLimitedDictionary(IDictionary<string, object> innerDictionary)
        {
            this._innerDictionary = innerDictionary;
        }

        public void Add(string key, object value)
        {
            if (++this._itemCount > LargeJsonValueProviderFactory.EntryLimitedDictionary._maximumDepth)
                throw new InvalidOperationException("JsonValueProviderFactory_RequestTooLarge");
            this._innerDictionary.Add(key, value);
        }

        private static int GetMaximumDepth()
        {
            NameValueCollection appSettings = ConfigurationManager.AppSettings;
            if (appSettings != null)
            {
                string[] values = appSettings.GetValues("aspnet:MaxJsonDeserializerMembers");
                int result;
                if (values != null && values.Length > 0 && int.TryParse(values[0], out result))
                    return result;
            }
            return 1000;
        }
    }
}

Then, in the Application_Start method from Global.asax.cs, replace the ValueProviderFactory with the new one:

protected void Application_Start()
    {
        ...

        //Add LargeJsonValueProviderFactory
        ValueProviderFactory jsonFactory = null;
        foreach (var factory in ValueProviderFactories.Factories)
        {
            if (factory.GetType().FullName == "System.Web.Mvc.JsonValueProviderFactory")
            {
                jsonFactory = factory;
                break;
            }
        }

        if (jsonFactory != null)
        {
            ValueProviderFactories.Factories.Remove(jsonFactory);
        }

        var largeJsonValueProviderFactory = new LargeJsonValueProviderFactory();
        ValueProviderFactories.Factories.Add(largeJsonValueProviderFactory);
    }
like image 4
Byt3 Avatar answered Sep 29 '22 05:09

Byt3