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
[csharp]public ActionResult SavePerson(Person person)[/csharp]
as the action method signature, SavePerson will be executed with the parameter equivalent to
[csharp]new Person() { FirstName = "John", LastName = "Smith" }[/csharp]
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.
The code is now up at github: DefaultDictionaryBinder.cs. Note that if you are using this on ASP.NET MVC 1, please define the macro ASPNETMVC1
. If you are using it with MVC 2 or MVC 3, it should work as is.
To use, you have to override the default model binder. In global.asax.cs in Application_Start(), add the line:
[csharp]
ModelBinders.Binders.DefaultBinder = new DefaultDictionaryBinder();
[/csharp]
The code is very flexible, only requiring the dictionary key to be of a basic type convertible from string, ie. Dictionary
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
[csharp]public ActionResult SavePersons(Dictionary<int, Person> persons)[/csharp]
the persons parameter would be
[csharp]
new Dictionary<int, Person>() {
{ 3, new Person() {"John", "Smith"} },
{ 4, new Person() {"Jane", "Doe"} },
}
[/csharp]
Simple and intuitive.
Download: DefaultDictionaryBinder.cs Simple Example Project (ASP.NET MVC 3 required)
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)
@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.
The code is now updated for MVC 2 and MVC 3. See this post.
Worked like a charm. Thanks a bunch!
Dude, this is seriously cool. I thought I was going to have to revert to some really spaghetti like code before I came across your post.
Thanks a million !
good work , tanx
Very useful code
It support nested dictionaries with Nullable generic types
(such as Dictionary<string, Dictionary> ).
However it seems that an attempt to assign invalid value to int? triggers not one but multiple duplicated errors in related ModelErrorCollection.
Anyway very good code.
Very useful, thanks a lot.
As a side note, I noticed that when I try to use an IDictionary instead of a Dictionary in my model, the GetInterface() cast is unsuccessful…
I think what in mvc 4 its much more easy http://msdn.microsoft.com/en-us/library/system.web.mvc.modelbinderdictionary%28v=vs.108%29.aspx
Properties of type IDictionary can be supported by replacing the declaration of idictType with
Type idictType = null;
if (modelType.IsGenericType && modelType.GetGenericTypeDefinition() == typeof(IDictionary))
{
idictType = modelType;
}
else
{
idictType = modelType.GetInterface(“System.Collections.Generic.IDictionary`2”);
}
A very helpful class. Thank you!
Do note that Microsoft does now support using the key syntax with the default model binder (in MVC 5), so this trick is no longer necessary. See the following unit test in the repo:
https://github.com/ASP-NET-MVC/aspnetwebstack/blob/master/test/System.Web.Mvc.Test/Test/DefaultModelBinderTest.cs#L224