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

如果我改用lambda,那么我似乎只获得对顶级Model对象“ Theme”的引用,而不是foreach循环中的那些对象。

我正在尝试做的事情甚至可能吗?或者我高估或误解了什么可能?

有了上述,我在TextboxFor,EditorFor等错误

CS0411:无法从用法中推断方法'System.Web.Mvc.Html.InputExtensions.TextBoxFor(System.Web.Mvc.HtmlHelper,System.Linq.Expressions.Expression>)'的类型参数。尝试显式指定类型参数。

谢谢。


1
你不应该有@所有的前foreachS' 您不应该在Html.EditorForHtml.EditorFor(m => m.Note)例如)和其余方法中也使用lambda 吗?我可能会误会,但是您可以粘贴您的实际代码吗?我对MVC还是很陌生,但是您可以使用局部视图或编辑器(如果是名称?)来轻松解决它。
科比2012年

category.name我确定它是a string...For并且不支持将字符串作为第一个参数
balexandre12,2012年

是的,我只是错过了@,现在添加了。谢谢。然而,随着对拉姆达,如果我开始键入@ Html.TextBoxFor(M =>米然后我只似乎得到顶端模型对象,参考不是那些foreach循环内。
大卫ç

@DavidC-我对MVC 3的了解还不够多,但是我怀疑那是你的问题:)
科比2012年

2
我在火车上,但是如果到我上班时仍未得到答复,请发布答案。快速的答案是使用常规for()而不是foreach。我会解释为什么,也造成了很长一段时间的混乱。
J. Holmes

Answers:


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>用于扩展LabelForTextBoxFor与其他人,你可能已经注意到了,当你调用它们,你通过他们的λ,它奇迹般地产生一些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);

看起来我们有点lambda,如果要猜测签名,可能会认为此参数的类型只是a Func<TModel, TProperty>,其中TModel视图模型的类型是TProperty 的类型是推断的,是属性的类型。

但是,如果您查看参数its 的实际类型,那是不对的Expression<Func<TModel, TProperty>>

因此,当您通常生成lambda时,编译器会将该lambda并将其编译为MSIL,就像其他任何函数一样(这就是为什么您可以或多或少地互换使用委托,方法组和lambda的原因,因为它们只是代码引用)

但是,当编译器看到类型为 Expression<>,它不会立即编译拉姆达下降到MSIL,而是产生一个表达式树!

什么是表达式树

因此,到底什么是表达树。好吧,这并不复杂,但也不是在公园散步。引用ms:

| 表达式树表示树状数据结构中的代码,其中每个节点都是表达式,例如,方法调用或诸如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如何工作?

因此,一旦您了解了这一点,我们就必须简要讨论一下模型绑定器。发布表单时,就像是一个flat Dictionary<string, string>,我们失去了嵌套视图模型可能具有的层次结构。模型绑定器的工作是采用此键值对组合并尝试为具有某些属性的对象补水。它是如何做到的?您使用发布的输入的“键”或名称来猜对了。

因此,如果表单发布看起来像

Foo.Bar.Baz.FooBar = Hello

然后,您将发布到名为的模型SomeViewModel,那么它的作用与助手最初所做的相反。它寻找一个名为“ Foo”的属性。然后从“ Foo”中查找一个名为“ Bar”的属性,然后从“ Baz”中查找...等等。

最后,它尝试将值解析为“ FooBar”的类型,并将其分配给“ FooBar”。

EW!

瞧,您有了模型。刚构造的Model Binder实例将移至请求的Action中。


因此您的解决方案不起作用,因为Html.[Type]For()助手需要表达。而您只是给他们一个价值。它不知道该值的上下文是什么,也不知道如何处理它。

现在有人建议使用局部视图进行渲染。现在,理论上这将起作用,但可能不会达到您期望的方式。渲染局部图像时,您将更改的类型TModel,因为您处于不同的视图上下文中。这意味着您可以用较短的表达式描述属性。这也意味着当助手为您的表达式生成名称时,它会很浅。它只会根据给定的表达式生成(而不是整个上下文)。

因此,可以说您有一个部分仅渲染了“ Baz”(来自之前的示例)。在该部分中,您只能说:

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

而不是

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

这意味着它将生成一个如下所示的输入标签:

<input name="FooBar" />

如果您将此表单发布到一个需要大型深度嵌套ViewModel的操作中,则它将尝试合并名为FooBaroff的属性TModel。充其量是没有的,而充其量是最糟糕的。如果您要发布到接受Baz而不是根模型的特定操作,那么效果很好!实际上,局部视图是更改视图上下文的好方法,例如,如果您的页面具有多种形式,并且所有页面都张贴到不同的动作,那么为每个页面呈现局部视图将是一个好主意。


现在,一旦您掌握了所有这些内容,就可以开始使用进行真正有趣的事情Expression<>,方法是以编程方式扩展它们并使用它们进行其他巧妙的处理。我什么都不会做。但是,希望这可以使您更好地了解幕后发生的事情以及事物按原样运行的原因。


4
很棒的回复。我正在尝试消化它。:)也犯有货物崇拜!就像那个描述。
大卫·C

4
感谢您提供详细的答案!
科比2012年

14
为此需要多个投票。+3(每个解释一个),货运文化人+1。绝对出色的答案!
Kyeotic

3
这就是为什么我如此爱:简短答案+深入解释+很棒的链接(崇拜)。我想向那些不认为关于货物内部运作的知识非常重要的人展示关于货运崇拜的帖子!
user1068352 2012年

18

您可以简单地使用EditorTemplates来做到这一点,您需要在控制器的view文件夹中创建一个名为“ 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将以有序的方式生成元素的名称,因此,从整体上获取发布的Theme实体不会有任何其他问题


1
虽然可接受的答案是一个很好的答案(我也赞成),但此答案是更可维护的选择。
亚伦

4

您可以添加一个Category部分和一个Product部分,它们各自将占据主模型的一小部分,因为它是自己的模型,即Category的模型类型可能是IEnumerable,您可以将Model.Theme传递给它。产品的部分可能是将Model.Products传递到(从Category部分中)的IEnumerable。

我不确定这是否是正确的方法,但有兴趣了解。

编辑

自发布此答案以来,我一直使用EditorTemplates并找到了处理重复输入组或项目的最简单方法。它可以处理您所有的验证消息问题,并自动解决表单提交/模型绑定问题。


这发生在我身上,只是不确定当我读回它进行更新时它将如何处理。
David

1
已经接近了,但是由于这是要作为一个单元发布的表格,因此无法正常使用。一旦进入部分视图内部,视图上下文就会发生变化,并且不再具有深度嵌套的表达式。发回Theme模型将无法正确补充水分。
J. Holmes

我也很担心。我通常将上述方法作为显示产品的只读方法,然后在每个产品上提供一个链接,也许可以通过/ Product / Edit / 123操作方法以自己的形式对其进行编辑。我认为您可能会在MVC的一页上尝试执行过多操作而被撤消。
阿德里安·汤普森·菲利普斯

@AdrianThompsonPhillips是的,我很有可能。我来自Forms背景,所以我仍然不能总是习惯必须离开页面进行编辑的想法。:(
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。
antonio

0

从错误中可以明显看出。

附加了“ For”的HtmlHelpers希望将lambda表达式作为参数。

如果直接传递该值,最好使用“普通”值。

例如

代替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中对我而言的内容。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.