MVC Razor ve el modelo nested de foreach

Imagine un escenario común, esta es una versión más simple de lo que estoy cruzando. De hecho, tengo un par de capas de anidación adicional en la mía …

Pero este es el escenario

El tema contiene la lista La categoría contiene la lista El producto contiene la lista

Mi controlador proporciona un tema totalmente poblado, con todas las categorías para ese tema, los productos dentro de estas categorías y sus pedidos.

La colección de pedidos tiene una propiedad llamada Quantity (entre muchos otros) que debe ser editable.

@model ViewModels.MyViewModels.Theme @Html.LabelFor(Model.Theme.name) @foreach (var category in Model.Theme) { @Html.LabelFor(category.name) @foreach(var product in theme.Products) { @Html.LabelFor(product.name) @foreach(var order in product.Orders) { @Html.TextBoxFor(order.Quantity) @Html.TextAreaFor(order.Note) @Html.EditorFor(order.DateRequestedDeliveryFor) } } } 

Si utilizo lambda en su lugar, entonces solo parece obtener una referencia al objeto modelo superior, “Tema” no aquellos dentro del ciclo foreach.

¿Es posible que lo que trato de hacer sea posible o he sobreestimado o malinterpretado lo que es posible?

Con lo anterior me sale un error en el TextboxFor, EditorFor, etc.

CS0411: Los argumentos de tipo para el método ‘System.Web.Mvc.Html.InputExtensions.TextBoxFor (System.Web.Mvc.HtmlHelper, System.Linq.Expressions.Expression>)’ no se pueden deducir del uso. Intente especificar los argumentos de tipo explícitamente.

Gracias.

La respuesta rápida es usar un bucle for() en lugar de tus bucles foreach() . Algo como:

 @for(var themeIndex = 0; themeIndex < Model.Theme.Count(); themeIndex++) { @Html.LabelFor(model => model.Theme[themeIndex]) @for(var productIndex=0; productIndex < Model.Theme[themeIndex].Products.Count(); productIndex++) { @Html.LabelFor(model=>model.Theme[themeIndex].Products[productIndex].name) @for(var orderIndex=0; orderIndex < Model.Theme[themeIndex].Products[productIndex].Orders; orderIndex++) { @Html.TextBoxFor(model => model.Theme[themeIndex].Products[productIndex].Orders[orderIndex].Quantity) @Html.TextAreaFor(model => model.Theme[themeIndex].Products[productIndex].Orders[orderIndex].Note) @Html.EditorFor(model => model.Theme[themeIndex].Products[productIndex].Orders[orderIndex].DateRequestedDeliveryFor) } } } 

Pero esto aclara por qué esto soluciona el problema.

Hay tres cosas que usted tiene al menos una comprensión superficial antes de poder resolver este problema. Tengo que admitir que llevé esto a cabo durante mucho tiempo cuando comencé a trabajar con el framework. Y me tomó bastante tiempo entender realmente lo que estaba pasando.

Esas tres cosas son:

  • ¿Cómo funciona LabelFor y otros ...For ayudantes que trabajan en MVC?
  • ¿Qué es un árbol de expresiones?
  • ¿Cómo funciona la Carpeta de Modelos?

Los tres conceptos se vinculan para obtener una respuesta.

¿Cómo funciona LabelFor y otros ...For ayudantes que trabajan en MVC?

Entonces, has usado las HtmlHelper para LabelFor y TextBoxFor y otras, y probablemente hayas notado que cuando las TextBoxFor , les pasas una lambda y mágicamente genera algo de HTML. ¿Pero cómo?

Entonces, lo primero que debe notar es la firma de estos ayudantes. TextBoxFor sobrecarga más simple para TextBoxFor

 public static MvcHtmlString TextBoxFor( this HtmlHelper htmlHelper, Expression> expression ) 

Primero, este es un método de extensión para un HtmlHelper fuertemente tipificado, de tipo . Entonces, para simplemente decir lo que ocurre detrás de las escenas, cuando la afeitadora rinde esta vista genera una clase. Dentro de esta clase hay una instancia de HtmlHelper (como la propiedad Html , por lo que puede usar @Html... ), donde TModel es el tipo definido en su statement @model . Entonces, en su caso, cuando mira esta vista, TModel siempre será del tipo ViewModels.MyViewModels.Theme .

Ahora, el siguiente argumento es un poco complicado. Así que veamos una invocación

 @Html.TextBoxFor(model=>model.SomeProperty); 

Parece que tenemos un pequeño lambda, y si uno adivinara la firma, uno podría pensar que el tipo para este argumento sería simplemente un Func , donde TModel es el tipo del modelo de vista y TProperty es inferido como el tipo de la propiedad.

