MVC Razor visualizza il modello di foreach annidato


94

Immagina uno scenario comune, questa è una versione più semplice di ciò che sto incontrando. In realtà ho un paio di strati di ulteriore nidificazione sul mio ...

Ma questo è lo scenario

Il tema contiene l'elenco La categoria contiene l'elenco Il prodotto contiene l'elenco

Il mio controller fornisce un tema completamente popolato, con tutte le categorie per quel tema, i prodotti all'interno di queste categorie e i relativi ordini.

La raccolta degli ordini ha una proprietà chiamata Quantità (tra molte altre) che deve essere modificabile.

@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 invece utilizzo lambda, mi sembra di ottenere solo un riferimento all'oggetto Model in alto, "Theme", non quelli all'interno del ciclo foreach.

Quello che sto cercando di fare è possibile o ho sopravvalutato o frainteso ciò che è possibile?

Con quanto sopra ottengo un errore su TextboxFor, EditorFor, ecc

CS0411: gli argomenti di tipo per il metodo 'System.Web.Mvc.Html.InputExtensions.TextBoxFor (System.Web.Mvc.HtmlHelper, System.Linq.Expressions.Expression>)' non possono essere dedotti dall'utilizzo. Prova a specificare esplicitamente gli argomenti del tipo.

Grazie.


1
Non avresti dovuto @prima di tutto foreach? Non dovresti avere anche lambda in Html.EditorFor( Html.EditorFor(m => m.Note), ad esempio) e nel resto dei metodi? Potrei sbagliarmi, ma puoi incollare il tuo codice effettivo? Sono abbastanza nuovo per MVC, ma puoi risolverlo abbastanza facilmente con visualizzazioni parziali o editor (se questo è il nome?).
Kobi

category.nameSono sicuro che è un stringe ...Fornon supporta una stringa come primo parametro
balexandre

sì, ho appena perso le @, ora aggiunte. Grazie. Tuttavia, come per lambda, se inizio a digitare @ Html.TextBoxFor (m => m. Allora mi sembra di ottenere solo un riferimento all'oggetto Model superiore, non quelli all'interno del ciclo foreach.
David C

@DavidC - Non so ancora abbastanza MVC 3 per rispondere - ma sospetto che sia il tuo problema :).
Kobi

2
Sono sul treno, ma se non ho risposta prima di arrivare al lavoro, posta una risposta. La risposta rapida è usare un normale for()piuttosto che un file foreach. Spiegherò perché, perché mi ha confuso a morte anche per molto tempo.
J. Holmes

Risposte:


304

La risposta rapida è usare un for()loop al posto dei tuoi foreach()loop. Qualcosa di simile a:

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

Ma questo sorvola sul motivo per cui questo risolve il problema.

Ci sono tre cose che hai almeno una comprensione superficiale prima di poter risolvere questo problema. Devo ammettere che l'ho coltivato per molto tempo quando ho iniziato a lavorare con il framework. E mi ci è voluto un bel po 'per capire davvero cosa stava succedendo.

Queste tre cose sono:

  • Come funzionano gli helper LabelFore gli altri ...Forin MVC?
  • Cos'è un albero delle espressioni?
  • Come funziona il Model Binder?

Tutti e tre questi concetti si collegano insieme per ottenere una risposta.

Come funzionano gli helper LabelFore gli altri ...Forin MVC?

Quindi, hai usato le HtmlHelper<T>estensioni per LabelFore TextBoxFore altri, e probabilmente hai notato che quando le invochi, passi loro un lambda e questo genera magicamente dell'html. Ma come?

Quindi la prima cosa da notare è la firma di questi aiutanti. Vediamo il sovraccarico più semplice per TextBoxFor

public static MvcHtmlString TextBoxFor<TModel, TProperty>(
    this HtmlHelper<TModel> htmlHelper,
    Expression<Func<TModel, TProperty>> expression
) 

