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.
@
antes de todosforeach
? ¿No debería tener lambdas también enHtml.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?).category.name
Estoy seguro de que es unstring
y...For
no admite una cadena como primer parámetro:)
.for()
lugar de unoforeach
. Explicaré por qué, porque también me confundió muchísimo durante mucho tiempo.Respuestas:
La respuesta rápida es usar un
for()
bucle en lugar de susforeach()
bucles. Algo como: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:
LabelFor
y otros...For
ayudantes en MVC?Los tres conceptos se vinculan para obtener una respuesta.
¿Cómo funcionan los
LabelFor
y otros...For
ayudantes en MVC?Entonces, ha usado las
HtmlHelper<T>
extensiones paraLabelFor
yTextBoxFor
y 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
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 deHtmlHelper<TModel>
(como propiedadHtml
, razón por la cual puede usar@Html...
), dondeTModel
está el tipo definido en su@model
declaración. Entonces, en su caso, cuando esté mirando, esta vistaTModel
siempre será del tipoViewModels.MyViewModels.Theme
.Ahora, el siguiente argumento es un poco complicado. Así que veamos una invocación
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>
, dondeTModel
es el tipo del modelo de vista yTProperty
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<>
oAction<>
, una vez que los tienes, son bastante atómicos. Todo lo que realmente puede hacer esInvoke()
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 volverHtml.TextBoxFor
. Cuando renderiza un cuadro de texto, necesita generar algunas cosas sobre la propiedad que le está dando. Cosas comoattributes
en 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
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:
Más bien que
Eso significa que generará una etiqueta de entrada como esta:
Lo cual, si está publicando este formulario en una acción que espera un ViewModel grande y profundamente anidado, intentará hidratar una propiedad llamada
FooBar
fuera deTModel
. 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 unBaz
modelo, 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.fuente
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 :
Vista de categoría (/MyController/EditorTemplates/Category.cshtml):
Vista del producto (/MyController/EditorTemplates/Product.cshtml):
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
fuente
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.
fuente
Theme
modelo no se hidrataría correctamente.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
fuente
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.
fuente
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.
fuente