ASP.NET Wire Format for Model Binding to Arrays, Lists, Collections, Dictionaries
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!
About Scott
Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.
About Newsletter
people%5B0%5D.FirstName=George
Why are those square brackets important? Why not just look at index as just another property? So if we have this:
<input type="text" name="people.0.FirstName" value="George" />
Then it also looks nicer on the wire:
people.0.FirstName=George
But nothing in HTML FORMS says you cannot have INPUT tags with the duplicate names. In fact, browsers will submit them all and ASP.NET will even parse and make their value available like an array. The NameValueCollection class, the type of HttpRequest.Form, has a GetValues method that can support this scenario. As a result, it would be possible to support even a simpler, cleaner, meaner and leaner wire format like this:
people.FirstName=George&people.FirstName=Abraham
This is not as important when you are doing a FORM POST as when you are doing a FORM GET. In the latter case, the URL gets polluted unnecessarily. It would nice if the model binding facility could first cater to the simpler and leaner cases.
It's caused a few problems for us, and having to re-write the form elements when you delete an element is a major PITA, especially when the previous method worked flawlessly for us. But if there's valid reasoning behind it, then I may forgive you ;-)
Note that simply reintroducing the ".index" syntax won't fix collection binding for the power users, it will just provide a bandage for one unwanted behavior. The issue that we don't know what you meant to do with data - whether you meant to insert, modify, or delete a particular entry in your collection - remains, and we're working on ways of solving it. Right now the behavior we have when binding to an existing collection is to blow the entire collection away and to replace every element in it, which isn't so great for EF or Linq2Sql objects. I have a feeling that if we were to get a good answer to this problem, many of the issues that power users have with collection binding would disappear, including oddities with indexes.
You're right, I quite liked the way that collection binding used to work (once I got my head around it). I'm actually quite surprised to find out that there was that much backlash against the Beta method, I guess that makes me a "power user"..
However, it still seems that the RC binding method gives you more restrictions over the old method, on what you can do with it before dropping back to writing your own custom model binder. Getting a decent solution for dealing with "gaps" in the collection would make it much more flexible, like you say.
<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" />
could have been
<input type="text" name="Key" value="MSFT" />
<input type="text" name="CompanyName" value="Microsoft Corporation" />
<input type="text" name="Industry" value="Computer Software" />
<input type="text" name="Key" value="AAPL" />
<input type="text" name="CompanyName" value="Apple, Inc." />
<input type="text" name="Industry" value="Consumer Devices" />
and FORM variables on the server-side would have been:
Key=MSFT,AAPL
CompanyName=Microsoft Corporation,Apple, Inc.
Industry=Computer Software,Computer Devices
and URL-encoded so you could distinguish between the comma separator and the comma in "Apple, Inc."
So now when I'm writing forms with a list that have a natural primary key... I have to make up some arbitrary number.
So now when I have ajax where I can dynamically add and remove members of a list before doing a full page submit, I have to have javascript code ensure the indexes are 0 and unbroken?
I don't see how this is an improvement. Natural keys meant one object didn't depend on the group of objects. Now one object can't be written properly to be databound without knowing the other objects around it. This isn't a step backwards?
Or am I completely misunderstanding?
Pretty much, though there are workarounds (see Levi's reply to my comment).
Hopefully the RTM will have a better solution, otherwise I think MvcContrib will be getting another submission or two!
So, basically, you're going to goon up the whole syntax so that binding to EF and Linq2Sql works? So now people who aren't using EF or Linq2Sql (or who are more properly not binding directly to the generated classes) have to do work arounds?
So, basically, you're going to goon up the whole syntax so that binding to EF and Linq2Sql works? So now people who aren't using EF or Linq2Sql (or who are more properly not binding directly to the generated classes) have to do work arounds?
foreach(var item in items)
{
Html.RenderPartial("item_view", item);
}
Part of the item includes something like 'shipping id' or 'employee id' or other stuff from the db. I've been using that conveniently as the index.
Am I being dense here... but I would have to create a new class for item just so I can give it an index for the sole purpose of model binding? So:
int index = 0;
foreach(var item in items)
{
Html.RenderPartial("Item_view", new model_with_item_and_index(item, index));
index++;
}
I looked at the link Levi gave for the forums... but wow, that appears to be an amazingly ugly hack. I can understand the hidden index field being considered a hack... but it is amazingly clean compared to what his suggestion was. At least the ugly hidden index is just that, hidden in the view that I never have to care about again.
I can't see reasonably moving any of my existing Beta apps to RC1. I guess the solution is to bin deploy Beta for existing apps when I decide to start using the release version (when it comes out) for new ones. This will work yes?
@ Atif, Joe - We can't accept submissions like person.FirstName=Bob&person.FirstName=John for a multitude of reasons. Firstly, it's way, way too difficult to figure out what the nth index was, and if any property for any reason is left out of the submission, we're hosed. If any property is a Boolean property, we're hosed.
Additionally, ostensibly users are using the UI helpers - yeah, you have to fight with them a bit to make collections work, unfortunately :( - and the UI helpers won't have a clue which element you meant to reference if several of them share the same name. The only way to disambiguate this both for the UI helpers and for the binder is to make all of the field names unambiguous.
Note - if you're binding simple collection types (like collections of integers, collections of strings, etc.), you can use key=1&key=6&key=12 syntax, and we'll correctly turn key into the integer array { 1, 6, 12 }. This helps keep some simple data binding scenarios easy.
@ Jamie - No, I used Linq2Sql and EF as an example of collection binding that didn't work correctly in Beta and continues not to work correctly in RC.
The point of the changes between Beta and RC was to make binding to a new collection easier, such as if you accept an ICollection<T> or IDictionary<TKey, TValue> parameter to your action method. Binding to an existing collection is not really a supported scenario, so we didn't try to make that simple. The binder will attempt to do it, but I strongly recommend not trying to bind to an existing collection unless you understand the consequences, namely that the existing collection will be cleared and all new elements added. This was the same behavior that the Beta had, IIRC.
We're working on ways to make binding to an existing collection easier for vNext, but it was not in scope for this release.
Comments are closed.
I build my forms with sequential, unbroken indices but items can be deleted before the form is submitted. Instead of renaming the form elements, or using ajax to delete individual items and request the page again, I inherited from the DefaultModelBinder and used the following code.
Much nicer. Shame I had to rewrite so much of the DefaultModelBinder to just add this!!