In primo luogo, si tratta di un metodo di estensione per un fortemente tipizzato HtmlHelper, di tipo <TModel>. Quindi, per indicare semplicemente cosa succede dietro le quinte, quando razor esegue il rendering di questa vista, genera una classe. All'interno di questa classe c'è un'istanza di HtmlHelper<TModel>(come proprietà Html, motivo per cui puoi usare @Html...), dove TModelè il tipo definito nella tua @modelistruzione. Quindi nel tuo caso, quando guardi questa visualizzazione TModel sarà sempre del tipo ViewModels.MyViewModels.Theme.

Ora, il prossimo argomento è un po 'complicato. Quindi diamo un'occhiata a un'invocazione

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

Sembra che abbiamo un piccolo lambda, e se si dovesse indovinare la firma, si potrebbe pensare che il tipo per questo argomento sarebbe semplicemente a Func<TModel, TProperty>, dove TModelè il tipo del modello di visualizzazione e TProperty viene dedotto come il tipo della proprietà.

Ma non è del tutto corretto, se si guarda al tipo effettivo di argomento è Expression<Func<TModel, TProperty>>.

Quindi, quando normalmente generi un lambda, il compilatore prende lambda e lo compila in MSIL, proprio come qualsiasi altra funzione (motivo per cui puoi usare delegati, gruppi di metodi e lambda più o meno in modo intercambiabile, perché sono solo riferimenti a codice .)

Tuttavia, quando il compilatore vede che il tipo è an Expression<>, non compila immediatamente il lambda fino a MSIL, ma genera invece un albero delle espressioni!

Cos'è un albero delle espressioni ?

Quindi, cosa diavolo è un albero delle espressioni. Beh, non è complicato ma non è nemmeno una passeggiata nel parco. Per citare ms:

| Gli alberi delle espressioni rappresentano il codice in una struttura dati ad albero, in cui ogni nodo è un'espressione, ad esempio una chiamata a un metodo o un'operazione binaria come x <y.

In poche parole, un albero delle espressioni è una rappresentazione di una funzione come un insieme di "azioni".

Nel caso di model=>model.SomeProperty, l'albero delle espressioni avrebbe un nodo al suo interno che dice: "Ottieni 'alcune proprietà' da un 'modello'"

Questo albero delle espressioni può essere compilato in una funzione che può essere invocata, ma fintanto che è un albero delle espressioni, è solo una raccolta di nodi.

Allora a cosa serve?

Quindi Func<>o Action<>, una volta che li hai, sono praticamente atomici. Tutto quello che puoi davvero fare è Invoke()loro, ovvero dire loro di fare il lavoro che dovrebbero fare.

Expression<Func<>>d'altra parte, rappresenta una raccolta di azioni, che possono essere aggiunte, manipolate, visitate o compilate e invocate.

Allora perché mi stai dicendo tutto questo?

Quindi, con questa comprensione di cosa Expression<>sia, possiamo tornare a Html.TextBoxFor. Quando esegue il rendering di una casella di testo, deve generare alcune cose sulla proprietà che gli stai dando. Cose come attributessulla proprietà per la convalida, e in particolare in questo caso è necessario capire come denominare il <input>tag.

Lo fa "percorrendo" l'albero delle espressioni e costruendo un nome. Quindi per un'espressione come model=>model.SomeProperty, percorre l'espressione raccogliendo le proprietà che stai chiedendo e costruendo <input name='SomeProperty'>.

Per un esempio più complicato, come model=>model.Foo.Bar.Baz.FooBar, potrebbe generare<input name="Foo.Bar.Baz.FooBar" value="[whatever FooBar is]" />

Ha senso? Non è solo il lavoro che Func<>fa, ma come fa il suo lavoro è importante qui.

(Nota che altri framework come LINQ to SQL fanno cose simili camminando su un albero delle espressioni e costruendo una grammatica diversa, in questo caso una query SQL)

Come funziona il Model Binder?

