MVC Razor view nested no modelo de foreach

Imagine um cenário comum, esta é uma versão mais simples do que estou encontrando. Eu realmente tenho algumas camadas de nidificação adicionais nas minhas ….

Mas esse é o cenário

Tema contém Lista Categoria contém Lista Produto contém Lista

Meu controlador fornece um tema totalmente preenchido, com todas as categorias para esse tema, os produtos dentro dessas categorias e as suas ordens.

A coleção de pedidos tem uma propriedade chamada Quantity (entre muitas outras) que precisa ser editável.

@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) } } } 

Se eu usar lambda, então eu só pareço ter uma referência ao object top Model, “Theme”, não aqueles dentro do loop foreach.

O que eu estou tentando fazer é possível ou superestimou ou entendeu mal o que é possível?

Com o acima eu recebo um erro no TextboxFor, EditorFor, etc

CS0411: Os argumentos de tipo para o método ‘System.Web.Mvc.Html.InputExtensions.TextBoxFor (System.Web.Mvc.HtmlHelper, System.Linq.Expressions.Expression>)’ não podem ser inferidos do uso. Tente especificar os argumentos de tipo explicitamente.

Obrigado.

    A resposta rápida é usar um loop for() no lugar de seus loops 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) } } } 

    Mas isso encobre por que isso resolve o problema.

    Há três coisas que você tem pelo menos um entendimento superficial antes de resolver esse problema. Eu tenho que admitir que eu cultivei isso por muito tempo quando comecei a trabalhar com o framework. E levei um bom tempo para realmente entender o que estava acontecendo.

    Essas três coisas são:

    • Como o LabelFor e outros ...For ajudantes trabalham no MVC?
    • O que é uma tree de expressões?
    • Como o modelo Binder funciona?

    Todos esses três conceitos se unem para obter uma resposta.

    Como o LabelFor e outros ...For ajudantes trabalham no MVC?

    Então, você usou as extensões HtmlHelper para LabelFor e TextBoxFor e outras, e você provavelmente notou que, quando invocá-las, você passa um lambda e magicamente gera algum html. Mas como?

    Então, a primeira coisa a notar é a assinatura desses ajudantes. Vamos olhar para a sobrecarga mais simples para TextBoxFor

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

    Primeiro, este é um método de extensão para um HtmlHelper fortemente tipado, do tipo . Então, para simplesmente dizer o que acontece nos bastidores, quando o barbeador renderiza essa visão, gera uma class. Dentro desta class é uma instância de HtmlHelper (como a propriedade Html , e é por isso que você pode usar @Html... ), onde TModel é o tipo definido em sua instrução @model . Então, no seu caso, quando você está olhando para essa visão, o modelo será sempre do tipo ViewModels.MyViewModels.Theme .

    Agora, o próximo argumento é um pouco complicado. Então, vamos olhar para uma invocação

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

    Parece que temos um pouco lambda, E se alguém fosse adivinhar a assinatura, alguém poderia pensar que o tipo para este argumento seria simplesmente um Func , onde TModel é o tipo do modelo de visão e TProperty é inferida como o tipo da propriedade.

    Mas isso não está certo, se você olhar para o tipo real do argumento, sua Expression> .

    Então, quando você normalmente gera um lambda, o compilador pega o lambda eo compila em MSIL, assim como qualquer outra function (é por isso que você pode usar delegates, grupos de methods e lambdas mais ou menos intercambiáveis, porque são apenas referências de código .)

    No entanto, quando o compilador vê que o tipo é uma Expression<> , ele não imediatamente compila o lambda para MSIL, em vez disso, gera uma tree de expressões!

    O que é uma tree de expressões ?

    Então, o que diabos é uma tree de expressão. Bem, não é complicado, mas também não é um passeio no parque. Para citar ms:

    | As trees de expressão representam o código em uma estrutura de dados em forma de tree, em que cada nó é uma expressão, por exemplo, uma chamada de método ou uma operação binária, como x

    Simplificando, uma tree de expressão é uma representação de uma function como uma coleção de “ações”.

    No caso de model=>model.SomeProperty , a tree de expressão teria um nó que diz: “Get ‘Some Property’ from a ‘model'”

    Esta tree de expressões pode ser compilada em uma function que pode ser invocada, mas desde que seja uma tree de expressão, é apenas uma coleção de nós.

    Então, para que serve isso?

    Então Func<> ou Action<> , uma vez que você os tenha, eles são praticamente atômicos. Tudo o que você realmente pode fazer é Invoke() los, ou seja, dizer-lhes para fazer o trabalho que devem fazer.

    Expression> por outro lado, representa uma coleção de ações, que podem ser anexadas, manipuladas, visitadas ou compiladas e chamadas.

    Então, por que você está me dizendo tudo isso?

    Então, com esse entendimento do que é uma Expression<> , podemos voltar para o Html.TextBoxFor . Quando ele renderiza uma checkbox de texto, ele precisa gerar algumas coisas sobre a propriedade que você está dando. Coisas como attributes na propriedade para validação e, especificamente, neste caso, é necessário descobrir o nome da tag .

    Ele faz isso “andando” na tree de expressão e construindo um nome. Portanto, para uma expressão como model=>model.SomeProperty , ela percorre a expressão reunindo as propriedades que você está solicitando e constrói .

    Para um exemplo mais complicado, como model=>model.Foo.Bar.Baz.FooBar , ele pode gerar

    Faz sentido? Não é apenas o trabalho que o Func<> faz, mas como ele faz o seu trabalho é importante aqui.

    (Note que outros frameworks como o LINQ to SQL fazem coisas semelhantes andando por uma tree de expressão e construindo uma gramática diferente, que neste caso é uma consulta SQL)

    Como o modelo Binder funciona?

    Então, quando você conseguir isso, temos que falar brevemente sobre o fichário do modelo. Quando o formulário é postado, é simplesmente como um Dictionary , perdemos a estrutura hierárquica que nosso modelo de visão aninhada pode ter tido. É o trabalho do fichário do modelo tomar esse par de pares de valores-chave e tentar re-hidratar um object com algumas propriedades. Como isso acontece? Você adivinhou, usando a “chave” ou o nome da input que foi postada.

    Então, se o post do formulário se parece com

     Foo.Bar.Baz.FooBar = Hello 

    E você está postando em um modelo chamado SomeViewModel , então faz o inverso do que o ajudante fez em primeiro lugar. Procura uma propriedade chamada “Foo”. Então ele procura por uma propriedade chamada “Bar” de “Foo”, então ela procura por “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 o seu modelo. A instância que o Model Binder acabou de construir é transferida para a ação solicitada.


    Portanto, sua solução não funciona porque os auxiliares Html.[Type]For() precisam de uma expressão. E você está apenas dando a eles um valor. Não tem ideia do que é o contexto para esse valor e não sabe o que fazer com ele.

    Agora algumas pessoas sugeriram o uso de parciais para renderizar. Agora isso, em teoria, funcionará, mas provavelmente não do jeito que você espera. Quando você renderiza uma parcial, você está alterando o tipo de TModel , porque você está em um contexto de visualização diferente. Isso significa que você pode descrever sua propriedade com uma expressão mais curta. Isso também significa que quando o ajudante gera o nome para sua expressão, ele será superficial. Ele só irá gerar com base na expressão que é dada (não em todo o contexto).

    Então vamos dizer que você teve uma 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 input como esta:

      

    Que, se você está postando este formulário em uma ação que está esperando um ViewModel grande profundamente nested, ele tentará hidratar uma propriedade chamada FooBar fora do TModel . Que na melhor das hipóteses não está lá, e na pior das hipóteses é algo completamente diferente. Se você estivesse postando em uma ação específica que estivesse aceitando um Baz , em vez do modelo raiz, isso funcionaria muito bem! Na verdade, as parciais são uma boa maneira de alterar o contexto da visualização, por exemplo, se você tivesse uma página com vários formulários que publicassem todas as ações diferentes, renderizar uma parcial para cada uma 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 interessantes com eles. Eu não vou entrar em nada disso. Mas, espero, isso lhe dará uma melhor compreensão do que está acontecendo nos bastidores e porque as coisas estão agindo do jeito que estão.

    Você pode simplesmente usar EditorTemplates para fazer isso, você precisa criar um diretório chamado “EditorTemplates” na pasta de visualização do seu controlador e colocar uma visão separada para cada uma de suas entidades aninhadas (nomeadas como nome da class de entidade)

    Vista principal :

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

    Visualização de categoria (/MyController/EditorTemplates/Category.cshtml):

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

    Visualização do produto (/MyController/EditorTemplates/Product.cshtml):

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

    e assim por diante

    Desta forma, o Html.EditorFor helper irá gerar os nomes dos elementos de uma maneira ordenada e, portanto, você não terá mais problemas para recuperar a entidade Theme lançada como um todo

    Você poderia adicionar uma parcial de categoria e uma parcial de produto, cada um levaria uma parte menor do modelo principal como seu próprio modelo, ou seja, tipo de modelo da categoria pode ser um IEnumerable, você passaria em Model.Theme para ele. O produto parcial pode ser um IEnumerable que você passar Model.Products em (a partir da categoria parcial).

    Não tenho certeza se esse seria o caminho certo a seguir, mas estaria interessado em saber.

    EDITAR

    Desde postar essa resposta, usei o EditorTemplates e descubro que é a maneira mais fácil de lidar com grupos ou itens de input repetidos. Ele lida com todos os seus problemas de mensagem de validação e problemas de envio de formulário / vinculação de modelo automaticamente.

    Quando você está usando foreach loop dentro da visão do modelo binded … Seu modelo deve estar no formato listado.

    ou seja

     @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 } } 

    É claro a partir do erro.

    O HtmlHelpers acrescentado com “For” espera a expressão lambda como um parâmetro.

    Se você estiver passando o valor diretamente, use melhor o Normal.

    por exemplo

    Em vez de TextboxFor (….) use Textbox ()

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

    Em seu cenário, você pode usar o loop básico, pois ele lhe dará um í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) } } }