Pero eso no está del todo bien, si nos fijamos en el tipo real del argumento, su Expression> .

Entonces, cuando normalmente se genera un lambda, el comstackdor toma el lambda y lo comstack en MSIL, como cualquier otra función (por lo que puedes usar delegates, grupos de métodos y lambdas de forma más o menos intercambiable, porque son solo referencias de código). .)

Sin embargo, cuando el comstackdor ve que el tipo es una Expression<> , no comstack inmediatamente la lambda en MSIL, ¡sino que genera un árbol de expresiones!

¿Qué es un árbol de expresiones ?

Entonces, ¿qué diablos es un árbol de expresión? Bueno, no es complicado, pero tampoco es un paseo por el parque. Para citar ms:

| Los árboles de expresiones representan el código en una estructura de datos similar a un árbol, donde cada nodo es una expresión, por ejemplo, una llamada al método o una operación binaria como x

En pocas palabras, un árbol de expresiones es una representación de una función como una colección de “acciones”.

En el caso de model=>model.SomeProperty , el árbol de expresiones tendría un nodo que dice: “Obtener ‘Some Property’ from ” model” ”

Este árbol de expresiones se puede comstackr en una función que se puede invocar, pero siempre que sea un árbol de expresiones, solo se trata de una colección de nodos.

Entonces, ¿para qué sirve eso?

Entonces Func<> o Action<> , una vez que los tienes, son casi atómicos. Todo lo que realmente puedes hacer es Invoke() , también decirles que hagan el trabajo que se supone que deben hacer.

Expression> por otro lado, representa una colección de acciones, que se pueden anexar, manipular, visitar o comstackr e invocar.

Entonces, ¿por qué me estás diciendo todo esto?

Entonces, con esa comprensión de lo que es una Expression<> , podemos volver a Html.TextBoxFor . Cuando representa un cuadro de texto, necesita generar algunas cosas sobre la propiedad que le está dando. Cosas como los attributes en la propiedad para la validación, y específicamente en este caso, necesita averiguar a qué nombrar la etiqueta .

Hace esto “caminando” el árbol de expresiones y construyendo un nombre. Entonces, para una expresión como model=>model.SomeProperty , recorre la expresión que reúne las propiedades que está pidiendo y construye .

Para un ejemplo más complicado, como model=>model.Foo.Bar.Baz.FooBar , podría generar

¿Tener sentido? No es solo el trabajo que hace el Func<> , pero cómo hace su trabajo es importante aquí.

(Tenga en cuenta que otros marcos como LINQ to SQL hacen cosas similares al recorrer un árbol de expresiones y crear una gramática diferente, que en este caso es una consulta SQL)

¿Cómo funciona la Carpeta de Modelos?

Entonces, una vez que lo tienes, tenemos que hablar brevemente sobre la carpeta del modelo. Cuando se publica el formulario, es simplemente como un Dictionary plano Dictionary , hemos perdido la estructura jerárquica que nuestro modelo de vista anidada puede haber tenido. Es trabajo del encuadernador de modelos tomar este combo de par clave-valor e intentar rehidratar un objeto con algunas propiedades. ¿Como hace esto? Lo adivinó, al usar la “clave” o el nombre de la entrada que se publicó.

Entonces, si la publicación del formulario parece

 Foo.Bar.Baz.FooBar = Hello 

Y está publicando en un modelo llamado SomeViewModel , luego hace lo contrario de lo que hizo el ayudante en primer lugar. Busca una propiedad llamada “Foo”. Luego busca una propiedad llamada “Bar” fuera de “Foo”, luego busca “Baz” … y así sucesivamente …

Finalmente trata de analizar el valor en el tipo de “FooBar” y asignarlo a “FooBar”.

¡¡¡UF!!!

Y listo, tienes tu modelo. La instancia que acaba de construir el Encuadernador de modelos se entrega a la Acción solicitada.


Por lo tanto, su solución no funciona porque los helpers Html.[Type]For() necesitan una expresión. Y solo les estás dando un valor. No tiene idea de cuál es el contexto para ese valor, y no sabe qué hacer con él.

Ahora algunas personas sugirieron usar parciales para renderizar. Ahora esto en teoría funcionará, pero probablemente no de la manera que esperas. Cuando renderiza un parcial, está cambiando el tipo de TModel , porque se encuentra en un contexto de vista diferente. Esto significa que puede describir su propiedad con una expresión más corta. También significa que cuando el ayudante genera el nombre de tu expresión, será superficial. Solo se generará en función de la expresión que se le dé (no del contexto completo).

