Correct, idiomatic way to use custom editor templates with IEnumerable models in ASP.NET MVC

泄露秘密 提交于 2019-11-26 16:03:49
GSerg

After discussion with Erik Funkenbusch, which led to looking into the MVC source code, it would appear there are two nicer (correct and idiomatic?) ways to do it.

Both involve providing correct html name prefix to the helper, and generate HTML identical to the output of the default EditorFor.

I'll just leave it here for now, will do more testing to make sure it works in deeply nested scenarios.

For the following examples, suppose you already have two templates for OrderLine class: OrderLine.cshtml and DifferentOrderLine.cshtml.


Method 1 - Using an intermediate template for IEnumerable<T>

Create a helper template, saving it under any name (e.g. "ManyDifferentOrderLines.cshtml"):

@model IEnumerable<OrderLine>

@{
    int i = 0;

    foreach (var line in Model)
    { 
        @Html.EditorFor(m => line, "DifferentOrderLine", "[" + i++ + "]")
    }
}

Then call it from the main Order template:

@model Order

@Html.EditorFor(m => m.Lines, "ManyDifferentOrderLines")

Method 2 - Without an intermediate template for IEnumerable<T>

In the main Order template:

@model Order

@{
    int i = 0;

    foreach (var line in Model.Lines)
    {
        @Html.EditorFor(m => line, "DifferentOrderLine", "Lines[" + i++ + "]")
    }
}
Erik Funkenbusch

There are a number of ways to address this problem. There is no way to get default IEnumerable support in editor templates when specifying a template name in the EditorFor. First, i'd suggest that if you have multiple templates for the same type in the same controller, your controller probably has too many responsibilities and you should consider refactoring it.

Having said that, the easiest solution is a custom DataType. MVC uses DataTypes in addition to UIHints and typenames. See:

Custom EditorTemplate not being used in MVC4 for DataType.Date

So, you need only say:

[DataType("MyCustomType")]
public IEnumerable<MyOtherType> {get;set;}

Then you can use MyCustomType.cshtml in your editor templates. Unlike UIHint, this does not suffer from the lack of IEnuerable support. If your usage supports a default type (say, Phone or Email, then prefer to use the existing type enumeration instead). Alternatively, you could derive your own DataType attribute and use DataType.Custom as the base.

You can also simply wrap your type in another type to create a different template. For example:

public class MyType {...}
public class MyType2 : MyType {}

Then you can create a MyType.cshtml and MyType2.cshtml quite easily, and you can always treat a MyType2 as a MyType for most purposes.

If this is too "hackish" for you, you can always build your template to render differently based on parameters passed via the "additionalViewData" parameter of the editor template.

Another option would be to use the version where you pass the template name to do "setup" of the type, such as create table tags, or other kinds of formatting, then use the more generic type version to render just the line items in a more generic form from inside the named template.

This allows you to have a CreateMyType template and an EditMyType template which are different except for the individual line items (which you can combine with the previous suggestion).

One other option is, if you're not using DisplayTemplates for this type, you can use DisplayTempates for your alternate template (when creating a custom template, this is just a convention.. when using the built-in template then it will just create display versions). Granted, this is counter-intuitive but it does solve the problem if you only have two templates for the same type you need to use, with no corresponding Display template.

Of course, you could always just convert the IEnumerable to an array in the template, which does not require redeclaring the model type.

@model IEnumerable<MyType>
@{ var model = Model.ToArray();}
@for(int i = 0; i < model.Length; i++)
{
    <p>@Html.TextBoxFor(x => model[i].MyProperty)</p>
}

I could probably think of a dozen other ways to solve this problem, but in all reality, any time I've ever had it, I've found that if I think about it, I can simply redesign my model or views in such a way as to no longer require it to be solved.

In other words, I consider having this problem to be a "code smell" and a sign that i'm probably doing something wrong, and rethinking the process usually yields a better design that doesn't have the problem.

So to answer your question. The correct, idiomatic way would be to redesign your controllers and views so that this problem does not exist. barring that, choose the least offensive "hack" to achieve what you want.

Anders

There seems to be no easier way of achieving this than described in the answer by @GSerg. Strange that the MVC Team has not come up with a less messy way of doing it. I've made this Extension Method to encapsulate it at least to some extent:

public static MvcHtmlString EditorForEnumerable<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, IEnumerable<TValue>>> expression, string templateName)
{
    var fieldName = html.NameFor(expression).ToString();
    var items = expression.Compile()(html.ViewData.Model);
    return new MvcHtmlString(string.Concat(items.Select((item, i) => html.EditorFor(m => item, templateName, fieldName + '[' + i + ']'))));
}

You can use an UIHint attribute to direct MVC which view you would like to load for the editor. So your Order object would look like this using the UIHint

public class Order
{
    [UIHint("Non-Standard-Named-View")]
    public IEnumerable<OrderLine> Lines { get; set; }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!