在 MVC 中使用带有 IE无数模型 ASP.NET 自定义编辑器模板的正确惯用方法

本文关键字:编辑器 自定义 方法 NET 模型 MVC IE ASP | 更新日期: 2023-09-27 18:18:07

这个问题是为什么我的 DisplayFor 没有循环遍历我的 IEnumerable


快速刷新。

什么时候:

  • 模型具有类型 IEnumerable<T>
  • 的属性
  • 使用仅接受 lambda 表达式的重载将此属性传递给Html.EditorFor()
  • 您有一个视图/共享/编辑器模板下类型 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; }
}

还有一个视图 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 和名称。当您稍后 POST 它们时,模型绑定器将无法构造OrderLine数组,并且您在控制器的 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 的所有逻辑链接的正确、惯用方法是什么?

在 MVC 中使用带有 IE无数模型 ASP.NET 自定义编辑器模板的正确惯用方法

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

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

我现在就把它留在这里,做更多的测试,以确保它在深度嵌套的场景中工作。

对于以下示例,假设您已经有两个用于OrderLine类的模板: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++ + "]")
    }
}
似乎

没有比@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 + ']'))));
}

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

话虽如此,最简单的解决方案是自定义数据类型。 MVC 除了使用 UIHints 和类型名之外,还使用数据类型。 看:

自定义编辑器模板未在 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。

如果这对您来说太"黑客"了,您可以随时构建模板,以便根据通过编辑器模板的"additionalViewData"参数传递的参数进行不同的渲染。

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

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

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

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

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

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

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

所以回答你的问题。 正确的惯用方法是重新设计控制器和视图,以便不存在此问题。 除此之外,选择最不令人反感的"黑客"来实现您想要的

IEnumerable<T> 属性上使用 FilterUIHint 而不是常规UIHint

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

不需要其他任何东西。

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

现在,这将为Lines中的每个OrderLine显示一个"OrderLine"编辑器模板。

您可以使用

UIHint 属性来指导 MVC 您希望为编辑器加载的视图。因此,您的Order对象使用UIHint如下所示

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