Post an Array of Objects via JSON to ASP.Net MVC3
Asked Answered
D

6

65

I'm looking for a solution to POSTing an array of objects to MVC3 via JSON.

Example code I'm working off of: http://weblogs.asp.net/scottgu/archive/2010/07/27/introducing-asp-net-mvc-3-preview-1.aspx

JS:

var data = { ItemList: [ {Str: 'hi', Enabled: true} ], X: 1, Y: 2 };

$.ajax({
    url: '/list/save',
    data: JSON.stringify(data),
    success: success,
    error: error,
    type: 'POST',
    contentType: 'application/json, charset=utf-8',
    dataType: 'json'
});

ListViewModel.cs:

public class ListViewModel
{
    public List<ItemViewModel> ItemList { get; set; }
    public float X { get; set; }
    public float Y { get; set; }
}

ItemViewModel.cs:

public class ItemViewModel
{
    public string Str;   // originally posted with: { get; set; }
    public bool Enabled; // originally posted with: { get; set; }
}

ListController.cs:

public ActionResult Save(ListViewModel list)
{
    // Do something
}

The result of this POST:

list is set, to a ListViewModel
Its X and Y properties are set
The underlying ItemList property is set
The ItemList contains one item, as it should
The item in that ItemList is uninitialized. Str is null and Enabled is false.

Put another way, this is what I get from MVC3's model binding:

list.X == 1
list.Y == 2
list.ItemList != null
list.ItemList.Count == 1
list.ItemList[0] != null
list.ItemList[0].Str == null

It would appear the MVC3 JsonValueProvider is not working for complex objects. How do I get this to work? Do I need to modify the existing MVC3 JsonValueProvider and fix it? If so, how do I get at it and replace it in an MVC3 project?

Related StackOverflow questions I've already pursued to no avail:

Asp.net Mvc Ajax Json (post Array) Uses MVC2 and older form-based encoding - that approach fails with an object that contains an array of objects (JQuery fails to encode it properly).

Post an array of complex objects with JSON, JQuery to ASP.NET MVC Controller Uses a hack I'd like to avoid where the Controller instead receives a plain string which it then manually deserializes itself, rather than leveraging the framework.

MVC3 RC2 JSON Post Binding not working correctly Didn't have his content-type set - it's set in my code.

How to post an array of complex objects with JSON, jQuery to ASP.NET MVC Controller? This poor guy had to write a JsonFilter just to parse an array. Another hack I'd prefer to avoid.

So, how do I make this happen?

Decoction answered 25/1, 2011 at 2:39 Comment(3)
Update: I've dug into the JsonValueProviderFactory. It correctly consumes the "str" property and adds it to the backing Dictionary as Key="ItemList[0].Str", Value="hi", which looks correct. ModelBinder?Decoction
You sir, just received an upvote and a star for your excellent execution of a stack overflow-question! :)China
Same here, this was the one finally made it work for me. Thanks!Tuttifrutti
D
30

The problem was that the properties in the models that were in the List did not have get/set on their public properties. Put another way, MVC3's automatic JSON binding only works on object properties that have get and set.

This will not bind:

public string Str;

This will bind:

public string Str { get; set; }
Decoction answered 2/2, 2011 at 23:53 Comment(3)
I would just like to say: for (;;) { Console.WriteLine("Thank you, Chris!"); }Lamarlamarck
Chris, I like how you put everything in another way. +1 ;)Fecund
Do note that public string Str; is not a property but merely a field. "Fields are ordinary member variables or member instances of a class. Properties are an abstraction to get and set (field) values." Why JsonValueProvider refused to update fields is another question altogether!Autogiro
B
43

In addition to { get; set; }, these are some of the conditions for JSON Binding Support:

  1. This is new feature in ASP.NET MVC 3 (See “JavaScript and AJAX Improvements“).
  2. The JSON object’s strings (‘X’, ‘Y’, ‘Str’, and ‘Enabled’) must match ViewModel object’s properties.
  3. ViewModel object’s properties must have { get; set; } method.
  4. Must specify Content Type as “application/json” in the request.
  5. If it's still not working, check the JSON string to make sure it's valid one.

Read more at my post.

Hope that helps!

Betel answered 21/3, 2011 at 4:54 Comment(2)
Thanks for the details on this. Turns out I had and ajax call that didn't specify the 'application/json' content type, and everything worked exception populating the array item properties!Different
Absolute, clear and concise overview of the requirements for json biding support to work out of the box w/asp.net mvc. A valuable reference and great answer.Stylish
D
30

The problem was that the properties in the models that were in the List did not have get/set on their public properties. Put another way, MVC3's automatic JSON binding only works on object properties that have get and set.

This will not bind:

public string Str;

This will bind:

public string Str { get; set; }
Decoction answered 2/2, 2011 at 23:53 Comment(3)
I would just like to say: for (;;) { Console.WriteLine("Thank you, Chris!"); }Lamarlamarck
Chris, I like how you put everything in another way. +1 ;)Fecund
Do note that public string Str; is not a property but merely a field. "Fields are ordinary member variables or member instances of a class. Properties are an abstraction to get and set (field) values." Why JsonValueProvider refused to update fields is another question altogether!Autogiro
G
29

That's strange. I am unable to reproduce your behavior. Here's my setup (ASP.NET MVC 3 RTM):

