MVC Razor view anidado modelo de foreach

94

Imagina un escenario común, esta es una versión más simple de lo que estoy encontrando. 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

My Controller proporciona un Tema completamente 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 Cantidad (entre muchas otras) 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 parece que solo obtengo una referencia al objeto Model superior, "Theme", no a los del bucle foreach.

¿Es posible lo que estoy tratando de hacer allí o he sobreestimado o malinterpretado lo que es posible?

Con lo anterior, aparece un error en 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 inferir del uso. Intente especificar los argumentos de tipo explícitamente.

Gracias.

David C
fuente
1
¿No debería haberlo hecho @antes de todos foreach? ¿No debería tener lambdas también en Html.EditorFor( Html.EditorFor(m => m.Note), por ejemplo) y el resto de los métodos? Puede que me esté equivocando, pero ¿puede pegar su código real? Soy bastante nuevo en MVC, pero puedes resolverlo con bastante facilidad con vistas parciales o editores (¿si ese es el nombre?).
Kobi
category.nameEstoy seguro de que es un stringy ...Forno admite una cadena como primer parámetro
balexandre
sí, me perdí las @, ahora agregadas. Gracias. Sin embargo, en cuanto a lambda, si empiezo a escribir @ Html.TextBoxFor (m => m. Entonces parece que solo obtengo una referencia al objeto del modelo superior, no a los que están dentro del bucle foreach.
David C
@DavidC: todavía no sé lo suficiente sobre MVC 3 para responder, pero sospecho que ese es su problema :).
Kobi
2
Estoy en el tren, pero si esto no es respondido cuando llegue al trabajo, publicaré una respuesta. La respuesta rápida es usar un archivo regular en for()lugar de uno foreach. Explicaré por qué, porque también me confundió muchísimo durante mucho tiempo.
J. Holmes

Respuestas:

304

La respuesta rápida es usar un for()bucle en lugar de sus foreach()bucles. 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 pasa por alto por qué esto soluciona el problema.

Hay tres cosas que tiene al menos un conocimiento superficial antes de poder resolver este problema. Tengo que admitir que lo hice durante mucho tiempo cuando comencé a trabajar con el marco. Y me tomó bastante tiempo comprender realmente lo que estaba pasando.

Esas tres cosas son:

  • ¿Cómo funcionan los LabelFory otros ...Forayudantes en MVC?
  • ¿Qué es un árbol de expresión?
  • ¿Cómo funciona Model Binder?

Los tres conceptos se vinculan para obtener una respuesta.

¿Cómo funcionan los LabelFory otros ...Forayudantes en MVC?

Entonces, ha usado las HtmlHelper<T>extensiones para LabelFory TextBoxFory otros, y probablemente notó que cuando las invoca, les pasa una lambda y mágicamente genera algo de html. ¿Pero cómo?

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

public static MvcHtmlString TextBoxFor<TModel, TProperty>(
    this HtmlHelper<TModel> htmlHelper,
    Expression<Func<TModel, TProperty>> expression
) 

En primer lugar, se trata de un método de extensión para un establecimiento inflexible HtmlHelper, de tipo <TModel>. Entonces, para decir simplemente lo que sucede detrás de escena, cuando razor muestra esta vista, genera una clase. Dentro de esta clase hay una instancia de HtmlHelper<TModel>(como propiedad Html, razón por la cual puede usar @Html...), donde TModelestá el tipo definido en su @modeldeclaración. Entonces, en su caso, cuando esté mirando, 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, podría pensar que el tipo para este argumento sería simplemente a Func<TModel, TProperty>, donde TModeles el tipo del modelo de vista y TProperty se infiere como el tipo de la propiedad.

Pero eso no es del todo correcto, si observa el tipo real de argumento, es Expression<Func<TModel, TProperty>>.

Entonces, cuando normalmente genera una lambda, el compilador toma la lambda y la compila en MSIL, al igual que cualquier otra función (por lo que puede usar delegados, grupos de métodos y lambdas de manera más o menos intercambiable, porque son solo referencias de código .)

