MVC Razor 视图嵌套 foreach 的模型

Posted

技术标签:

【中文标题】MVC Razor 视图嵌套 foreach 的模型【英文标题】:MVC Razor view nested foreach's model 【发布时间】:2012-02-12 05:54:22 【问题描述】:

想象一个常见的场景,这是我遇到的更简单的版本。我实际上还有几层进一步的嵌套......

但这就是场景

主题包含列表 类别包含列表 产品包含列表

我的控制器提供了一个完全填充的主题,其中包含该主题的所有类别、该类别中的产品及其订单。

orders 集合有一个称为 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)
      
   

如果我改用 lambda,那么我似乎只能获得对顶层模型对象的引用,“主题”而不是 foreach 循环中的那些。

我在那里尝试做的事情是否可能,或者我是否高估或误解了什么是可能的?

通过上述操作,我在 TextboxFor、EditorFor 等上遇到错误

CS0411:方法的类型参数 'System.Web.Mvc.Html.InputExtensions.TextBoxFor(System.Web.Mvc.HtmlHelper, System.Linq.Expressions.Expression>)' 不能从用法中推断出来。尝试指定类型参数 明确的。

谢谢。

【问题讨论】:

在所有foreachs 之前你不应该有@ 吗?你不应该在Html.EditorFor(例如Html.EditorFor(m => m.Note))和其他方法中也有lambdas吗?我可能弄错了,但你能粘贴你的实际代码吗?我对 MVC 很陌生,但是您可以使用部分视图或编辑器(如果这是名称?)很容易地解决它。 category.name 我确定是string...For 不支持字符串作为第一个参数 是的,我只是错过了@,现在添加了。谢谢。但是,对于 lambda,如果我开始输入 @Html.TextBoxFor(m => m. 那么我似乎只获得了对顶部模型对象的引用,而不是那些在 foreach 循环中的引用。 @DavidC - 我对 MVC 3 的了解还不够多,无法回答 - 但我怀疑这是你的问题 :) 我在火车上,但如果在我上班时没有回答,请发布答案。快速的答案是使用常规的for() 而不是foreach。我会解释原因,因为它也让我很困惑。 【参考方案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)
      
   

但这掩盖了为什么这可以解决问题。

在解决此问题之前,您至少大致了解了三件事。我有 承认我cargo-culted这个 很长一段时间,当我开始使用框架时。我花了很长时间 真正了解发生了什么。

这三件事是:

LabelFor 和其他 ...For 助手如何在 MVC 中工作? 什么是表达式树? 模型绑定器如何工作?

所有这三个概念联系在一起以获得答案。

LabelFor 和其他 ...For 助手如何在 MVC 中工作?

所以,您已经为 LabelForTextBoxFor 和其他人使用了 HtmlHelper&lt;T&gt; 扩展,并且 您可能注意到,当您调用它们时,您向它们传递了一个 lambda,它神奇地 生成 一些html。但是怎么做呢?

所以首先要注意的是这些助手的签名。让我们看看最简单的重载 TextBoxFor

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

首先,这是对&lt;TModel&gt; 类型的强类型HtmlHelper 的扩展方法。所以,简单来说 说明幕后发生的事情,当 razor 呈现此视图时,它会生成一个类。 这个类的内部是HtmlHelper&lt;TModel&gt; 的一个实例(作为属性Html,这就是你可以使用@Html... 的原因), 其中TModel 是在@model 语句中定义的类型。因此,就您而言,当您查看此视图​​时TModel 将始终为 ViewModels.MyViewModels.Theme 类型。

现在,下一个论点有点棘手。那么让我们看一个调用

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

看起来我们有一个小 lambda,如果要猜测签名,可能会认为类型为 这个参数只是一个Func&lt;TModel, TProperty&gt;,其中TModel是视图模型的类型,TProperty 被推断为属性的类型。

但这并不完全正确,如果您查看参数的 实际 类型,它的 Expression&lt;Func&lt;TModel, TProperty&gt;&gt;

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

但是,当编译器看到类型是 Expression&lt;&gt; 时,它不会立即将 lambda 编译为 MSIL,而是生成一个 表达式树!

什么是Expression Tree?

那么,到底什么是表达式树。嗯,这并不复杂,但也不是在公园里散步。引用 ms:

|表达式树表示树状数据结构中的代码,其中每个节点都是一个表达式,例如方法调用或二元运算,如 x

简单地说,表达式树是将函数表示为“动作”的集合。

model=&gt;model.SomeProperty 的情况下,表达式树中会有一个节点,上面写着:“从‘模型’获取‘某些属性’”

这个表达式树可以编译成一个可以调用的函数,但只要是表达式树,就只是节点的集合。

那有什么好处呢?

所以Func&lt;&gt;Action&lt;&gt;,一旦你拥有它们,它们几乎是原子的。你真正能做的就是Invoke()他们,也就是告诉他们 做他们应该做的工作。

另一方面,Expression&lt;Func&lt;&gt;&gt; 表示一组动作,可以附加、操作、visited 或compiled 并调用。

那你为什么要告诉我这些?

所以了解了Expression&lt;&gt; 是什么之后,我们可以回到Html.TextBoxFor。当它呈现一个文本框时,它需要 生成一些关于你给它的属性的东西。属性上的 attributes 之类的东西用于验证,特别是 在这种情况下,它需要弄清楚&lt;input&gt; 标签的名称

