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
LabelFor
y otros ...For
ayudantes 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 LabelFor
y otros ...For
ayudantes en MVC?
Entonces, ha usado las HtmlHelper<T>
extensiones para LabelFor
y TextBoxFor
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
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 TModel
está el tipo definido en su @model
declaració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 TModel
es 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.
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 attributes
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
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 FooBar
fuera 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 Baz
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.
@
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?).