Widok Razor MVC zagnieżdżony model foreach

94

Wyobraź sobie typowy scenariusz, to prostsza wersja tego, z czym się spotykam. W rzeczywistości mam kilka warstw dalszego zagnieżdżenia na moim ...

Ale taki jest scenariusz

Motyw zawiera listę Kategoria zawiera listę Produkt zawiera listę

Mój kontroler zapewnia w pełni wypełniony motyw ze wszystkimi kategoriami dla tego motywu, produktami w tych kategoriach i ich zamówieniami.

Kolekcja zamówień ma właściwość o nazwie Ilość (między innymi), którą należy edytować.

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

Jeśli zamiast tego użyję lambdy, to wydaje mi się, że otrzymuję tylko odniesienie do najwyższego obiektu Model, „Theme”, a nie tych w pętli foreach.

Czy to, co próbuję tam zrobić, jest w ogóle możliwe, czy też przeszacowałem lub źle zrozumiałem, co jest możliwe?

W związku z powyższym otrzymuję błąd w TextboxFor, EditorFor itp

CS0411: Na podstawie użycia nie można wywnioskować argumentów typu metody „System.Web.Mvc.Html.InputExtensions.TextBoxFor (System.Web.Mvc.HtmlHelper, System.Linq.Expressions.Expression>)”. Spróbuj jawnie określić argumenty typu.

Dzięki.

David C.
źródło
1
Nie powinieneś tego robić @przed wszystkim foreach? Czy nie powinieneś również mieć lambd w Html.EditorFor( Html.EditorFor(m => m.Note)na przykład) i pozostałych metodach? Mogę się mylić, ale czy możesz wkleić swój rzeczywisty kod? Jestem całkiem nowy w MVC, ale można go dość łatwo rozwiązać za pomocą częściowych widoków lub edytorów (jeśli tak się nazywają?).
Kobi
category.nameJestem pewien, że jest to stringi ...Fornie obsługuje łańcucha jako pierwszego parametru
balexandre
tak, właśnie przegapiłem @, teraz dodane. Dzięki. Jednak, jak dla lambda, jeśli zacznę wpisywać @ Html.TextBoxFor (m => m potem tylko wydają się uzyskać odwołanie do górnej Object Model, a nie te, wewnątrz pętli foreach..
David C
@DavidC - Nie znam jeszcze wystarczająco MVC 3, aby odpowiedzieć - ale podejrzewam, że to twój problem :).
Kobi
2
Jestem w pociągu, ale jeśli nie otrzymam odpowiedzi, zanim dojdę do pracy, złóż odpowiedź. Szybką odpowiedzią jest użycie zwykłego for()zamiast pliku foreach. Wytłumaczę dlaczego, bo przez długi czas też mnie to zdezorientowało.
J. Holmes,

Odpowiedzi:

304

Szybką odpowiedzią jest użycie for()pętli zamiast foreach()pętli. Coś jak:

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

Ale to wyjaśnia, dlaczego to rozwiązuje problem.

Są trzy rzeczy, które musisz przynajmniej pobieżnie zrozumieć, zanim będziesz mógł rozwiązać ten problem. Muszę przyznać, że bardzo długo to kultywowałem, kiedy zacząłem pracować z frameworkiem. Zajęło mi trochę czasu, zanim naprawdę zrozumiałem, co się dzieje.

Te trzy rzeczy to:

  • Jak działają pomocnicy LabelFori inni ...Forpomocnicy w MVC?
  • Co to jest drzewo wyrażeń?
  • Jak działa segregator modeli?

Wszystkie trzy pojęcia łączą się, aby uzyskać odpowiedź.

Jak działają pomocnicy LabelFori inni ...Forpomocnicy w MVC?

Więc użyłeś HtmlHelper<T>rozszerzeń dla LabelFori TextBoxFori innych, i prawdopodobnie zauważyłeś, że kiedy je wywołujesz, przekazujesz im lambdę i magicznie generuje jakiś kod HTML. Ale jak?

Więc pierwszą rzeczą, na którą należy zwrócić uwagę, jest podpis tych pomocników. Spójrzmy na najprostsze przeciążenie dla TextBoxFor

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

