MVC3 Custom Validation Error Message Does Not Display When Using ViewModel

ESSENCE

Question: Why a custom validation error message is not displayed when using ViewModel.

Answer: Custom validation should apply to the ViewModel, not the class. See End of @ JaySilk84 answer, for example code.

MVC3 project using

  • Jquery-1.7.2.min.js
  • Modernizr-2.5.3.js
  • jquery-ui-1.8.22.custom.min.js (generated by jQuery.com for the Accordion plugin)
  • jquery.validate.min.js and
  • jquery.validate.unobtrusive.min.js

I have a validation that works in my project for both data and ModelState.AddModelError in the controller, so I know that I set up the validation code correctly.

But during user verification, an error is generated in the code, but the error message is not displayed.

 public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) { if (DOB > DateTime.Now.AddYears(-18)) { yield return new ValidationResult("Must be 18 or over."); } } 

A debugging failure in the POST action of a user check leads to a failure of the model state and the error message is placed in the corresponding value field, but when the model is sent back to the view, the error message is not displayed. In the controller, I also have ModelState.AddModelError code, and its message is displayed. How will this work otherwise than with one, and not with the other? If this is not the case, what else could prevent the error message from being displayed?

Update 1:

I am using ViewModel to create a model in a view. I uninstalled the ViewModel and an error message appeared as soon as I added that the ViewModel again in the message again stopped showing. Has anyone successfully used custom validation with ViewModel? Was there anything you needed to do to make it work?

Update 2:

I created a new MVC3 project with these two simple classes (Agency and Person).

  public class Agency : IValidatableObject { public int Id { get; set; } public string Name { get; set; } public DateTime DOB { get; set; } public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) { if (DOB > DateTime.Now.AddYears(-18)) { yield return new ValidationResult("Must be over 18."); } } } public class Person { public int Id { get; set; } public string Name { get; set; } } 

Here is the controller code

  public ActionResult Create() { return View(); } // // POST: /Agency/Create [HttpPost] public ActionResult Create(Agency agency) { if (ModelState.IsValid) { db.Agencies.Add(agency); db.SaveChanges(); return RedirectToAction("Index"); } return View(agency); } //[HttpPost] //public ActionResult Create(AgencyVM agencyVM) //{ // if (ModelState.IsValid) // { // var agency = agencyVM.Agency; // db.Agencies.Add(agency); // db.SaveChanges(); // return RedirectToAction("Index"); // } // return View(agencyVM); //} 

View

 @model CustValTest.Models.Agency @*@model CustValTest.Models.AgencyVM*@ @* When using VM (model => model.Name) becomes (model => model.Agency.Name) etc. *@ @{ ViewBag.Title = "Create"; } <h2>Create</h2> <script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script> @using (Html.BeginForm()) { @Html.ValidationSummary(true) <fieldset> <legend>Agency</legend> <div class="editor-label"> @Html.LabelFor(model => model.Name) </div> <div class="editor-field"> @Html.EditorFor(model => model.Name) @Html.ValidationMessageFor(model => model.Name) </div> <div class="editor-label"> @Html.LabelFor(model => model.DOB) </div> <div class="editor-field"> @Html.EditorFor(model => model.DOB) @Html.ValidationMessageFor(model => model.DOB) </div> <p> <input type="submit" value="Create" /> </p> </fieldset> } <div> @Html.ActionLink("Back to List", "Index") </div> 

ViewModel

  public class AgencyVM { public Agency Agency { get; set; } public Person Person { get; set; } } 

When only the Agency is displayed, the view shows a validation error (DOB under 18 years old). When presenting ViewModel, an error is not displayed. Custom validation always catches the error, although it causes a ModelState.IsValid error, and the view must be resubmitted. Can anyone repeat this? Any ideas on why and how to fix it?

Update 3:

As a temporary work, I changed Validation to one level level (compared to the model level) by adding a parameter to the ValidationResult:

 if (DOB > DateTime.Now.AddYears(-18)) { yield return new ValidationResult("Must be over 18.", new [] { "DOB" }); } 

The problem with this is that now the error message appears next to the field, and not at the top of the form (which does not say very well about the accordion, since the user will be returned to the form without a visible error message). To fix this secondary problem, I added this code to the POST action of the controller.

  ModelState.AddModelError(string.Empty, errMsgInvld); return View(agencyVM); } string errMsgInvld = "There was an entry error, please review the entire form. Invalid entries will be noted in red."; 

The question still remains unanswered, why the model level error message is not displayed using ViewModel (see my answer on JaySilk84 for more information about this)?

+4
source share
1 answer

The problem is that your models are nested, the error message is placed in ModelState under Agency without .DOB because you did not specify it in ValidationResult . The ValidationMessageFor() helper looks for a key named Agency.DOB (see the corresponding code below from the ValidationMessageFor () helper):

 string fullHtmlFieldName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(expression); FormContext clientValidation = htmlHelper.ViewContext.GetFormContextForClientValidation(); if (!htmlHelper.ViewData.ModelState.ContainsKey(fullHtmlFieldName) && clientValidation == null) return (MvcHtmlString) null; 

GetFullHtmlFieldName() returns a .DOB agency, not an agency

I think that if you add a DOB to the ValidationResult , it will work:

 public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) { if (DOB > DateTime.Now.AddYears(-18)) { yield return new ValidationResult("Must be over 18.", new List<string>() { "DOB" }); } } 

This second ValidationResult parameter will tell him which key to use in ModelState (by default it will add a parent object, which is Agency ), so ModelState will have a key named Agency.DOB, which is the one t22> is looking for.

Edit:

If you do not need field level validation, you will not need Html.ValidationMessageFor() . You just need ValidationSummary() . Strike>

The view processes AgencyVM as a model. If you want it to be checked correctly, set the check at AgencyVM level and check its child objects. Alternatively, you can put validation on child objects, but the parent object (AgencyVM) should combine it with the view. Another thing you can do is save it as it is and change ValidationSummary(true) to ValidationSummary(false) . This will print everything in ModelState in a summary. I think that removing verification from Agency and including it on AgencyVM might be the best approach:

  public class AgencyVM : IValidatableObject { public Agency Agency { get; set; } public Person Person { get; set; } public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) { if (Agency.DOB > DateTime.Now.AddYears(-18)) { yield return new ValidationResult("Must be over 18."); } if (string.IsNullOrEmpty(Agency.Name)) { yield return new ValidationResult("Need a name"); } } } 
+3
source

All Articles