ValidationMessage strange behaviour with custom attribute - validation

I have a problem with displaying validation message in a Blazor app.
I have the following model (for testing purposes):
public class MyModel
{
[Required(ErrorMessage = "Amount is required")]
public int Amount { get; set; }
[Required(ErrorMessage = "NullableAmount is required")]
public int? NullableAmount { get; set; }
[RequiredIf(nameof(Foo), new[] { "bar" }, ErrorMessage = "Id is required")]
public int Id { get; set; }
[RequiredIf(nameof(Foo), new[] { "bar" }, ErrorMessage = "NullableId is required")]
public int? NullableId { get; set; }
public string Foo { get; set; }
}
I decorated the properties with the built-in RequiredAttribute, and created a custom RequiredIfAttribute, here's the code for that:
public class RequiredIfAttribute : ValidationAttribute
{
private readonly string _otherPropertyName;
private readonly string[] _otherPropertyValues;
public RequiredIfAttribute(string otherPropertyName, string[] otherPropertyValues)
{
_otherPropertyName = otherPropertyName;
_otherPropertyValues = otherPropertyValues;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var instance = validationContext.ObjectInstance;
var type = instance.GetType();
var propertyValue = type.GetProperty(_otherPropertyName)?.GetValue(instance);
if (!_otherPropertyValues.Contains(propertyValue?.ToString()))
{
return ValidationResult.Success;
}
if(value == null)
{
return new ValidationResult(ErrorMessage);
}
if(int.TryParse(value.ToString(), out var intValue))
{
if(intValue == 0)
{
return new ValidationResult(ErrorMessage);
}
}
return ValidationResult.Success;
}
}
I takes the name of the other property, the values for that property, and if the value of that other property matches one of the specified values, it checks if the property decorated with RequiredIf is null, or if it is an integer, 0. (I use it for a model with a nullable int property, which should not be null or 0 if the other property has a certain value).
On the WebAPI side it works fine, but in the Blazor form I have some problems. Here's my form:
<EditForm Model="MyModel" OnValidSubmit="OnSubmit">
<div style="display: flex; flex-direction: column; width: 300px;">
<DataAnnotationsValidator />
<ValidationSummary />
<label>
Foo:
<InputText #bind-Value="MyModel.Foo" />
</label>
<ValidationMessage For="#(() => MyModel.Foo)" />
<label>
Amount:
<InputNumber #bind-Value="MyModel.Amount" />
</label>
<ValidationMessage For="#(() => MyModel.Amount)" />
<label>
Null Amount:
<InputNumber #bind-Value="MyModel.NullableAmount" />
</label>
<ValidationMessage For="#(() => MyModel.NullableAmount)" />
<label>
Id:
<InputNumber #bind-Value="MyModel.Id" />
</label>
<ValidationMessage For="#(() => MyModel.Id)" />
<label>
Null Id:
<InputNumber #bind-Value="MyModel.NullableId" />
</label>
<ValidationMessage For="#(() => MyModel.NullableId)" />
<button type="submit">submit</button>
</div>
</EditForm>
I open the form, enter the value "bar" for Foo, than click submit, I get the following result:
Amount is valid, as it has the default value 0, and RequiredAttribute only checks for nulls.
NullAmount is invalid, it is shown in the ValidationSummary, the input field is also red, as it is invalid, and the validation message is also shown.
Id is invalid, as in the RequiredIf attribute I check int value against 0, it has the default 0 value, the error message is shown in the ValidationSummary, and here comes the interesting part, the input field is not red, and the validation message is not shown.
The same applies for the Nullable Id field, the error message is shown in the validation summary, but the field is not red, and the validation message is not shown.
Now, if I enter a valid value for Id and NullableId (the fields with my custom attribute), and after that I change Id to 0, and NullableId to empty, this is what happens:
Both input fields change to red, validation message is shown for both properties, but in the validation summary, both error messages are displayed twice.
Now, if I click submit, both input fields change to green, and the validation messages are gone, but still displayed in the summary.
I'm totally lost, don't know if I have a problem with my custom validation attribute, a problem in the Blazor component, or it is an unknown issue. The built-in Required attribute works fine, I just added it to the code to see if my form works with that.
Do you have any idea?

I've finally found the problem. In the validation attribute, when returning an error result, I have to pass memberName in the constructor.
return new ValidationResult(ErrorMessage, validationContext.MemberName)

