views:

2245

answers:

5

I'm using jQuery to make an Ajax call using an Http Post in ASP.NET MVC. I would like to be able to pass a Dictionary of values.

The closest thing I could think of was to pass in a multi-dimensional array of strings, but the result that actually gets passed to the ActionResult method is a single dimensional string array containing a string concatenation of the "key/value" pair.

For instance the first item in the below "values" array contains the below value:

"id,200"

Here's an example of my ActionResult method:

public ActionResult AddItems(string[] values)
{
    // do something
}

Here's an example of how I'm calling the method from jQuery:

$.post("/Controller/AddItems",
    {
        values: [
            ["id", "200"],
            ["FirstName", "Chris"],
            ["DynamicItem1", "Some Value"],
            ["DynamicItem2", "Some Other Value"]
        ]
    },
    function(data) { },
    "json");

Does anyone know how to pass a Dictionary object from jQuery to the ActionResult method instead of an Array?

I would really like to define my ActionResult like this:

public ActionResult AddItems(Dictionary<string, object> values)
{
    // do something
}

Any suggestions?

UPDATE: I tried passing in a comma within the value and it basically just makes it impossible to actually parse the key/value pair using string parsing.

Pass this:

values: [
    ["id", "200,300"],
    ["FirstName", "Chris"]
]

results in this:

values[0] = "id,200,300";
values[1] = "FirstName,Chris";
+1  A: 

I think you're right J.W. Javascript doesn't understand the model I don't think.

I think you'll probably need to pass the members individualy.

griegs
I can't pass the data members individually as parameters since they will each be determined dynamically on the client.
Chris Pietschmann
If you're doing a post then can't you use UpdateModel or even the FormCollection to get your values?That would only work if your values are in textboxes which at this point I don't know about.
griegs
I guess I could use the FormCollection to get the values, but that makes it a little more difficult to unit test the ActionResult. I'm finding that passing in JSON is probably my best/easiest option.
Chris Pietschmann
Chris are you certain that UpdateModel won't work? If you set the pages model up such that it includes the values you are trying to pass back and set them as properties then you should be able to get them easily and that would also then solve the unit test issue.
griegs
griegs, yeah but the values wont translate to a Model object's properties. The key/value pairs are completely dynamically determined on the client, and are always going to be different.
Chris Pietschmann
Hmmm, tricky. In the past I used JS to place the data into textboxes before posting back. Seemed to work ok. :)
griegs
+1  A: 

It's possible with custom model binders or filters. Behind the scenes - you will have to do it manually anyway (Request.Form, parse strings, create dictionary tralala), but at least - your controller will be clean and code will be reusable for another actions.

Arnis L.
+1  A: 

I don't think it's possible to pass in a Dictionary from jQuery/Ajax to an ActionResult method via an Http Post. One thing I figured out that seems to be the easiest to work with is to pass in a JSON object and then parse that out into a Dictionary.

Here's the modified version of of the above calling "$.post" from jQuery that sends JSON as a pseudo-Dictionary:

$.post("/Controller/AddItems",
    {
        values: Sys.Serialization.JavaScriptSerializer.serialize(
                {
                    id: 200,
                    "name": "Chris"
                }
            )
    },
    function(data) { },
    "json");

The "Sys.Serialization.JavaScriptSerializer.serialize" function is a method of the ASP.NET AJAX JavaScript library.

Here's the modified version of the above ActionResult method:

public ActionResult AddItems(Dictionary<string, object> values)
{
    // Must Reference "System.Web.Extensions" in order to use the JavaScriptSerializer
    var json = new System.Web.Script.Serialization.JavaScriptSerializer();
    var data = json.Deserialize<Dictionary<string, string>>(routeValues);

    // do something
}

I think this makes it much easier to Unit Test by passing JSON, instead of using the Form Collection to send/retrieve the collection of key/value pairs. Also, it's easier to get working than figuring out how to build a custom IModelBinder, and a custom IModelBinder might cause issues with other ActionResult methods when this is the only one I need to do this.

Chris Pietschmann
Chris, see my comment above. I still think you are going about this the hard way. I'm not 100% sure but I have this niggling feeling that you are .
griegs
A: 

DefaultModelBinder is able to bind your POST to array or dictionary. For example:

for arrays:

public ActionResult AddItems(string[] values)

$.post("/Controller/AddItems", { values: "values[0]=200&values[1]=300" },
    function(data) { }, "json");

