Jquery unobtrusive validation works by applying attributes to INPUT elements that instruct the client library to validate that element using a rule that is mapped to the respective attribute. For instance: the data-val-required
html attribute is recognized by the unobtrusive library, and causes it to validate that element against the corresponding rule.
In .NET MVC, you can make this happen automatically for some specific rules by applying attributes to your model properties. Attributes like Required
and MaxLength
work because the Html helpers know how to read those attributes and add corresponding HTML attributes to their output that the unobtrusive library understands.
If you add validation rules to your models in IValidatableObject
or using FluentValidation, the HTML Helper will not see these rules, and therefore not try to translate them to unobtrusive attributes.
In other words the "free" coordination you've seen thus far by applying attributes to your model and getting client validation is limited to validation attributes, and further, is limited (by default) only to those attributes that map directly to unobtrusive rules.
The bright side is, you are free to create your own custom validation attributes, and by implementing IClientValidatable
, the Html Helper will add an unobtrusive attribute with the name of your choosing that you can then teach the unobtrusive library to respect.
This is a custom attribute we use that ensures that one date falls after another date:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class DateGreaterThanAttribute : ValidationAttribute, IClientValidatable
{
string otherPropertyName;
public DateGreaterThanAttribute(string otherPropertyName, string errorMessage = null)
: base(errorMessage)
{
this.otherPropertyName = otherPropertyName;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
ValidationResult validationResult = ValidationResult.Success;
// Using reflection we can get a reference to the other date property, in this example the project start date
var otherPropertyInfo = validationContext.ObjectType.GetProperty(this.otherPropertyName);
// Let's check that otherProperty is of type DateTime as we expect it to be
if (otherPropertyInfo.PropertyType.Equals(new DateTime().GetType()))
{
DateTime toValidate = (DateTime)value;
DateTime referenceProperty = (DateTime)otherPropertyInfo.GetValue(validationContext.ObjectInstance, null);
// if the end date is lower than the start date, than the validationResult will be set to false and return
// a properly formatted error message
if (toValidate.CompareTo(referenceProperty) < 1)
{
validationResult = new ValidationResult(this.GetErrorMessage(validationContext));
}
}
else
{
// do nothing. We're not checking for a valid date here
}
return validationResult;
}
public override string FormatErrorMessage(string name)
{
return "must be greater than " + otherPropertyName;
}
private string GetErrorMessage(ValidationContext validationContext)
{
if (!this.ErrorMessage.IsNullOrEmpty())
return this.ErrorMessage;
else
{
var thisPropName = !validationContext.DisplayName.IsNullOrEmpty() ? validationContext.DisplayName : validationContext.MemberName;
var otherPropertyInfo = validationContext.ObjectType.GetProperty(this.otherPropertyName);
var otherPropName = otherPropertyInfo.Name;
// Check to see if there is a Displayname attribute and use that to build the message instead of the property name
var displayNameAttrs = otherPropertyInfo.GetCustomAttributes(typeof(DisplayNameAttribute), false);
if (displayNameAttrs.Length > 0)
otherPropName = ((DisplayNameAttribute)displayNameAttrs[0]).DisplayName;
return "{0} must be on or after {1}".FormatWith(thisPropName, otherPropName);
}
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
//string errorMessage = this.FormatErrorMessage(metadata.DisplayName);
string errorMessage = ErrorMessageString;
// The value we set here are needed by the jQuery adapter
ModelClientValidationRule dateGreaterThanRule = new ModelClientValidationRule();
dateGreaterThanRule.ErrorMessage = errorMessage;
dateGreaterThanRule.ValidationType = "dategreaterthan"; // This is the name the jQuery adapter will use
//"otherpropertyname" is the name of the jQuery parameter for the adapter, must be LOWERCASE!
dateGreaterThanRule.ValidationParameters.Add("otherpropertyname", otherPropertyName);
yield return dateGreaterThanRule;
}
}
We may apply the attribute to the model as such:
[DateGreaterThan("Birthdate", "You have to be born before you can die")]
public DateTime DeathDate { get; set; }
This causes the Html helper to render the following two attributes on the INPUT
element when calling Html.EditorFor
on a model property that has this attribute:
data-val-dategreaterthan="You have to be born before you can die"
data-val-dategreaterthan-otherpropertyname="Birthdate"
So far so good, but now I have to teach unobtrusive validation what to do with those attributes. First, I have to create a named rule for jquery validation:
// Value is the element to be validated, params is the array of name/value pairs of the parameters extracted from the HTML, element is the HTML element that the validator is attached to
jQuery.validator.addMethod("dategreaterthan", function (value, element, params) {
return Date.parse(value) > Date.parse($(params).val());
});
And then add an unobtrusive adaptor for that rule that maps the attribute to the rule:
jQuery.validator.unobtrusive.adapters.add("dategreaterthan", ["otherpropertyname"], function (options) {
options.rules["dategreaterthan"] = "#" + options.params.otherpropertyname;
options.messages["dategreaterthan"] = options.message;
});
After Ive done all this, I can get this validation rule for "free" anywhere else in my application just by applying that attribute to the model.
To address your question of how to apply rules conditionally based on whether the model is being used in an add or an edit operation: this can probably be done by adding additional logic to your custom attributes and having both the IsValid
method the GetClientValidation
rules method attempt to glean some context from the model using reflection. But honestly, that seems like a mess to me. For this, I'd just rely on server validation and whatever rules you choose to apply using IValidatableObject.Validate()
method.
$('#some-ajax-form').data('validator', null); $.validator.unobtrusive.parse('#some-ajax-form');
– Sawyor