Po pierwsze, jest to metoda rozszerzająca dla silnie wpisanego HtmlHelpertypu <TModel>. Tak więc, aby po prostu stwierdzić, co dzieje się za kulisami, kiedy brzytwa renderuje ten widok, generuje klasę. Wewnątrz tej klasy znajduje się instancja HtmlHelper<TModel>(jako właściwość Html, dlatego możesz użyć @Html...), gdzie TModeljest typem zdefiniowanym w @modelinstrukcji. Więc w twoim przypadku, kiedy patrzysz na ten widok TModel , zawsze będzie to typ ViewModels.MyViewModels.Theme.

Teraz następny argument jest nieco skomplikowany. Spójrzmy więc na inwokację

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

Wygląda na to, że mamy małą lambdę. Gdyby zgadnąć sygnaturę, można by pomyśleć, że typem tego argumentu będzie po prostu a Func<TModel, TProperty>, gdzie TModeljest typem modelu widoku i TProperty jest wywnioskowany jako typ właściwości.

Ale to nie do końca w porządku, jeśli spojrzeć na rzeczywisty typ argumentu its Expression<Func<TModel, TProperty>>.

Więc kiedy normalnie generujesz lambdę, kompilator pobiera lambdę i kompiluje ją do MSIL, tak jak każda inna funkcja (dlatego możesz używać delegatów, grup metod i lambd mniej lub bardziej zamiennie, ponieważ są one tylko odwołaniami do kodu .)

Jednak gdy kompilator widzi, że typ to an Expression<>, nie kompiluje natychmiast lambda do MSIL, zamiast tego generuje drzewo wyrażeń!

Co to jest drzewo wyrażeń ?

A więc, do cholery, jest drzewo ekspresji. Cóż, to nie jest skomplikowane, ale nie jest to też spacer po parku. Cytując ms:

| Drzewa wyrażeń reprezentują kod w strukturze danych podobnej do drzewa, gdzie każdy węzeł jest wyrażeniem, na przykład wywołaniem metody lub operacją binarną, taką jak x <y.

Mówiąc najprościej, drzewo wyrażeń jest reprezentacją funkcji jako zbiór „akcji”.

W przypadku model=>model.SomePropertydrzewa wyrażenia byłoby w nim węzeł, który mówi: „Pobierz 'jakąś właściwość' z 'modelu'”

To drzewo wyrażeń można skompilować w funkcję, którą można wywołać, ale dopóki jest to drzewo wyrażeń, jest to po prostu zbiór węzłów.

Więc po co to jest dobre?

Więc Func<>albo Action<>kiedy już je masz, są prawie atomowe. Wszystko, co naprawdę możesz zrobić, to Invoke()ich, czyli powiedzieć im, aby wykonali pracę, którą powinni wykonać.

Expression<Func<>>z drugiej strony reprezentuje zbiór działań, które mogą być dołączane, manipulowane, odwiedzane lub kompilowane i wywoływane.

Więc dlaczego mi to wszystko mówisz?

Więc mając zrozumienie tego, czym Expression<>jest, możemy wrócić do Html.TextBoxFor. Kiedy renderuje pole tekstowe, musi wygenerować kilka informacji o właściwości, którą mu nadajesz. Rzeczy takie jak attributesna nieruchomości dla walidacji, a konkretnie w tym przypadku musi dowiedzieć się, co nazwać ten <input>tag.

Odbywa się to poprzez „chodzenie” po drzewie wyrażeń i budowanie nazwy. Tak więc w przypadku wyrażenia typu model=>model.SomePropertyprzechodzi przez wyrażenie, zbierając właściwości, o które prosisz i które tworzy <input name='SomeProperty'>.

Dla bardziej skomplikowanego przykładu, model=>model.Foo.Bar.Baz.FooBarmoże wygenerować<input name="Foo.Bar.Baz.FooBar" value="[whatever FooBar is]" />

Ma sens? Nie tylko praca, którą Func<>wykonuje, ale to, jak wykonuje swoją pracę, jest tutaj ważne.

(Zwróć uwagę, że inne struktury, takie jak LINQ to SQL, robią podobne rzeczy, chodząc po drzewie wyrażeń i budując inną gramatykę, w tym przypadku zapytanie SQL)

Jak działa segregator modeli?

