Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

This question is a follow-up for Why is my DisplayFor not looping through my IEnumerable<DateTime>?


A quick refresh.

When:

  • the model has a property of type IEnumerable<T>
  • you pass this property to Html.EditorFor() using the overload that only accepts the lambda expression
  • you have an editor template for the type T under Views/Shared/EditorTemplates

then the MVC engine will automatically invoke the editor template for each item in the enumerable sequence, producing a list of the results.

E.g., when there is a model class Order with property Lines:

public class Order {     public IEnumerable<OrderLine> Lines { get; set; } }  public class OrderLine {     public string Prop1 { get; set; }     public int Prop2 { get; set; } } 

And there is a view Views/Shared/EditorTemplates/OrderLine.cshtml:

@model TestEditorFor.Models.OrderLine  @Html.EditorFor(m => m.Prop1) @Html.EditorFor(m => m.Prop2) 

Then, when you invoke @Html.EditorFor(m => m.Lines) from the top-level view, you will get a page with text boxes for each order line, not just one.


However, as you can see in the linked question, this only works when you use that particular overload of EditorFor. If you provide a template name (in order to use a template that is not named after the OrderLine class), then the automatic sequence handling will not happen, and a runtime error will happen instead.

At which point you will have to declare your custom template's model as IEnumebrable<OrderLine> and manually iterate over its items in some way or another to output all of them, e.g.

@foreach (var line in Model.Lines) {     @Html.EditorFor(m => line) } 

And that is where problems begin.

The HTML controls generated in this way all have same ids and names. When you later POST them, the model binder will not be able to construct an array of OrderLines, and the model object you get in the HttpPost method in the controller will be null.
This makes sense if you look at the lambda expression - it does not really link the object being constructed to a place in the model from which it comes.

I have tried various ways of iterating over the items, and it would seem the only way is to redeclare the template's model as IList<T> and enumerate it with for:

@model IList<OrderLine>  @for (int i = 0; i < Model.Count(); i++) {      @Html.EditorFor(m => m[i].Prop1)     @Html.EditorFor(m => m[i].Prop2) } 

Then in the top-level view:

@model TestEditorFor.Models.Order  @using (Html.BeginForm()) {     @Html.EditorFor(m => m.Lines, "CustomTemplateName") } 

which gives properly named HTML controls that are properly recognized by the model binder on a submit.


While this works, it feels very wrong.

What is the correct, idiomatic way to use a custom editor template with EditorFor, while preserving all the logical links that allow the engine to generate HTML suitable for the model binder?

like image 714
GSerg Avatar asked Aug 15 '14 19:08

GSerg


People also ask

What is MVC editor template?

An EditorTemplate is a Razor file placed in the EditorTemplates folder: For Razor Pages apps, in the Pages/Shared/EditorTemplates folder. For MVC apps, in the Views/Shared/EditorTemplates folder or the Views/ControllerName/EditorTemplates folder.

How to create model in asp net Mvc?

Adding a Model Class In the MVC application in Visual Studio, and right-click on the Model folder, select Add -> and click on Class... It will open the Add New Item dialog box. In the Add New Item dialog box, enter the class name Student and click Add. This will add a new Student class in model folder.

What is@ model in cshtml?

This @model directive allows you to access the movie that the controller passed to the view by using a Model object that's strongly typed. For example, in the Details. cshtml template, the code passes each movie field to the DisplayNameFor and DisplayFor HTML Helpers with the strongly typed Model object.


2 Answers

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++ + "]")     } } 
like image 92
GSerg Avatar answered Oct 02 '22 18:10

GSerg


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 + ']')))); } 
like image 45
Anders Avatar answered Oct 02 '22 18:10

Anders