在这个示例ASP.Net MVC 4程序中,我有一个用户填写有关赛马的详细信息。比赛有一个名字,以及所涉及的马匹列表。每匹马都有一个名字和年龄。
表单使用ajax和javascript来允许此人动态添加和删除马输入字段,然后在按下提交按钮时立即提交。
为了让这个过程变得简单,我使用html helper制作的Matt Lunn。
public static MvcHtmlString EditorForMany<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, IEnumerable<TValue>>> expression, string htmlFieldName = null) where TModel : class
{
var items = expression.Compile()(html.ViewData.Model);
var sb = new StringBuilder();
if (String.IsNullOrEmpty(htmlFieldName))
{
var prefix = html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix;
htmlFieldName = (prefix.Length > 0 ? (prefix + ".") : String.Empty) + ExpressionHelper.GetExpressionText(expression);
}
foreach (var item in items)
{
var dummy = new { Item = item };
var guid = Guid.NewGuid().ToString();
var memberExp = Expression.MakeMemberAccess(Expression.Constant(dummy), dummy.GetType().GetProperty("Item"));
var singleItemExp = Expression.Lambda<Func<TModel, TValue>>(memberExp, expression.Parameters);
sb.Append(String.Format(@"<input type=""hidden"" name=""{0}.Index"" value=""{1}"" />", htmlFieldName, guid));
sb.Append(html.EditorFor(singleItemExp, null, String.Format("{0}[{1}]", htmlFieldName, guid)));
}
return new MvcHtmlString(sb.ToString());
}
虽然我不了解所有细节(请阅读博客文章),但我知道它将索引值更改为guids而不是顺序整数。这允许我删除列表中间的项目而无需重新计算索引。
以下是我的MCVE的其余代码
HomeController.cs
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index()
{
var model = new Race();
//start with one already filled in
model.HorsesInRace.Add(new Horse() { Name = "Scooby", Age = 10 });
return View(model);
}
[HttpPost]
public ActionResult Index(Race postedModel)
{
if (ModelState.IsValid)
//model is valid, redirect to another page
return RedirectToAction("ViewHorseListing");
else
//model is not valid, show the page again with validation errors
return View(postedModel);
}
[HttpGet]
public ActionResult AjaxMakeHorseEntry()
{
//new blank horse for ajax call
var model = new List<Horse>() { new Horse() };
return PartialView(model);
}
}
Models.cs
public class Race
{
public Race() { HorsesInRace = new List<Horse>(); }
[Display(Name = "Race Name"), Required]
public string RaceName { get; set; }
[Display(Name = "Horses In Race")]
public List<Horse> HorsesInRace { get; set; }
}
public class Horse
{
[Display(Name = "Horse's Name"), Required]
public string Name { get; set; }
[Display(Name = "Horse's Age"), Required]
public int Age { get; set; }
}
Index.cshtml
@model CollectionAjaxPosting.Models.Race
<h1>Race Details</h1>
@using (Html.BeginForm())
{
@Html.ValidationSummary()
<hr />
<div>
@Html.DisplayNameFor(x => x.RaceName)
@Html.EditorFor(x => x.RaceName)
@Html.ValidationMessageFor(x => x.RaceName)
</div>
<hr />
<div id="horse-listing">@Html.EditorForMany(x => x.HorsesInRace)</div>
<button id="btn-add-horse" type="button">Add New Horse</button>
<input type="submit" value="Enter Horses" />
}
<script type="text/javascript">
$(document).ready(function () {
//add button logic
$('#btn-add-horse').click(function () {
$.ajax({
url: '@Url.Action("AjaxMakeHorseEntry")',
cache: false,
method: 'GET',
success: function (html) {
$('#horse-listing').append(html);
}
})
});
//delete-horse buttons
$('#horse-listing').on('click', 'button.delete-horse', function () {
var horseEntryToRemove = $(this).closest('div.horse');
horseEntryToRemove.prev('input[type=hidden]').remove();
horseEntryToRemove.remove();
});
});
</script>
查看/共享/ EditorTemplates / Horse.cshtml
@model CollectionAjaxPosting.Models.Horse
<div class="horse">
<div>
@Html.DisplayNameFor(x => x.Name)
@Html.EditorFor(x => x.Name)
@Html.ValidationMessageFor(x => x.Name)
</div>
<div>
@Html.DisplayNameFor(x => x.Age)
@Html.EditorFor(x => x.Age)
@Html.ValidationMessageFor(x => x.Age)
</div>
<button type="button" class="delete-horse">Remove Horse</button>
<hr />
</div>
查看/主页/ AjaxMakeHorseEntry.cshtml
@model IEnumerable<CollectionAjaxPosting.Models.Horse>
@Html.EditorForMany(x => x, "HorsesInRace")
数据流适用于此代码。一个人可以在页面上尽可能多地创建和删除马条目,并且在提交表单时,所有输入的值都将被赋予操作方法。
但是,如果用户未输入有关马条目的[Required]
信息,则ModelState.IsValid
将显示错误,但会再次显示该表单,但不会显示验证消息为马属性。验证错误确实显示在ValidationSummary
列表中。
例如,如果Race Name
与一个Horse's Name
一起留空,则会显示前者的验证消息。后者将使用“field-validation-valid”类进行验证<span>
。
我非常确定这是因为EditorForMany
方法在每次创建页面时为每个属性创建新的guid,因此验证消息无法与正确的字段匹配。
我该怎么做才能解决这个问题?我是否需要放弃guid索引创建,还是可以对EditorForMany
方法进行更改以允许验证消息正确传递?
答案 0 :(得分:7)
我非常确定这是因为
EditorForMany
方法在每次创建页面时为每个属性创建新的guid,因此验证消息无法与正确的字段匹配。
是的;这正是这里发生的事情。
要解决此问题,我们需要修改EditorForMany()
,以便重新使用项目的GUID,而不是生成新项目。反过来,这意味着我们需要跟踪GUID分配给哪个项目,以便可以重复使用。
前者可以通过对EditorForMany()
的内部修改来完成。后者要求我们:
EditorForMany()
添加一个参数,告诉帮助者哪个属性包含要重复使用的GUID(如果有)。这使EditorForMany
助手看起来像这样;
public static class HtmlHelperExtensions
{
public static MvcHtmlString EditorForMany<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, IEnumerable<TValue>>> propertyExpression, Expression<Func<TValue, string>> indexResolverExpression = null, string htmlFieldName = null) where TModel : class
{
htmlFieldName = htmlFieldName ?? ExpressionHelper.GetExpressionText(propertyExpression);
var items = propertyExpression.Compile()(html.ViewData.Model);
var htmlBuilder = new StringBuilder();
var htmlFieldNameWithPrefix = html.ViewData.TemplateInfo.GetFullHtmlFieldName(htmlFieldName);
Func<TValue, string> indexResolver = null;
if (indexResolverExpression == null)
{
indexResolver = x => null;
}
else
{
indexResolver = indexResolverExpression.Compile();
}
foreach (var item in items)
{
var dummy = new { Item = item };
var guid = indexResolver(item);
var memberExp = Expression.MakeMemberAccess(Expression.Constant(dummy), dummy.GetType().GetProperty("Item"));
var singleItemExp = Expression.Lambda<Func<TModel, TValue>>(memberExp, propertyExpression.Parameters);
if (String.IsNullOrEmpty(guid))
{
guid = Guid.NewGuid().ToString();
}
else
{
guid = html.AttributeEncode(guid);
}
htmlBuilder.Append(String.Format(@"<input type=""hidden"" name=""{0}.Index"" value=""{1}"" />", htmlFieldNameWithPrefix, guid));
if (indexResolverExpression != null)
{
htmlBuilder.Append(String.Format(@"<input type=""hidden"" name=""{0}[{1}].{2}"" value=""{1}"" />", htmlFieldNameWithPrefix, guid, ExpressionHelper.GetExpressionText(indexResolverExpression)));
}
htmlBuilder.Append(html.EditorFor(singleItemExp, null, String.Format("{0}[{1}]", htmlFieldName, guid)));
}
return new MvcHtmlString(htmlBuilder.ToString());
}
}
然后我们还需要更改模型,添加存储GUID的属性;
public class Race
{
public Race() { HorsesInRace = new List<Horse>(); }
[Display(Name = "Race Name"), Required]
public string RaceName { get; set; }
[Display(Name = "Horses In Race")]
public List<Horse> HorsesInRace { get; set; }
}
public class Horse
{
[Display(Name = "Horse's Name"), Required]
public string Name { get; set; }
[Display(Name = "Horse's Age"), Required]
public int Age { get; set; }
// Note the addition of Index here.
public string Index { get; set; }
}
...最后,更改EditorForMany()
的使用情况以使用新签名;
Index.cshtml ;
<div id="horse-listing">@Html.EditorForMany(x => x.HorsesInRace, x => x.Index)</div>
AjaxMakeHorseEntry.cshtml ;
@Html.EditorForMany(x => x, x => x.Index, "HorsesInRace")
...然后应该显示验证消息。
另外,我建议不要将htmlFieldName
参数用于EditorForMany
,而是将控制器操作更改为;
功能
[HttpGet]
public ActionResult AjaxMakeHorseEntry()
{
var model = new Race();
model.HorsesInRace.Add(new Horse());
return PartialView(model);
}
...然后你的 AjaxMakeHorseEntry.cshtml 视图就是这样;
@model Models.Race
@Html.EditorForMany(x => x.HorsesInRace, x => x.Index)
否则,在嵌套name
的使用时,生成的EditorForMany()
属性会中断。
因为这个原因,我要更新博客帖子以使用上面版本的EditorForMany()
,而不是接受htmlFieldName
参数。