Quindi, una volta ottenuto, dobbiamo parlare brevemente del raccoglitore del modello. Quando il modulo viene pubblicato, è semplicemente come un appartamento Dictionary<string, string>, abbiamo perso la struttura gerarchica che poteva avere il nostro modello di visualizzazione annidata. È compito del raccoglitore di modelli prendere questa combinazione di coppia chiave-valore e tentare di reidratare un oggetto con alcune proprietà. Come lo fa? Hai indovinato, usando la "chiave" o il nome dell'ingresso che è stato pubblicato.

Quindi, se il modulo post assomiglia

Foo.Bar.Baz.FooBar = Hello

E stai postando su un modello chiamato SomeViewModel, quindi fa il contrario di ciò che ha fatto l'aiutante in primo luogo. Cerca una proprietà chiamata "Foo". Quindi cerca una proprietà chiamata "Bar" fuori da "Foo", quindi cerca "Baz" ... e così via ...

Infine cerca di analizzare il valore nel tipo di "FooBar" e di assegnarlo a "FooBar".

PHEW !!!

E voilà, hai il tuo modello. L'istanza appena costruita dal Raccoglitore di modelli viene passata all'azione richiesta.


Quindi la tua soluzione non funziona perché gli Html.[Type]For()helper hanno bisogno di un'espressione. E stai solo dando loro un valore. Non ha idea di quale sia il contesto per quel valore e non sa cosa farne.

Ora alcune persone hanno suggerito di utilizzare i parziali per il rendering. Ora, in teoria funzionerà, ma probabilmente non nel modo in cui ti aspetti. Quando si esegue il rendering di un parziale, si cambia il tipo di TModel, perché ci si trova in un contesto di visualizzazione diverso. Ciò significa che puoi descrivere la tua proprietà con un'espressione più breve. Significa anche che quando l'assistente genera il nome per la tua espressione, sarà superficiale. Verrà generato solo in base all'espressione fornita (non all'intero contesto).

Quindi diciamo che hai un parziale che ha appena reso "Baz" (dal nostro esempio prima). All'interno di quel parziale potresti semplicemente dire:

@Html.TextBoxFor(model=>model.FooBar)

Piuttosto che

@Html.TextBoxFor(model=>model.Foo.Bar.Baz.FooBar)

Ciò significa che genererà un tag di input come questo:

<input name="FooBar" />

Il che, se stai inviando questo modulo a un'azione che prevede un ViewModel grande e profondamente annidato, tenterà di idratare una proprietà FooBarannullata TModel. Che nel migliore dei casi non c'è, e nel peggiore è qualcosa di completamente diverso. Se stavi postando per un'azione specifica che accettava un Baz, piuttosto che il modello radice, allora funzionerebbe benissimo! In effetti, i parziali sono un buon modo per cambiare il contesto di visualizzazione, ad esempio se avessi una pagina con più moduli che pubblicano tutti in azioni diverse, quindi rendere un parziale per ciascuno sarebbe un'ottima idea.


Ora, una volta ottenuto tutto questo, puoi iniziare a fare cose davvero interessanti con Expression<>, estendendole programmaticamente e facendo altre cose pulite con esse. Non entrerò in niente di tutto ciò. Ma, si spera, questo ti darà una migliore comprensione di cosa sta succedendo dietro le quinte e perché le cose si stanno comportando come stanno.


4
Risposta fantastica. Attualmente sto cercando di digerirlo. :) Anche colpevole di Cargo Culting! Come quella descrizione.
David C

4
Grazie per questa risposta dettagliata!
Kobi

14
Serve più di un voto positivo per questo. +3 (uno per ogni spiegazione) e +1 per Cargo-Cultists. Risposta assolutamente geniale!
Kyeotic

3
Questo è il motivo per cui adoro SO: risposta breve + spiegazione approfondita + link fantastico (cargo-cult). Mi piacerebbe mostrare il post sul cargo-cult a chiunque non pensi che la conoscenza del funzionamento interno delle cose sia estremamente importante!
user1068352

18

Puoi semplicemente usare EditorTemplates per farlo, devi creare una directory denominata "EditorTemplates" nella cartella della vista del tuo controller e posizionare una vista separata per ciascuna delle tue entità nidificate (denominata come nome della classe di entità)