or:

$.post("/Controller/AddItems", { values: "values=200&values=300" },
    function(data) { }, "json");

for dictionaries:

public ActionResult AddItems(Dictionary<string, object> values)

$.post("/Controller/AddItems", {
    values: "values[0].Key=value0&values[0].Value=200&values[1].Key=value1&values[1].Value=300" }, function(data) { }, "json");

UPDATED:

If your values are in HTML inputs then in jQuery you can do something like this:

var postData = $('input#id1, input#id2, ..., input#idN").serialize();
// or
var postData = $('input.classOfYourInputs").serialize();

$.post("/Controller/AddItems", { values: postData }, function(data) { }, "json");

UPDATED:

Also check this: Scott Hanselman's ComputerZen.com - ASP.NET Wire Format for Model Binding to Arrays, Lists, Collections, Dictionaries

eu-ge-ne
I tried your Dictionary example, but it doesn't end up populating the "values" parameter; it just ends up being an empty dictionary.
Chris Pietschmann
Did you try Dictionary<string, string> values instead Dictionary<string, object>. Also check if your values[n] index is zero based and unbroken. Check http://stackoverflow.com/questions/1031416/asp-net-mvc-model-binding-a-set-of-dynamically-generated-checkboxes-how-to/1031694#1031694 also
eu-ge-ne
+3  A: 

At last I figured it out!! Thanks for the suggestions everyone! I finally figured out the best solution is to pass JSON via the Http Post and use a custom ModelBinder to convert the JSON to a Dictionary. One thing I did in my solution is created a JsonDictionary object that inherits from Dictionary so that I can attach the custom ModelBinder to the JsonDictionary type, and it wont cause any conflicts in the future if I use Dictionary as a ActionResult parameter later on for a different purpose than JSON.

Here's the final ActionResult method:

public ActionResult AddItems([Bind(Include="values")] JsonDictionary values)
{
    // do something
}

And the jQuery "$.post" call:

$.post("/Controller/AddItems",
{
    values: Sys.Serialization.JavaScriptSerializer.serialize(
            {
                id: 200,
                "name": "Chris"
            }
        )
},
function(data) { },
"json");

Then the JsonDictionaryModelBinder needs to be registered, I added this to the Application_Start method within the Global.asax.cs:

protected void Application_Start()
{
    ModelBinders.Binders.Add(typeof(JsonDictionary), new JsonDictionaryModelBinder());
}

And, finally here's the JsonDictionaryModelBinder object and JsonDictionary object I created:

public class JsonDictionary : Dictionary<string, object>
{
    public JsonDictionary() { }

    public void Add(JsonDictionary jsonDictionary)
    {
        if (jsonDictionary != null)
        {
            foreach (var k in jsonDictionary.Keys)
            {
                this.Add(k, jsonDictionary[k]);
            }
        }
    }
}

public class JsonDictionaryModelBinder : IModelBinder
{
    #region IModelBinder Members

    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        if (bindingContext.Model == null) { bindingContext.Model = new JsonDictionary(); }
        var model = bindingContext.Model as JsonDictionary;

        if (bindingContext.ModelType == typeof(JsonDictionary))
        {
            // Deserialize each form/querystring item specified in the "includeProperties"
            // parameter that was passed to the "UpdateModel" method call

            // Check/Add Form Collection
            this.addRequestValues(
                model,
                controllerContext.RequestContext.HttpContext.Request.Form,
                controllerContext, bindingContext);

            // Check/Add QueryString Collection
            this.addRequestValues(
                model,
                controllerContext.RequestContext.HttpContext.Request.QueryString,
                controllerContext, bindingContext);
        }

        return model;
    }

    #endregion

    private void addRequestValues(JsonDictionary model, NameValueCollection nameValueCollection, ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        foreach (string key in nameValueCollection.Keys)
        {
            if (bindingContext.PropertyFilter(key))
            {
                var jsonText = nameValueCollection[key];
                var newModel = deserializeJson(jsonText);
                // Add the new JSON key/value pairs to the Model
                model.Add(newModel);
            }
        }
    }

    private JsonDictionary deserializeJson(string json)
    {
        // Must Reference "System.Web.Extensions" in order to use the JavaScriptSerializer
        var serializer = new System.Web.Script.Serialization.JavaScriptSerializer();
        return serializer.Deserialize<JsonDictionary>(json);
    }
}
Chris Pietschmann