MVC Razor view aninhado modelo foreach

94

Imagine um cenário comum, esta é uma versão mais simples do que estou encontrando. Na verdade, tenho algumas camadas de aninhamento adicional na minha ...

Mas este é o cenário

O tema contém a lista A categoria contém a lista O produto contém a lista

Meu controlador fornece um tema totalmente preenchido, com todas as categorias desse tema, os produtos dentro dessas categorias e seus pedidos.

A coleção de pedidos possui uma propriedade chamada Quantidade (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 em vez disso, então pareço obter apenas uma referência ao objeto Model superior, "Theme", não aqueles dentro do loop foreach.

O que estou tentando fazer lá é mesmo possível ou superestimei ou não entendi o que é possível?

Com o acima, 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.

David C
fonte
1
Você não deveria ter @antes de tudo foreach? Você também não deveria ter lambdas em Html.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?).
Kobi
category.nameTenho certeza que é um stringe ...Fornão suporta uma string como o primeiro parâmetro
balexandre
sim, acabei de perder os @'s, agora adicionados. Obrigado. No entanto, quanto a lambda, se eu começar a digitar @ Html.TextBoxFor (m => m. Então, pareço obter apenas uma referência ao objeto Model superior, não aqueles dentro do loop foreach.
David C
@DavidC - Não conheço o suficiente sobre MVC 3 para responder ainda - mas suspeito que esse seja o seu problema :).
Kobi
2
Estou no trem, mas se isso não for respondido quando eu chegar ao trabalho, postarei uma resposta. A resposta rápida é usar um regular em for()vez de um foreach. Vou explicar o porquê, porque isso me confundiu por muito tempo também.
J. Holmes

Respostas:

304

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 LabelFore outros ...Forajudantes 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 LabelFore outros ...Forajudantes funcionam no MVC?

Então, você já usou as HtmlHelper<T>extensões para LabelFore TextBoxForentre 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 @modelinstruçã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!

O que é 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 attributesa 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 FooBarde 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.

J. Holmes
fonte
4
Resposta incrível. Atualmente estou tentando digeri-lo. :) Também culpado de Cultivo de Carga! Como essa descrição.
David C
4
Obrigado por esta resposta detalhada!
Kobi
14
Precisa de mais de um voto positivo para isso. +3 (um para cada explicação), e +1 para Cargo-Cultistas. Resposta absolutamente brilhante!
Kyeotic
3
É por isso que eu amo o SO: resposta curta + explicação detalhada + link incrível (culto ao cargo). Eu gostaria de mostrar o post sobre o culto ao carregamento para quem não acha que o conhecimento sobre o funcionamento interno das coisas é extremamente importante!
user1068352
18

Você pode simplesmente usar EditorTemplates para fazer isso, você precisa criar um diretório chamado "EditorTemplates" na pasta de visualização de seu controlador e colocar uma visualização separada para cada uma de suas entidades aninhadas (nomeado como nome de classe 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 helper Html.EditorFor irá gerar os nomes dos elementos de uma maneira ordenada e, portanto, você não terá mais nenhum problema para recuperar a entidade Theme postada como um todo

Alireza Sabouri
fonte
1
Embora a resposta aceita seja muito boa (eu também votei), essa é a opção mais sustentável.
Aaron
4

Você poderia adicionar um parcial de Categoria e um parcial de Produto, cada um pegaria uma parte menor do modelo principal como seu próprio modelo, ou seja, o tipo de modelo de Categoria pode ser um IEnumerable, você passaria Model.Theme para ele. O parcial do produto pode ser um IEnumerable para o qual você passa Model.Products (de dentro do parcial da categoria).

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

EDITAR

Desde a publicação desta resposta, usei EditorTemplates e considero a maneira mais fácil de lidar com grupos ou itens de entrada repetidos. Ele lida com todos os problemas de mensagem de validação e problemas de envio de formulário / vinculação de modelo automaticamente.

Adrian Thompson Phillips
fonte
Isso tinha me ocorrido, só não tinha certeza de como iria lidar com isso quando li de volta para atualizar.
David C
1
Está perto, mas como se trata de um formulário a ser postado como uma unidade, não funcionará muito bem. Uma vez dentro da parcial, o contexto da visão mudou e não tem mais a expressão profundamente aninhada. A postagem de volta para o Thememodelo não seria hidratada corretamente.
J. Holmes
Essa é a minha preocupação também. Eu normalmente faria o acima como uma abordagem somente leitura para exibir os produtos e, em seguida, forneceria um link em cada produto para talvez um método de ação / Produto / Editar / 123 para editar cada um em seu próprio formulário. Eu acho que você pode ficar desfeito tentando fazer muito em uma página no MVC.
Adrian Thompson Phillips
@AdrianThompsonPhillips sim, é bem possível que eu tenha. Eu vim de um background do Forms, então ainda não consigo me acostumar com a ideia de ter que sair da página para fazer uma edição. :(
David C
2

Quando você está usando o loop foreach dentro da visualização para o modelo vinculado ... Seu modelo deve estar no formato listado.

ie

@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
fonte
Estou surpreso que o código acima até compila. @ Html.LabelFor requer uma operação FUNC como parâmetro, a sua não
Jenna Leaf
Não sei se o código acima compila ou não, mas aninhado @foreach funciona para mim. MVC5.
antonio
0

O erro está claro.

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

Se você estiver passando o valor diretamente, é melhor usar um Normal.

por exemplo

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

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

Em seu cenário, você pode usar o loop for básico, pois ele fornecerá um í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
fonte
0

Outra possibilidade muito mais simples é que um dos nomes de sua propriedade esteja errado (provavelmente um que você acabou de alterar na classe). Isso é o que era para mim no RazorPages .NET Core 3.

Primeira divisão
fonte