An intuitive Dictionary Model Binder for ASP.NET MVC

The other day I was working on an ASP.NET MVC website and came across a need to post an array from the browser into the web app. The framework comes with something called a Model Binder that automagically converts submitted form data into action parameters of the controller. For example, if we have form submitted data such as
person.FirstName=John&person.LastName=smith
for a theoretical model class ‘Person’, and

public ActionResult SavePerson(Person person)

as the action method signature, SavePerson will be executed with the parameter equivalent to

new Person() { FirstName = "John", LastName = "Smith" }

The default model binder is pretty powerful, using reflection to dig out and assign all the fields. It also supports arrays and dictionaries, but with big limitations. The array must start at 0 and be unbroken. That is understandable for arrays, but what if you had a dictionary? Surely it can start at any position? Not so. The dictionary has even more obscure requirements, with the need to specify explicit .Key and .Value parameters in your form submission. For example:
dict[0].Key=mykey&dict[0].Value=myvalue
This represented extra work to generate the form on the client side. I just want to input something more intuitive like:
dict[mykey]=myvalue
The ASP.NET MVC framework is highly extensible. It allows you to define your own custom model binder so that’s exactly what I did. Inheriting off DefaultModelBinder, I created DefaultDictionaryBinder that overrode the BindModel method and intercepts when a IDictionary<,> class is being bound.

public class DefaultDictionaryBinder : DefaultModelBinder
{
	public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
	{
		Type modelType = bindingContext.ModelType;
		Type idictType = modelType.GetInterface("System.Collections.Generic.IDictionary`2");
		if (idictType != null)
		{
			object result = null;

			Type[] ga = idictType.GetGenericArguments();
			IModelBinder valueBinder = Binders.GetBinder(ga[1]);

			foreach (string key in bindingContext.ValueProvider.Keys)
			{
				if (key.StartsWith(bindingContext.ModelName + "[", StringComparison.InvariantCultureIgnoreCase))
				{
					int endbracket = key.IndexOf("]", bindingContext.ModelName.Length + 1);
					if (endbracket == -1)
						continue;

					object dictKey;
					try
					{
						dictKey = Convert.ChangeType(key.Substring(bindingContext.ModelName.Length + 1, endbracket - bindingContext.ModelName.Length - 1), ga[0]);
					}
					catch (FormatException)
					{
						continue;
					}

					ModelBindingContext innerBindingContext = new ModelBindingContext()
					{
						Model = null,
						ModelName = key.Substring(0, endbracket + 1),
						ModelState = bindingContext.ModelState,
						ModelType = ga[1],
						ValueProvider = bindingContext.ValueProvider
					};
					object newPropertyValue = valueBinder.BindModel(controllerContext, innerBindingContext);

					if (result == null)
						result = CreateModel(controllerContext, bindingContext, modelType);

					if (!(bool)idictType.GetMethod("ContainsKey").Invoke(result, new object[] { dictKey }))
						idictType.GetProperty("Item").SetValue(result, newPropertyValue, new object[] { dictKey });
				}
			}

			return result;
		}
		return base.BindModel(controllerContext, bindingContext);
	}
}

To use, you have to override the default model binder. In global.asax.cs in Application_Start(), add the line:

ModelBinders.Binders.DefaultBinder = new DefaultDictionaryBinder();

The code is very flexible, only requiring the dictionary key to be of a basic type convertible from string, ie. Dictionary or Dictionary. The value can be any object that is able to be bound by the default model binder.
An example follows:
If your form input is
persons[3].FirstName=John&persons[3].LastName=Smith&persons[4].FirstName=Jane&persons[4].LastName=Doe&
and our action signature

public ActionResult SavePersons(Dictionary<int, Person> persons)

the persons parameter would be

new Dictionary<int, Person>() {
{ 3, new Person() {"John", "Smith"} },
{ 4, new Person() {"Jane", "Doe"} },
}

Simple and intuitve.

Tags: ,

2 Responses to “An intuitive Dictionary Model Binder for ASP.NET MVC”

  1. prokofyev says:

    Very useful code. Unfortunately in MVC 2 they changed ValueProvider’s type so Keys property is no more accessible.

    The IValueProvider interface replaces all uses of IDictionary (http://www.asp.net/learn/whitepapers/what-is-new-in-aspnet-mvc)

  2. Loune says:

    @prokofyev I actually have a MVC 2 version. As you said they changes the interface so you can no longer enumerate the keys, so in the new version which I’ll post up in the weekend, I just loop through Request.QueryString Request.Form and Request.Files.

Leave a Reply