MVC Razor视图嵌套foreach的模型

时间:2012-01-17 12:16:47

标签: c# asp.net asp.net-mvc-3 view razor

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

但这是情景

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

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

订单集合中有一个名为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然后我似乎只获得对顶级Model对象的引用,“Theme”不是foreach循环中的那些。

我试图在那里做甚至可能或者我高估或误解了可能的事情?

有了上述内容,我在TextboxFor,EditorFor等上出现错误

  

CS0411:方法的类型参数   “System.Web.Mvc.Html.InputExtensions.TextBoxFor(System.Web.Mvc.HtmlHelper,   System.Linq.Expressions.Expression&GT)”   无法从使用中推断出来。尝试指定类型参数   明确。

感谢。

6 个答案:

答案 0 :(得分:302)

快速回答是使用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中运作?
  • 什么是表达式树?
  • Model Binder如何运作?

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

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

因此,您已使用HtmlHelper<T>LabelFor及其他人的TextBoxFor个附加信息,以及 您可能已经注意到,当您调用它们时,您将它们传递给lambda并且神奇地生成它 一些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,如果有人猜测签名,人们可能会认为它的类型 这个参数只是Func<TModel, TProperty>,其中TModel是视图模型的类型,TProperty 被推断为财产的类型。

但是,如果你查看参数的实际类型Expression<Func<TModel, TProperty>>,那就不太对了。

因此,当您正常生成lambda时,编译器会将lambda编译为MSIL,就像任何其他 函数(这就是为什么你可以或多或少地交替使用委托,方法组和lambdas,因为它们只是 代码引用。)

但是,当编译器发现类型为Expression<>时,它不会立即将lambda编译为MSIL,而是生成一个 表达树!

什么是Expression Tree

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

|表达式树表示树状数据结构中的代码,其中每个节点是表达式,例如,方法调用或二进制操作,例如x&lt;收率

简单地说,表达式树是一个函数的表示,作为&#34; actions&#34;的集合。

对于model=>model.SomeProperty,表达式树中会有一个节点,其中包含:&#34;获取&#39;某些属性&#39;来自&#39;模型&#39;&#34;

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

那有什么用呢?

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

另一方面,

Expression<Func<>>表示一系列操作,可以追加,操纵,visitedcompiled并调用。

那你为什么要告诉我这一切?

因此,通过对Expression<>的理解,我们可以回到Html.TextBoxFor。当它呈现文本框时,它需要 生成一些关于你要提供的属性的东西。像attributes这样的东西在属性上进行验证,具体而言 在这种情况下,它需要找出名称 <input>标记的内容。

这是通过&#34; walk&#34;表达式树和建立名称。因此,对于像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>,我们已经失去了嵌套视图模型可能具有的层次结构。它是 模型绑定器的工作是获取此键值对组合并尝试使用某些属性对对象进行再水化。它是怎么做的 这个?你猜对了,通过使用&#34;键&#34;或已发布的输入的名称。

因此,如果表单帖子看起来像

Foo.Bar.Baz.FooBar = Hello

你正在发布一个名为SomeViewModel的模型,然后它与帮助者首先做的相反。它寻找 一个名为&#34; Foo&#34;的财产。然后它寻找一个名为&#34; Bar&#34;离开&#34; Foo&#34;然后它寻找&#34; Baz&#34; ......等等......

最后,它尝试将值解析为&#34; FooBar&#34;并将其分配给&#34; FooBar&#34;。

呼!!!

瞧,你有自己的模特。刚刚构建的Model Binder实例将被移交给请求的Action。


所以你的解决方案不起作用,因为Html.[Type]For()助手需要一个表达式。你只是给他们一个价值。它不知道 这个价值的背景是什么,它不知道如何处理它。

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

所以,让我们说你有一部分只是呈现了#34; Baz&#34; (来自我们之前的例子)。在那部分内你可以说:

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

而不是

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

这意味着它将生成如下输入标记:

<input name="FooBar" />

其中,如果您要将此表单发布到期望大型深度嵌套ViewModel的操作,那么它将尝试为属性添加水合 从FooBar调用TModel。充其量不存在,最糟糕的是完全不同的东西。如果您要发布的是接受Baz的特定操作,而不是根模型,那么这将非常有用!事实上,partials是改变视图上下文的好方法,例如,如果你有一个页面有多个表单,所有表单都发布到不同的操作,那么为每个表单渲染一个部分将是一个好主意。


现在,一旦你完成所有这些,你就可以开始用Expression<>做一些非常有趣的事情,通过编程扩展它们并做 与他们其他整洁的事情。我没有进入任何一个。但是,希望,这会 让您更好地了解幕后发生的事情以及为什么事情按照他们的方式行事。

答案 1 :(得分: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帮助器将以有序的方式生成元素的名称,因此您将不会有任何进一步的问题来检索整个发布的主题实体

答案 2 :(得分:4)

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

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

修改

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

答案 3 :(得分: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>
            }
        }

答案 4 :(得分:0)

从错误中可以清楚地看出。

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

如果直接传递值,最好使用Normal。

e.g。

而不是TextboxFor(....)使用Textbox()

TextboxFor的

语法类似于Html.TextBoxFor(m =&gt; 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)
      }
   }
}

答案 5 :(得分:0)

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