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 OrderLine
s, 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?
select(value,i)
withIEnumerable
as the model, you'd then have to callEditorFor(m => m.ElementAt(item.i).PropertyName)
, and that creates HTML without indices, just like regularforeach
would, becauseElementAt
is not recognized by Razor in this way. – OleviaolfactionMyDataType
use this templateCustomTemplateName
. – DiamagneticOrderLine
)? If so you can create anEditorTemplates
folder in the Views folder associated with the Controller (Views/ControllerName/EditorTemplates
). Razor will search this folder first an use it, even if anotherOrderLine.cshtml
file exists in theViews/Shared/EditorTemplates
folder – Pecoraro