Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NameFor generates incorrect name when iterating over collection in editor template

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 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.
  • 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.

like image 542
Ant P Avatar asked Sep 21 '13 15:09

Ant P


1 Answers

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)?

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:

  1. Add ItemViewModel.cshtml editor template with following code:

    @model ItemViewModel
    @Html.EditorFor(m => m.Foo)
    @Html.EditorFor(m => m.Bar) 
    
  2. Remove _ItemView.cshtml editor template.

  3. Remove [UIHint("_ItemView")] attribute from WrappingViewModel.

A little bit harder:

  1. Add ItemViewModel.cshtml editor template (same as above).

  2. 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

like image 82
Alexander Simonov Avatar answered Oct 17 '22 06:10

Alexander Simonov