Вигляд MVC Razor вкладеної моделі foreach


94

Уявіть собі загальний сценарій, це простіша версія того, з чим я стикаюся. У мене насправді є пара шарів подальшого гніздування на моєму ....

Але це сценарій

Тема містить список Категорія містить список Продукт містить список

Мій контролер надає повністю заповнену тему з усіма категоріями для цієї теми, продуктами цієї категорії та їх замовленнями.

Колекція замовлень має властивість "Кількість" (серед багатьох інших), яку потрібно редагувати.

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

Якщо я використовую замість цього лямбда, тоді я, здається, отримую посилання лише на верхній об'єкт Моделі, "Тема", а не на те, що знаходиться в циклі foreach.

Чи взагалі можливе те, що я намагаюся робити там, чи переоцінив чи неправильно зрозумів, що можливо?

З вищесказаним я отримую повідомлення про помилку в TextboxFor, EditorFor тощо

CS0411: Аргументи типу для методу 'System.Web.Mvc.Html.InputExtensions.TextBoxFor (System.Web.Mvc.HtmlHelper, System.Linq.Expressions.Expression>)' не можна вивести з використання. Спробуйте явно вказати аргументи типу.

Дякую.


1
Чи не слід було б мати @перед усіма foreachs? Чи не слід вам також мати лямбди в Html.EditorFor( Html.EditorFor(m => m.Note)наприклад) та інших методах? Можливо, я помиляюся, але чи можете ви вставити ваш власний код? Я досить новачок у MVC, але ви можете досить легко вирішити це за допомогою часткових переглядів або редакторів (якщо це ім'я?).
Кобі

category.nameЯ впевнений, що це stringі ...Forне підтримує рядок як перший параметр
balexandre

так, я просто пропустив @, тепер додано. Дякую. Однак, що стосується лямбда-сигналу, якщо я почну вводити @ Html.TextBoxFor (m => m.), Тоді я, здається, отримую посилання лише на найпопулярніший об'єкт Model, а не на ті, що знаходяться в циклі foreach.
David C

@DavidC - Я поки не знаю достатньо MVC 3, щоб відповісти - але я підозрюю, що це ваша проблема :).
Кобі

2
Я в поїзді, але якщо на це не відповість, поки я не прийду на роботу, напишу відповідь. Швидка відповідь полягає у використанні звичайного, for()а не a foreach. Поясню чому, тому що це мене теж дуже довго бентежило.
J. Holmes

Відповіді:


304

Швидка відповідь полягає у використанні for()циклу замість ваших foreach()циклів. Щось на зразок:

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

Але це зачіпає, чому це вирішує проблему.

Є три речі, які ви, як мінімум, побіжно зрозуміли, перш ніж вирішити цю проблему. Я повинен визнати, що я довго займався цим, коли почав працювати з фреймворком. І мені знадобився досить довгий час, щоб справді зрозуміти, що відбувається.

Це три речі:

  • Як працюють LabelForта інші ...Forпомічники в MVC?
  • Що таке дерево виразів?
  • Як працює Model Binder?

Всі три ці поняття зв’язуються між собою, щоб отримати відповідь.

Як працюють LabelForта інші ...Forпомічники в MVC?

Отже, ви використовували HtmlHelper<T>розширення для LabelForта TextBoxForта інших, і ви, напевно, помітили, що коли ви їх викликаєте, ви передаєте їм лямбда, і це магічно генерує якийсь html. Але як?

Тож перше, що слід помітити, - це підпис цих помічників. Давайте розглянемо найпростіші перевантаження для TextBoxFor

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

По-перше, це метод розширення для сильно набраного HtmlHelperтипу <TModel>. Отже, щоб просто сказати, що відбувається за лаштунками, коли бритва надає цю точку зору, вона створює клас. Усередині цього класу знаходиться екземпляр HtmlHelper<TModel>(як властивість Html, саме тому ви можете використовувати його @Html...), де TModelє тип, визначений у вашій @modelзаяві. Отже, у вашому випадку, коли ви дивитесь на цей погляд, TModel він завжди буде такого типу ViewModels.MyViewModels.Theme.

Тепер наступний аргумент трохи хитрий. Тож давайте розглянемо виклик

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

Схоже, у нас є трохи лямбда, і якби вгадати підпис, можна подумати, що типом для цього аргументу буде просто a Func<TModel, TProperty>, де TModelє тип моделі подання і TProperty виводиться як тип властивості.

Але це не зовсім правильно, якщо поглянути на фактичний тип аргументу Expression<Func<TModel, TProperty>>.

Отже, коли ви зазвичай генеруєте лямбда, компілятор бере ламбду і компілює її в MSIL, як і будь-яку іншу функцію (саме тому ви можете використовувати делегати, групи методів та лямбди більш-менш взаємозамінні, оскільки це просто посилання на код .)

