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:
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.List<ItemViewModel>
in another property (e.g. by implementing ItemListViewModel
) 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.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.
Why does
NameFor
(and thereforeEditorFor
) exhibit this behaviour in this particular scenario when it works fine for slight variations (i.e. is it intentional and, if so, why)?
It is a bug (link) and it will be fixed with release of ASP.NET MVC 5.
Is there a simple way of working around this behaviour without any of the shortcomings of the above?
Simple:
Add ItemViewModel.cshtml
editor template with following code:
@model ItemViewModel
@Html.EditorFor(m => m.Foo)
@Html.EditorFor(m => m.Bar)
Remove _ItemView.cshtml
editor template.
[UIHint("_ItemView")]
attribute from WrappingViewModel
.A little bit harder:
Add ItemViewModel.cshtml
editor template (same as above).
Modify _ItemView.cshtml
:
@model List<ItemViewModel>
@{
string oldPrefix = ViewData.TemplateInfo.HtmlFieldPrefix;
try
{
ViewData.TemplateInfo.HtmlFieldPrefix = string.Empty;
for (int i = 0; i < Model.Count; i++)
{
var item = Model[i];
string itemPrefix = string.Format("{0}[{1}]", oldPrefix, i.ToString(CultureInfo.InvariantCulture));
@Html.EditorFor(m => item, null, itemPrefix)
}
}
finally
{
ViewData.TemplateInfo.HtmlFieldPrefix = oldPrefix;
}
}
UPDATE
In case if you don't want to add ItemViewModel.cshtml
editor template for the second option then instead of @Html.EditorFor(m => item, null, itemPrefix)
you have to write something like that:
@Html.EditorFor(m => item.Foo, null, Html.NameFor(m => item.Foo).ToString().Replace("item", itemPrefix))
@Html.EditorFor(m => item.Bar, null, Html.NameFor(m => item.Bar).ToString().Replace("item", itemPrefix))
NOTE: It's better to wrap that piece of code as extension method
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With