Więc kiedy już to zrozumiesz, musimy krótko porozmawiać o segregatorze modelowym. Kiedy formularz jest wysyłany, jest po prostu jak płaski Dictionary<string, string>, straciliśmy strukturę hierarchiczną, którą mógł mieć nasz zagnieżdżony model widoku. Zadaniem segregatora modelu jest pobranie tej kombinacji klucz-wartość i próba ponownego uwodnienia obiektu z pewnymi właściwościami. Jak to się dzieje? Zgadłeś, używając „klucza” lub nazwy opublikowanego wejścia.

Więc jeśli formularz wygląda jak post

Foo.Bar.Baz.FooBar = Hello

Piszesz do modelu o nazwie SomeViewModel, a następnie robi to odwrotnie niż w pierwszej kolejności. Szuka właściwości o nazwie „Foo”. Następnie szuka właściwości o nazwie „Bar” poza „Foo”, potem wyszukuje „Baz”… i tak dalej…

W końcu próbuje przeanalizować wartość do typu „FooBar” i przypisać ją do „FooBar”.

PHEW !!!

I voila, masz swój model. Wystąpienie, które właśnie skonstruowano Model Binder, zostaje przekazane do żądanej akcji.


Twoje rozwiązanie nie działa, ponieważ Html.[Type]For()pomocnicy potrzebują wyrażenia. A ty po prostu nadajesz im wartość. Nie ma pojęcia, jaki jest kontekst tej wartości i nie wie, co z nią zrobić.

Teraz niektórzy zasugerowali użycie podszablonów do renderowania. Teoretycznie to zadziała, ale prawdopodobnie nie tak, jak się spodziewasz. Kiedy renderujesz częściową, zmieniasz typ TModel, ponieważ znajdujesz się w innym kontekście widoku. Oznacza to, że możesz opisać swoją nieruchomość za pomocą krótszego wyrażenia. Oznacza to również, że kiedy pomocnik wygeneruje nazwę dla twojego wyrażenia, będzie ono płytkie. Generuje się tylko na podstawie podanego wyrażenia (nie całego kontekstu).

Powiedzmy, że masz podrzędny fragment, który właśnie wyrenderował „Baz” (z naszego przykładu wcześniej). Wewnątrz tej części możesz po prostu powiedzieć:

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

Zamiast

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

Oznacza to, że wygeneruje taki tag wejściowy:

<input name="FooBar" />

Które, jeśli Piszesz tego formularza do działania, że spodziewa się dużego głęboko zagnieżdżony ViewModel, a następnie spróbuje nawilżają właściwość o nazwie FooBaroff TModel. Czego w najlepszym razie nie ma, aw najgorszym jest czymś zupełnie innym. Gdybyś wysyłał do określonej akcji, która akceptowała model Baz, a nie model główny, to działałoby świetnie! W rzeczywistości częściowe są dobrym sposobem na zmianę kontekstu widoku, na przykład jeśli masz stronę z wieloma formularzami, które wszystkie publikują w różnych działaniach, renderowanie części dla każdego z nich byłoby świetnym pomysłem.


Teraz, gdy już to wszystko osiągniesz, możesz zacząć robić naprawdę interesujące rzeczy Expression<>, programowo je rozszerzając i robiąc z nimi inne fajne rzeczy. Nie będę się tym zajmować. Ale miejmy nadzieję, że da ci to lepsze zrozumienie tego, co dzieje się za kulisami i dlaczego rzeczy działają tak, jak są.

J. Holmes
źródło
4
Świetna odpowiedź. Obecnie próbuję to przetrawić. :) Również winien Cargo Culting! Jak ten opis.
David C
4
Dziękuję za szczegółową odpowiedź!
Kobi
14
Potrzebujesz więcej niż jednego głosu za. +3 (po jednym za każde wyjaśnienie) i +1 dla Cargo-Cultists. Absolutnie genialna odpowiedź!
Kyeotic
3
Dlatego uwielbiam TAK: krótka odpowiedź + dogłębne wyjaśnienie + niesamowity link (kult cargo). Post o kulcie cargo będę chciał pokazać każdemu, kto nie uważa, że ​​wiedza o wewnętrznym działaniu rzeczy jest niezwykle ważna!
user1068352
18

