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
LabelFor
i inni ...For
pomocnicy 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 LabelFor
i inni ...For
pomocnicy w MVC?
Więc użyłeś HtmlHelper<T>
rozszerzeń dla LabelFor
i TextBoxFor
i 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 HtmlHelper
typu <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 TModel
jest typem zdefiniowanym w @model
instrukcji. 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 TModel
jest 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ń!
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.SomeProperty
drzewa 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 attributes
na 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.SomeProperty
przechodzi 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.FooBar
moż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 FooBar
off 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ą.
@
przed wszystkimforeach
? Czy nie powinieneś również mieć lambd wHtml.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ą?).