开发者

Compare Dates DataAnnotations Validation asp.net mvc

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 : ValidationAttribute 
 {
    public String StartDate { get; set; }
    public String EndDate { get; set; }

    //Constructor to take in the property names that are supposed to be checked
    public DateCompare(String startDate, String endDate)
    {
        StartDate = startDate;
        End开发者_StackOverflow社区Date = endDate;
    }

    public override bool IsValid(object value)
    {
        var str = value.ToString();
        if (string.IsNullOrEmpty(str))
            return true;

        DateTime theEndDate = DateTime.ParseExact(EndDate, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
        DateTime theStartDate = DateTime.ParseExact(StartDate, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture).AddMonths(3);
        return (DateTime.Compare(theStartDate, theEndDate) > 0);
    }
}

and I would like to implement this into my validation

[DateCompare("StartDate", "EndDate", ErrorMessage = "The Deal can only be 3 months long!")]

I know I get an error here... but how can I do this sort of business rule validation in asp.net mvc


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:

  1. 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

  2. Add the attributes to the field as folows:
    [DateCompareValidation(DateCompareValidationAttribute.CompareType.GreatherThenOrEqualTo, "This Date must be on or after another date", compareWith: "AnotherDate")]

  3. 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.

  4. 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.


  1. Dates

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);
        }
}


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));


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

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