Thank you so much #petyusa, I also ran into this issue and it was driving me nuts. I stumbled across your solution and it solved it immediately. However, I had to pass an array of string to the second argument (perhaps because I'm using .NET 5.0) instead of a simple string:
return new ValidationResult(ErrorMessage, new []{validationContext.MemberName});

Related

Is it possible to check if Blazor ValidationMessageStore has ANY error message

I validate my Blazor form input according to this pattern with ValidationMessageStore:
https://learn.microsoft.com/en-us/aspnet/core/blazor/forms-validation?view=aspnetcore-5.0#basic-validation-1
I then output ValidationMessage on each control.
BUT it is a long form so I also want to indicate to the user somewhere close to the submit button that there are some errors that need to be fixed, and that's why we didn't accept the input yet.
I know I can use a ValidationSummary but I don't want to repeat all possible errors, just have a note.
ValidationMessageStore obviously holds all messages in an internal collection, but they are not accessible. Is it possible to somehow check if there are ANY error messages?
I found a simpler solution for my problem. On the EditContext I found a method called GetValidationMessages.
#if (editContext.GetValidationMessages().Any())
{
<div class="alert alert-danger">
Some input was incomplete. Please review detailed messages above.
</div>
}
Take a look at the ValidationSummary code - the validation message store is available. It's not very complicated, so you should be able to build yourself a similar but simpler component to display what you want.
The code is here: https://github.com/dotnet/aspnetcore/blob/main/src/Components/Web/src/Forms/ValidationSummary.cs
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components.Rendering;
namespace Microsoft.AspNetCore.Components.Forms
{
// Note: there's no reason why developers strictly need to use this. It's equally valid to
// put a #foreach(var message in context.GetValidationMessages()) { ... } inside a form.
// This component is for convenience only, plus it implements a few small perf optimizations.
/// <summary>
/// Displays a list of validation messages from a cascaded <see cref="EditContext"/>.
/// </summary>
public class ValidationSummary : ComponentBase, IDisposable
{
private EditContext? _previousEditContext;
private readonly EventHandler<ValidationStateChangedEventArgs> _validationStateChangedHandler;
/// <summary>
/// Gets or sets the model to produce the list of validation messages for.
/// When specified, this lists all errors that are associated with the model instance.
/// </summary>
[Parameter] public object? Model { get; set; }
/// <summary>
/// Gets or sets a collection of additional attributes that will be applied to the created <c>ul</c> element.
/// </summary>
[Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; }
[CascadingParameter] EditContext CurrentEditContext { get; set; } = default!;
/// <summary>`
/// Constructs an instance of <see cref="ValidationSummary"/>.
/// </summary>
public ValidationSummary()
{
_validationStateChangedHandler = (sender, eventArgs) => StateHasChanged();
}
/// <inheritdoc />
protected override void OnParametersSet()
{
if (CurrentEditContext == null)
{
throw new InvalidOperationException($"{nameof(ValidationSummary)} requires a cascading parameter " +
$"of type {nameof(EditContext)}. For example, you can use {nameof(ValidationSummary)} inside " +
$"an {nameof(EditForm)}.");
}
if (CurrentEditContext != _previousEditContext)
{
DetachValidationStateChangedListener();
CurrentEditContext.OnValidationStateChanged += _validationStateChangedHandler;
_previousEditContext = CurrentEditContext;
}
}
/// <inheritdoc />
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
// As an optimization, only evaluate the messages enumerable once, and
// only produce the enclosing <ul> if there's at least one message
var validationMessages = Model is null ?
CurrentEditContext.GetValidationMessages() :
CurrentEditContext.GetValidationMessages(new FieldIdentifier(Model, string.Empty));
var first = true;
foreach (var error in validationMessages)
{
if (first)
{
first = false;
builder.OpenElement(0, "ul");
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "class", "validation-errors");
}
builder.OpenElement(3, "li");
builder.AddAttribute(4, "class", "validation-message");
builder.AddContent(5, error);
builder.CloseElement();
}
if (!first)
{
// We have at least one validation message.
builder.CloseElement();
}
}
/// <inheritdoc/>
protected virtual void Dispose(bool disposing)
{
}
void IDisposable.Dispose()
{
DetachValidationStateChangedListener();
Dispose(disposing: true);
}
private void DetachValidationStateChangedListener()
{
if (_previousEditContext != null)
{
_previousEditContext.OnValidationStateChanged -= _validationStateChangedHandler;
}
}
}
}
If you need more help in building the component add some more detail of what you want to the question.
I had almost the same question. Based off of Jakob Lithner's answer, here is the solution I came up with.
In order to access whether or not the ValidationSummary has any error messages, you can bind the EditForm to an EditContext instead of the Model. This way you can directly reference the context programmatically. Here is a simple example using a model object and razor file that will display validation messages under each form that is validated, and will show a general error message if either form is invalid.
ExampleModel.cs
using System.ComponentModel.DataAnnotations;
namespace Example.Pages;
public class ExampleModel
{
[Required(ErrorMessage = "The object must have a name")]
[StringLength(100, ErrorMessage = "Object names cannot exceed 100 characters")]
public string Name { get; set; } = "New Example";
[StringLength(1000, ErrorMessage = "Description cannot exceed 1000 characters")]
public string Description { get; set; }
}
ExamplePage.razor
#page "/ExamplePage"
<EditForm EditContext="#EditContext" OnValidSubmit="#HandleValidSubmit">
<DataAnnotationsValidator/>
#* Where the edit context is checked for validation messages. *#
#* This can go anywhere you want it. *#
#* An alternative to "alert alert-danger" is "validation-message",
which contains the style for validation messages *#
#if (EditContext is not null && EditContext.GetValidationMessages().Any())
{
<p class="alert alert-danger text-center">
One or more errors must be fixed before changes can be saved
</p>
}
<div class="mb-3">
<div class="input-group">
<span class="input-group-text">Name</span>
<InputText class="form-control" #bind-Value="ExampleModel.Name"/>
</div>
<ValidationMessage For="#(() => ExampleModel.Name)"/>
</div>
<div class="mb-3">
<label class="form-label" for="queryDescription">Object Description</label>
<InputTextArea
class="form-control"
id="queryDescription" rows="3"
#bind-Value="ExampleModel.Description"/>
<ValidationMessage For="() => ExampleModel.Description"/>
</div>
<div class="btn-group">
<a class="btn btn-warning" href="/ExamplePage">Cancel</a>
#* By signifying the type as submit, this is the button that triggers
the validation event *#
<button class="btn btn-success" type="submit">Create</button>
</div>
</EditForm>
#code {
private ExampleModel ExampleModel { get; set; } = new();
private EditContext? EditContext { get; set; }
protected override Task OnInitializedAsync()
{
// This binds the Model to the Context
EditContext = new EditContext(ExampleModel);
return Task.CompletedTask;
}
private void HandleValidSubmit()
{
// Process the valid form
}
}
See the form and validation documentation section on Binding a form for more details on how to use the Context instead of the Model for the EditForm.

