Google analytics script

Latest jQuery CDN with code tiggling.

Friday, 10 December 2010

Sending complex JSON objects to Asp.net MVC using jQuery Ajax

jQuery has great support for sending HTML form data (that is input, select and textarea element values) back to the server using Ajax technologies by providing two main ways of preparing form data for submission - namely serialize() and serializeArray() functions. But sometimes we just don't have a form to send but rather JSON objects that we'd like to transfer over to the server.

Asp.net MVC on the other hand has built-in capabilities of transforming sent data to strong type objects while also validating their state. But data we're sending has to be prepared in the right way so default data binder can it and populate controller action parameters objects' properties. We can of course parse values ourselves but then we also loose the simplest yet efficient built-in validation using data annotations which is something we definitely want to use.

I know I've been sending JSON objects back to the server before, but this time I came across a problem that felt odd, since based on my previous experience should work out of the box. To my surprise it didn't. Let me explain what was going on.

Let's describe the situation

It worked every time in the past when I wanted to send a single JSON object with simple value properties (no sub-objects). What do I mean by that? Well it's easier to show you some code. This is an example of such an object I used to send and worked like a charm:

   1:  var person = {
   2:      Id: 1,
   3:      Name: "This is my name"
   4:  };

This kind of JSON object would easily data bind to a server side Asp.net MVC controller action parameter: public ActionResult DoSomething(Person person) ... Person parameter would be populated with sent values and would be auto validated by the server-side validation data annotations attributes set on the Person class definition. Of course when using Asp.net MVC 2+. In MVC 1.0 one has to manually run validation (I suggest you write a custom action filter that you put on a base controller class and does the trick for you, so you can just set it and forget it). Fine by me I'm using MVC 2.

The thing is I have a bit different controller action signature that has more than just one single parameter similar to this: public ActionResult DoSomething(Person person, Car car, DateTime dateOfPurchase) ...

I thought I could just create a similar JSON object on the client side and send it back to the server:

   1:  var dataToSend = {
   2:      person: {
   3:          Id: 1,
   4:          Name: "This is my name"
   5:      },
   6:      car: {
   7:          Id: 100,
   8:          Manufacturer: "Ford",
   9:          Model: "Ka"
  10:      },
  11:      dateOfPurchase: new Date(2010, 0, 1)
  12:  };
The problem is that providing this object to jQuery.ajax() function call doesn't work. At all. Data doesn't get data bound on the server so controller action parameters have their default values that are probably invalid anyway. The thing is it doesn't work.

Behind the scenes

The problem is that my JSON object got converted by jQuery to request query string and my second level property values got mangled into a form that Asp.net MVC default model binder doesn't understand:

   1:  $.ajax({
   2:      url: "/SomeUrl",
   3:      type: "POST",
   4:      data: dataToSend,
   5:      ...
   6:  });

Serialized data looked something more or less equivalent to this:

   1:  person[Id] = 1
   2:  person[Name] = "This is my name"
   3:  car[Id] = 100
   4:  ...
Not exactly the same, but the main thing here is the property naming conversion to a kind of dictionary-like definitions with keys and values. This is of course not something Asp.net MVC default mode binder understands hence it doesn't populate my action parameters.

Possible solutions

So I basically have three options:

  1. I could use JsonValueProviderFactory class that Phil Haack blogged about and also use a client side Javascript library to serialize my objects to JSON strings, because jQuery doesn't have built-in capabilities for such things (John Resig recommends using json2.js library for this kind of stuff)
  2. I could write a custom JQueryValueProviderFactory class that would understand request values sent by jQuery.
  3. or I could write a jQuery plug-in that would convert my JSON object into a form that Asp.net MVC default model binder would be able to consume.

Second and third solutions included much less coding and third doesn't change the inner workings of Asp.net MVC (so I can avoid additional own server-side application-wide bugs) so I decided to give it a try with the third solution. But first we have to understand what kind of value naming Asp.net MVC default model binder does understand. Knowing how input HTML input elements get named we can see that object properties should be named this way:

   1:  person.Id = 1
   2:  person.Name = "This is my name"
   3:  car.Id = 100
   4:  ...
But this is not all. We have to understand how jQuery data serializer works as well. We can either provide a prepared string to jQuery.ajax() function call or provide a JSON object similar to what serializeArray() does. The later prepares an array of name-value pairs for every single JSON object property (like { name: "person.Id", value: 1 }). Kind of like a dictionary initializer. This is something I could quite easily do and then pass that to jQuery.ajax() function to do the serialization for me.

jQuery plug-in that does the trick

