假设我有一个简单的模型来解释这个目的。
public class Category
{
...
public IEnumerable<Product> Products { get; set; }
}
视图:
@model Category
...
<ul>
@Html.EditorFor(m => m.Products)
</ul>
EditorTemplate。
@model Product
...
<li>
@Html.EditorFor(m => m.Name)
</li>
请注意,我不需要为EditorTemplate定义。IEnumerable<Product>
,我只能创建它为 Product
模型和MVC框架足够聪明,可以为IEnumerable使用自己的模板。它遍历我的集合并调用我的EditorTemplate。
输出的html将是这样的
...
<li>
<input id="Products_i_Name" name="Products[i].Name" type="text" value="SomeName">
</li>
我毕竟可以把它发布到我的控制器上。
但是为什么当我用模板名定义EditorTemplate时,MVC没有做到这一点呢?
@Html.EditorFor(m => m.Products, "ProductTemplate")
在这种情况下,我必须将属性的类型改为 IList<Product>
,我自己遍历这个集合,然后调用EditorTemplate。
@for (int i = 0; i < Model.Products.Count; i++)
{
@Html.EditorFor(m => m.Products[i], "ProductTemplate")
}
这在我看来是一种肮脏的变通方法。有没有其他更干净的解决方案?
有没有其他更简洁的解决方案呢?
答案很简单,没有,烂得很厉害,我完全同意你的观点,但框架的设计者就是这样决定实现这个功能的。
所以我做的是我坚持约定俗成。由于我对每个视图和partials都有特定的视图模型,所以有一个相应的编辑器模板也不是什么大不了的事情,命名方式和集合的类型一样。
好了,现在我只欠达林9999瓶啤酒。
public static MvcHtmlString EditorForMany<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, IEnumerable<TValue>>> expression, string templateName = null) where TModel : class
{
StringBuilder sb = new StringBuilder();
// Get the items from ViewData
var items = expression.Compile()(html.ViewData.Model);
var fieldName = ExpressionHelper.GetExpressionText(expression);
var htmlFieldPrefix = html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix;
var fullHtmlFieldPrefix = String.IsNullOrEmpty(htmlFieldPrefix) ? fieldName : String.Format("{0}.{1}", htmlFieldPrefix, fieldName);
int index = 0;
foreach (TValue item in items)
{
// Much gratitude to Matt Hidinger for getting the singleItemExpression.
// Current html.DisplayFor() throws exception if the expression is anything that isn't a "MemberAccessExpression"
// So we have to trick it and place the item into a dummy wrapper and access the item through a Property
var dummy = new { Item = item };
// Get the actual item by accessing the "Item" property from our dummy class
var memberExpression = Expression.MakeMemberAccess(Expression.Constant(dummy), dummy.GetType().GetProperty("Item"));
// Create a lambda expression passing the MemberExpression to access the "Item" property and the expression params
var singleItemExpression = Expression.Lambda<Func<TModel, TValue>>(memberExpression,
expression.Parameters);
// Now when the form collection is submitted, the default model binder will be able to bind it exactly as it was.
var itemFieldName = String.Format("{0}[{1}]", fullHtmlFieldPrefix, index++);
string singleItemHtml = html.EditorFor(singleItemExpression, templateName, itemFieldName).ToString();
sb.AppendFormat(singleItemHtml);
}
return new MvcHtmlString(sb.ToString());
}
我决定写一个库来提供一个更简洁的方式来解决这个问题。有了它,你可以为列表的每个部分指定单独的模板,而且它对显示和编辑都有效。
@Html.DisplayListFor(x => x.Books,
itemTemplate: "BookViewModel",
itemContainerTemplate: "ContainerForMyItem"
listTemplate: "MyList"
listContainerTemplate: "ContainerForMyList")
请注意,自定义是可选的,你可以简单地写下:
@Html.DisplayListFor(x => x.Books)
或
@Html.ListEditorFor(x => x.Books)
它应该就能用了。
这个库叫做 动态视图-模型列表 而其在NuGet上可以。