56

这个问题是为什么我的 DisplayFor 没有循环通过我的 IEnumerable<DateTime>?


快速刷新。

什么时候:

  • 该模型具有类型的属性IEnumerable<T>
  • 您将此属性传递给Html.EditorFor()使用仅接受 lambda 表达式的重载
  • T您在 Views/Shared/EditorTemplates 下有一个类型的编辑器模板

然后 MVC 引擎将自动为可枚举序列中的每个项目调用编辑器模板,生成结果列表。

例如,当有一个Order具有属性的模型类时Lines

public class Order
{
    public IEnumerable<OrderLine> Lines { get; set; }
}

public class OrderLine
{
    public string Prop1 { get; set; }
    public int Prop2 { get; set; }
}

并且有一个视图Views/Shared/EditorTemplates/OrderLine.cshtml:

@model TestEditorFor.Models.OrderLine

@Html.EditorFor(m => m.Prop1)
@Html.EditorFor(m => m.Prop2)

然后,当您@Html.EditorFor(m => m.Lines)从顶级视图调用时,您将获得一个页面,其中包含每个订单行的文本框,而不仅仅是一个。


但是,正如您在链接问题中看到的那样,这仅在您使用EditorFor. 如果您提供模板名称(为了使用不是以OrderLine类命名的模板),则不会发生自动序列处理,而是会发生运行时错误。

此时,您必须将自定义模板的模型声明为IEnumebrable<OrderLine>并以某种方式手动迭代其项目以输出所有项目,例如

@foreach (var line in Model.Lines) {
    @Html.EditorFor(m => line)
}

这就是问题开始的地方。

以这种方式生成的 HTML 控件都具有相同的 id 和名称。当您稍后发布它们时,模型绑定器将无法构造OrderLines 数组,您在控制器中的 HttpPost 方法中获得的模型对象将是null.
如果您查看 lambda 表达式,这是有道理的——它并没有真正将正在构造的对象链接到它来自的模型中的某个位置。

我尝试了各种迭代项目的方法,似乎唯一的方法是将模板的模型重新声明为IList<T>并用以下方式枚举它for

@model IList<OrderLine>

@for (int i = 0; i < Model.Count(); i++)
{ 
    @Html.EditorFor(m => m[i].Prop1)
    @Html.EditorFor(m => m[i].Prop2)
}

然后在顶层视图中:

@model TestEditorFor.Models.Order

@using (Html.BeginForm()) {
    @Html.EditorFor(m => m.Lines, "CustomTemplateName")
}

它提供了正确命名的 HTML 控件,这些控件在提交时被模型绑定器正确识别。


虽然这有效,但感觉非常错误。

什么是使用自定义编辑器模板的正确惯用方式EditorFor,同时保留允许引擎生成适合模型绑定器的 HTML 的所有逻辑链接?

4

5 回答 5

35

与 Erik Funkenbusch 讨论后,研究了MVC 源代码,似乎有两种更好的(正确和惯用的?)方法可以做到这一点。

两者都涉及为帮助程序提供正确的 html 名称前缀,并生成与 default 的输出相同的 HTML EditorFor

我暂时将其留在这里,将进行更多测试以确保它在深度嵌套的场景中工作。

对于以下示例,假设您已经有两个模板用于OrderLineclass:OrderLine.cshtmlDifferentOrderLine.cshtml.


方法 1 - 使用中间模板IEnumerable<T>

创建一个帮助模板,以任何名称保存它(例如“ManyDifferentOrderLines.cshtml”):

@model IEnumerable<OrderLine>

@{
    int i = 0;

    foreach (var line in Model)
    { 
        @Html.EditorFor(m => line, "DifferentOrderLine", "[" + i++ + "]")
    }
}

然后从主订单模板调用它:

@model Order

@Html.EditorFor(m => m.Lines, "ManyDifferentOrderLines")

方法 2 - 没有中间模板IEnumerable<T>

在主订单模板中:

@model Order

@{
    int i = 0;

    foreach (var line in Model.Lines)
    {
        @Html.EditorFor(m => line, "DifferentOrderLine", "Lines[" + i++ + "]")
    }
}
于 2014-10-09T17:02:09.603 回答
4

似乎没有比@GSerg 的答案中描述的更简单的方法来实现这一点。奇怪的是,MVC 团队还没有想出一个不那么混乱的方法。我已经制作了这个扩展方法来至少在某种程度上封装它:

