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.
Dictionary<string, string>
only. – Satisfactory