Forma correcta e idiomática de utilizar plantillas personalizadas de editor con modelos IEnumerable en ASP.NET MVC

Esta pregunta es un seguimiento de ¿Por qué mi pantalla no está pasando por mi IEnumerable ?


Una actualización rápida.

Cuando:

  • el modelo tiene una propiedad de tipo IEnumerable
  • pasas esta propiedad a Html.EditorFor() usando la sobrecarga que solo acepta la expresión lambda
  • usted tiene una plantilla de editor para el tipo T en Vistas / Compartidas / EditorTemplates

luego, el motor de MVC invocará automáticamente la plantilla del editor para cada elemento en la secuencia enumerable, produciendo una lista de los resultados.

Por ejemplo, cuando hay un modelo de clase Order con propiedad Lines :

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

Y hay una vista Vistas / Compartidas / EditorTemplates / OrderLine.cshtml:

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

Luego, cuando invoque @Html.EditorFor(m => m.Lines) desde la vista de nivel superior, obtendrá una página con cuadros de texto para cada línea de pedido, no solo una.


Sin embargo, como puede ver en la pregunta vinculada, esto solo funciona cuando usa esa sobrecarga particular de EditorFor . Si proporciona un nombre de plantilla (para utilizar una plantilla que no recibe el nombre de la clase OrderLine ), no se OrderLine la secuencia automáticamente y, en su lugar, se producirá un error de tiempo de ejecución.

En ese momento, deberá declarar el modelo de su plantilla personalizada como IEnumebrable e iterar manualmente sus elementos de una forma u otra para mostrarlos todos, por ejemplo

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

Y ahí es donde comienzan los problemas.

Los controles HTML generados de esta manera tienen todos los mismos identificadores y nombres. Cuando más tarde los publique, la carpeta modelo no podrá construir una matriz de OrderLine , y el objeto modelo que obtenga en el método HttpPost en la controladora será null .
Esto tiene sentido si observas la expresión lambda; en realidad, no vincula el objeto que se está construyendo con un lugar del modelo del que proviene.

He intentado varias formas de iterar sobre los elementos, y parece que la única forma es redeclarar el modelo de la plantilla como IList y enumerarlo con for :

 @model IList @for (int i = 0; i  m[i].Prop1) @Html.EditorFor(m => m[i].Prop2) } 

Luego, en la vista de nivel superior:

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

que proporciona controles HTML con el nombre adecuado que el encuadernador del modelo reconoce correctamente en un envío.


Mientras esto funciona, se siente muy mal.

¿Cuál es la forma correcta e idiomática de utilizar una plantilla de editor personalizada con EditorFor , preservando al mismo tiempo todos los enlaces lógicos que permiten que el motor genere HTML adecuado para la carpeta de modelo?

Después de la discusión con Erik Funkenbusch , lo que llevó a investigar el código fuente de MVC , parece que hay dos formas más agradables (¿correctas e idiomáticas?) De hacerlo.

Ambas implican proporcionar el prefijo de nombre html correcto al helper y generar HTML idéntico al resultado del EditorFor defecto.

Lo dejaré aquí por ahora, haré más pruebas para asegurarme de que funciona en escenarios profundamente nesteds.

Para los siguientes ejemplos, supongamos que ya tiene dos plantillas para la clase OrderLine.cshtml : OrderLine.cshtml y DifferentOrderLine.cshtml .


Método 1 – Usar una plantilla intermedia para IEnumerable

Cree una plantilla auxiliar, guardándola bajo cualquier nombre (por ejemplo, “ManyDifferentOrderLines.cshtml”):

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

Luego llámelo desde la plantilla de pedido principal:

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

Método 2: sin una plantilla intermedia para IEnumerable

En la plantilla de pedido principal:

 @model Order @{ int i = 0; foreach (var line in Model.Lines) { @Html.EditorFor(m => line, "DifferentOrderLine", "Lines[" + i++ + "]") } } 