public static MvcHtmlString EditorForEnumerable<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, IEnumerable<TValue>>> expression, string templateName)
{
    var fieldName = html.NameFor(expression).ToString();
    var items = expression.Compile()(html.ViewData.Model);
    return new MvcHtmlString(string.Concat(items.Select((item, i) => html.EditorFor(m => item, templateName, fieldName + '[' + i + ']'))));
}
于 2016-09-06T09:19:03.917 回答
3

有很多方法可以解决这个问题。在 EditorFor 中指定模板名称时,无法在编辑器模板中获得默认的 IEnumerable 支持。首先,我建议如果你在同一个控制器中有多个相同类型的模板,你的控制器可能有太多的责任,你应该考虑重构它。

话虽如此,最简单的解决方案是自定义 DataType。除了 UIHints 和 typenames,MVC 还使用 DataTypes。看:

自定义 EditorTemplate 未在 MVC4 中用于 DataType.Date

所以,你只需要说:

[DataType("MyCustomType")]
public IEnumerable<MyOtherType> {get;set;}

然后,您可以在编辑器模板中使用 MyCustomType.cshtml。 与 UIHint 不同,这不会因缺乏 IEnuerable 支持而受到影响。如果您的使用支持默认类型(例如,电话或电子邮件,那么更喜欢使用现有的类型枚举)。或者,您可以派生自己的 DataType 属性并使用 DataType.Custom 作为基础。

您也可以简单地将您的类型包装在另一种类型中以创建不同的模板。例如:

public class MyType {...}
public class MyType2 : MyType {}

然后,您可以很容易地创建 MyType.cshtml 和 MyType2.cshtml,并且在大多数情况下,您始终可以将 MyType2 视为 MyType。

如果这对您来说太“hackish”,您始终可以构建您的模板以根据通过编辑器模板的“additionalViewData”参数传递的参数进行不同的渲染。

另一种选择是使用传递模板名称的版本来执行类型的“设置”,例如创建表格标签或其他类型的格式,然后使用更通用的类型版本来仅呈现行项目来自命名模板内部的更通用的表单。

这允许您拥有一个不同的 CreateMyType 模板和一个 EditMyType 模板,除了单个行项目(您可以与之前的建议结合使用)。

另一种选择是,如果您没有为此类型使用 DisplayTemplates,则可以将 DisplayTempates 用作备用模板(创建自定义模板时,这只是一种约定。使用内置模板时,它只会创建显示版本)。当然,这是违反直觉的,但如果您只有两个需要使用的相同类型的模板,而没有相应的 Display 模板,它确实可以解决问题。

当然,您总是可以将 IEnumerable 转换为模板中的数组,这不需要重新声明模型类型。

@model IEnumerable<MyType>
@{ var model = Model.ToArray();}
@for(int i = 0; i < model.Length; i++)
{
    <p>@Html.TextBoxFor(x => model[i].MyProperty)</p>
}

我可能会想出十几种其他方法来解决这个问题,但实际上,任何时候我遇到过这种情况,我发现如果我考虑一下,我可以简单地重新设计我的模型或视图不再需要解决它的方式。

换句话说,我认为这个问题是一种“代码味道”,并且表明我可能做错了什么,重新思考这个过程通常会产生一个没有问题的更好的设计。

所以回答你的问题。正确的惯用方法是重新设计您的控制器和视图,以便不存在此问题。 除此之外,选择最不具攻击性的“hack”来实现你想要的。

于 2014-10-07T02:59:23.280 回答
-1

在属性上使用FilterUIHint而不是常规的 , 。UIHintIEnumerable<T>

public class Order
{
    [FilterUIHint("OrderLine")]
    public IEnumerable<OrderLine> Lines { get; set; }
}

不需要其他任何东西。

@Html.EditorFor(m => m.Lines)

这现在"OrderLine"为每个OrderLinein显示一个 EditorTemplate Lines

于 2014-10-07T01:54:42.300 回答
-2

您可以使用UIHint属性来指导 MVC 为编辑器加载哪个视图。所以你的Order对象看起来像这样使用UIHint

public class Order
{
    [UIHint("Non-Standard-Named-View")]
    public IEnumerable<OrderLine> Lines { get; set; }
}
于 2014-08-27T22:12:34.023 回答