Entonces digamos que tuviste un parcial que acaba de traducir “Baz” (de nuestro ejemplo anterior). Dentro de ese parcial podrías decir:

 @Html.TextBoxFor(model=>model.FooBar) 

Más bien que

 @Html.TextBoxFor(model=>model.Foo.Bar.Baz.FooBar) 

Eso significa que generará una etiqueta de entrada como esta:

  

Lo cual, si publicas este formulario en una acción que está esperando un gran ViewModel profundamente nested, entonces intentará hidratar una propiedad llamada FooBar fuera de TModel . Que en el mejor de los casos no está allí, y en el peor es algo completamente distinto. Si estuvieras publicando en una acción específica que aceptara un Baz , en lugar del modelo raíz, ¡esto funcionaría muy bien! De hecho, los parciales son una buena manera de cambiar el contexto de su vista, por ejemplo, si tiene una página con múltiples formularios que todos publican en diferentes acciones, entonces la representación de un parcial para cada uno sería una gran idea.


Ahora, una vez que obtiene todo esto, puede comenzar a hacer cosas realmente interesantes con Expression<> , extendiéndolo programáticamente y haciendo otras cosas ordenadas con ellos. No entraré en nada de eso. Pero, con suerte, esto le dará una mejor comprensión de lo que está sucediendo entre bastidores y por qué las cosas están actuando como son.

Simplemente puede usar EditorTemplates para hacer eso, necesita crear un directorio llamado “EditorTemplates” en la carpeta de visualización de su controlador y colocar una vista separada para cada una de sus entidades anidadas (nombrada como nombre de clase de entidad)

Vista principal :

 @model ViewModels.MyViewModels.Theme @Html.LabelFor(Model.Theme.name) @Html.EditorFor(Model.Theme.Categories) 

Vista de categoría (/MyController/EditorTemplates/Category.cshtml):

 @model ViewModels.MyViewModels.Category @Html.LabelFor(Model.Name) @Html.EditorFor(Model.Products) 

Vista del producto (/MyController/EditorTemplates/Product.cshtml):

 @model ViewModels.MyViewModels.Product @Html.LabelFor(Model.Name) @Html.EditorFor(Model.Orders) 

y así

De esta forma, Html.EditorFor helper generará los nombres de los elementos de forma ordenada y, por lo tanto, no tendrás ningún problema adicional para recuperar la entidad Theme publicada como un todo.

Podría agregar un parcial de Categoría y un Producto parcial, cada uno tomaría una parte más pequeña del modelo principal como su propio modelo, es decir, el tipo de modelo de Categoría podría ser un IEnumerable, usted pasaría el Modelo. Tema al mismo. El parcial del Producto puede ser un IEnumerable al que usted transfiere Model.Products en (desde dentro de la Categoría parcial).

No estoy seguro de si ese sería el camino correcto, pero estaría interesado en saberlo.

EDITAR

Desde que publiqué esta respuesta, he usado EditorTemplates y encuentro que esta es la manera más fácil de manejar grupos de entrada o elementos repetitivos. Maneja todos sus problemas de validación de mensajes y problemas de envío de formularios / encuadernaciones de modelos automáticamente.

Cuando está utilizando el bucle foreach dentro de la vista para el modelo encuadernado … Se supone que su modelo está en el formato listado.

es decir

 @model IEnumerable @{ if (Model.Count() > 0) { @Html.DisplayFor(modelItem => Model.Theme.FirstOrDefault().name) @foreach (var theme in Model.Theme) { @Html.DisplayFor(modelItem => theme.name) @foreach(var product in theme.Products) { @Html.DisplayFor(modelItem => product.name) @foreach(var order in product.Orders) { @Html.TextBoxFor(modelItem => order.Quantity) @Html.TextAreaFor(modelItem => order.Note) @Html.EditorFor(modelItem => order.DateRequestedDeliveryFor) } } } }else{ No Theam avaiable } } 

Está claro del error.

Los HtmlHelpers anexados con “For” esperan la expresión lambda como parámetro.

Si está pasando el valor directamente, mejor use Normal.

p.ej

En lugar de TextboxFor (….) use Textbox ()

la syntax para TextboxFor será como Html.TextBoxFor (m => m.Property)

En su escenario, puede usar el bucle básico for, ya que le dará índice para usar.

 @for(int i=0;im.Theme[i].name) @for(int j=0;jm.Theme[i].Products[j].name) @for(int k=0;kModel.Theme[i].Products[j].Orders[k].Quantity) @Html.TextAreaFor(m=>Model.Theme[i].Products[j].Orders[k].Note) @Html.EditorFor(m=>Model.Theme[i].Products[j].Orders[k].DateRequestedDeliveryFor) } } }