Hay varias maneras de abordar este problema. No hay forma de obtener compatibilidad predeterminada con IEnumerable en plantillas de editor al especificar un nombre de plantilla en EditorFor. Primero, sugeriría que si tiene varias plantillas para el mismo tipo en el mismo controlador, su controlador probablemente tenga demasiadas responsabilidades y debería considerar refaccionarlo.

Habiendo dicho eso, la solución más fácil es un tipo de datos personalizado . MVC usa DataTypes además de UIHints y typenames. Ver:

Custom EditorTemplate no se usa en MVC4 para DataType.Date

Entonces, solo necesitas decir:

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

Luego puede usar MyCustomType.cshtml en sus plantillas de editor. A diferencia de UIHint, esto no adolece de la falta de soporte IEnuerable . Si su uso admite un tipo predeterminado (por ejemplo, Teléfono o Correo electrónico, entonces prefiera usar la enumeración de tipo existente en su lugar). Alternativamente, puede derivar su propio atributo de tipo de datos y utilizar DataType.Custom como base.

También puede simplemente ajustar su tipo en otro tipo para crear una plantilla diferente. Por ejemplo:

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

Entonces puede crear un MyType.cshtml y MyType2.cshtml con bastante facilidad, y siempre puede tratar un MyType2 como MyType para la mayoría de los propósitos.

Si esto es demasiado “hackish” para usted, siempre puede crear su plantilla para procesarla de forma diferente en función de los parámetros aprobados a través del parámetro “additionalViewData” de la plantilla del editor.

Otra opción sería usar la versión donde pasa el nombre de la plantilla para hacer “configuración” del tipo, como crear tags de tabla u otros tipos de formato, luego use la versión de tipo más genérica para representar solo las líneas de pedido en un forma más genérica desde el interior de la plantilla con nombre.

Esto le permite tener una plantilla CreateMyType y una plantilla EditMyType que son diferentes, excepto por las líneas de pedido individuales (que puede combinar con la sugerencia anterior).

Otra opción es que, si no está utilizando DisplayTemplates para este tipo, puede usar DisplayTempates para su plantilla alternativa (cuando se crea una plantilla personalizada, esto es solo una convención … cuando usa la plantilla incorporada, entonces simplemente creará versiones de visualización). Por supuesto, esto no es intuitivo, pero resuelve el problema si solo tiene dos plantillas para el mismo tipo que necesita usar, sin una plantilla de visualización correspondiente.

Por supuesto, siempre puedes convertir IEnumerable a una matriz en la plantilla, que no requiere volver a definir el tipo de modelo.

 @model IEnumerable @{ var model = Model.ToArray();} @for(int i = 0; i < model.Length; i++) { 

@Html.TextBoxFor(x => model[i].MyProperty)

}

Probablemente podría pensar en una docena de otras maneras de resolver este problema, pero en toda realidad, en cualquier momento que lo haya tenido, he descubierto que si lo pienso, simplemente puedo rediseñar mi modelo o vistas de una manera manera de no requerir más que se resuelva.

En otras palabras, considero que este problema es un “olor a código” y una señal de que probablemente estoy haciendo algo mal, y el repensar el proceso generalmente produce un mejor diseño que no tiene el problema.

Entonces para responder a tu pregunta. La forma correcta e idiomática sería rediseñar sus controladores y vistas para que este problema no exista. salvo eso, elija el “truco” menos ofensivo para lograr lo que quiere .

No parece haber una manera más fácil de lograr esto que la que se describe en la respuesta de @GSerg . Es extraño que el MVC Team no haya tenido una manera menos complicada de hacerlo. Creé este Método de extensión para encapsularlo al menos hasta cierto punto:

 public static MvcHtmlString EditorForEnumerable(this HtmlHelper html, Expression>> 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 + ']')))); } 

Puede usar un atributo UIHint para dirigir a MVC qué vista le gustaría cargar para el editor. Entonces, su objeto Order se vería así usando UIHint

 public class Order { [UIHint("Non-Standard-Named-View")] public IEnumerable Lines { get; set; } }