它通过“遍历”表达式树并建立一个名称来做到这一点。所以对于像model=&gt;model.SomeProperty 这样的表达式,它会遍历表达式 收集您要求的属性并构建&lt;input name='SomeProperty'&gt;

对于更复杂的例子,比如model=&gt;model.Foo.Bar.Baz.FooBar,它可能会生成&lt;input name="Foo.Bar.Baz.FooBar" value="[whatever FooBar is]" /&gt;

有意义吗? Func&lt;&gt; 的工作不仅仅是Func&lt;&gt; 所做的工作,如何它的工作在这里很重要。

(请注意,LINQ to SQL 等其他框架通过遍历表达式树并构建不同的语法来做类似的事情,本例是 SQL 查询)

模型绑定器是如何工作的?

所以,一旦你明白了,我们就必须简单地谈谈模型绑定器。当表格被张贴时,它就像一个公寓 Dictionary&lt;string, string&gt;,我们失去了嵌套视图模型可能拥有的层次结构。这是 模型绑定器的工作是采用此键值对组合并尝试对具有某些属性的对象进行再水化。它是怎么做的 这?您猜对了,通过使用发布的输入的“键”或名称。

所以如果表单帖子看起来像

Foo.Bar.Baz.FooBar = Hello

并且您正在发布到一个名为SomeViewModel 的模型,然后它会与助手最初所做的相反。它寻找 一个名为“Foo”的属性。然后它从“Foo”中寻找一个名为“Bar”的属性,然后寻找“Baz”……以此类推……

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

呼!!!

瞧,你有你的模型。 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 的操作,那么它将尝试水合属性 从TModel 中调用FooBar。充其量是不存在的,最坏的情况是完全不同的东西。如果您要发布到接受Baz 而不是根模型的特定操作,那么这会很好用!事实上,partials 是更改视图上下文的好方法,例如,如果您有一个页面包含多个表单,所有表单都发布到不同的操作,那么为每个表单呈现一个 partial 将是一个好主意。


现在,一旦你掌握了所有这些,你就可以开始用 Expression&lt;&gt; 做一些真正有趣的事情,通过编程扩展它们并做 其他整洁的东西。我不会参与其中。但是,希望这会 让您更好地了解幕后发生的事情以及为什么事情会这样发展。

【讨论】:

很棒的回复。我目前正在尝试消化它。 :) 还犯了 Cargo Culting 罪!喜欢那个描述。 感谢您的详细解答! 需要不止一个赞成票。 +3(每个解释一个),以及货物崇拜者 +1。绝对精彩的答案! 这就是我喜欢 SO 的原因:简短的回答 + 深入的解释 + 很棒的链接(货物崇拜)。我想把关于货物崇拜的帖子展示给那些不认为关于物质内部运作的知识非常重要的人!【参考方案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 助手将按顺序生成元素的名称,因此您在检索整个发布的主题实体时不会有任何进一步的问题

【讨论】:

虽然接受的答案是一个非常好的答案(我也赞成),但这个答案是更易于维护的选项。【参考方案3】:

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

我不确定这是否是正确的前进方向,但我很想知道。

编辑

自从发布此答案后,我使用了 EditorTemplates 并发现这是处理重复输入组或项目的最简单方法。它会自动处理所有验证消息问题和表单提交/模型绑定问题。

【讨论】:

我想到了,只是不确定当我读回来更新时它会如何处理。 已经很接近了,但是由于这是一个要作为一个单元发布的表格,所以它不能正常工作。一旦进入局部视图上下文已经改变并且它不再具有深度嵌套的表达式。回帖到 Theme 模型将无法正常补充水分。 这也是我的担心。我通常会将上述方法作为显示产品的只读方法,然后在每个产品上提供一个链接,以使用 /Product/Edit/123 操作方法以在其自己的表单上编辑每个产品。我认为在 MVC 的一个页面上尝试做太多事情可能会失败。 @AdrianThompsonPhillips 是的,我很有可能拥有。我来自表单背景,所以我仍然不能总是习惯必须离开页面进行编辑的想法。 :(【参考方案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>
            
        

【讨论】:

我很惊讶上面的代码甚至可以编译。 @Html.LabelFor 需要一个 FUNC 操作作为参数,你的不是 我不知道上面的代码是否可以编译,但是 nested @foreach 对我有用。 MVC5。【参考方案5】:

从错误中可以看出。

附加“For”的 HtmlHelpers 需要 lambda 表达式作为参数。

如果你直接传递值,最好使用普通的。

例如

使用 Textbox() 代替 TextboxFor(....)

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)
      
   

【讨论】:

【参考方案6】:

另一个更简单的可能性是您的属性名称之一是错误的(可能是您刚刚在类中更改的名称)。这就是我在 RazorPages .NET Core 3 中的用处。

【讨论】:

以上是关于MVC Razor 视图嵌套 foreach 的模型的主要内容,如果未能解决你的问题,请参考以下文章

c# razor mvc textboxfor in foreach 循环

Razor语法 if嵌套foreach再嵌套if

使用 MVC/Razor 传递 C# 变量以用于 Dygraph

从模型中的嵌套对象获取 MVC4/Razor 中 JavaScript 的元素 id

如果 Razor 视图中的 foreach 无法识别条件

MVC3 Razor - 模型和视图