21

Suppose I have a simple model to explain the purpose:

public class Category
{
    ...
    public IEnumerable<Product> Products { get; set; }
}

View:

@model Category
...
<ul>
    @Html.EditorFor(m => m.Products)
</ul>

EditorTemplate:

@model Product
...
<li>
    @Html.EditorFor(m => m.Name)
</li>

Note that I don't have to define the EditorTemplate for IEnumerable<Product>, I can only create it for the Product model and MVC framework is smart enough to use its own template for IEnumerable. It iterates through my collection and calls my EditorTemplate.

The output html will be something like this

...
<li>
    <input id="Products_i_Name" name="Products[i].Name" type="text" value="SomeName">
</li>

which I can post to my controller after all.

But why doesn't the MVC do the trick when I define EditorTemplate with a template name?

@Html.EditorFor(m => m.Products, "ProductTemplate")

In that case I have to change the type of the property to IList<Product>, iterate through the collection by myself and call the EditorTemplate

@for (int i = 0; i < Model.Products.Count; i++)
{
    @Html.EditorFor(m => m.Products[i], "ProductTemplate")
}

which seems kind of dirty workaround to me. Is it any other, cleaner solution to do this?

Ryan Kohn
  • 11,921
  • 10
  • 50
  • 80
Zabavsky
  • 12,490
  • 7
  • 51
  • 75
  • possible duplicate of [Correct, idiomatic way to use custom editor templates with IEnumerable models in ASP.NET MVC](http://stackoverflow.com/questions/25333332/correct-idiomatic-way-to-use-custom-editor-templates-with-ienumerable-models-in) – GSerg Apr 13 '15 at 12:38
  • Not sure how I missed this question when searching before creating [that question](http://stackoverflow.com/questions/25333332/correct-idiomatic-way-to-use-custom-editor-templates-with-ienumerable-models-in) of mine. Chronologically my question should be closed as a duplicate of this one, but I believe it should be the other way round because there is a workaround after all. – GSerg Apr 13 '15 at 12:42
  • 2
    @GSerg, thanks for sharing your answer. I can't say I like any of the suggested workarounds better than a simple `for` loop through the collection. Sorry for not being clear with my question, but I wasn't looking for the workarounds to this issue, but for the clean and (I was hoping) the only right way to accomplish this. That's why I cannot say the answers to your question solved my problem, even though the questions themselves are very similar. – Zabavsky Apr 13 '15 at 13:34

3 Answers3

16

There, now I only owe Darin 9999 beers.

    public static MvcHtmlString EditorForMany<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, IEnumerable<TValue>>> expression, string templateName = null) where TModel : class
    {
        StringBuilder sb = new StringBuilder();

        // Get the items from ViewData
        var items = expression.Compile()(html.ViewData.Model);
        var fieldName = ExpressionHelper.GetExpressionText(expression);
        var htmlFieldPrefix = html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix;
        var fullHtmlFieldPrefix = String.IsNullOrEmpty(htmlFieldPrefix) ? fieldName : String.Format("{0}.{1}", htmlFieldPrefix, fieldName);
        int index = 0;

        foreach (TValue item in items)
        {
            // Much gratitude to Matt Hidinger for getting the singleItemExpression.
            // Current html.DisplayFor() throws exception if the expression is anything that isn't a "MemberAccessExpression"
            // So we have to trick it and place the item into a dummy wrapper and access the item through a Property
            var dummy = new { Item = item };

            // Get the actual item by accessing the "Item" property from our dummy class
            var memberExpression = Expression.MakeMemberAccess(Expression.Constant(dummy), dummy.GetType().GetProperty("Item"));

            // Create a lambda expression passing the MemberExpression to access the "Item" property and the expression params
            var singleItemExpression = Expression.Lambda<Func<TModel, TValue>>(memberExpression,
                                                                               expression.Parameters);

            // Now when the form collection is submitted, the default model binder will be able to bind it exactly as it was.
            var itemFieldName = String.Format("{0}[{1}]", fullHtmlFieldPrefix, index++);
            string singleItemHtml = html.EditorFor(singleItemExpression, templateName, itemFieldName).ToString();
            sb.AppendFormat(singleItemHtml);
        }

        return new MvcHtmlString(sb.ToString());
    }
DaveMorganTexas
  • 827
  • 10
  • 13
14

Is it any other, cleaner solution to do this?

The simple answer is no, it sucks badly, I completely agree with you, but that's how the designers of the framework decided to implement this feature.

So what I do is I stick to the conventions. Since I have specific view models for each views and partials it's not a big deal to have a corresponding editor template, named the same way as the type of the collection.

Darin Dimitrov
  • 960,118
  • 257
  • 3,196
  • 2,876
  • That I was afraid of. The problem is that I have to use the same ViewModel for some related Views and it depends on the View how to display this ViewModel. But thanks, Darin, for the answer, I appreciate it. – Zabavsky Dec 26 '12 at 10:02
  • I've decided to provide a cleaner solution for this. I've added it as an answer. – Cesar May 13 '20 at 06:57
0

I've decided to write a library to provide a cleaner way of solving this issue. With it you can specify individual templates for each part of the list, and it works for both displaying and editing:

@Html.DisplayListFor(x => x.Books,
    itemTemplate: "BookViewModel",
    itemContainerTemplate: "ContainerForMyItem"
    listTemplate:  "MyList"
    listContainerTemplate: "ContainerForMyList")

Note that the customization is optional, you could simply write

@Html.DisplayListFor(x => x.Books)

or

@Html.ListEditorFor(x => x.Books)

and it should just work.

The library is called Dynamic View-Model Lists and its available on NuGet.

Cesar
  • 1,877
  • 21
  • 26