mvc4 data annotation compare two dates
Borrowing heavily from the responses from Alexander Gore and Jaime Marín in a related StackOverflow question1, I created five classes that enable comparing two fields in the same model using GT, GE, EQ, LE, and LT operators, provided they implement IComparable. So it can be used for pairs of dates, times, integers, and strings, for example.
It would be nice to merge these all into one class and take the operator as an argument, but I don't know how. I left the three exceptions as is because, if thrown, they really represent a form design problem, not a user input problem.
You just use it in your model like this, and the file with the five classes follows:
[Required(ErrorMessage = "Start date is required.")]
public DateTime CalendarStartDate { get; set; }
[Required(ErrorMessage = "End date is required.")]
[AttributeGreaterThanOrEqual("CalendarStartDate",
ErrorMessage = "The Calendar end date must be on or after the Calendar start date.")]
public DateTime CalendarEndDate { get; set; }
using System;
using System.ComponentModel.DataAnnotations;
//Contains GT, GE, EQ, LE, and LT validations for types that implement IComparable interface.
//https://stackoverflow.com/questions/41900485/custom-validation-attributes-comparing-two-properties-in-the-same-model
namespace DateComparisons.Validations
{
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
public class AttributeGreaterThan : ValidationAttribute
{
private readonly string _comparisonProperty;
public AttributeGreaterThan(string comparisonProperty){_comparisonProperty = comparisonProperty;}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if(value==null) return new ValidationResult("Invalid entry");
ErrorMessage = ErrorMessageString;
if (value.GetType() == typeof(IComparable)) throw new ArgumentException("value has not implemented IComparable interface");
var currentValue = (IComparable)value;
var property = validationContext.ObjectType.GetProperty(_comparisonProperty);
if (property == null) throw new ArgumentException("Comparison property with this name not found");
var comparisonValue = property.GetValue(validationContext.ObjectInstance);
if(!ReferenceEquals(value.GetType(), comparisonValue.GetType()))
throw new ArgumentException("The types of the fields to compare are not the same.");
return currentValue.CompareTo((IComparable)comparisonValue) > 0 ? ValidationResult.Success : new ValidationResult(ErrorMessage);
}
}
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
public class AttributeGreaterThanOrEqual : ValidationAttribute
{
private readonly string _comparisonProperty;
public AttributeGreaterThanOrEqual(string comparisonProperty) { _comparisonProperty = comparisonProperty; }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value == null) return new ValidationResult("Invalid entry");
ErrorMessage = ErrorMessageString;
if (value.GetType() == typeof(IComparable)) throw new ArgumentException("value has not implemented IComparable interface");
var currentValue = (IComparable)value;
var property = validationContext.ObjectType.GetProperty(_comparisonProperty);
if (property == null) throw new ArgumentException("Comparison property with this name not found");
var comparisonValue = property.GetValue(validationContext.ObjectInstance);
if (!ReferenceEquals(value.GetType(), comparisonValue.GetType()))
throw new ArgumentException("The types of the fields to compare are not the same.");
return currentValue.CompareTo((IComparable)comparisonValue) >= 0 ? ValidationResult.Success : new ValidationResult(ErrorMessage);
}
}
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
public class AttributeEqual : ValidationAttribute
{
private readonly string _comparisonProperty;
public AttributeEqual(string comparisonProperty) { _comparisonProperty = comparisonProperty; }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value == null) return new ValidationResult("Invalid entry");
ErrorMessage = ErrorMessageString;
if (value.GetType() == typeof(IComparable)) throw new ArgumentException("value has not implemented IComparable interface");
var currentValue = (IComparable)value;
var property = validationContext.ObjectType.GetProperty(_comparisonProperty);
if (property == null) throw new ArgumentException("Comparison property with this name not found");
var comparisonValue = property.GetValue(validationContext.ObjectInstance);
if (!ReferenceEquals(value.GetType(), comparisonValue.GetType()))
throw new ArgumentException("The types of the fields to compare are not the same.");
return currentValue.CompareTo((IComparable)comparisonValue) == 0 ? ValidationResult.Success : new ValidationResult(ErrorMessage);
}
}
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
public class AttributeLessThanOrEqual : ValidationAttribute
{
private readonly string _comparisonProperty;
public AttributeLessThanOrEqual(string comparisonProperty) { _comparisonProperty = comparisonProperty; }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value == null) return new ValidationResult("Invalid entry");
ErrorMessage = ErrorMessageString;
if (value.GetType() == typeof(IComparable)) throw new ArgumentException("value has not implemented IComparable interface");
var currentValue = (IComparable)value;
var property = validationContext.ObjectType.GetProperty(_comparisonProperty);
if (property == null) throw new ArgumentException("Comparison property with this name not found");
var comparisonValue = property.GetValue(validationContext.ObjectInstance);
if (!ReferenceEquals(value.GetType(), comparisonValue.GetType()))
throw new ArgumentException("The types of the fields to compare are not the same.");
return currentValue.CompareTo((IComparable)comparisonValue) <= 0 ? ValidationResult.Success : new ValidationResult(ErrorMessage);
}
}
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
public class AttributeLessThan : ValidationAttribute
{
private readonly string _comparisonProperty;
public AttributeLessThan(string comparisonProperty) { _comparisonProperty = comparisonProperty; }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value == null) return new ValidationResult("Invalid entry");
ErrorMessage = ErrorMessageString;
if (value.GetType() == typeof(IComparable)) throw new ArgumentException("value has not implemented IComparable interface");
var currentValue = (IComparable)value;
var property = validationContext.ObjectType.GetProperty(_comparisonProperty);
if (property == null) throw new ArgumentException("Comparison property with this name not found");
var comparisonValue = property.GetValue(validationContext.ObjectInstance);
if (!ReferenceEquals(value.GetType(), comparisonValue.GetType()))
throw new ArgumentException("The types of the fields to compare are not the same.");
return currentValue.CompareTo((IComparable)comparisonValue) < 0 ? ValidationResult.Success : new ValidationResult(ErrorMessage);
}
}
}
Take a look at Fluent Validation or MVC Foolproof Validation: those can help you a lot.
With Foolproof for example there is a [GreaterThan("StartDate")]
annotation than you can use on your date property.
Or if you don't want to use other libraries, you can implement your own custom validation by implementing IValidatableObject
on your model:
public class ViewModel: IValidatableObject
{
[Required]
public DateTime StartDate { get; set; }
[Required]
public DateTime EndDate { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (EndDate < StartDate)
{
yield return new ValidationResult(
errorMessage: "EndDate must be greater than StartDate",
memberNames: new[] { "EndDate" }
);
}
}
}
IValidatableObject interface provides a way for an object to be validated which implements IValidatableObject.Validate(ValidationContext validationContext) method. This method always return IEnumerable object. That's why you should create list of ValidationResult objects and errors are added to this and return. Empty list means to validate your conditions. That is as follows in mvc 4...
public class LibProject : IValidatableObject
{
[Required(ErrorMessage="Project name required")]
public string Project_name { get; set; }
[Required(ErrorMessage = "Job no required")]
public string Job_no { get; set; }
public string Client { get; set; }
[DataType(DataType.Date,ErrorMessage="Invalid Date")]
public DateTime ExpireDate { get; set; }
IEnumerable<ValidationResult> IValidatableObject.Validate(ValidationContext validationContext)
{
List < ValidationResult > res =new List<ValidationResult>();
if (ExpireDate < DateTime.Today)
{
ValidationResult mss=new ValidationResult("Expire date must be greater than or equal to Today");
res.Add(mss);
}
return res;
}
}