Model:

public class ItemViewModel
{
    public string Str { get; set; }
    public bool Enabled { get; set; }
}

public class ListViewModel
{
    public List<ItemViewModel> ItemList { get; set; }
    public float X { get; set; }
    public float Y { get; set; }
}

Controller:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View();
    }

    [HttpPost]
    public ActionResult Save(ListViewModel list)
    {
        return Json(list);
    }
}

View:

@{
    ViewBag.Title = "Home Page";
}

<script type="text/javascript">
    $(function () {
        var data = { ItemList: [{ Str: 'hi', Enabled: true}], X: 1, Y: 2 };

        $.ajax({
            url: '@Url.Action("save", "home")',
            data: JSON.stringify(data),
            type: 'POST',
            contentType: 'application/json',
            dataType: 'json',
            success: function (result) {
                alert(result.ItemList[0].Str);
            }
        });
    });
</script>

Running this alerts "hi" and inside the Save action everything is correctly initialized.

And just for the record what doesn't work are Dictionaries. I've opened a ticket about the issue.

Gyatt answered 25/1, 2011 at 8:26 Comment(5)
Strange. Thanks for looking into this. For what it's worth, I'm using the version of MVC here: asp.net/mvc/mvc3 - It's possible we're using different versions of MVC, though it would be strange if this got WORSE in the final version. I'll try to isolate things a little better and verify my question's assertions before marking this answered or not.Decoction
I found the issue. The inner model was using public properties that did not have get/set. I'll fix the question to expand on the problem.Decoction
EVERYONE: Note this line >>> contentType: 'application/json'Pennsylvanian
+1: Thanks for going out of your way on this one (with the code examples). It helped me, and I wasn't even the OP.Hanseatic
I like this pattern for doing json requests and its nice to see it reproduced so clearly; plain and simple, straightforward and easy to parse. I could rly use a resharper macro for quickly adding the pieces for a new end to end json call; complete w/view model, as I seem to be using this pattern more and more lately.Stylish
L
3

I had a similar issue, and found that for a complex object, the numeric values were getting missed. They were coming in as zeros. i.e.

    var person = {
        Name: "john",
        Age: 9
    }

was being received by MVC controller as a Person object where the properties were being populated as Name=John and Age=0.

I then made the Age value in Javascript to be string... i.e.

    var person = {
        Name: "john",
        Age: "9"
    }

And this came through just fine...

Lamere answered 18/9, 2011 at 12:2 Comment(0)
O
0

Its because the MVC binders kind of suck. However, they do work pretty well if all JSON values come over as a string.

In JS if you do this

var myObject = {thisNumber:1.6};

myObject.thisNumber=myObject.thisNumber-.6;

It will evaluate to 1 not to 1.0

So when you sent it over to the server it will try to bind to a float of that name and it will not find it since it came over as 1 instead of 1.0. Its very lame and crazy that MS engineers did not come up with a default solution to this. I find if you string everything the bindings are smart enough to find things.

So before sending the data over run it though a stringifier that will also convert all values to strings.

Oui answered 9/1, 2012 at 17:55 Comment(0)
B
0

All previous answers were great to point me to solution of the similar problem. I had to POST x-www-form-urlencoding instead of application/json (default option if contentType parameter is missing) to be able to pass __RequestVerificationToken and simultaneously faced with problem when object properties being in the array do not bind their values. The way to solve the issue is to understand internal work of MVC model binder.

So, basically when you need to supply verification token you are restricted with validation attribute. And you must provide the token as the parameter not as a part of the JSON-object you are sending. If you would not use ValidateAntiForgeryToken, you could get along with JSON.stringify. But if you would, you could not pass the token.

I sniffed traffic to backend when ContentType was x-www-form-urlencoding and I remarked that my array of complex objects was serialized to something like that: klo[0][Count]=233&klo[0][Blobs]=94. This array initially was a part of root object, let's say some model. It looked like that: model.klo = [{ Count: 233, Blobs: 94}, ...].

At the backend side this klo property was creating by MVC binder with the same elements count that I sent. But these elements itself did not obtain values for their properties.

SOLUTION

To deal with this I excluded klo property from the model object at the client side. In the ajax function I wrote this code:

data: $.param(model) + "&" + arrayOfObjectsToFormEncoding("klo", [{ Count: 233, Blobs: 94}, ...])
....

    function arrayOfObjectsToFormEncoding (modelPropertyName, arrayOfObjects) {
        var result = "";
        if (arrayOfObjects && typeof arrayOfObjects == "object") {
            for (var i = 0; i < arrayOfObjects.length; i++) {
                var obj = arrayOfObjects[i];
                if (obj) {
                    for (var p in obj) {
                        if (obj.hasOwnProperty(p)) {
                            result += encodeURIComponent(modelPropertyName + "[" + i + "]." + p) + "=" + encodeURIComponent(obj[p]) + "&";
                        }
                    }
                }
            }
        }

        if (result[result.length - 1] == "&") {
            result = result.substr(0, result.length - 1);
        }

        return result;
    }

The function transforms array of complex object into form that is recognized by MVC-binder. The form is klo[0].Count=233&klo[0].Blobs=94.

Boomkin answered 15/1, 2015 at 18:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.