Lets say I have a StartDate and an EndDate and I wnt to check if the EndDate is not more then 3 months apart from the Start Date
public class DateCompare : V
The Attribute
public class CompareValidatorAttribute : ValidationAttribute, IInstanceValidationAttribute
{
public CompareValidatorAttribute(string prefix, string propertyName) {
Check.CheckNullArgument("propertyName", propertyName);
this.propertyName = propertyName;
this.prefix = prefix;
}
string propertyName, prefix;
public string PropertyName
{
get { return propertyName; }
}
public string Prefix
{
get { return prefix; }
}
#region IInstanceValidationAttribute Members
public bool IsValid(object instance, object value)
{
var property = instance.GetType().GetProperty(propertyName);
var targetValue = property.GetValue(instance, null);
if ((targetValue == null && value == null) || (targetValue != null && targetValue.Equals(value)))
return true;
return false;
}
#endregion
public override bool IsValid(object value)
{
throw new NotImplementedException();
}
}
The Interface
public interface IInstanceValidationAttribute
{
bool IsValid(object instance, object value);
}
The Validator
public class CompareValidator : DataAnnotationsModelValidator<CompareValidatorAttribute>
{
public CompareValidator(ModelMetadata metadata, ControllerContext context, CompareValidatorAttribute attribute)
: base(metadata, context, attribute)
{
}
public override IEnumerable<ModelValidationResult> Validate(object container)
{
if (!(Attribute as IInstanceValidationAttribute).IsValid(container, Metadata.Model))
yield return (new ModelValidationResult
{
MemberName = Metadata.PropertyName,
Message = Attribute.ErrorMessage
});
}
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
var rule = new ModelClientValidationRule() { ErrorMessage = Attribute.ErrorMessage, ValidationType = "equalTo" };
rule.ValidationParameters.Add("equalTo", "#" + (!string.IsNullOrEmpty(Attribute.Prefix) ? Attribute.Prefix + "_" : string.Empty)+ Attribute.PropertyName);
return new[] { rule };
}
}
Register it:
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(CompareValidatorAttribute), typeof(CompareValidator));
It's a late answer but I wanted to share it for others outhere. Here's how I've done it so that everything is validated using unobtrusive client validation:
Create an attribute class:
public class DateCompareValidationAttribute : ValidationAttribute, IClientValidatable
{
public enum CompareType
{
GreatherThen,
GreatherThenOrEqualTo,
EqualTo,
LessThenOrEqualTo,
LessThen
}
private CompareType _compareType;
private DateTime _fromDate;
private DateTime _toDate;
private string _propertyNameToCompare;
public DateCompareValidationAttribute(CompareType compareType, string message, string compareWith = "")
{
_compareType = compareType;
_propertyNameToCompare = compareWith;
ErrorMessage = message;
}
#region IClientValidatable Members
/// <summary>
/// Generates client validation rules
/// </summary>
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
ValidateAndGetCompareToProperty(metadata.ContainerType);
var rule = new ModelClientValidationRule();
rule.ErrorMessage = ErrorMessage;
rule.ValidationParameters.Add("comparetodate", _propertyNameToCompare);
rule.ValidationParameters.Add("comparetype", _compareType);
rule.ValidationType = "compare";
yield return rule;
}
#endregion
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
// Have to override IsValid method. If you have any logic for server site validation, put it here.
return ValidationResult.Success;
}
/// <summary>
/// verifies that the compare-to property exists and of the right types and returnes this property
/// </summary>
/// <param name="containerType">Type of the container object</param>
/// <returns></returns>
private PropertyInfo ValidateAndGetCompareToProperty(Type containerType)
{
var compareToProperty = containerType.GetProperty(_propertyNameToCompare);
if (compareToProperty == null)
{
string msg = string.Format("Invalid design time usage of {0}. Property {1} is not found in the {2}", this.GetType().FullName, _propertyNameToCompare, containerType.FullName);
throw new ArgumentException(msg);
}
if (compareToProperty.PropertyType != typeof(DateTime) && compareToProperty.PropertyType != typeof(DateTime?))
{
string msg = string.Format("Invalid design time usage of {0}. The type of property {1} of the {2} is not DateType", this.GetType().FullName, _propertyNameToCompare, containerType.FullName);
throw new ArgumentException(msg);
}
return compareToProperty;
}
}
Note: if you want to validate length of time, add another parameter to the constractor and change enumerator for this specific type of comparsion
Add the attributes to the field as folows: [DateCompareValidation(DateCompareValidationAttribute.CompareType.GreatherThenOrEqualTo, "This Date must be on or after another date", compareWith: "AnotherDate")]
Take a note how your generated html is changed. It should include your validation message, field name for the compare-to date, etc. The generated parms would start with "data-val-compare". You defined this "compare" when you set ValidationType="compare" in GetClientValidationRules method.
Now you need matching javascript code: to add an validation adapter and validation method. I used anonimous method here, but you don't have to. I recommend placing this code in a separate javascript file so that this file together with your attribute class become like a control and could be used anywhere.
$.validator.unobtrusive.adapters.add( 'compare', ['comparetodate', 'comparetype'], function (options) { options.rules['compare'] = options.params; options.messages['compare'] = options.message; } );
$.validator.addMethod("compare", function (value, element, parameters) {
// value is the actuall value entered
// element is the field itself, that contain the the value (in case the value is not enough)
var errMsg = "";
// validate parameters to make sure everyting the usage is right
if (parameters.comparetodate == undefined) {
errMsg = "Compare validation cannot be executed: comparetodate parameter not found";
alert(errMsg);
return false;
}
if (parameters.comparetype == undefined) {
errMsg = "Compare validation cannot be executed: comparetype parameter not found";
alert(errMsg);
return false;
}
var compareToDateElement = $('#' + parameters.comparetodate).get();
if (compareToDateElement.length == 0) {
errMsg = "Compare validation cannot be executed: Element to compare " + parameters.comparetodate + " not found";
alert(errMsg);
return false;
}
if (compareToDateElement.length > 1) {
errMsg = "Compare validation cannot be executed: more then one Element to compare with id " + parameters.comparetodate + " found";
alert(errMsg);
return false;
}
//debugger;
if (value && !isNaN(Date.parse(value))) {
//validate only the value contains a valid date. For invalid dates and blanks non-custom validation should be used
//get date to compare
var compareToDateValue = $('#' + parameters.comparetodate).val();
if (compareToDateValue && !isNaN(Date.parse(compareToDateValue))) {
//if date to compare is not a valid date, don't validate this
switch (parameters.comparetype) {
case 'GreatherThen':
return new Date(value) > new Date(compareToDateValue);
case 'GreatherThenOrEqualTo':
return new Date(value) >= new Date(compareToDateValue);
case 'EqualTo':
return new Date(value) == new Date(compareToDateValue);
case 'LessThenOrEqualTo':
return new Date(value) <= new Date(compareToDateValue);
case 'LessThen':
return new Date(value) < new Date(compareToDateValue);
default:
{
errMsg = "Compare validation cannot be executed: '" + parameters.comparetype + "' is invalid for comparetype parameter";
alert(errMsg);
return false;
}
}
return true;
}
else
return true;
}
else
return true;
});
This takes care only of client-side unobtrusive validation. If you need server side, you'd have to have some logic in the override of isValid method. Also, you can use Reflection to generate error message using display attributes, etc and make the message argument optional.
Entity:
[MetadataType(typeof(MyEntity_Validation))]
public partial class MyEntity
{
}
public class MyEntity_Validation
{
[Required(ErrorMessage="'Date from' is required")]
public DateTime DateFrom { get; set; }
[CompareDatesValidatorAttribute("DateFrom")]
public DateTime DateTo { get; set; }
}
Attribute:
public sealed class CompareDatesValidatorAttribute : ValidationAttribute
{
private string _dateToCompare;
private const string _errorMessage = "'{0}' must be greater or equal'{1}'";
public CompareDatesValidatorAttribute(string dateToCompare)
: base(_errorMessage)
{
_dateToCompare = dateToCompare;
}
public override string FormatErrorMessage(string name)
{
return string.Format(_errorMessage, name, _dateToCompare);
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var dateToCompare = validationContext.ObjectType.GetProperty(_dateToCompare);
var dateToCompareValue = dateToCompare.GetValue(validationContext.ObjectInstance, null);
if (dateToCompareValue != null && value != null && (DateTime)value < (DateTime)dateToCompareValue)
{
return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
}
return null;
}
}
2. Password
Entity:
public string Password { get; set; }
[Compare("Password", ErrorMessage = "ConfirmPassword must match Password")]
public string ConfirmPassword { get; set; }
I hope it helps
I have only figured out how to do this at the class level but not the at property level. If you create an MVC application, the Account model shows the approach illustrated below.
Class:
[PropertiesMustMatch("Password",
"ConfirmPassword", ErrorMessage =
"Password and confirmation password
do not match.")]
public class RegisterModel
{
[Required(ErrorMessage = "Required")]
[DataType(DataType.EmailAddress)]
[DisplayName("Your Email")]
public string Email { get; set; }
[Required(ErrorMessage = "Required")]
[ValidatePasswordLength]
[DataType(DataType.Password)]
[DisplayName("Password")]
public string Password { get; set; }
[Required(ErrorMessage = "Required")]
[DataType(DataType.Password)]
[DisplayName("Re-enter password")]
public string ConfirmPassword { get; set; }
}
Validation Method:
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public sealed class PropertiesMustMatchAttribute : ValidationAttribute
{
private const string _defaultErrorMessage = "'{0}' and '{1}' do not match.";
private readonly object _typeId = new object();
public PropertiesMustMatchAttribute(string originalProperty, string confirmProperty)
: base(_defaultErrorMessage)
{
OriginalProperty = originalProperty;
ConfirmProperty = confirmProperty;
}
public string ConfirmProperty
{
get;
private set;
}
public string OriginalProperty
{
get;
private set;
}
public override object TypeId
{
get
{
return _typeId;
}
}
public override string FormatErrorMessage(string name)
{
return String.Format(CultureInfo.CurrentUICulture, ErrorMessageString,
OriginalProperty, ConfirmProperty);
}
public override bool IsValid(object value)
{
PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(value);
object originalValue = properties.Find(OriginalProperty, true /* ignoreCase */).GetValue(value);
object confirmValue = properties.Find(ConfirmProperty, true /* ignoreCase */).GetValue(value);
return Object.Equals(originalValue, confirmValue);
}
}
Thanks for the info. I was left scratching my head when I wanted to bind the validation message to a property. If you switch the line
[AttributeUsage(AttributeTargets.Class)]
to...
[AttributeUsage(AttributeTargets.Property)]
you can move the compare above a specific property. Thanks for the info! Much help since my client is still running on 3.5 sp1. sad face