DefaultModelBinder cannot deserialize .NET Diction

2019-04-29 03:25发布

问题:

I have a very simple class:

public class FilterItem
{
    public Dictionary<string, string> ItemsDictionary { get; set; }

    public FilterItem()
    {
        ItemsDictionary = new Dictionary<string, string>();
    }
}

I want to populate the data in the dictionary on the client and then pass it to my controller action as a JSON object. However no matter what I try on the client, the DefaultModelBinder does not seem to be able to deserialize it.

Here is an example javascript code to call my action:

var simpleDictionary = {"ItemsDictionary": {"1": "5", "2": "7"}};

$.ajax({ cache: false, type: "POST", data: JSON.stringify(simpleDictionary),
contentType: "application/json; charset=utf-8", 
url: "/Catalog7Spikes/GetFilteredProductsJson", success: function (data) {...});

And here is a simplified version of my action method:

[HttpPost]
public ActionResult GetFilteredProductsJson(FilterItem filterItem)
{   
    ProductsModel productsModel = new ProductsModel();
    return View("SevenSpikes.Nop.UI.Views.Products", productsModel);
}

Please note that the opposite works. When passed as a JsonResult the FilterItem object is successfully serialized and passed as a JSON object to the client. However trying to go the other way round does not work.

I read the ticket on Connect and thought that the work around would work but it does not.

Is it possible at all to deserialize a .NET dictionary using the DefaultModelBinder in ASP.NET MVC 3?

回答1:

Hanselman talks about this:

Source: http://www.hanselman.com/blog/ASPNETWireFormatForModelBindingToArraysListsCollectionsDictionaries.aspx

The DefaultModelBinder expects some less-than-optimal syntax for dictionaries. Try using this kind of syntax:

 {
 "dictionary[0]":{"Key":"a", "Value":"b"},
 "dictionary[1]":{"Key":"b", "Value":"b"}
 }

It's kind of bulky but it binds. The following works as well, but I personally prefer the above; it's shorter.

 {
 "dictionary[0].Key":"a",
 "dictionary[0].Value":"b",
 "dictionary[1].Key":"b"
 "dictionary[1].Value":"b"
 }


回答2:

UPDATE

Based upon the blog post by Jeroen (see his answer below, with the link), and a brain flash I had after re-reviewing my code, I have updated the ExtendedJsonValueProviderFactory so that it will always properly create a BackingStore for a top-level dictionary submitted via JSON.

The code is available on GitHub at https://github.com/counsellorben/ASP.NET-MVC-JsonDictionaryBinding, and a working example is at http://oss.form.vu/json-dictionary-example/.


By removing the current JsonValueProviderFactory and substituting one which can handle dictionary creation, you can bind your dictionary. First, as Keith pointed out, in your Javascript, be sure to wrap your dictionary inside of "filterItem", since this is the name of the model variable in your controller action, and for JSON, the name of the variable in the controller action must match the name of the Json element being returned. Also, when passing a class, any nested elements must match the names of the properties in the class.

Next, create an ExtendedJsonValueProviderFactory class, as follows:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.IO;
using System.Web.Script.Serialization;

public sealed class ExtendedJsonValueProviderFactory : ValueProviderFactory
{

    private void AddToBackingStore(Dictionary<string, object> backingStore, string prefix, object value)
    {
        IDictionary<string, object> d = value as IDictionary<string, object>;
        if (d != null)
        {
            foreach (KeyValuePair<string, object> entry in d)
            {
                if (entry.Key.EndsWith("Dictionary", StringComparison.CurrentCulture))
                    CreateDictionary(backingStore, entry);
                else
                    AddToBackingStore(backingStore, MakePropertyKey(prefix, entry.Key), entry.Value);
            }
            return;
        }

        IList l = value as IList;
        if (l != null)
        {
            for (int i = 0; i < l.Count; i++)
            {
                AddToBackingStore(backingStore, MakeArrayKey(prefix, i), l[i]);
            }
            return;
        }

        // primitive
        backingStore[prefix] = value;
    }

    private void CreateDictionary(Dictionary<string, object> backingStore, KeyValuePair<string, object> source)
    {
        var d = source.Value as IDictionary<string, object>;
        var dictionary = new Dictionary<string, string>();
        foreach (KeyValuePair<string, object> entry in d)
            dictionary.Add(entry.Key, entry.Value.ToString());

        AddToBackingStore(backingStore, source.Key, dictionary);
        return;
    }

    private static object GetDeserializedObject(ControllerContext controllerContext)
    {
        if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
        {
            // not JSON request
            return null;
        }

        StreamReader reader = new StreamReader(controllerContext.HttpContext.Request.InputStream);
        string bodyText = reader.ReadToEnd();
        if (String.IsNullOrEmpty(bodyText))
        {
            // no JSON data
            return null;
        }

        JavaScriptSerializer serializer = new JavaScriptSerializer();
        object jsonData = serializer.DeserializeObject(bodyText);
        return jsonData;
    }

    public override IValueProvider GetValueProvider(ControllerContext controllerContext)
    {
        if (controllerContext == null)
        {
            throw new ArgumentNullException("controllerContext");
        }

        object jsonData = GetDeserializedObject(controllerContext);
        if (jsonData == null)
        {
            return null;
        }

        Dictionary<string, object> backingStore = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
        AddToBackingStore(backingStore, String.Empty, jsonData);

        return new DictionaryValueProvider<object>(backingStore, CultureInfo.CurrentCulture);
    }

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

    private static string MakePropertyKey(string prefix, string propertyName)
    {
        return (String.IsNullOrEmpty(prefix)) ? propertyName : prefix + "." + propertyName;
    }
}

You may notice that this class is almost identical to the standard JsonValueProviderFactory class, except for the extension to build an entry into the DictionaryValueProvider of type Dictionary<string,string>. You also should notice that, in order to be processed as a dictionary, an element must have a name ending in "Dictionary" (and while I think this is a significant code smell, I cannot think of another alternative at this time ... I am open to suggestions).

Next, add the following to Application_Start in Global.asax.cs:

var j = ValueProviderFactories.Factories.FirstOrDefault(f => f.GetType().Equals(typeof(JsonValueProviderFactory)));
if (j != null)
    ValueProviderFactories.Factories.Remove(j);
ValueProviderFactories.Factories.Add(new ExtendedJsonValueProviderFactory());

This will remove the standard JsonValueProviderFactory, and replace it with our extended class.

Final step: enjoy the goodness.



回答3:

Have you tried the following?

var simpleDictionary = {"ItemsDictionary": {"1": "5", "2": "7"}};

$.ajax({ cache: false, type: "POST", data: {filterItem : JSON.stringify(simpleDictionary)},
contentType: "application/json; charset=utf-8", 
url: "/Catalog7Spikes/GetFilteredProductsJson", success: function (data) {...});


回答4:

Yesterday I had exactly the same problem while trying to post a JavaScript (JSON) dictionary to a controller action method. I created a custom model binder that processes generic dictionaries with different type arguments, both directly (in a action method parameter) or contained in a model class. I have only tested it in MVC 3.

For the details of my experiences and the source code of the custom model binder, please see my blog post at http://buildingwebapps.blogspot.com/2012/01/passing-javascript-json-dictionary-to.html



回答5:

Default model binder cannot handle list. I resolved this issue in my open source project: http://jsaction.codeplex.com and wrote an article about this issue: have a read here http://jsaction.codeplex.com/wikipage?title=AllFeatures&referringTitle=Documentation

...Asp.net MVC has built-in capabilities of transforming sent data to strong type objects. 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. The problem is that providing JSON 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 problem is that JSON object got converted by jQuery to request query string and second level property values got mangled into a form that Asp.net MVC default model binder doesn't understand...