Sin embargo, cuando el compilador ve que el tipo es an Expression<>, no compila inmediatamente la lambda en MSIL, sino que genera un árbol de expresión.

¿Qué es un árbol de expresión ?

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 expresión representan código en una estructura de datos en forma de árbol, donde cada nodo es una expresión, por ejemplo, una llamada a un método o una operación binaria como x <y.

En pocas palabras, un árbol de expresión es una representación de una función como una colección de "acciones".

En el caso de model=>model.SomeProperty, el árbol de expresión tendría un nodo que dice: "Obtener 'Alguna propiedad' de un 'modelo'"

Este árbol de expresión se puede compilar en una función que se puede invocar, pero siempre que sea un árbol de expresión, es solo una colección de nodos.

Entonces, ¿para qué sirve eso?

Entonces , Func<>o Action<>, una vez que los tienes, son bastante atómicos. Todo lo que realmente puede hacer es Invoke()ellos, es decir, decirles que hagan el trabajo que se supone que deben hacer.

Expression<Func<>>por otro lado, representa una colección de acciones, que se pueden agregar, manipular, visitar o compilar e invocar.

Entonces, ¿por qué me cuentas todo esto?

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

Lo hace "caminando" por el árbol de expresiones y construyendo un nombre. Entonces, para una expresión como model=>model.SomeProperty, recorre la expresión reuniendo las propiedades que está solicitando y construye <input name='SomeProperty'>.

Para un ejemplo más complicado, como model=>model.Foo.Bar.Baz.FooBar, podría generar<input name="Foo.Bar.Baz.FooBar" value="[whatever FooBar is]" />

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

(Tenga en cuenta que otros marcos como LINQ to SQL hacen cosas similares recorriendo un árbol de expresión y construyendo una gramática diferente, que en este caso una consulta SQL)

¿Cómo funciona Model Binder?

Entonces, una vez que lo entiendas, tenemos que hablar brevemente sobre el modelo de carpeta. Cuando se publica el formulario, es simplemente como un plano Dictionary<string, string>, hemos perdido la estructura jerárquica que pudo haber tenido nuestro modelo de vista anidada. El trabajo del enlazador de modelos es tomar este combo de pares clave-valor e intentar rehidratar un objeto con algunas propiedades. ¿Como hace esto? Lo adivinó, usando la "clave" o el nombre de la entrada que se publicó.

Entonces, si la publicación del formulario se parece a

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, intenta analizar el valor en el tipo de "FooBar" y asignarlo a "FooBar".

¡¡¡UF!!!

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


Entonces su solución no funciona porque los Html.[Type]For()ayudantes 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 bien, esto en teoría funcionará, pero probablemente no de la forma que esperas. Cuando renderizas un parcial, estás cambiando el tipo de TModel, porque estás 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 su expresión, será poco profundo. Solo se generará en función de la expresión que se proporcione (no en todo el contexto).

Entonces, digamos que tiene un parcial que acaba de representar "Baz" (de nuestro ejemplo anterior). Dentro de ese parcial, solo 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:

<input name="FooBar" />

Lo cual, si está publicando este formulario en una acción que espera un ViewModel grande y profundamente anidado, intentará hidratar una propiedad llamada FooBarfuera de TModel. Lo que en el mejor de los casos no está ahí, y en el peor es algo completamente diferente. Si estuviera publicando en una acción específica que aceptaba un Bazmodelo, en lugar del 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 tuviera una página con múltiples formularios que publican en diferentes acciones, entonces generar un parcial para cada uno sería una gran idea.


Ahora, una vez que obtenga todo esto, puede comenzar a hacer cosas realmente interesantes Expression<>, extendiéndolas programáticamente y haciendo otras cosas interesantes con ellas. No entraré en nada de eso. Pero, con suerte, esto le dará una mejor comprensión de lo que está sucediendo detrás de escena y por qué las cosas están actuando de la manera en que están.