Однак, коли компілятор бачить, що тип є Expression<>, він не відразу компілює лямбду до MSIL, натомість генерує Дерево виразів!

Що таке дерево виразів ?

Отже, що за біса - це дерево виразів. Ну, це не складно, але це також не прогулянка в парку. Щоб процитувати мс:

| Дерева виразів представляють код у деревоподібній структурі даних, де кожен вузол є виразом, наприклад, викликом методу або двійковою операцією, такою як x <y.

Простіше кажучи, дерево виразів є поданням функції як сукупності "дій".

У випадку model=>model.SomeProperty, у дереві виразів у ньому буде вузол із написом: "Отримати" деяке властивість "із" моделі ""

Це дерево виразів може бути скомпільоване у функцію, до якої можна викликати, але поки це дерево виразів, це лише сукупність вузлів.

То для чого це добре?

Отже, Func<>або Action<>, як тільки ви їх отримаєте, вони в значній мірі атомні. Все, що ви дійсно можете зробити, це Invoke()вони, вони ж кажуть їм виконувати роботу, яку вони повинні робити.

Expression<Func<>>з іншого боку, представляє сукупність дій, які можна додавати, маніпулювати ними, відвідувати або компілювати та викликати.

То чому ти мені все це кажеш?

Тож із цим розумінням того, що Expression<>таке, ми можемо повернутися до Html.TextBoxFor. Коли він відображає текстове поле, йому потрібно створити кілька речей про властивість, яку ви йому надаєте. Такі речі , як attributesна майно , для перевірки, і в Зокрема , в цьому випадку необхідно з'ясувати , що назвати в <input>тег.

Це робиться шляхом "прогулянки" дерева виразів та побудови імені. Отже, для виразу типу model=>model.SomeProperty, він обробляє вираз, збираючи властивості, про які ви запитуєте, і будує їх <input name='SomeProperty'>.

Для більш складного прикладу, наприклад model=>model.Foo.Bar.Baz.FooBar, він може генерувати<input name="Foo.Bar.Baz.FooBar" value="[whatever FooBar is]" />

Мати сенс? Тут важлива не просто робота Func<>, а те, як вона виконує свою роботу.

(Зверніть увагу, що інші фреймворки, такі як LINQ to SQL, роблять подібні речі, проходячи дерево виразів та будуючи іншу граматику, в цьому випадку запит SQL)

Як працює Model Binder?

Отже, як тільки ви це отримаєте, ми повинні коротко поговорити про в'яжучу модель. Коли форма розміщується, це просто як квартира Dictionary<string, string>, ми втратили ієрархічну структуру, яку могла мати наша вкладена модель подання. Завдання в’яжучого моделі - взяти цю комбіновану пару ключ-значення та спробувати регідратати об’єкт з деякими властивостями. Як це робиться? Ви здогадалися, використовуючи "ключ" або ім'я введення, яке було розміщено.

Отже, якщо публікація форми виглядає так

Foo.Bar.Baz.FooBar = Hello

І ви SomeViewModelрозміщуєте повідомлення в моделі з назвою , тоді вона робить зворотне, що робив помічник. Він шукає властивість під назвою "Foo". Потім він шукає властивість під назвою "Bar" від "Foo", потім шукає "Baz" ... і так далі ...

Нарешті, він намагається проаналізувати значення за типом "FooBar" і призначити його "FooBar".

ТУТ !!!

І вуаля, у вас є ваша модель. Екземпляр щойно побудованого Модельного зв’язку надходить у запитувану Дію.


Отже, ваше рішення не працює, оскільки Html.[Type]For()помічники потребують виразу. І ви просто надаєте їм значення. Він не уявляє, що таке контекст для цієї цінності, і він не знає, що з цим робити.

Зараз деякі люди пропонували використовувати часткові для обробки. Зараз це теоретично спрацює, але, мабуть, не так, як ви очікуєте. Коли ви частково відображаєте, ви змінюєте тип TModel, оскільки ви перебуваєте в іншому контексті подання. Це означає, що ви можете описати свою власність коротшим виразом. Це також означає, що коли помічник генерує ім'я для вашого виразу, воно буде дрібним. Він генеруватиметься лише на основі висловленого виразу (а не всього контексту).

Отже, скажімо, у вас частково був щойно виведений "Baz" (з нашого прикладу раніше). Всередині цього часткового ви можете просто сказати:

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

Швидше ніж

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

Це означає, що він генеруватиме вхідний тег наступним чином:

<input name="FooBar" />

Що, якщо ви публікуєте цю форму до дії, яка очікує великий глибоко вкладений ViewModel, тоді вона спробує гідратувати властивість, яку FooBarвідкликають TModel. Якого в кращому випадку немає, а в гіршому - зовсім іншого. Якщо ви розміщували повідомлення до певної дії, яка приймала Bazа не кореневу модель, тоді це було б чудово! Насправді часткові - це хороший спосіб змінити контекст вашого перегляду, наприклад, якщо у вас є сторінка з декількома формами, які всі публікують у різних діях, то надання часткової для кожної з них буде чудовою ідеєю.