I wanted to make my code reusable so basically all I have to do is to write a jQuery plug-in that transforms an arbitrary JSON object to an array of name-value pairs and appropriately call it jQuery.toDictionary(...). This plug-in is able to covert any JSON object with any sub object depth and also support arbitrary arrays that will be correctly data bound to IList on the server side. You can use it at your own risk.

   1:  /*!
   2:   * jQuery toDictionary() plugin
   3:   *
   4:   * Version 1.2 (11 Apr 2011)
   5:   *
   6:   * Copyright (c) 2011 Robert Koritnik
   7:   * Licensed under the terms of the MIT license
   8:   * http://www.opensource.org/licenses/mit-license.php
   9:   */
  10:   
  11:  (function ($) {
  12:   
  13:      // #region String.prototype.format
  14:      // add String prototype format function if it doesn't yet exist
  15:      if ($.isFunction(String.prototype.format) === false)
  16:      {
  17:          String.prototype.format = function () {
  18:              var s = this;
  19:              var i = arguments.length;
  20:              while (i--)
  21:              {
  22:                  s = s.replace(new RegExp("\\{" + i + "\\}", "gim"), arguments[i]);
  23:              }
  24:              return s;
  25:          };
  26:      }
  27:      // #endregion
  28:   
  29:      // #region Date.prototype.toISOString
  30:      // add Date prototype toISOString function if it doesn't yet exist
  31:      if ($.isFunction(Date.prototype.toISOString) === false)
  32:      {
  33:          Date.prototype.toISOString = function () {
  34:              var pad = function (n, places) {
  35:                  n = n.toString();
  36:                  for (var i = n.length; i < places; i++)
  37:                  {
  38:                      n = "0" + n;
  39:                  }
  40:                  return n;
  41:              };
  42:              var d = this;
  43:              return "{0}-{1}-{2}T{3}:{4}:{5}.{6}Z".format(
  44:                  d.getUTCFullYear(),
  45:                  pad(d.getUTCMonth() + 1, 2),
  46:                  pad(d.getUTCDate(), 2),
  47:                  pad(d.getUTCHours(), 2),
  48:                  pad(d.getUTCMinutes(), 2),
  49:                  pad(d.getUTCSeconds(), 2),
  50:                  pad(d.getUTCMilliseconds(), 3)
  51:              );
  52:          };
  53:      }
  54:      // #endregion
  55:   
  56:      var _flatten = function (input, output, prefix, includeNulls) {
  57:          if ($.isPlainObject(input))
  58:          {
  59:              for (var p in input)
  60:              {
  61:                  if (includeNulls === true || typeof (input[p]) !== "undefined" && input[p] !== null)
  62:                  {
  63:                      _flatten(input[p], output, prefix.length > 0 ? prefix + "." + p : p, includeNulls);
  64:                  }
  65:              }
  66:          }
  67:          else
  68:          {
  69:              if ($.isArray(input))
  70:              {
  71:                  $.each(input, function (index, value) {
  72:                      _flatten(value, output, "{0}[{1}]".format(prefix, index));
  73:                  });
  74:                  return;
  75:              }
  76:              if (!$.isFunction(input))
  77:              {
  78:                  if (input instanceof Date)
  79:                  {
  80:                      output.push({ name: prefix, value: input.toISOString() });
  81:                  }
  82:                  else
  83:                  {
  84:                      var val = typeof (input);
  85:                      switch (val)
  86:                      {
  87:                          case "boolean":
  88:                          case "number":
  89:                              val = input;
  90:                              break;
  91:                          case "object":
  92:                              // this property is null, because non-null objects are evaluated in first if branch
  93:                              if (includeNulls !== true)
  94:                              {
  95:                                  return;
  96:                              }
  97:                          default:
  98:                              val = input || "";
  99:                      }
 100:                      output.push({ name: prefix, value: val });
 101:                  }
 102:              }
 103:          }
 104:      };
 105:   
 106:      $.extend({
 107:          toDictionary: function (data, prefix, includeNulls) {
 108:              /// <summary>Flattens an arbitrary JSON object to a dictionary that Asp.net MVC default model binder understands.</summary>
 109:              /// <param name="data" type="Object">Can either be a JSON object or a function that returns one.</data>
 110:              /// <param name="prefix" type="String" Optional="true">Provide this parameter when you want the output names to be prefixed by something (ie. when flattening simple values).</param>
 111:              /// <param name="includeNulls" type="Boolean" Optional="true">Set this to 'true' when you want null valued properties to be included in result (default is 'false').</param>
 112:   
 113:              // get data first if provided parameter is a function
 114:              data = $.isFunction(data) ? data.call() : data;
 115:   
 116:              // is second argument "prefix" or "includeNulls"
 117:              if (arguments.length === 2 && typeof (prefix) === "boolean")
 118:              {
 119:                  includeNulls = prefix;
 120:                  prefix = "";
 121:              }
 122:   
 123:              // set "includeNulls" default
 124:              includeNulls = typeof (includeNulls) === "boolean" ? includeNulls : false;
 125:   
 126:              var result = [];
 127:              _flatten(data, result, prefix || "", includeNulls);
 128:   
 129:              return result;
 130:          }
 131:      });
 132:  })(jQuery);

Plugin usage

It's very easy to use this plugin. Function is defined this way: $.toDictionary([mandatory]data, [optional]prefix, [optional]includeNulls);

So how do we use it actually? Just use this code:

   1:  $.ajax({
   2:      url: "/SomeURL",
   3:      type: "POST",
   4:      data: $.toDictionary(dataToSend),
   5:      ...
   6:  });
and your JSON object will get transformed into a dictionary that Asp.net MVC default model binder will understand and use to populate your controller action parameters. the good part is that you can as well convert simple values, since you can provide an additional prefix parameter to my plug-in function call. So to send a single integer value and call it id, you'd simply call it like:
   1:  $.ajax({
   2:      url: "/SomeURL",
   3:      type: "POST",
   4:      data: $.toDictionary(111, "id"),
   5:      ...
   6:  });

Additional notes related to plugin updates

Since version 1.2 (11th April 2011) this plugin supports an additional optional parameter includeNulls which tells the conversion process what to do with null valued properties. They're are omitted by default (as if this parameter was set to false), but if you set it to true all such properties will be included in the end result.

Any questions?

If you have any further questions, simply comment on this post and I'll do my best to answer them.