J. Holmes
fuente
4
Respuesta impresionante. Actualmente estoy tratando de digerirlo. :) ¡También culpable de Cargo Culting! Como esa descripción.
David C
4
¡Gracias por esta respuesta detallada!
Kobi
14
Necesita más de un voto a favor para esto. +3 (uno por cada explicación) y +1 para Cargo-Cultists. ¡Respuesta absolutamente brillante!
Kyeotic
3
Es por eso que me encanta SO: respuesta corta + explicación en profundidad + enlace impresionante (cargo-cult). ¡Me gustaría mostrar la publicación sobre el culto a la carga a cualquiera que no crea que el conocimiento sobre el funcionamiento interno de las cosas es extremadamente importante!
user1068352
18

Simplemente puede usar EditorTemplates para hacer eso, necesita crear un directorio llamado "EditorTemplates" en la carpeta de vista de su controlador y colocar una vista separada para cada una de sus entidades anidadas (nombradas 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 manera, Html.EditorFor helper generará los nombres de los elementos de manera ordenada y, por lo tanto, no tendrá ningún problema adicional para recuperar la entidad Theme publicada en su totalidad

Alireza Sabouri
fuente
1
Si bien la respuesta aceptada es muy buena (también la voté a favor), esta respuesta es la opción más fácil de mantener.
Aaron
4

Puede agregar un parcial de Categoría y un parcial de Producto, 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, le pasaría Model.Theme. El parcial del producto podría ser un IEnumerable al que pasa Model.Products (desde dentro del parcial de categoría).

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

EDITAR

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

Adrian Thompson Phillips
fuente
Eso se me había ocurrido, pero no estaba seguro de cómo lo manejaría cuando lo leí para actualizarlo.
David C
1
Está cerca, pero dado que este es un formulario que se publicará como una unidad, no funcionará del todo bien. Una vez dentro del parcial, el contexto de la vista ha cambiado y ya no tiene la expresión profundamente anidada. Publicar de nuevo en el Thememodelo no se hidrataría correctamente.
J. Holmes
Esa también es mi preocupación. Por lo general, haría lo anterior como un enfoque de solo lectura para mostrar los productos y luego proporcionaría un enlace en cada producto para tal vez un método de acción / Producto / Editar / 123 para editar cada uno en su propio formulario. Creo que puede deshacerse tratando de hacer demasiado en una página en MVC.
Adrian Thompson Phillips
@AdrianThompsonPhillips sí, es muy posible que lo haya hecho. Vengo de una experiencia en Formularios, por lo que todavía no puedo acostumbrarme a la idea de tener que salir de la página para hacer una edición. :(
David C
2

Cuando utiliza el bucle foreach dentro de la vista para el modelo enlazado ... Se supone que su modelo debe estar en el formato listado.

es decir

@model IEnumerable<ViewModels.MyViewModels>


        @{
            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{
                   <span>No Theam avaiable</span>
            }
        }
Pranav Labhe
fuente
Me sorprende que el código anterior incluso se compile. @ Html.LabelFor requiere una operación FUNC como parámetro, el tuyo no lo es
Jenna Leaf
No sé si el código anterior se compila o no, pero @foreach anidado funciona para mí. MVC5.
antonio
0

Está claro por el error.

Los HtmlHelpers adjuntos con "For" esperan una expresión lambda como parámetro.

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

p.ej

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

la sintaxis de TextboxFor será como Html.TextBoxFor (m => m.Property)

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

@for(int i=0;i<Model.Theme.Count;i++)
 {
   @Html.LabelFor(m=>m.Theme[i].name)
   @for(int j=0;j<Model.Theme[i].Products.Count;j++) )
     {
      @Html.LabelFor(m=>m.Theme[i].Products[j].name)
      @for(int k=0;k<Model.Theme[i].Products[j].Orders.Count;k++)
          {
           @Html.TextBoxFor(m=>Model.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)
      }
   }
}
Manas
fuente
0

Otra posibilidad mucho más simple es que uno de los nombres de sus propiedades sea incorrecto (probablemente uno que acaba de cambiar en la clase). Esto es lo que fue para mí en RazorPages .NET Core 3.

Primera Division
fuente