9

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.

4

1 回答 1

5

为什么NameFor(并且因此EditorFor)在这个特定场景中表现出这种行为,当它适用于轻微的变化时(即它是故意的,如果是的话,为什么)?

这是一个错误(链接),它将随着 ASP.NET MVC 5 的发布而得到修复。

是否有一种简单的方法可以解决这种行为而没有上述任何缺点?

简单的:

  1. ItemViewModel.cshtml使用以下代码添加编辑器模板:

    @model ItemViewModel
    @Html.EditorFor(m => m.Foo)
    @Html.EditorFor(m => m.Bar) 
    
  2. 删除_ItemView.cshtml编辑器模板。

  3. 从 中删除[UIHint("_ItemView")]属性WrappingViewModel

有点难:

  1. 添加ItemViewModel.cshtml编辑器模板(同上)。

  2. 修改_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;
        }
    }
    

更新

如果您不想为第二个ItemViewModel.cshtml选项添加编辑器模板,那么您必须编写类似的内容:@Html.EditorFor(m => item, null, itemPrefix)

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

注意:最好将那段代码包装为扩展方法

于 2013-09-25T10:47:52.843 回答