在ASP.NET MVC中使用IEnumerable模型的自定义编辑器模板的正确,惯用的方法

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


快速刷新。

什么时候:

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

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

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

 public class Order { public IEnumerable<OrderLine> Lines { get; set; } } public class OrderLine { public string Prop1 { get; set; } public int Prop2 { get; set; } } 

并有一个视图/共享/ EditorTemplates / OrderLine.cshtml:

 @model TestEditorFor.Models.OrderLine @Html.EditorFor(m => m.Prop1) @Html.EditorFor(m => m.Prop2) 

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


但是,正如您在链接问题中所看到的那样,这只有在使用EditorFor特定重载EditorFor 。 如果您提供了一个模板名称(为了使用一个没有以OrderLine类命名的模板),那么自动序列处理将不会发生,而是会发生运行时错误。

在这一点上,您将不得不将自定义模板的模型声明为IEnumebrable<OrderLine>并手动迭代它的项目以输出所有项目,例如

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

这就是问题出现的地方。

以这种方式生成的HTML控件都具有相同的ID和名称。 当您稍后POST它们时,模型联编程序将无法构buildOrderLine的数组,并且您在控制器中的HttpPost方法中获得的模型对象将为null
如果您查看lambdaexpression式,这是有道理的 – 它并不真正将正在构build的对象链接到模型中的某个位置。

我已经尝试了遍历这些项目的各种方法,似乎唯一的方法是将模板的模型重新声明为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的逻辑链接?

在和Erik Funkenbusch讨论后,他们开始研究MVC源代码 ,看起来有两种更好的(正确和习惯的)方法来做到这一点。

两者都涉及提供正确的html名称前缀给助手,并生成与默认EditorFor的输出相同的HTML。

我现在就把它留在这里,将做更多的testing,以确保它在深度嵌套的情况下工作。

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


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

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

 @model IEnumerable<OrderLine> @{ int i = 0; foreach (var line in Model) { @Html.EditorFor(m => line, "DifferentOrderLine", "[" + i++ + "]") } } 

然后从主Order模板中调用它:

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

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

在主Order模板中:

 @model Order @{ int i = 0; foreach (var line in Model.Lines) { @Html.EditorFor(m => line, "DifferentOrderLine", "Lines[" + i++ + "]") } } 

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

话虽如此, 最简单的解决scheme是一个自定义的数据types 。 除了UIHints和typenames之外,MVC还使用DataTypes。 看到:

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

所以,你只需要说:

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

然后,您可以在编辑器模板中使用MyCustomType.cshtml。 不像UIHint,这不会受到缺乏IEnuerable支持 。 如果您的使用情况支持默认types(例如,电话或电子邮件,则优先使用现有的types枚举)。 或者,您可以派生自己的DataType属性并使用DataType.Custom作为基础。

您也可以简单地将您的types换成另一种types以创build不同的模板。 例如:

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

然后,您可以很容易地创build一个MyType.cshtml和MyType2.cshtml,并且您可以始终将MyType2作为MyType用于大多数目的。

如果对您来说这太“黑客”了,您可以始终根据通过编辑器模板的“additionalViewData”parameter passing的参数来构build模板以进行不同的呈现。

另一个select是使用传递模板名称来进行types设置的版本,如创build表格标签或其他types的格式,然后使用更通用的types版本来渲染来自命名模板的更一般的forms。

这使您可以拥有一个CreateMyType模板和一个EditMyType模板,这些模板除了各个订单项(您可以与之前的build议结合使用)之外都是不同的。

另外一个select是,如果你不使用DisplayTemplates这种types,你可以使用DisplayTempates替代模板(当创build一个自定义模板时,这只是一个惯例..当使用内置的模板,那么它只会创build显示版本)。 当然,这是违反直觉的,但是如果只有两个你需要使用的相同types的模板,并且没有相应的显示模板,它确实可以解决这个问题。

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

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

我大概可以想出其他十几种方法来解决这个问题,但实际上,无论我有没有这样做,我发现如果我考虑这个问题,我可以简单地重新devise我的模型或视图。方式不再需要解决。

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

所以要回答你的问题。 正确的,惯用的方法是重新devise你的控制器和视图,以便这个问题不存在。 除此之外, select最less的攻击性“黑客”来实现你想要的

似乎没有比@GSerg的回答更容易实现这一点的方法 。 奇怪的是,MVC团队还没有拿出一个混乱的方式来做到这一点。 我已经使这个扩展方法至less在一定程度上封装它:

 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 + ']')))); } 

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

 public class Order { [UIHint("Non-Standard-Named-View")] public IEnumerable<OrderLine> Lines { get; set; } }