How do you deal with Razor Pages PageRemote validation on 'loaded' data (e.g edit ViewModel Page)?

I am probably missing the obvious, but nevertheless a little stuck with PageRemote validation. Like a lot of us, I am following Mike’s helpful tutorial on the subject: https://www.mikesdotnetting.com/article/343/improved-remote-validation-in-razor-pages
I won’t copy his code here, as it is easy to follow, and works exactly as demonstrated. Great for a ‘Create User’ page!
My problem is though, when applying it to an ‘Edit User’ page, then I have hit a couple snags. In the OnGet() I load the ‘User’ from a QueryString, and populate the form, including the remote validated field. If I touch no fields, and straight away hit the submit button, it doesn’t trigger the submit’s OnPostSubmit() handler, but the PageRemote’s validation OnPost() instead (as presumably the field is dirty, even if the user didn’t do it).
So how do I make sure the submit button fires as expected, in this scenario? According to my break point, it never fires the OnPostSubmit() handler, in this scenario.
Following this scenario, that PageRemote’s OnPost returns ‘true’ (as nothing changed, and everything is still valid), but something else seems to be going on, as a SelectList that is normally loaded OnGet() is now empty, and means the form is now not complete. If before I click the submit button, I enter any of the form’s fields, and force the PageRemote to normally fire, my SelectList is fine. The loss of loaded SelectList values being lost, is only when the PageRemote fires when immediately clicking submit without touching any fields. Why does it behave differently? Surely I am not suppose to be reloading data in this PageRemote validation OnPost() handler, especially in the normal scenario’s, I don’t have to…
I hope this makes sense, and I hope I have not upset anyone by not putting any code up. I am happy to edit my questions with some code, but it is 99% as in Mike’s article. The only difference I have, is populating the ViewModel and SelectList OnGet().
EDIT for code:
#page
#model Redbook.Pages.Test.EditAccountModel
#{
ViewData["Title"] = "EditAccount";
}
<h1>EditAccount</h1>
<form method="post" id="frmUserDetails">
<div class="form-group">
<label class="pt-1">Email</label>
<input id="txtEmail" type="email" inputmode="email" class="form-control" asp-for="Email">
<span class="text-danger" asp-validation-for="Email"></span>
</div>
<div class="form-group">
<label class="pt-1">User Select Option</label>
<select class="form-control" asp-for="UserSelectListOption" asp-items="Model.UserSelectListOptions"></select>
<span class="text-danger" asp-validation-for="UserSelectListOption"></span>
</div>
<button id="btnContinue" type="submit" asp-page-handler="Continue" class="btn btn-outline-info">
Save
</button>
</form>
#section Scripts
{
<script src="~/lib/jquery/dist/jquery.min.js"></script>
#await Html.PartialAsync("_ValidationScriptsPartial")
<script src="~/lib/jquery-ajax-unobtrusive/dist/jquery.unobtrusive-ajax.min.js"></script>
}
CodeBehind
public class EditAccountModel : PageModel
{
[Required(ErrorMessage = "Email Address Required")]
[EmailAddress(ErrorMessage = "Invalid Email Address")]
[PageRemote(
ErrorMessage = "Email/User already in use.",
AdditionalFields = "__RequestVerificationToken",
HttpMethod = "post",
PageHandler = "CheckEmail"
)]
[BindProperty]
public string Email { get; set; }
[Required(ErrorMessage = "User Select List Option Required")]
[BindProperty]
public int UserSelectListOption { get; set; }
public SelectList UserSelectListOptions { get; set; }
public async Task<IActionResult> OnGet()
{
//Normally would pass param (querystring) to load 'user' to edit, but this is just a test!
await Task.CompletedTask;
Email = "joe.bloggs#test.com";
UserSelectListOption = 2;
//Our user form needs a drop down option.
LoadSelectList();
return Page();
}
public async Task<IActionResult> OnPostContinueAsync()
{
if (!ModelState.IsValid)
{
LoadSelectList();
return Page();
}
//Normally we would do something here (e.g get UserID), but again, this is just a test!
await Task.CompletedTask;
//We dont hit this when we first hit submit, unless we pass focus to the 'Email' control first.
//Instead 'OnPostCheckEmail' is triggered only
//Not only that, but when that happens, 'UserSelectListOptions' is empty, so we fail the required validation for that control.
//If we do touch the 'Email' control before submission, validation triggers normally,
//It doesn't affect 'UserSelectListOptions'
//Then we do hit this point successfully.
return Page();
}
public JsonResult OnPostCheckEmail()
{
var existingEmails = new[] { "jane#test.com", "claire#test.com", "dave#test.com" };
var valid = !existingEmails.Contains(Email);
return new JsonResult(valid);
}
public void LoadSelectList()
{
List<SelectListOption> selectListOptions = new List<SelectListOption>();
selectListOptions.Add(new SelectListOption(1, "Option1"));
selectListOptions.Add(new SelectListOption(2, "Option2"));
selectListOptions.Add(new SelectListOption(3, "Option3"));
UserSelectListOptions = new SelectList(selectListOptions, "OptionID", "OptionName");
}
public class SelectListOption
{
public SelectListOption(int optionID, string optionName)
{
this.OptionID = optionID;
this.OptionName = optionName;
}
public int OptionID { get; set; }
public string OptionName { get; set; }
}
}
The "[PageRemote ...]" example didn't work with my bound viewModel; so I created some script to onblur put the email text into my asp-for viewMode.Email input; that way onsubmit, my viewModel.Email has the email value already
enter code here
<input class="viewEmail-input" asp-for="viewModel.Email" />
...
<input asp-for="Email" placeholder="Email" class="email-input" />
$(document).ready(function () {
$('.email-label').on("blur", function () {
var value = $('.email-input').val();
$('.viewEmail-input').val(value);
});
});
I also put my PageRemote into a seperate common cshtml file so more than one razor page can call the same code RemoteValidation_cshtml_cs
Then modified the PageRemote as follows:
enter code here
[PageRemote(
ErrorMessage = "Email Address already exists",
AdditionalFields = "__RequestVerificationToken",
HttpMethod = "post",
PageHandler = "CheckEmail",
PageName = "RemoteValidation"
)]
[Required, EmailAddress]
[RegularExpression(#"^[a-zA-Z0-9_.+-]+#[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$", ErrorMessage = "Invalid Email format")]
[BindProperty]
public string Email { get; set; }
Finally, I notice that there was a database call everytime a character was typed in or deleted from the email input textbox; to prevent this I created a quick routine to verify the passed in email was actually an email address before checking the database; for now, it still checks after typing "c" "o" and "m", but that's only three calls instead of dozens.
enter code here
public async Task<JsonResult> OnPostCheckEmail(string email)
{
// In order to not call the database, check if email is valid email before calling database
// Have to return true... so error is not shown to user; other validation will catch it on submit.
if (!IsValidEmailFormat(email)) return new JsonResult(true);
var user = await _userManager.FindByEmailAsync(email);
if (user == null)
{
return new JsonResult(true);
}
return new JsonResult($"Email { email } is already in use");
}
private bool IsValidEmailFormat(string email)
{
String AllowedChars = #"^[a-zA-Z0-9_.+-]+#[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$";
if (Regex.IsMatch(email, AllowedChars))
{
return true;
}
return false;
}
}

How to bind a list of objects in ASP.NET Core MVC when the posted list is smaller than original?

Using "disabled" attribute on inputs on form does not post them, which is expected and wanted. However, if you prepare a form of 3 objects in a list, disable the first and third, and submit, the 2nd object appears in post header, but does not bind to the list correctly, because it has an index [1] instead of [0].
I understand how model binding works and why it does not bind the posted object that I want, but I don't know how else to describe the problem to get specific results that would lead me to my solution. Anything I search for leads to basic post and binding examples.
List inside the model I'm using:
public IList<_Result> Results { get; set; }
Class _Result has one of the properties:
public string Value { get; set; }
I fill up the list and use it in view like so:
#for (int i = 0; i < Model.Results.Count; i++)
{
...
<td>
<input asp-for="Results[i].Value" disabled />
</td>
...
}
I have checkboxes on form, which remove (with javascript) the "disabled" attribute from the inputs and thus allow them to be posted.
When I fill up the said list with 3 _Result objects, they are shown on form and all have the "disabled" attribute. If I remove the "disabled" attribute from the first two objects and click on submit button, I receive the Results list with first 2 _Result objects, which is as expected.
However, if I remove the "disabled" attribute only from the second _Result object (the first _Result object still has "disabled" attribute), the Results list comes back empty in my Controller method.
In my Form Data Header, I see this: "Results[1].Value: Value that I want posted", which means that post occurs, but list does not bind the object due to the index.
Any idea on how I can achieve that proper binding? Also, the reason I'm using "disabled" attribute is because I'm showing many results on a single page and want to only post those that are selected.
For getting selected items, you could try checkbox with View Model instead of using jquery to control the disable property.
Change ViewModel
public class ModelBindVM
{
public IList<_ResultVM> Results { get; set; }
}
public class _ResultVM
{
public bool IsSelected { get; set; }
public string Value { get; set; }
}
Controller
[HttpGet]
public IActionResult ModelBindTest()
{
ModelBindVM model = new ModelBindVM
{
Results = new List<_ResultVM>() {
new _ResultVM{ Value = "T1" },
new _ResultVM{ Value = "T2" },
new _ResultVM{ Value = "T3" }
}
};
return View(model);
}
[HttpPost]
public IActionResult ModelBindTest(ModelBindVM modelBind)
{
return View();
}
View
<div class="row">
<div class="col-md-4">
<form asp-action="ModelBindTest">
#for (int i = 0; i < Model.Results.Count; i++)
{
<input type="checkbox" asp-for="Results[i].IsSelected" />
<label asp-for="#Model.Results[i].IsSelected">#Model.Results[i].Value</label>
<input type="hidden" asp-for="#Model.Results[i].Value" />
}
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
</div>
</div>

ASP.Net MVC 6 PartialView model binding confusion

Background
We need to submit model with items. For that purpose, we make this complex model:
public class PaymentViewModel
{
public decimal? Payment { get; set; }
public string Description { get; set; }
public List<SplittedPaymentViewModel> SplittedPayment { get; set; } = new List<SplittedPaymentViewModel>();
}
public class SplittedPaymentViewModel
{
public short SplittedPaymentId { get; set; }
public decimal? Payment { get; set; }
public string Description { get; set; }
}
For rendering html, we this two views, one regular: CreatePayment
#model ZevApp.ViewModels.Record.PaymentViewModel
...
<input class="form-control" asp-for="Payment" type="number" />
<input class="form-control" asp-for="Description" />
...
<div class="panel-body" id="SplittedPayments">
#foreach (var spItem in Model.SplittedPayment)
{
#Html.Partial("SplittedPaymentPartial", spItem);
}
...
</div>
And the other Partial: SplittedPaymentPartial
#model ZevApp.ViewModels.Record.SplittedPaymentViewModel
...
<input class="form-control" asp-for="Payment" type="number" />
<input class="form-control" asp-for="Description" />
...
As you can see, from the first view, we call partial view and pass them item by item form the SplittedPayment list.
Unexpectedly behavior
We run the code but unexpectedly behavior is occur:
Each partial view bind Payment and Description form parent PaymentViewModel, not from a passed item?
Does anybody know what is wrong?
Each view define model at the beginning of the file.
From the controller we return PaymentViewModel, and for a test, there are tree SplittedPaymentViewModels. But we can't see values from the items.
I found that problem was CreatePayment.cshtml (thanks to the people from MVC community). It doesn't provide any information about the spItem expression when invoking the partial view.
var savePrefix = ViewData.TemplateInfo.HtmlFieldPrefix;
for (var i = 0; i < Model.SplittedPayment.Count; i++)
{
ViewData.TemplateInfo.HtmlFieldPrefix = Html.NameFor(m => m.SplittedPayment[i]);
var spItem = Model.SplittedPayment[i];
#Html.Partial("SplittedPaymentPartial", spItem);
ViewData.TemplateInfo.HtmlFieldPrefix = savePrefix;
}
You can find full description on https://github.com/aspnet/Mvc/issues/4106

MVC3 Custom Validation error message doesn't display when using ViewModel

SUMMARY
Question: Why doesn't the custom validation error message show when using a ViewModel.
Answer: The custom validation should be applied to the ViewModel not the Class. See the end of #JaySilk84's 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 validation working in my project for both dataannotations in the View and for ModelState.AddModelError in the Controller so I know I have all the validation code configured properly.
But with custom validation an error is generated in the code but the error message doesn't display.
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{ if (DOB > DateTime.Now.AddYears(-18))
{ yield return new ValidationResult("Must be 18 or over."); } }
Drilling down in debug in the POST action the custom validation causes Model state to fail and the error message is placed in the proper value field but when the model is sent back to the view the error message doesn't display. In the controller I also have ModelState.AddModelError code and its message does display. How is that handled differently as to one would work and not the other? If not that what else would prevent the error message from displaying?
Update 1 :
I'm using a ViewModel to create the model in the view. I stripped out the ViewModel and the error message started displaying, as soon I added the ViewModel back in the message again stopped displaying. Has anyone successfully used a custom validation with a ViewModel? Was there anything you had to do extra to get it to 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's 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);
//}
The 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>
The ViewModel
public class AgencyVM
{
public Agency Agency { get; set; }
public Person Person { get; set; }
}
When just Agency is presented in the View the validation error displays (DOB under 18). When the ViewModel is presented the error doesn't display. The custom validation always catches the error though and causes ModelState.IsValid to fail and the view to be re-presented. Can anyone replicate this? Any ideas on why and how to fix?
Update 3 :
As a temporary work around I have changed the Validation into a field level one (vs. a model level one) 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 now the error message is showing up next to the field rather than at the top of the form (which is not good in say an accordion view since the user will be returned to the form with no visible error message). To fix this secondary problem I added this code to the Controller POST action.
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 is still unanswered, why doesn't the model level error message show with a ViewModel (see my response to JaySilk84 for more on this)?
The issue is now that your models are nested, the error message is being placed into ModelState under Agency without the .DOB because you didn't specify it in the ValidationResult. The ValidationMessageFor() helper is looking for a key named Agency.DOB (see relevant code below from 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() is returning Agency.DOB, not Agency
I think if you add the 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" }); }
}
That second parameter to ValidationResult will tell it what key to use in ModelState (By default it will append the parent object which is Agency) so the ModelState will have a key named Agency.DOB which is what your ValidationMessageFor() is looking for.
Edit:
If you don't want field level validation then you don't need the Html.ValidationMessageFor(). You just need the ValidationSummary().
The view is treating AgencyVM as the model. If you want it to validate properly then put the validation at the AgencyVM level and have it validate the child objects. Alternatively you could put validation on the child objects but the parent object (AgencyVM) has to aggregate it to the view. Another thing you can do is keep it as it is and change ValidationSummary(true) to ValidationSummary(false). This will print everything in ModelState to the summary. I think removing the validation from Agency and putting 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"); }
}
}

Resources