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: };
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: ...
Possible solutions
So I basically have three options:
- 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) - I could write a custom
JQueryValueProviderFactory
class that would understand request values sent by jQuery. - 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: ...
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: });
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.
do we have any way to pass complex jSON for a GET method?
ReplyDelete@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.
ReplyDeleteBut 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.
Thanks for your comment.
ReplyDeleteThat means that the jSON object can only be of a limited size, isn't it?
@amishra: Yes, because browsers and servers have limitations related to this even though HTTP spec doesn't limit URI length.
ReplyDeleteWhat is the maximum length of URL
Also works for struts.
ReplyDeleterealy a good jobs, thanks
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.
ReplyDeleteRegards
Dave
Wow, a developer website that is tidy. Congrats.
ReplyDelete@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.
ReplyDelete@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)
I used this method this way:
ReplyDeletepublic 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?
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).
ReplyDeleteTry 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...
It worked! Many thanks :-)
ReplyDeleteHi
ReplyDeleteThis 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);
}
})
@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:
ReplyDelete$.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));
Hi
ReplyDeleteI'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!
@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:
ReplyDeletevar 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.
In using this approach...
ReplyDeleteI 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...
@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.
ReplyDeleteBasically, if I create a stand-alone model with a PersonInputModel ...
ReplyDelete[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...
@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.
ReplyDeleteno, my post did not take for some reason and did not get any error.
ReplyDelete@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
ReplyDeletefunction 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.
Oops, I meant
ReplyDelete`$.isPlainObject(thing)` returns false.
Having issues with posting a comment here. Disregard the above comment. Lost the following comment:
ReplyDeleteIf 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 });
}
}
}
};
@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@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.
ReplyDeleteIf 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.
ReplyDeletefilterList = 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".
@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):
ReplyDeletepublic 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.
Robert:
ReplyDeleteAwesome 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!
@Jorin: Thanks for the info Jorin. I appreciate it. Your code changes seem reasonable and I will include them in the plugin.
ReplyDeleteAbout 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.
Robert:
DeleteI'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.
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.
ReplyDeleteThanks.
@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.
ReplyDeleteHi,
ReplyDeleteI'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.
I forgot to write that I already added jquery and toDictionary js files in the head section in order of jquery, toDictionary files.
ReplyDeletesrc="/Scripts/jquery-1.7.min.js"
src="/Scripts/jquery.toDictionary.js"
But I got the error in the upper comment anyway.
Thank you.
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...
DeleteWhatever 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.
Great work Robert - this is an excellent little plugin that will be useful in so many places.
ReplyDeleteThanks Joe. It is very useful. I'm using it all over the place myself. :)
DeleteRobert, I tried as you mentioned in earlier post, but it is not working for me, any help will be greatly appreciated.
ReplyDeleteCLASS:
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"),
});
Robert, I am adding the CONTROLLER method again
ReplyDeletepublic 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)
}
}
Just wanted to say thanks. Works like a charm :D
ReplyDeleteHi
ReplyDeleteI 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"!
Has anyone gotten a 500 error when they try to send a string of html? For some reason it doesn't like it.
ReplyDeleteOf 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...
DeleteIf 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.
Do I have to worry if this HTML is being generated by tinyMCE that provides only the simplest of formatting?
DeleteYes 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.
DeleteBTW: 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...
DeleteI have some strange problem with your plugin when combining passing simple values and complex.
ReplyDeleteHere 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
Here is a sample project: http://www.sendspace.com/file/fyg6qs
ReplyDeleteOk, I fixed it.
ReplyDeleteFirst, 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.
Robert,
ReplyDeleteThank 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
I think that way as well. Otherwise we end up with too many customization while default functionality works just as well.
DeleteOMG I owe you a beer at least.
ReplyDeleteTHANK YOU
If you're anywhere near Dublin, Ireland (or will be in the next 3 months while I'm here) let's have one then. :)
DeleteThank you sir! I too prefer the route you took with this over tweaking the core .Net functionality.
ReplyDeleteThis worked for me : http://stackoverflow.com/questions/9610214/pass-complex-json-object-to-an-mvc-3-action
ReplyDeleteBasically 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.
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
ReplyDelete... 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
There are two ways.
DeleteFirst 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.
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.
ReplyDeleteThanks, you're welcome
DeleteI 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.
ReplyDeleteI wish that I had stumbled upon this last week. Thank you so much.
Thanks, a lot... This saved me quite some time!
ReplyDeleteSuch an old code but it still helps a lot of people thanks
ReplyDeleteThis plug-in was created some time ago now but it's worked really well for me. Thank you for sharing
ReplyDeleteThank 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
ReplyDeleteThanks Dan. I'm glad it helped.
DeleteHi Rob,
ReplyDeleteLooks 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?
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.
DeleteBut 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.
Rob,
DeleteCalling .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');
}
});
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.
Deletedata: $.toDictionary(saveData, "model")
Also omit Ajax request property "dataType" and likely "contentType" as well.
very useful. thx.
ReplyDeleteHi 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....
ReplyDeletevar 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
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:
Deletevar 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...
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.
DeleteWhy 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.
DeleteJust use my original code suggestion and create your data as I've written it.So instead making it an array, make it an object.
BTW also remove invalid (at least invalid with this request) Ajax call parameters:
Delete- 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
I tried with object creation and seems to be working now. Many thanks Robert. Regards Puran
ReplyDeleteYou're welcome.
DeleteHi Robert,
ReplyDeleteI 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.
Fixed it by slightly amending my server side classes:
Deletepublic 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.
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:
Deletepublic 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.
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.
DeleteI 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.
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.
DeleteThe 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.
I needed object to be serialized ordered by property name. see this, just before returning result:
ReplyDeleteresult.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;
}
}
});
Can you rather show an example of your current data and what you would like it to be?
Deletevery nice and good idea - thanks for posting. Your plugin worked perfectly for a complex object involving arrays.
ReplyDeleteThanks 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.
DeleteExcellent article, your jquery plugin saved my day. :D
ReplyDeleteThanks. I'm glad it did. :)
DeleteLate to the party, but thank you for this brilliant piece of helpful code!
ReplyDeleteReally 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.
Deletelint is suggesting that you are missing a break statement before the default in your case statement.
ReplyDelete