Levi Broderick, a Senior Developer on ASP.NET MVC and all around smart dude, posted some details to an internal Microsoft mailing list today and I thought it was worth sharing. Levi agreed and I've expanded on it some. Phil blogged some about Model Binding to a List last October.
The default model binder will handle a number of collection types for you, if you play by the rules and make your HTML form elements follow a certain format.
If the method signature looks like this:
public ActionResult Blah(Person[] people) { // ...}
And we are given this input in our HTML:
<input type="text" name="people[0].FirstName" value="George" /><input type="text" name="people[0].LastName" value="Washington" /><input type="text" name="people[1].FirstName" value="Abraham" /><input type="text" name="people[1].LastName" value="Lincoln" /><input type="text" name="people[3].FirstName" value="Thomas" /><input type="text" name="people[3].LastName" value="Jefferson" />
Which turns into this as a HTTP POST:
people%5B0%5D.FirstName=George&people%5B0%5D.LastName=Washington&people%5B1%5D.FirstName=Abraham&people%5B1%5D.LastName=Lincoln&people%5B3%5D.FirstName=Thomas&people%5B3%5D.LastName=Jefferson
Which is basically:
people[0].FirstName = "George"people[0].LastName = "Washington"people[1].FirstName = "Abraham"people[1].LastName = "Lincoln"people[3].FirstName = "Thomas"people[3].LastName = "Jefferson"
Then it will be just as if we had written this in code:
people = new Person[] { new Person() { FirstName = "George", LastName = "Washington" }, new Person() { FirstName = "Abraham", LastName = "Lincoln" }};
The way that we read in the properties is by looking for parameterName[index].PropertyName. The index must be zero-based and unbroken. In the above example, because there was no people[2], we stop after Abraham Lincoln and don’t continue to Thomas Jefferson.
If the signature looks like this:
public ActionResult Blah(IDictionary<string, Company> stocks) { // ...}
And we are given this in HTML:
<input type="text" name="stocks[0].Key" value="MSFT" /><input type="text" name="stocks[0].Value.CompanyName" value="Microsoft Corporation" /><input type="text" name="stocks[0].Value.Industry" value="Computer Software" /><input type="text" name="stocks[1].Key" value="AAPL" /><input type="text" name="stocks[1].Value.CompanyName" value="Apple, Inc." /><input type="text" name="stocks[1].Value.Industry" value="Consumer Devices" />
Which like this:
stocks[0].Key = "MSFT"stocks[0].Value.CompanyName = "Microsoft Corporation"stocks[0].Value.Industry = "Computer Software"stocks[1].Key = "AAPL"stocks[1].Value.CompanyName = "Apple, Inc."stocks[1].Value.Industry = "Consumer Devices"
Then it will be just as if we had written:
stocks = new Dictionary<string, Company>() { { "MSFT", new Company() { CompanyName = "Microsoft Corporation", Industry = "Computer Software" } }, { "AAPL", new Company() { CompanyName = "Apple, Inc.", Industry = "Consumer Devices" } }};
The way that we read in the keys is by looking for parameterName[index].Key, and the way that we read in the values is by looking for parameterName[index].Value. If the key or value type is a complex type (like Company, in the above example), then we treat parameterName[index].Key or parameterName[index].Value as the entire field prefix and start appending the .PropertyName suffix to the end of it. The index must also be zero-based and unbroken, as mentioned previously.
Parameters of type IEnumerable<T>, ICollection<T>, IList<T>, T[], Collection<T>, and List<T> are bound using the first syntax. Parameters of type IDictionary<TKey, TValue> and Dictionary<TKey, TValue> are bound using the second syntax.
Of course, as with most of ASP.NET MVC, if you don't like this behavior you're welcome to change it by writing your own binders for specific types or by pulling the information from a FormCollection directly and doing your own thing.
Levi adds:
FWIW – you don’t need the bracket notation if you’re submitting simple types to the server. That is, if your request contains key=foo&key=bar&key=baz, we’ll correctly bind that to an IEnumerable<T>, IList<T>, ICollection<T>, T[], Collection<T>, or List<T>. In the first sentence in this paragraph, "simple type" means a type for which TypeDescriptor.GetConverter(typeof(T)).CanConvertFrom(typeof(string)) returns true. This makes a handful of cases simpler.
Thanks to Levi for the nitty gritty!
Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. I am a failed stand-up comic, a cornrower, and a book author.
protected virtual List<string> SubIndexNames(ModelBindingContext context, string prefix) { var list = new List<string>(); var subIndexPattern = new Regex( "^" + prefix + @"\[[^\]]+\]"); // "[" then one or more not "]", then "]" foreach( var key in context.ValueProvider.Keys ) { var match = subIndexPattern.Match( key ); if( match.Success && !list.Contains( match.Value ) ) list.Add( match.Value ); } return list; }
people%5B0%5D.FirstName=George
<input type="text" name="people.0.FirstName" value="George" />
people.0.FirstName=George
people.FirstName=George&people.FirstName=Abraham
Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.