92 comments:

  1. do we have any way to pass complex jSON for a GET method?

    ReplyDelete
  2. @amishra: If you're talking about Ajax GET requests then the answer is yes. If you look at Plugin usage at the end of blog post, you can see that both Ajax requests are POST requests. But if you'd change that to GET it would still work as expected. The main change would be that all variables will be serialized into query string.

    But if you're talking about non-Ajax GET requests then the answer would still be yes. All you'd have to do is to call

    $.param($.toDictionary(yourComplexObj));

    This will serialize your object into a query string that you can attach to your URL.

    ReplyDelete
  3. Thanks for your comment.
    That means that the jSON object can only be of a limited size, isn't it?

    ReplyDelete
  4. @amishra: Yes, because browsers and servers have limitations related to this even though HTTP spec doesn't limit URI length.

    What is the maximum length of URL

    ReplyDelete
  5. Also works for struts.

    realy a good jobs, thanks

    ReplyDelete
  6. Can't get this to work at all. Action is never entered no matter what number or type of parameters I use (even tried you code here). Is this JQuery version dependent? I'm using 1.5.2 with MVC3 (Razor) and EF4.

    Regards
    Dave

    ReplyDelete
  7. Wow, a developer website that is tidy. Congrats.

    ReplyDelete
  8. @Dave: If your action is never entered, than you must have problems with your routing. This $.toDictionary() plugin doesn't change URLs and has nothing to do with EF4. And it does require you to use jQuery 1.4 or later which means you're covered. You should check request URLs whether they are formed according to your routing definition.

    @Anonymous: Thank you very much. I try to keep my life tidy (including code as well as my blog - although I'm not completely satisfied with it)

    ReplyDelete
  9. I used this method this way:
    public virtual ActionResult DeleteItem(IList<ComplianceItemModel> model)

    and in my client code:

    function GetComplianceItemsIds() {
    var ids = new Array();
    var i = 0;
    $(':checked[name="Select"]').each(function () {
    ids[i] = { ComplianceItemModel: { ComplianceItemId: this.value} };
    i++;
    });

    return ids;
    }

    and used data: $.toDictionary(ids) in jQuery.ajax

    When I send the request I can see in Fiddler that these data are sent to my action:
    %5B0%5D.ComplianceItemModel.ComplianceItemId=8&%5B1%5D.ComplianceItemModel.ComplianceItemId=3

    but when debugging the ActionMethod both ComplianceItemId values are 0.

    Any idea?

    ReplyDelete
  10. Mahdi: Of course they are all zeros, because you generated your object incorrectly. Javascript array corresponds to server-side IList<T> hence each item in Javascript array should represent your ComplianceItemModel object instance which has the ComplianceItemId property (and not ComplianceItemModel property which you generated).

    Try this function instead:

    function GetComplianceItemsIds()
    {
        var ids = [];
        $(':checked[name="Select"]').each(function()
        {
            ids.push({ ComplianceItemId: this.value });
        });
        return ids;
    }


    this will generate an array of objects as:

    [{ ComplianceItemId: X }, { ComplianceImteId: Y }, ...]

    which actually looks the same as IList<ComplianceModel> on the server-side (as it should).

    Then use it the way you did before by either calling:

    $.toDictionary(ids, "model");

    which will give you a more verbose result that will be correctly understood by server side even when you'd have more than just one action method parameter:

    model[0].ComplianceItemId = X
    model[1].ComplianceItemId = Y
    ...


    Since you have only one parameter you can of course still use it as:

    $.toDictionary(ids);

    Hope this makes sense...

    ReplyDelete
  11. It worked! Many thanks :-)

    ReplyDelete
  12. Hi

    This looks really nice, but I cant make it work.
    The json object is a dictionary ok, but on the controller side Person and Car objects are populated with null values. I have also tried with and without parameter "traditional".

    I'm trying with these objects:

    public class Person
    {
    public int Id { get; set; }
    public string Name { get; set; }
    }

    public class Car
    {
    public int Id { get; set; }
    public string Manufacturer { get; set; }
    public string Model { get; set; }
    }

    ActionMethod is like:

    public ActionResult About(Person p, Car c)

    JavaScript side:

    var dataToSend = {
    person: {
    Id: 1,
    Name: "This is my name"
    },
    car: {
    Id: 100,
    Manufacturer: "Ford",
    Model: "Ka"
    }
    };

    $.ajax({
    type: "POST",
    url: "/Home/About",
    data: dataReally,
    dataType: 'application/json; charset=utf-8',
    traditional: true,
    success: function (result) {
    //alert("Data Returned: " + result);
    }
    })

    ReplyDelete
  13. @Timonardo: As per your code, your data to be send is stored in dataToSend object, but you're sending dataReally object which is not seen in your code. It may just be a typo but what you'd have to do is to:

    $.ajax({
    type: "POST",
    url: "/Home/About",
    data: $.toDictionary(dataToSend),
    success: ...,
    error: ...
    });

    And it should work as expected. No need to set any data types or traditional... Just use this code and things should work as expected. You can check what will get send to server by using FireBug or even checking manually by calling

    $.param($.toDictionary(dataToSend));

    ReplyDelete
  14. Hi

    I'm having an issue using your plugin, and was wondering if you could help?

    I have an MVC action method as follows:

    public ActionResult(....., IList filters)


    Here is the JavaScript I'm attempting to use:

    var filters = [];

    $(selectedFilters).each(function() {
    filters.push({ FilterName = this.FilterName, FilterValue = this.FilterValue });
    });

    return $.toDictionary(filters, "filters");


    Looking in firebug I the POST data looks as follows:
    0[name] = filters[0].FilterName
    0[value] = Category
    1[name] = filters[0].FilterValue
    1[value] = Sold

    I was expecting the following in firebug POST data
    filters[0].FilterName = Category
    filters[0].FilterValue = 8
    filters[1].FilterName = Status
    filters[1].FilterValue = Sold

    Any ideas? Thanks!

    ReplyDelete
  15. @Anonymous: If I don't consider the fact that your code isn't valid (your objects pushed to array shouldn't use equality sign) I tried this code which returned correct result:

    var filters = [];
    filters.push({ FilterName: "Category", FilterValue: 8 });
    filters.push({ FilterName: "Status", FilterValue: "Sold" });

    $.toDictionary(filters, "filters");


    The result that Firebug gives me is:
    [Object { name="filters[0].FilterName", value="Category"}, Object { name="filters[0].FilterValue", value=8}, Object { name="filters[1].FilterName", value="Status"}, Object { name="filters[1].FilterValue", value="Sold"}]

    which surely seems right. And if I try to run the same thing over this array that jQuery will run before POSTing it to the server

    $.param($.toDictionary(filters, "filters"));

    I also get expected results:
    "filters%5B0%5D.FilterName=Category&filters%5B0%5D.FilterValue=8&filters%5B1%5D.FilterName=Status&filters%5B1%5D.FilterValue=Sold"

    I suggest you take a thorough look into your array before transforming it using my plugin. There must be something wrong with your code and the way that you push data into your filters array.

    ReplyDelete
  16. In using this approach...
    I seem to have trouble with passing a model to a action result that contains derived classes, if I use a standalone model it works fine...

    ReplyDelete
  17. @Anonymous: Care to elabore a bit more on this? Like provide some code, so I can see what the problem is. Or write a question on stackoverflow and I'll look into it.

    ReplyDelete
  18. Basically, if I create a stand-alone model with a PersonInputModel ...

    [Serializable]
    public class PersonInputModel {
    public string Name { get; set; }
    public int Age { get; set; }
    }

    I can access the Name & Age values just fine from the Save ActionResult via the inputModel parameter...

    [HttpPost]
    public ActionResult Save(PersonInputModel inputModel)
    {
    ...
    }

    But, if i use a class derived from a base class that looks like this...

    // Summary:
    // Base class used to create ViewModel objects that contain the Model object
    // and related elements.
    //
    // Type parameters:
    // T:
    // Type of the Model object.
    public abstract class ViewModelBase : IViewModel where T : class
    {
    protected ViewModelBase();

    // Summary:
    // Gets or sets the Model object.
    public T ModelObject { get; set; }
    }

    derived class looks like this...

    [Serializable]
    public class DerivedInputModel : Test.Web.Mvc.ViewModelBase
    {
    ...
    }

    age and name are set in view viz...
    <%: Html.DisplayTextFor(model => model.ModelObject.Age)%>
    <%: Html.DisplayTextFor(model => model.ModelObject.Name)%>

    when my Save ActionResult uses DerivedInputModel as a parameter JSON/MVC Futures/MVC Framework does not send/find any values for Age/Name...

    basically, while the DerivedInputModel gets to the save action result everything is null...

    ReplyDelete
  19. @Anonymous: I suppose you've solved your problem because I can see that you've posted further details, but obviously deleted your own comment. I hope my assumptions are correct.

    ReplyDelete
  20. no, my post did not take for some reason and did not get any error.

    ReplyDelete
  21. @Robert I found that if I have an object that was created via a function, then the plugin did not work as expected. For example

    function MyThing(x){ this.X=x; }
    var thing = new MyThing('test');

    and then

    var data = $.toDictionary(thing);

    In this situation, the first call to _flatten in the plugin skips the whole object because `$.toDocument(thing)` returns false.

    I ended up changing that call to `if (typeof(input) === "object")` to make it work. I then had to move the $.isArray() test to the beginning of the function since an array is also an object.

    Here's my modified _flatten:

    var _flatten = function (input, output, prefix, includeNulls)
    {
    if ($.isArray(input))
    {
    $.each(input, function (index, value)
    {
    _flatten(value, output, "{0}[{1}]".format(prefix, index));
    });
    return;
    }

    if (typeof(input) === "object")
    {
    for (var p in input)
    {
    if (!$.isFunction(input[p]) && (includeNulls === true || typeof (input[p]) !== "undefined" && input[p] !== null))
    {
    _flatten(input[p], output, prefix.length > 0 ? prefix + "." + p : p, includeNulls);
    }
    }
    }
    else
    {
    if (!$.isFunction(input))
    {
    if (input instanceof Date)
    {
    output.push({ name: prefix, value: input.toISOString() });
    }
    else
    {
    var val = typeof (input);
    switch (val)
    {
    case "boolean":
    case "number":
    val = input;
    break;
    case "object":
    // this property is null, because non-null objects are evaluated in first if branch
    if (includeNulls !== true)
    {
    return;
    }
    default:
    val = input || "";
    }
    output.push({ name: prefix, value: val });
    }
    }
    }
    };

    As far as I can tell, this works equally as well as your original.

    ReplyDelete
  22. Oops, I meant

    `$.isPlainObject(thing)` returns false.

    ReplyDelete
  23. Having issues with posting a comment here. Disregard the above comment. Lost the following comment:

    If you have an object that was created via a function:

    function MyThing(x) { this.X = x;}
    var thing = new MyThing('test');

    and then,

    var data = $.toDictionary(thing);

    data will be blank. That's because in the plugin there is a test of $.isPlainObject() on your top level object.

    In this case, thing is an object, but not a plain object. However typeof(thing) === 'object' does return true for thing.

    Now, in your plugin, that had two side effects. One is that functions also pass that test. The other is that arrays also pass that test. I made modifications to _flatten() to deal with those.

    Here's my modifications:

    var _flatten = function (input, output, prefix, includeNulls)
    {
    if ($.isArray(input))
    {
    $.each(input, function (index, value)
    {
    _flatten(value, output, "{0}[{1}]".format(prefix, index));
    });
    return;
    }

    if (typeof(input) === "object")
    {
    for (var p in input)
    {
    if (!$.isFunction(input[p]) && (includeNulls === true || typeof (input[p]) !== "undefined" && input[p] !== null))
    {
    _flatten(input[p], output, prefix.length > 0 ? prefix + "." + p : p, includeNulls);
    }
    }
    }
    else
    {
    if (!$.isFunction(input))
    {
    if (input instanceof Date)
    {
    output.push({ name: prefix, value: input.toISOString() });
    }
    else
    {
    var val = typeof (input);
    switch (val)
    {
    case "boolean":
    case "number":
    val = input;
    break;
    case "object":
    // this property is null, because non-null objects are evaluated in first if branch
    if (includeNulls !== true)
    {
    return;
    }
    default:
    val = input || "";
    }
    output.push({ name: prefix, value: val });
    }
    }
    }
    };

    ReplyDelete
  24. @Steve Mallory: I don't know what's been going on lately with comments, because you're the second person that had their comments disappeared. No worries. I got all three in my mailbox. Will answer...

    ReplyDelete
  25. @Steve Mallory: Thanks for pointing this out, but I think your change will not work, because there are several more object instances that falsely report they're objects. Two of them being null and new Date(). They shouldn't be processed by your if statement you've put after array checking.

    ReplyDelete
  26. If one of my object properties is itself an array, this doesn't seem to work. The MVC action sees this as an object type and not an array type.

    filterList = new Array();

    var sourceArray = new Array();
    sourceArray.push("some source");

    filterList.push(
    {
    "PropertyName": "SiteSelection",
    "Value1": sourceArray, // the name of the selected source
    "FilterType": "IsInCollection",
    "PropertyCategory": "SourceName",
    "Value2": ""
    });

    $.toDictionary(filterList)


    Value1 is of type "object".

    ReplyDelete
  27. @Danno: Since your root object is an array already it's wise to give it a certain name - the same as your controller action method parameter. In your case you probably have action defined as (could have a different name and additional stuff):

    public ActionResult Index(IList<Filter> filterList)

    Then call $.toDictionary with an additional parameter:

    $.toDictionary(filterList, "filterList");

    which will give your root object a name and turn [0].PropertyName into filterList[0].PropertyName. This will make more sense to Asp.net MVC server side model binder code to bind your objects to your list/array method parameter.

    Hope this helps. And sorry for my vacation answer delay.

    ReplyDelete
  28. Robert:

    Awesome plugin...been using it for a while on an ajax heavy project I've been working on. Noticed one thing that might be worth fixing. To do some tests, I needed a bunch of emails, so I used my gmail with their "+" syntax to simulate multiple addresses. Well, the problem is that the "+" wasnt being encoded, so on the server, it was being interpreted as a " " (space). So, I just made two quick edits to your source (lines 86 and 106) to add a call to encodeURIComponent to safely escape all values sent to the server. So, line 86 becomes output.push({ name: prefix, value: encodeURIComponent(input.toISOString()) }); and 106 becomes output.push({ name: prefix, value: encodeURIComponent(val) });

    Seems to be working for me so far. If you see any pitfalls, let me know, otherwise, it might be a good improvement to add into your source. I found the project on github, but there's nothing in the repo now for me to do a pull request.

    Thanks for the code!

    ReplyDelete
  29. @Jorin: Thanks for the info Jorin. I appreciate it. Your code changes seem reasonable and I will include them in the plugin.

    About GitHub project you're right. I've prepared project space there but haven't found enough time to add all plugin versions on it. Will do that some time (soon I hope). That will also make it possible to provide minified version of the plugin.

    ReplyDelete
    Replies
    1. Robert:

      I'm finally back on this project and I actually noticed an issue with my suggestion above. On a GET, you need to encode values that aren't URL friendly, but on a POST, you actually want to avoid encoding them as ASP.NET MVC doesn't decode them properly. So, looking at the code, it looks like you didnt put my suggestions in place -- so that's good.

      Instead, I just added a "encode" parameter to the primary method call and then added these lines right before returning `result`.

      // set "encode" default and then apply based on value
      encode = typeof (encode) === "boolean" ? encode : false;
      if (encode)
      result = $.makeArray($.map(result, function (n) { n.value = encodeURIComponent(n.value); return n; }));

      which just iterates through and encodes the value parameter if encode is true. Probably not something that everyone needs (I only pass `true` and use it in 2-3 cases) but thought I'd mention it.

      Thanks again for the code.

      Delete
  30. I am trying to use the plugin, but the function toDictionary appears as undefined, do I have to do somethign specific for using the code? I just paste it in a page, which the jquery 1.4 files already included, but the explorer crashes everytime it hits the line of the function.
    Thanks.

    ReplyDelete
  31. @Anonymous: Did you add it before or after jQuery library? If it's after, then there must be something wrong on your side because this plugin works and doesn't throw sporadic errors.

    ReplyDelete
  32. Hi,
    I'm getting an error on the client side like "Object doesn't support property or method 'toDictionary'"

    This error has been thrown at $.toDictionary(aT) line.

    Here is my code:

    var ss = $('input[id^="txtSip"][value!="0"]');
    var aT = [];
    ss.each(function () {
    aT.push({ StokID: this.id.substr(6, this.id.length - 6), Miktar: this.value });
    });
    //for(var i = 0; i < ss.length; i++)
    //{
    //aT.push({ StokID: ss[i].id.substr(6, ss[i].id.length - 6), Miktar: ss[i].value });
    //}

    $.ajax({
    url: '/Siparisler/SepeteEkle',
    type: 'POST',
    dataType: 'json',
    data: $.toDictionary(aT),
    contentType: 'application/json; charset=utf-8',
    success: function (data) {
    // get the result and do some magic with it
    var message = data.Message;
    //$("#resultMessage").html(message);
    }
    });


    I need urgent help, thanks.

    ReplyDelete
  33. I forgot to write that I already added jquery and toDictionary js files in the head section in order of jquery, toDictionary files.

    src="/Scripts/jquery-1.7.min.js"
    src="/Scripts/jquery.toDictionary.js"

    But I got the error in the upper comment anyway.

    Thank you.

    ReplyDelete
    Replies
    1. Based on the code you provided it seems that you're not doing anything unusual. But since you're getting back the error that you, there's no other reason but that toDictionary not loaded. Is it because you're reference has a typo? Or is it because you haven't copied all that code correctly...

      Whatever the reason I suggest you check your page using Firebug and check all those resource loading. Check whether toDictionary plugin actually got to the client and in console check whether it's defined (mind there aren't any parentheses):

      > $.toDictionary
      > function()

      this is what it should respond. If it says:

      > $.toDictionary
      > undefined

      then your plugin didn't load as it should or got overridden by some other plugin or code.

      Delete
  34. Great work Robert - this is an excellent little plugin that will be useful in so many places.

    ReplyDelete
    Replies
    1. Thanks Joe. It is very useful. I'm using it all over the place myself. :)

      Delete
  35. Robert, I tried as you mentioned in earlier post, but it is not working for me, any help will be greatly appreciated.

    CLASS:
    public class TempFeatureAssignRequest
    {
    public int AssignStatusType;
    public int FeatureDetailId;
    public int EntityId;
    }
    CONTROLLER:
    public ActionResult FeatureAssignSave(IList featureModel)
    {
    if (featureModel != null)
    {
    //The featureModel is not null but if I look at featureModel[0]. property name the value is 0 for all
    }
    }
    JAVASCRIPT CODE:

    var fm = [];
    fm.push({AssignStatusType: 1, FeatureDetailId: 1, EntityId: 1});

    $.ajax({ url: 'FeatureAssignSave',
    type: 'post',
    data: $.toDictionary(fm, "featureModel"),
    });

    ReplyDelete
  36. Robert, I am adding the CONTROLLER method again

    public ActionResult FeatureAssignSave(IList featureModel)
    {
    if (featureModel != null)
    {
    //The featureModel is not null but if I look at featureModel[0]. property name the value is 0 for all
    // featureModel[0].AssignStatusType; (shows zero)
    // featureModel[0].FeatureDetailId;(shows zero)
    // featureModel[0].EntityId; (shows zero)

    }
    }

    ReplyDelete
  37. Just wanted to say thanks. Works like a charm :D

    ReplyDelete
  38. Hi

    I named the form elements as one would in C# (with dots) and used the jQuery function: serializeArray(). It seem to work out of the box with the MVC3 model binder!

    E.g.

    // example model
    class Person {
    string Name,
    ContactInfo ContactInfo
    }

    class ContactInfo {
    string Email;
    }


    Then create a form with two inputs named "Name" and "ContactInfo.Email".

    Send it to the controller using serializeArray()

    $.ajax({
    url: "/Person/EditPerson",
    type: "POST",
    dataType: "json",
    data: $("#Edit_ContactPerson_Form").serializeArray(),
    ...

    And in the controller action

    public JsonResult EditPerson(Person person)
    {
    person.ContactInfo.Email <-- is now the value in the input named "ContactInfo.Email"!

    ReplyDelete
  39. Has anyone gotten a 500 error when they try to send a string of html? For some reason it doesn't like it.

    ReplyDelete
    Replies
    1. Of course... Asp.net doesn't like sending HTML to server because of possible security vulnerability of your application. You could easily post a bad script to the server this way...

      If you do want to send bare HTML then you can disable this security in Asp.net MVC. Just put [AllowHtml] on your action that you want it to receive bare HTML. But make sure you sanitize it on the server.

      Delete
    2. Do I have to worry if this HTML is being generated by tinyMCE that provides only the simplest of formatting?

      Delete
    3. Yes most definitely! Your interface may have this tinyMCE control but that doesn't prevent the attacker to issue manual HTTP requests with malicious HTML code. You have to sanitize it. Always.

      Delete
    4. BTW: Having a control like that kind of an editor is a good indicator to any attacker that your page may be vulnerable, since you obviously must let HTML through to server. And they may (just because of this tinyMCE) be even more tempted to try...

      Delete
  40. I have some strange problem with your plugin when combining passing simple values and complex.

    Here is a javascript: http://pastebin.com/qYtU9Ju0
    And here is my controller: http://pastebin.com/174CmQSw
    The result has nullified data object: http://pastebin.com/CdTiqk4c

    ReplyDelete
  41. Here is a sample project: http://www.sendspace.com/file/fyg6qs

    ReplyDelete
  42. Ok, I fixed it.
    First, default binder can not correctly parse floating-point numbers for different than system default format - namely if floating point sepparator is not "."
    Second, default binder can not initialize fields of the model class - it can work only with properties.

    ReplyDelete
  43. Robert,

    Thank you so much for this plug-in. I rather use your solution to convert the object into a format that MVC default binder understand. It saved me lots of time!!

    Cheers,
    Luis

    ReplyDelete
    Replies
    1. I think that way as well. Otherwise we end up with too many customization while default functionality works just as well.

      Delete
  44. OMG I owe you a beer at least.



    THANK YOU

    ReplyDelete
    Replies
    1. If you're anywhere near Dublin, Ireland (or will be in the next 3 months while I'm here) let's have one then. :)

      Delete
  45. Thank you sir! I too prefer the route you took with this over tweaking the core .Net functionality.

    ReplyDelete
  46. This worked for me : http://stackoverflow.com/questions/9610214/pass-complex-json-object-to-an-mvc-3-action

    Basically adding
    contentType: 'application/json',

    to the $.ajax parameters...

    just to clarify, without this, the array item's properties are always NULL, with the contentType, the array items are populated.

    ReplyDelete
  47. I'd like to use the toDictionary plugin to pass the MVC ViewModel back to the controller. Being relatively new to this, I need to ask: how do I refer to the ViewModel in the ajax call? My view gets the view model via Inherits=System.Web.Mvc.ViewPage

    ... then to post back to controller I envision -
    $.ajax({
    url: '/Home/ReturnMapObjects',
    type: "POST",
    data: $.toDictionary(??what goes here??),
    datatype: "json",
    contentType: "application/json; charset=utf-8"
    });

    How do I pass back the viewmodel that came to the view? -Bill

    ReplyDelete
    Replies
    1. There are two ways.
      First one is the common way that serializes your form (when your ViewModel data is presented in the view as a form). Ajax call will likely be issued within form submit handler (meaning that this will refer to form DOM element):

      $.ajax({ ..., data: $(this).serialize(), ...});

      As you can see, there's no use of toDictionary in this case because most of the things will work out of the box. Vast majority of forms aren't dynamic so they don't just add additional fields to them.

      Second way is less common and uses a JSON object and posts back that one. In this case there's no completely direct way of providing your server-side ViewModel on the client (unless you JSON-encode it and send it within the view). So when you want your actual ViewModel instance to access on the client then supposedly JSON encode it and put it i.e. as an attribute on some element that represents your model. All you'd have to do afterwards is to
      1. read that data,
      2. create Javascript instance of this JSON object
      3. manipulate it
      4. serialize it and send it back to server

      var model = $.parseJSON($("#someEl").data("view-model-data"));
      //manipulate model
      $.ajax({ ..., data: $.toDictionary(model),... });

      And that's it. Depending on your given scenario you can choose between one or the other.

      Delete
  48. Would like to thank you for this, I was trying to break my head over passing complex object with multiple data arrays as JSON for a long time. Great help.

    ReplyDelete
  49. I have been fighting this issue for a couple of days now. I have a complex object that includes a list of complex objects with lists of other complex objects, and this worked perfectly.

    I wish that I had stumbled upon this last week. Thank you so much.

    ReplyDelete
  50. Thanks, a lot... This saved me quite some time!

    ReplyDelete
  51. Such an old code but it still helps a lot of people thanks

    ReplyDelete
  52. This plug-in was created some time ago now but it's worked really well for me. Thank you for sharing

    ReplyDelete
  53. Thank you very much - exactly what I needed and works perfectly with my complex type! - mvc is binding w/o issue types nested 4 levels deep

    ReplyDelete
  54. Hi Rob,

    Looks like lot of people used this plugin made their job easier. But for me I am using a very complex json data sample shown as follows

    [{"DId":0,"DData":[{"Date":"","C0":{"D":"National","Id":"National"},"C1":{"D":"National","Id":"National"},"C2":{"D":"National","Id":"National"}},{"Date":"1/2/2010","C0":{"D":0.74,"Id":1334337,"Dirty":"False"},"C1":{"D":0.6,"Id":1334597,"Dirty":"False"},"C2":{"D":1,"Id":1334857,"Dirty":"False"}}]}]

    Ajax Call :

    var saveData = [];
    saveData.push(postdata);
    var myData = $.toDictionary(postdata);
    $.ajax({
    url: "savedata",
    dataType: 'json',
    contentType: 'application/json',
    type: 'POST',
    data: $.toDictionary(myData,"model"), //contains data
    success: function(data) {
    console.log('Success');
    },
    error: function(ex) {
    console.log('Error');
    }
    });

    Controller
    public JsonResult savedata(IList model){
    }

    Save data url is correct but I dont get 500 internal error and controller is not called. When I pass with simple parameters all looks good. I see data in my variable myData = $.toDictionary(postdata);

    Hope I am making sense. Any help?

    ReplyDelete
    Replies
    1. One thing that's straightforward is that you're calling .toDictionary twice. First when you create your myData and later transforming myData again in your Ajax call. You should just convert it once and pass myData directly to your Ajax call.
      But I'm not sure why you should be getting HTTP error 500? Even though your URL may be "savedata", you should check your actual request URL in browser using Developer tools like Chrome Dev tools. You're not using absolute path so it may be an issue here as well.

      Delete
    2. Rob,

      Calling .toDictionary twice was a typo (cut & paste). Actually I am calling only once. My URL is correct. When I change the ajax call as follows I am able to access my controller.

      var saveData = [];
      saveData.push(postdata);
      var myData = $.toDictionary(saveData);
      $.ajax({
      url: "savedata",
      dataType: 'json',
      contentType: 'application/json',
      type: 'POST',
      data: JSON.stringify({ model: saveData}), //contains data
      success: function(data) {
      console.log('Success');
      },
      error: function(ex) {
      console.log('Error');
      }
      });

      Delete
    3. If this is your code, then it seems you're not using results from ".toDictionary" call. Pass "myData" directly to "data" property of your Ajax request or do the conversion at that point. That's all.

      data: $.toDictionary(saveData, "model")

      Also omit Ajax request property "dataType" and likely "contentType" as well.

      Delete
  55. Hi Robert, Thanks for this solution. I'm trying to pass the parent and its children together via json to wcf method and using the same approach. I'm new to json so got stuck while creating the json object with multiple entities. Could you please guide me what's wrong in below code....

    var OrderProcessingDetail = new Array();

    for (var i = 0; i < $("#processingDetailstable > tbody > tr").length; i++) {

    OrderProcessingDetail.push({
    "ProcessTypeCode": $.trim($("#dropdownProcessTypeCode" + i).val()), "ProcessDateFrom": $.trim($("#inputProcessDateFrom" + i).val()),
    "ProcessDateTo": $.trim($("#inputProcessDateTo" + i).val()), "AppliedBy": $.trim($("#inputAppliedBy" + i).val())
    });
    }

    var OrderItemDetail= new Array();
    OrderItemDetail.push({
    "orderHeaderID": + orderHeaderId,
    "originCode": + originCode,
    "intendedUse": + intendedUse,
    "storageCode": + storageCode,
    "isActive": + isActive
    });


    var data = new Array();
    data.push(OrderItemDetail);
    data.push(OrderProcessingDetail);


    $.ajax({
    type: "POST",
    url: serviceUrl,
    data: $.toDictionary(data),
    processdata: true,
    contentType: "application/json; charset=utf-8",
    dataType: "json",
    success: function (response) {
    var results = eval(JSON.parse(response.CreateResult));
    DisplayFaultsDialog(results.Faults);

    },
    error: function (msg) {
    alert(msg);
    }
    });

    here is my webmethod signature.....

    string CreateItems(OrderItemDetail orderItemDetail, List orderProcessingDetails);

    Many Thanks
    Puran

    ReplyDelete
    Replies
    1. I'm not saying that is the problem here as you've provided quite a bit of code, but it seems you're sending over an array while WCF expects two objects. I suggest you try changing your data array to object:


      var data = {
      orderItemDetail: OrderItemDetail,
      orderProcessingDetails: OrderProcessingDetails
      };


      This object's properties' names match WCF method parameter names so data binder should be able to match them.

      Well at least this would be the first thing to change...

      Delete
    2. Thanks Robert, I tried like this var data = '{"OrderItemDetail": ' +OrderItemDetail +', "OrderProcessingDetail":' +OrderProcessingDetail +'"}';, but while trying to send via $.toDictionary(data); getting error in trace and unable to reach to wcf webmethod....Any idea or may be wrong way to create json object?, please let me know the possible way out to resolve this.

      Delete
    3. Why didn't you try with my original code. I was creating actual object instance while you've been creating a string. This won't work anyway you look at it, as you're passing through a plain string.

      Just use my original code suggestion and create your data as I've written it.So instead making it an array, make it an object.

      Delete
    4. BTW also remove invalid (at least invalid with this request) Ajax call parameters:
      - contentType
      - dataType - you likely don't need to ask your server to provide appropriate result as you're processing it and passing such results back
      - processData - it's true by default anyway

      Delete
  56. I tried with object creation and seems to be working now. Many thanks Robert. Regards Puran

    ReplyDelete
  57. Hi Robert,

    I have an array nested in an object in a JSON string which I need deserialized at the server:

    var orderStatus = {"auth": "xxxx", "resourceType": "order.status", "idSet": "2980", "lifecycleEvent": "modified", "objects": { "orders": [ { "id": "2980", "statusId": "6" } ] }

    I use your plugin like this:

    $.ajax({url: "receiveJson", type: "POST", data: $.toDictionary(orderStatus) });

    My .net class file is:

    public class orders
    {
    public string Id { get; set; }
    public string statusId { get; set; }
    }

    public class objects
    {
    public orders orders { get; set; }
    }

    public class OrderStatus
    {
    public string clientName { get; set; }
    public string source { get; set; }
    public string auth { get; set; }
    public string resourceType { get; set; }
    public string idSet { get; set; }
    public string lifecycleEvent { get; set; }

    public objects objects { get; set; }

    }

    my controller code is: public JsonResult receiveJson(OrderStatus orderStatus)

    So the orders object is the array. It works up to creating orders as an object but id and status id in the orders object are null.

    I have no control over the JSON I will receive, it has to be in this format.

    I am new to JSON and .NET MVC. Don't know how to specify server side orders object as an array.

    ReplyDelete
    Replies
    1. Fixed it by slightly amending my server side classes:

      public class order
      {
      public string Id { get; set; }
      public string statusId { get; set; }
      }

      public class objects
      {
      public List < order > orders { get; set; }
      }

      public class OrderStatus
      {
      public string clientName { get; set; }
      public string source { get; set; }
      public string auth { get; set; }
      public string resourceType { get; set; }
      public string idSet { get; set; }
      public string lifecycleEvent { get; set; }

      public objects objects { get; set; }

      }
      So the "orders" class has been changed to "order". "objects.orders" property is amended to be a list.

      Now the jsondata is deserialized all the way down.

      Delete
    2. I'm glad you've already fixed it yourself, but I would suggest you remove your additional superfluous class objects as it only holds the list of orders. I would change the OrderStatus class to have the last property defined as:

      public IList<order> objects { get; set; }

      And likely also change properties' types of order to be integers. And you should seriously rethink your naming conventions because sometimes your using camel casing, sometimes Pascal casing and is generally a mess which will prove to be very hard to maintain on the long(er) run. I suggest you look into C# naming conventions to clean it up (http://msdn.microsoft.com/en-us/library/ms229042.aspx). This will align your code to .net Framework naming conventions so it will feel natural and self explanatory.

      Delete
    3. My previous response was written immediately after receiving yours, before I had checked my code and was therefore inaccurate, which is why I deleted it.

      I am receiving camel casing from the php app where the JSON originates and any deviation from this was from typos by me. I am also yet to check details like correct field types as my main concern was getting the deserialization to work. But yes all of this needs to be corrected.

      As for the rest, thanks for the advice and for creating this wonderful plugin which has saved me a lot of time.

      Delete
    4. I'm not sure about case sensitivity in current versions of Asp.net MVC (I understand that's what you're using), but this link on Stackoverflow (http://stackoverflow.com/a/19565393/75642) may help you with that if casing should be a problem.

      The important thing (for now) is that it works as expected. But don't collect too much technical dept for the future hence I suggest cleaning up your code.

      Delete
  58. I needed object to be serialized ordered by property name. see this, just before returning result:



    result.sort(function (a, b) {
    //debugger;
    return $.sortAlphaNumeric(a.name, b.name);
    });




    // Taken from http://stackoverflow.com/a/4340339/833846
    $.extend({
    sortAlphaNumeric: function (a, b) {
    var reA = /[^a-zA-Z]/g;
    var reN = /[^0-9]/g;

    var aA = a.replace(reA, "");
    var bA = b.replace(reA, "");

    if (aA === bA) {
    var aN = parseInt(a.replace(reN, ""), 10);
    var bN = parseInt(b.replace(reN, ""), 10);
    return aN === bN ? 0 : aN > bN ? 1 : -1;
    } else {
    return aA > bA ? 1 : -1;
    }
    }
    });

    ReplyDelete
    Replies
    1. Can you rather show an example of your current data and what you would like it to be?

      Delete
  59. very nice and good idea - thanks for posting. Your plugin worked perfectly for a complex object involving arrays.

    ReplyDelete
    Replies
    1. Thanks for your comment letting me know it solves your problem as it did mine. Without I can't know whether my plugin helped anybody or not. So thanks again Joseph.

      Delete
  60. Excellent article, your jquery plugin saved my day. :D

    ReplyDelete
  61. Late to the party, but thank you for this brilliant piece of helpful code!

    ReplyDelete
    Replies
    1. Really late, but I suppose this plugin can be used outside of Asp.net MVC too. The JSON to Dict conversion may still be useful elsewhere. So thanks.

      Delete
  62. lint is suggesting that you are missing a break statement before the default in your case statement.

    ReplyDelete

This is a fully moderated comments section. All spam comments are marked as spam without exception. Don't even try.

Note: only a member of this blog may post a comment.