Możesz po prostu użyć EditorTemplates, aby to zrobić, musisz utworzyć katalog o nazwie „EditorTemplates” w folderze widoku kontrolera i umieścić oddzielny widok dla każdej zagnieżdżonej encji (nazwanej jako nazwa klasy encji)

Główny widok :

@model ViewModels.MyViewModels.Theme

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

Widok kategorii (/MyController/EditorTemplates/Category.cshtml):

@model ViewModels.MyViewModels.Category

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

Widok produktu (/MyController/EditorTemplates/Product.cshtml):

@model ViewModels.MyViewModels.Product

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

i tak dalej

w ten sposób Html.EditorFor helper wygeneruje nazwy elementów w uporządkowany sposób, dzięki czemu nie będziesz mieć żadnego problemu z odzyskaniem opublikowanej encji Theme jako całości

Alireza Sabouri
źródło
1
Chociaż zaakceptowana odpowiedź jest bardzo dobra (również ją głosowałem), ta odpowiedź jest bardziej łatwa do utrzymania.
Aaron
4

Możesz dodać częściową kategorię i część produktu, każdy z nich zająłby mniejszą część modelu głównego, ponieważ jest to własny model, tj. Typem modelu kategorii może być IEnumerable, należy przekazać do niego Model.Theme. Częścią produktu może być IEnumerable, do którego przekazujesz Model.Products (z części częściowej Category).

Nie jestem pewien, czy byłaby to właściwa droga naprzód, ale chciałbym wiedzieć.

EDYTOWAĆ

Od opublikowania tej odpowiedzi korzystam z EditorTemplates i uważam, że jest to najłatwiejszy sposób obsługi powtarzających się grup lub elementów wejściowych. Obsługuje wszystkie problemy z wiadomościami walidacyjnymi i automatycznie zgłasza problemy z przesłaniem formularza / wiązaniem modelu.

Adrian Thompson Phillips
źródło
Przyszło mi to do głowy, po prostu nie byłem pewien, jak sobie z tym poradzi, gdy przeczytałem go ponownie, aby zaktualizować.
David C
1
Jest blisko, ale ponieważ jest to formularz do wysłania jako jednostka, nie będzie działać poprawnie. Po wejściu do częściowej kontekst widoku uległ zmianie i nie ma już głęboko zagnieżdżonego wyrażenia. Wpisanie z powrotem do Thememodelu nie byłoby odpowiednio nawodnione.
J. Holmes,
To też moje zmartwienie. Zwykle robiłbym powyższe jako podejście tylko do odczytu do wyświetlania produktów, a następnie dostarczałbym link do każdego produktu może być metodą działania / Produkt / Edit / 123, aby edytować każdy z nich w jego własnym formularzu. Myślę, że możesz się cofnąć, próbując zrobić zbyt wiele na jednej stronie w MVC.
Adrian Thompson Phillips
@AdrianThompsonPhillips tak, jest bardzo możliwe, że tak. Pochodzę z tła w Formularzach, więc nadal nie zawsze mogę przyzwyczaić się do pomysłu opuszczenia strony, aby dokonać edycji. :(
David C
2

Gdy używasz pętli foreach w widoku dla modelu powiązanego ... Twój model powinien być w formacie listy.

to znaczy

@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
źródło
Dziwię się, że powyższy kod nawet się kompiluje. @ Html.LabelFor wymaga operacji FUNC jako parametru, twoja nie
Jenna Leaf
Nie wiem, czy powyższy kod się kompiluje, czy nie, ale zagnieżdżony @foreach działa dla mnie. MVC5.
antonio
0

Wynika to jasno z błędu.

HtmlHelpers dołączony z „For” oczekuje wyrażenia lambda jako parametru.

Jeśli przekazujesz wartość bezpośrednio, lepiej użyj normalnej.

na przykład

Zamiast TextboxFor (....) użyj Textbox ()

składnia TextboxFor będzie wyglądać jak Html.TextBoxFor (m => m.Property)

W swoim scenariuszu możesz użyć podstawowej pętli for, ponieważ da ci ona indeks do użycia.

@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
źródło
0

Inną znacznie prostszą możliwością jest to, że jedna z nazw właściwości jest nieprawidłowa (prawdopodobnie ta, którą właśnie zmieniłeś w klasie). Tak właśnie było dla mnie w RazorPages .NET Core 3.

Pierwsza dywizja
źródło