快速的答案是使用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 中工作?
所以,您已经为 LabelFor 和 TextBoxFor 和其他人使用了 HtmlHelper<T> 扩展,并且
您可能注意到,当您调用它们时,您向它们传递了一个 lambda,它神奇地 生成
一些html。但是怎么做呢?
所以首先要注意的是这些助手的签名。让我们看看最简单的重载
TextBoxFor
public static MvcHtmlString TextBoxFor<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression
)
首先,这是对<TModel> 类型的强类型HtmlHelper 的扩展方法。所以,简单来说
说明幕后发生的事情,当 razor 呈现此视图时,它会生成一个类。
这个类的内部是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,就像任何其他
函数(这就是为什么您可以或多或少地互换使用委托、方法组和 lambda,因为它们只是
代码参考。)
但是,当编译器看到类型是 Expression<> 时,它不会立即将 lambda 编译为 MSIL,而是生成一个
表达式树!
那么,到底什么是表达式树。嗯,这并不复杂,但也不是在公园里散步。引用 ms:
|表达式树表示树状数据结构中的代码,其中每个节点都是一个表达式,例如方法调用或二元运算,如 x
简单地说,表达式树是将函数表示为“动作”的集合。
在 model=>model.SomeProperty 的情况下,表达式树中会有一个节点,上面写着:“从‘模型’获取‘某些属性’”
这个表达式树可以编译成一个可以调用的函数,但只要是表达式树,就只是节点的集合。
那有什么好处呢?
所以Func<> 或Action<>,一旦你拥有它们,它们几乎是原子的。你真正能做的就是Invoke()他们,也就是告诉他们
做他们应该做的工作。
另一方面,Expression<Func<>> 表示一组动作,可以附加、操作、visited 或compiled 并调用。
那你为什么要告诉我这些?
所以了解了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<> 的工作不仅仅是Func<> 所做的工作,如何它的工作在这里很重要。
(请注意,LINQ to SQL 等其他框架通过遍历表达式树并构建不同的语法来做类似的事情,本例是 SQL 查询)
模型绑定器是如何工作的?
所以,一旦你明白了,我们就必须简单地谈谈模型绑定器。当表格被张贴时,它就像一个公寓
Dictionary<string, string>,我们失去了嵌套视图模型可能拥有的层次结构。这是
模型绑定器的工作是采用此键值对组合并尝试对具有某些属性的对象进行再水化。它是怎么做的
这?您猜对了,通过使用发布的输入的“键”或名称。
所以如果表单帖子看起来像
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<> 做一些真正有趣的事情,通过编程扩展它们并做
其他整洁的东西。我不会参与其中。但是,希望这会
让您更好地了解幕后发生的事情以及为什么事情会这样发展。