A resposta rápida é usar um for()
loop no lugar de seus foreach()
loops. 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)
}
}
}
Mas isso ignora por que isso corrige o problema.
Há três coisas que você tem pelo menos um entendimento superficial antes de resolver esse problema. Tenho que admitir que cultivei isso por muito tempo quando comecei a trabalhar com a estrutura. E demorei um pouco para entender realmente o que estava acontecendo.
Essas três coisas são:
- Como o
LabelFor
e outros ...For
ajudantes funcionam no MVC?
- O que é uma árvore de expressão?
- Como funciona o Model Binder?
Todos os três conceitos se conectam para obter uma resposta.
Como o LabelFor
e outros ...For
ajudantes funcionam no MVC?
Então, você já usou as HtmlHelper<T>
extensões para LabelFor
e TextBoxFor
entre outros, e você deve ter notado que quando você invocá-los, você passá-los um lambda e magicamente gera algum html. Mas como?
Portanto, a primeira coisa a notar é a assinatura desses ajudantes. Vejamos a sobrecarga mais simples para
TextBoxFor
public static MvcHtmlString TextBoxFor<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression
)
Em primeiro lugar, este é um método de extensão para um tipo forte HtmlHelper
, do tipo <TModel>
. Portanto, para simplesmente declarar o que acontece nos bastidores, quando o razor renderiza essa visualização, ele gera uma classe. Dentro dessa classe está uma instância de HtmlHelper<TModel>
(como a propriedade Html
, que é o motivo pelo qual você pode usar @Html...
), onde TModel
é o tipo definido em sua @model
instrução. Portanto, no seu caso, quando você estiver olhando para essa vista, TModel
será sempre do tipo ViewModels.MyViewModels.Theme
.
Agora, o próximo argumento é um pouco complicado. Então, vamos dar uma olhada em uma invocação
@Html.TextBoxFor(model=>model.SomeProperty);
Parece que temos um pequeno lambda, e se alguém fosse adivinhar a assinatura, poderia pensar que o tipo para este argumento seria simplesmente um Func<TModel, TProperty>
, onde TModel
é o tipo do modelo de visualização eTProperty
é inferido como o tipo da propriedade.
Mas isso não está certo, se você olhar para o tipo real do argumento, é Expression<Func<TModel, TProperty>>
.
Então, quando você normalmente gera um lambda, o compilador pega o lambda e o compila no MSIL, assim como qualquer outra função (é por isso que você pode usar delegados, grupos de métodos e lambdas mais ou menos indistintamente, porque são apenas referências de código .)
No entanto, quando o compilador vê que o tipo é um Expression<>
, ele não compila imediatamente o lambda para MSIL, em vez disso, gera uma árvore de expressão!
Então, o que diabos é uma árvore de expressão. Bem, não é complicado, mas também não é um passeio no parque. Para citar ms:
| Árvores de expressão representam o código em uma estrutura de dados semelhante a uma árvore, em que cada nó é uma expressão, por exemplo, uma chamada de método ou uma operação binária, como x <y.
Simplificando, uma árvore de expressão é uma representação de uma função como uma coleção de "ações".
No caso de model=>model.SomeProperty
, a árvore de expressão teria um nó que diz: "Obtenha 'Alguma Propriedade' de um 'modelo'"
Esta árvore de expressão pode ser compilada em uma função que pode ser invocada, mas contanto que seja uma árvore de expressão, é apenas uma coleção de nós.
Então, para que isso é bom?
Então , Func<>
ou Action<>
, uma vez que você os tenha, eles são praticamente atômicos. Tudo o que você pode realmente fazer éInvoke()
eles, ou , dizer-lhes para fazer o trabalho que devem fazer.
Expression<Func<>>
por outro lado, representa uma coleção de ações, que podem ser anexadas, manipuladas, visitadas ou compiladas e invocadas.
Então por que você está me contando tudo isso?
Assim, com esse entendimento do que Expression<>
é, podemos voltar Html.TextBoxFor
. Quando ele renderiza uma caixa de texto, ele precisa gerar algumas coisas sobre a propriedade que você está atribuindo a ele. Coisas como attributes
a propriedade para validação e, especificamente, neste caso, ele precisa descobrir como nomear a <input>
tag.
Ele faz isso "percorrendo" a árvore de expressão e construindo um nome. Portanto, para uma expressão como model=>model.SomeProperty
, ele percorre a expressão reunindo as propriedades que você está solicitando e constrói<input name='SomeProperty'>
.
Para um exemplo mais complicado, como model=>model.Foo.Bar.Baz.FooBar
, pode gerar<input name="Foo.Bar.Baz.FooBar" value="[whatever FooBar is]" />
Faz sentido? Não é apenas o trabalho que Func<>
ele faz, mas como ele faz seu trabalho é importante aqui.
(Observe que outras estruturas, como LINQ to SQL, fazem coisas semelhantes percorrendo uma árvore de expressão e construindo uma gramática diferente, que neste caso é uma consulta SQL)
Como funciona o Model Binder?
Assim que você conseguir isso, teremos que falar brevemente sobre o fichário de modelos. Quando o formulário é postado, é simplesmente como um plano
Dictionary<string, string>
, perdemos a estrutura hierárquica que nosso modelo de visualização aninhado pode ter tido. É função do fichário do modelo pegar esse combo de par de valores-chave e tentar reidratar um objeto com algumas propriedades. Como isso faz? Você adivinhou, usando a "chave" ou nome da entrada que foi postada.
Então, se a postagem do formulário parecer
Foo.Bar.Baz.FooBar = Hello
E você está postando em um modelo chamado SomeViewModel
, então ele faz o inverso do que o ajudante fez inicialmente. Procura uma propriedade chamada "Foo". Em seguida, procura uma propriedade chamada "Bar" fora de "Foo", em seguida, procura "Baz" ... e assim por diante ...
Finalmente, ele tenta analisar o valor no tipo de "FooBar" e atribuí-lo a "FooBar".
PHEW !!!
E voila, você tem seu modelo. A instância que o Model Binder acabou de construir é entregue na ação solicitada.
Portanto, sua solução não funciona porque os Html.[Type]For()
ajudantes precisam de uma expressão. E você está apenas dando a eles um valor. Ele não tem ideia de qual é o contexto para esse valor e não sabe o que fazer com ele.
Agora, algumas pessoas sugeriram o uso de parciais para renderizar. Em teoria, isso funcionará, mas provavelmente não da maneira que você espera. Ao renderizar um parcial, você está alterando o tipo de TModel
, porque está em um contexto de visualização diferente. Isso significa que você pode descrever sua propriedade com uma expressão mais curta. Também significa que quando o auxiliar gerar o nome para sua expressão, ele será superficial. Ele só será gerado com base na expressão fornecida (não em todo o contexto).
Digamos que você tenha um parcial que acabou de renderizar "Baz" (do nosso exemplo anterior). Dentro dessa parcial, você poderia apenas dizer:
@Html.TextBoxFor(model=>model.FooBar)
Ao invés de
@Html.TextBoxFor(model=>model.Foo.Bar.Baz.FooBar)
Isso significa que ele irá gerar uma tag de entrada como esta:
<input name="FooBar" />
Que, se você estiver postando este formulário para uma ação que espera um ViewModel grande e profundamente aninhado, ele tentará hidratar uma propriedade chamada FooBar
de TModel
. Que, na melhor das hipóteses, não está lá e, na pior, é algo totalmente diferente. Se você estivesse postando para uma ação específica que estava aceitando um Baz
, em vez do modelo raiz, isso funcionaria muito bem! Na verdade, os parciais são uma boa maneira de alterar o contexto de visualização, por exemplo, se você tiver uma página com vários formulários que postam em ações diferentes, renderizar um parcial para cada um seria uma ótima ideia.
Agora, depois de obter tudo isso, você pode começar a fazer coisas realmente interessantes com o Expression<>
, estendendo-os programaticamente e fazendo outras coisas legais com eles. Não vou entrar em nada disso. Mas, com sorte, isso lhe dará uma melhor compreensão do que está acontecendo nos bastidores e por que as coisas estão agindo da maneira que estão.
@
antes de tudoforeach
? Você também não deveria ter lambdas emHtml.EditorFor
(Html.EditorFor(m => m.Note)
, por exemplo) e no resto dos métodos? Posso estar enganado, mas você pode colar seu código real? Eu sou muito novo no MVC, mas você pode resolver isso facilmente com visualizações parciais ou editores (se esse é o nome?).