MVC Razor просмотр вложенной модели foreach

94

Представьте себе общий сценарий, это более простая версия того, с чем я столкнулся. У меня на самом деле есть пара слоев дальнейшего вложения ....

Но это сценарий

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

My Controller предоставляет полностью заполненную тему со всеми категориями для этой темы, продуктами в этих категориях и их заказами.

Коллекция заказов имеет свойство Quantity (среди многих других), которое необходимо редактировать.

@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
Разве вы не должны были @раньше всех foreach? Разве у вас не должны быть лямбды в Html.EditorFor( Html.EditorFor(m => m.Note)например) и в остальных методах? Я могу ошибаться, но не могли бы вы вставить свой настоящий код? Я новичок в MVC, но вы можете довольно легко решить эту проблему с помощью частичных представлений или редакторов (если это имя?).
Коби
category.nameЯ уверен, что это a stringи ...Forне поддерживает строку в качестве первого параметра
balexandre
да, я просто пропустил @, теперь добавил. Спасибо. Однако, что касается лямбда-выражения, если я начну набирать @ Html.TextBoxFor (m => m. Тогда я, похоже, получу ссылку только на верхний объект модели, а не на те, которые находятся в цикле foreach.
Дэвид К.
@DavidC - я еще недостаточно знаю MVC 3, чтобы ответить, но я подозреваю, что это ваша проблема :).
Коби
2
Я в поезде, но если к тому времени, как я доберусь до работы, мне не ответят, опубликую ответ. Быстрый ответ - использовать обычный, for()а не foreach. Я объясню почему, потому что это меня тоже чертовски смущало.
Дж. Холмс

Ответы:

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?
  • Что такое дерево выражений?
  • Как работает подшивка моделей?

Все три концепции связаны друг с другом, чтобы получить ответ.

Как этот 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>. Итак, чтобы просто указать, что происходит за кулисами, когда razor визуализирует это представление, он генерирует класс. Внутри этого класса находится экземпляр HtmlHelper<TModel>(в качестве свойства Html, поэтому вы можете использовать @Html...), где TModel- тип, определенный в вашем @modelоператоре. Так что в вашем случае, когда вы смотрите на это представление TModel всегда будет типа ViewModels.MyViewModels.Theme.

Теперь следующий аргумент немного сложен. Итак, давайте посмотрим на призыв

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

Похоже, у нас есть небольшая лямбда, и если бы кто-то угадал сигнатуру, можно было бы подумать, что типом для этого аргумента будет просто a Func<TModel, TProperty>, где TModel- это тип модели представления и TProperty выводится как тип свойства.

Но это не совсем так, если вы посмотрите на фактический тип аргумента its 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)

Как работает подшивка моделей?

Итак, как только вы это получите, мы должны кратко поговорить о подшивке модели. Когда форма публикуется, она похожа на плоскую Dictionary<string, string>, мы потеряли иерархическую структуру, которая могла быть у нашей модели вложенного представления. Задача связывателя модели - взять эту комбинацию пары ключ-значение и попытаться повторно гидратировать объект с некоторыми свойствами. Как оно работает? Вы уже догадались, используя «ключ» или имя введенного сообщения.

Итак, если сообщение формы выглядит как

Foo.Bar.Baz.FooBar = Hello

И вы отправляете сообщение в модель с именем SomeViewModel, а затем она делает обратное тому, что делал помощник в первую очередь. Он ищет свойство под названием «Foo». Затем он ищет свойство под названием «Bar» в «Foo», затем ищет «Baz» ... и так далее ...

Наконец, он пытается преобразовать значение в тип «FooBar» и присвоить его «FooBar».

PHEW !!!

И вуаля, у вас есть ваша модель. Экземпляр, который только что сконструировал связыватель модели, передается в запрошенное действие.


Итак, ваше решение не работает, потому что 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
Отличный ответ. Я сейчас пытаюсь это переварить. :) Также виноват Cargo Culting! Как это описание.
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 сгенерирует имена элементов в упорядоченном порядке, и, следовательно, у вас не возникнет дополнительных проблем с получением опубликованного объекта Theme в целом

Алиреза Сабури
источник
1
Хотя принятый ответ является очень хорошим (я также поддержал его), этот ответ является более удобным вариантом.
Aaron
4

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

Я не уверен, что это правильный путь вперед, но мне было бы интересно узнать.

РЕДАКТИРОВАТЬ

После публикации этого ответа я использовал EditorTemplates и считаю, что это самый простой способ обрабатывать повторяющиеся группы ввода или элементы. Он автоматически обрабатывает все ваши проблемы с сообщениями проверки и проблемы с отправкой формы / привязкой модели.

Адриан Томпсон Филлипс
источник
Это пришло мне в голову, просто я не был уверен, как он с этим справится, когда я прочитал его, чтобы обновить.
David C
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)

В вашем сценарии вы можете использовать базовый цикл 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.

Первая дивизия
источник