Skip to content

Instantly share code, notes, and snippets.

@gunr2171
Created January 22, 2016 04:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gunr2171/c54d30a4d138b77bde00 to your computer and use it in GitHub Desktop.
Save gunr2171/c54d30a4d138b77bde00 to your computer and use it in GitHub Desktop.

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".

enter image description here

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment