一尘不染

MVC Razor视图嵌套了foreach的模型

c#

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

但这是场景

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

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

订单集合具有一个名为“数量”(在其他属性中)的属性,需要对其进行编辑。

@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>)’的类型参数。尝试显式指定类型参数。

谢谢。


阅读 230

收藏
2020-05-19

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

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

在解决此问题之前,您至少应该对三件事有所了解。我不得不承认,我货物culted这个很长一段时间,当我开始与框架的工作。我花了相当长时间才真正了解正在发生的事情。

这三件事是:

  • 如何在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
)

首先,这是HtmlHelpertype
的强类型的扩展方法<TModel>。因此,为了简单说明幕后发生的事情,当剃刀渲染此视图时,它将生成一个类。该类的内部是HtmlHelper<TModel>(作为属性Html,这就是为什么可以使用@Html...)的实例,其中TModel@model语句中定义的类型。因此,在您的情况下,当您查看此视图​​时,TModel
将始终为类型ViewModels.MyViewModels.Theme

现在,下一个参数有些棘手。因此,让我们看一下调用

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

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

但这并不完全正确,如果您查看参数的 实际 类型Expression<Func<TModel, TProperty>>

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

但是,当编译器看到类型为时Expression<>,它不会立即将lambda编译为MSIL,而是生成一个Expression Tree!

什么是表情树

因此,到底是一个表达式树。好吧,这并不复杂,但也不是在公园散步。引用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<>,方法是对它们进行编程扩展,并使用它们进行其他巧妙的工作。我什么都不会做。但是,希望这可以使您更好地了解幕后发生的事情以及事物按原样运行的原因。

2020-05-19