How can I get validation messages to render on collection properties when using new guid indexes each time?
In this example ASP.Net MVC 4 program I have a user fill in details about a horse race. The race has a name a well as a list of horses involved. Each horse has a name and an age.
The form uses ajax and javascript to allow the person to add and delete horse input fields on the fly, which is then submitted all at once when the submit button is pressed.
To make this process easy for me, I'm using an html helper made by 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());
}
While I don't understand all the details (please read the blog post), I do know that it changes the index values into guids rather than sequential integers. This allows me to delete items in the middle of the list without needing to recalculate indexes.
Here is the rest of my code for my MVCE
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>
Views/Shared/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>
Views/Home/AjaxMakeHorseEntry.cshtml
@model IEnumerable<CollectionAjaxPosting.Models.Horse>
@Html.EditorForMany(x => x, "HorsesInRace")
The data flow works with this code. A person is able to create and delete horse entries as much as they want on the page, and when the form is submitted all entered values are given to the action method.
However, if the user does not enter in the [Required]
information on a horse entry, ModelState.IsValid
will be false showing the form again, but no validation messages will be shown for the Horse properties. The validation error do show up in the ValidationSummary
list though.
For example, if Race Name
is left blank, along with one Horse's Name
, a validation message will be shown for the former. The latter will have a validation <span>
with the class "field-validation-valid".
I'm very sure this is caused because the EditorForMany
method creates new guids for each property each time the page is created, so validation messages can't be matched to the correct field.
What can I do to fix this? Do I need to abandon guid index creation or can an alteration be made to the EditorForMany
method to allow validation messages to be passed along correctly?
c# asp.net-mvc asp.net-mvc-4