MVC Razor отображает вложенную модель foreach

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

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

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

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

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

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

Если я использую лямбда вместо этого, тогда мне кажется, что я получаю ссылку на верхний объект Model, "Theme", а не те, что находятся внутри цикла foreach.

Я пытаюсь сделать то, что я пытаюсь сделать, даже если я переоценил или неправильно понял, что возможно?

С приведенным выше я получаю сообщение об ошибке в TextboxFor, EditorFor и т.д.

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

Спасибо.

Ответ 1

Быстрый ответ заключается в использовании цикла 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?

Все три понятия объединяются для получения ответа.

Как работают помощники 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);

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

Но это не совсем правильно, если вы посмотрите на фактический тип аргумента Expression<Func<TModel, TProperty>>.

Итак, когда вы обычно генерируете лямбда, компилятор берет лямбду и компилирует ее в MSIL, как и любой другой (поэтому вы можете использовать делегаты, группы методов и лямбды более или менее взаимозаменяемо, потому что они просто ссылки на код.)

Однако, когда компилятор видит, что тип является Expression<>, он не сразу компилирует лямбду в MSIL, вместо этого генерирует Дерево выражений!

Что такое Дерево выражений?

Итак, что это за дерево выражений. Ну, это не сложно, но и не прогулка в парке. Чтобы указать ms:

| Деревья выражений представляют код в древовидной структуре данных, где каждый node является выражением, например вызовом метода или двоичной операцией, такой как x < у.

Проще говоря, дерево выражений представляет собой представление функции как совокупности "действий".

В случае model=>model.SomeProperty в дереве выражений будет node, в котором говорится: "Получить" Некоторое свойство "из" модели "

Это дерево выражений может быть скомпилировано в функцию, которую можно вызвать, но до тех пор, пока это дерево выражений, это всего лишь набор узлов.

Итак, что хорошего для?

Итак 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-запрос)

Как работает модель Binder?

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

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

Foo.Bar.Baz.FooBar = Hello

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

Наконец, он пытается проанализировать значение в виде "FooBar" и назначить его "FooBar".

Уф!!!

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


Итак, ваше решение не работает, потому что помощникам Html.[Type]For() требуется выражение. И вы просто даете им ценность. Это не имеет понятия какой контекст для этого значения, и он не знает, что с ним делать.

Теперь некоторые люди предложили использовать partials для рендеринга. Теперь это теоретически будет работать, но, вероятно, не так, как вы ожидаете. Когда вы выполняете частичное, вы меняете тип TModel, потому что вы находитесь в другом контексте представления. Это означает, что вы можете описать ваша собственность с более коротким выражением. Это также означает, что хелпер генерирует имя для вашего выражения, оно будет мелким. Это будет генерироваться только на основе выраженного им выражения (а не всего контекста).

Итак, давайте скажем, что у вас было частичное, что только что появилось "Baz" (из нашего примера раньше). Внутри этого части вы могли просто сказать:

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

Вместо

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

Это означает, что он будет генерировать входной тег следующим образом:

<input name="FooBar" />

Что, если вы отправляете эту форму в действие, ожидающее большой глубоко вложенной ViewModel, тогда он попытается увлажнить свойство называется FooBar off TModel. Который в лучшем случае не существует, а в худшем - что-то совсем другое. Если вы отправляете на конкретное действие, принимающее Baz, а не корневую модель, тогда это будет отлично! Фактически, частичные файлы - это хороший способ изменить контекст представления, например, если у вас есть страница с несколькими формами, которые все отправляют в разные действия, тогда предоставление частичного для каждого из них будет отличной идеей.


Теперь, когда вы получите все это, вы можете начать делать действительно интересные вещи с помощью Expression<>, программно расширяя их и делая другие аккуратные вещи с ними. Я не буду вникать в это. Но, надеюсь, это будет дать вам лучшее представление о том, что происходит за кулисами, и почему все действует так, как они есть.

Ответ 2

Вы можете просто использовать 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 helper будет генерировать имена элементов в упорядоченном порядке, и поэтому у вас не будет никакой дополнительной проблемы для получения отправленного объекта темы в целом

Ответ 3

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

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

ИЗМЕНИТЬ

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

Ответ 4

Когда вы используете цикл 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>
            }
        }

Ответ 5

Это ясно из ошибки.

HtmlHelpers, добавленный в "For", ожидает в качестве параметра лямбда-выражения.

Если вы передаете значение напрямую, лучше используйте Normal.

например.

Вместо TextboxFor (....) используйте Textbox()

Синтаксис

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

В вашем сценарии вы можете использовать basic for loop, поскольку он даст вам индекс для использования.

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