Vista principale:

@model ViewModels.MyViewModels.Theme

@Html.LabelFor(Model.Theme.name)
@Html.EditorFor(Model.Theme.Categories)

Vista per categoria (/MyController/EditorTemplates/Category.cshtml):

@model ViewModels.MyViewModels.Category

@Html.LabelFor(Model.Name)
@Html.EditorFor(Model.Products)

Vista prodotto (/MyController/EditorTemplates/Product.cshtml):

@model ViewModels.MyViewModels.Product

@Html.LabelFor(Model.Name)
@Html.EditorFor(Model.Orders)

e così via

in questo modo Html.EditorFor helper genererà i nomi degli elementi in modo ordinato e quindi non avrai ulteriori problemi per recuperare l'entità Theme inserita nel suo insieme


1
Sebbene la risposta accettata sia molto buona (l'ho anche votata positivamente), questa risposta è l'opzione più gestibile.
Aaron

4

È possibile aggiungere un parziale di categoria e un parziale di prodotto, ognuno prenderebbe una parte più piccola del modello principale in quanto è il proprio modello, cioè il tipo di modello di categoria potrebbe essere un IEnumerable, si passerà in Model.Theme ad esso. Il parziale del prodotto potrebbe essere un oggetto IEnumerable a cui si passa Model.Products (dall'interno del parziale della categoria).

Non sono sicuro che sia la strada giusta da seguire, ma sarei interessato a saperlo.

MODIFICARE

Da quando ho pubblicato questa risposta, ho usato EditorTemplates e trovo che questo sia il modo più semplice per gestire gruppi o elementi di input ripetuti. Gestisce automaticamente tutti i problemi relativi ai messaggi di convalida e ai problemi di invio del modulo / associazione del modello.


Mi era venuto in mente, ma non ero sicuro di come l'avrebbe gestito quando l'ho riletto per aggiornarlo.
David C

1
È vicino, ma poiché questo è un modulo da pubblicare come unità, non funzionerà correttamente. Una volta all'interno del parziale, il contesto della vista è cambiato e non ha più l'espressione profondamente annidata. Postare di nuovo sul Thememodello non verrebbe idratato correttamente.
J. Holmes

Questa è anche la mia preoccupazione. Di solito eseguo quanto sopra come approccio di sola lettura alla visualizzazione dei prodotti e quindi fornisco un collegamento su ciascun prodotto a un metodo di azione / Prodotto / Modifica / 123 per modificare ciascuno nel proprio modulo. Penso che tu possa essere annullato cercando di fare troppo su una pagina in MVC.
Adrian Thompson Phillips,

@AdrianThompsonPhillips sì, è molto probabile che io abbia. Vengo da uno sfondo di Moduli, quindi non riesco ancora ad abituarmi all'idea di dover lasciare la pagina per apportare una modifica. :(
David C

2

Quando si utilizza il ciclo foreach nella vista per il modello vincolato ... Il modello dovrebbe essere nel formato elencato.

cioè

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

Sono sorpreso che il codice sopra venga compilato. @ Html.LabelFor richiede un'operazione FUNC come parametro, il tuo non lo è
Jenna Leaf

Non so se il codice sopra viene compilato o meno, ma @foreach annidato funziona per me. MVC5.
antonio

0

È chiaro dall'errore.

HtmlHelpers aggiunto con "For" prevede un'espressione lambda come parametro.

Se stai passando il valore direttamente, meglio usare Normale.

per esempio

Invece di TextboxFor (....) usa Textbox ()

la sintassi per TextboxFor sarà come Html.TextBoxFor (m => m.Property)

Nel tuo scenario puoi usare il ciclo for di base, poiché ti darà l'indice da usare.

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

0

Un'altra possibilità molto più semplice è che uno dei nomi delle proprietà sia sbagliato (probabilmente uno che hai appena modificato nella classe). Questo è quello che è stato per me in RazorPages .NET Core 3.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.