Suppose I have a view typed to a collection, e.g. a List<ItemViewModel>:
@model List<ItemViewModel>
@for(int i = 0; i < Model.Count; i++)
{
@Html.EditorFor(m => m[i].Foo)
@Html.EditorFor(m => m[i].Bar)
}
Foo and Bar are simply string properties.
This generates HTML name attributes of the form [i].Foo and [i].Bar, which, of course, is correct and binds correctly when posted in a form.
Now suppose, instead, that the above view is an editor template, which is rendered like so (where Model.Items is a List<ItemViewModel>):
@model WrappingViewModel
@Html.EditorFor(m => m.Items)
All of a sudden, the names generated within the editor template are of the form - for example - Items.[i].Foo. The default model binder can't bind this as it expects the form Items[i].Foo.
This works fine in the first scenario - where the view is not an editor template - and also works fine where the collection is a property, rather than the entire model:
@Html.EditorFor(m => m.Items[i].Foo)
It only fails when the model itself is a collection and the view is an editor template.
There are a few ways of working around this, none of which are ideal:
- Type the editor template to an individual instance of
ItemViewModel- this is no good as the actual template in question contains additional markup for adding to/removing from the collection. I need to be able to work with the entire collection inside the template. - Wrap the
List<ItemViewModel>in another property (e.g. by implementingItemListViewModel) and pass that to the template - this is not ideal, either, as this is an enterprise application that I would rather not clutter with superfluous wrapping view models. - Generate the markup for the inner editor templates manually to produce the correct name - this is what I am currently doing but I would rather avoid it as I lose the flexibility of HtmlHelpers.
So, the question: Why does NameFor (and therefore EditorFor) exhibit this behaviour in this particular scenario when it works fine for slight variations (i.e. is it intentional and, if so, why)? Is there a simple way of working around this behaviour without any of the shortcomings of the above?
As requested, full code to reproduce:
Models:
public class WrappingViewModel
{
[UIHint("_ItemView")]
public List<ItemViewModel> Items { get; set; }
public WrappingViewModel()
{
Items = new List<ItemViewModel>();
}
}
public class ItemViewModel
{
public string Foo { get; set; }
public string Bar { get; set; }
}
Controller action:
public ActionResult Index()
{
var model = new WrappingViewModel();
model.Items.Add(new ItemViewModel { Foo = "Foo1", Bar = "Bar1" });
model.Items.Add(new ItemViewModel { Foo = "Foo2", Bar = "Bar2" });
return View(model);
}
Index.cshtml:
@model WrappingViewModel
@using (Html.BeginForm())
{
@Html.EditorFor(m => m.Items)
<input type="submit" value="Submit" />
}
_ItemView.cshtml (editor template):
@model List<ItemViewModel>
@for(int i = 0; i < Model.Count; i++)
{
@Html.EditorFor(m => m[i].Foo)
@Html.EditorFor(m => m[i].Bar)
}
Name attributes for Foo and Bar inputs will be of the form Model.[i].Property and will not bind back when posted to an action method with the signature ActionResult Index(WrappingViewModel). Note that, as mentioned above, this works fine if you iterate over Items in the main view or if you get rid of WrappingViewModel, make the top-level model a List<ItemViewModel> and iterate over Model directly. It only fails for this specific scenario.