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.