Тепер, як тільки ви отримаєте все це, ви можете почати робити справді цікаві речі Expression<>, програмно розширюючи їх та роблячи з ними інші акуратні речі. Я ні в що з цього не потраплю. Але, сподіваємось, це дасть вам краще зрозуміти, що відбувається за лаштунками і чому речі поводяться так, як вони.


4
Чудова відповідь. На даний момент я намагаюся його перетравити. :) Також винен у групі вантажних перевезень! Як цей опис.
David C

4
Дякую за цю детальну відповідь!
Кобі

14
Для цього потрібно більше одного голосу. +3 (по одному за кожне пояснення) та +1 для вантажо-культистів. Абсолютно блискуча відповідь!
Kyeotic

3
Ось чому я люблю ТАК: коротка відповідь + поглиблене пояснення + чудове посилання (вантажний культ). Я хотів би показати допис про культ карго всім, хто не вважає, що знання про внутрішню роботу речей надзвичайно важливі!
user1068352

18

Ви можете просто використовувати EditorTemplates для цього, вам потрібно створити каталог з назвою "EditorTemplates" у папці подання контролера та розмістити окремий вигляд для кожного з ваших вкладених об'єктів (з іменем класу сутності)

Основний вид:

@model ViewModels.MyViewModels.Theme

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

Перегляд категорій (/MyController/EditorTemplates/Category.cshtml):

@model ViewModels.MyViewModels.Category

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

Перегляд продукту (/MyController/EditorTemplates/Product.cshtml):

@model ViewModels.MyViewModels.Product

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

і так далі

таким чином Html.EditorFor помічник буде генерувати імена елементів впорядковано, і тому у вас не буде жодної подальшої проблеми з отриманням розміщеної сутності теми в цілому


1
Хоча прийнята відповідь є дуже доброю (я її також підтримав), ця відповідь є найбільш підтримуваним варіантом.
Аарон

4

Ви можете додати часткову категорію та часткову категорію продукту, кожна з них займе меншу частину основної моделі як власну модель, тобто тип моделі категорії може бути IEnumerable, ви б передали до неї Model.Theme. Частковою Продукцією може бути IEnumerable, яку ви передаєте Model.Products у (з частинної категорії).

Я не впевнений, чи це буде правильним шляхом, але мені буде цікаво знати.

EDIT

Опублікувавши цю відповідь, я використав EditorTemplates і знайшов це найпростіший спосіб обробляти повторювані групи або елементи введення. Він обробляє всі ваші проблеми з повідомленнями про перевірку та автоматично подає проблеми з поданням форми / прив'язки моделі.


Це мені спало на думку, просто я не був впевнений, як це впорається, коли я прочитав його для оновлення.
Девід Сі

1
Це близько, але оскільки це форма, яку потрібно опублікувати як одиницю, це буде працювати не зовсім правильно. Опинившись частково, контекст подання змінився, і він більше не має глибоко вкладеного виразу. Повідомлення до Themeмоделі не гідруватиме належним чином.
Дж. Холмс,

Це теж моє занепокоєння. Зазвичай я роблю вищезазначене як підхід лише для читання до відображення товарів, а потім надаю посилання на кожен товар, можливо, до методу дії / Product / Edit / 123, щоб редагувати кожен у власній формі. Я думаю, що ви можете скасувати роботу, намагаючись зробити занадто багато на одній сторінці в MVC.
Адріан Томпсон Філіпс,

@AdrianThompsonPhillips так, дуже можливо, що я маю. Я походив із фонової форми, тому досі не завжди можу звикнути до думки, що потрібно залишити сторінку, щоб внести зміни. :(
David C

2

Коли ви використовуєте цикл foreach у поданні для прив'язаної моделі ... Ваша модель повинна мати формат у списку.

тобто

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

Я здивований, що код вище навіть компілюється. @ Html.LabelFor вимагає операції FUNC як параметр, ваша - ні,
Jenna Leaf

Я не знаю, компілюється код вище чи ні, але вкладений @foreach працює для мене. MVC5.
Антоніо

0

З помилки це зрозуміло.

HtmlHelpers, доданий до "For", очікує як параметр лямбда-вираз.

Якщо ви передаєте значення безпосередньо, краще скористайтесь звичайним.

напр

Замість TextboxFor (....) використовуйте Textbox ()

синтаксис для TextboxFor буде схожий на Html.TextBoxFor (m => m.Property)

У вашому сценарії ви можете використовувати цикл basic for, оскільки це дасть вам індекс для використання.

@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

Інша набагато простіша можливість полягає в тому, що одне з ваших імен властивостей є неправильним (можливо, те, що ви щойно змінили в класі). Це те, що було для мене в RazorPages .NET Core 3.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.