简体   繁体   中英

How to conditionally validate in Data Annotation in MVC?

I have the following Model:

public class PasswordResetModel {

public bool PasswordCreationStatus { get; set; }

[Display(Name = "AcctNumber", ResourceType = typeof(Resources.Register))]
    [ValidateMustHaveAtLeastOne("AtLeastOneShoudlbeThere", "")]
    //Account number is numeric
    [RegularExpression(@"^[0-9]+$", ErrorMessageResourceType = typeof(Resources.Register),
      ErrorMessageResourceName = "AccountNumberRegularExpr")]
    [ScriptIgnore]
    public int? AccountNumber { get; set; }

    [Display(Name = "BirthDate", ResourceType = typeof(Resources.Register))]
    [ValidateMustHaveAtLeastOne("AtLeastOneShoudlbeThere", "")]
    [ScriptIgnore]
    public string BirthDate { get; set; }

    [Display(Name = "SSN", ResourceType = typeof(Resources.Register))]
    [ValidateMustHaveAtLeastOne("AtLeastOneShoudlbeThere", "")]
    //Accepts only four digits
    [RegularExpression(@"^\d{4}$", ErrorMessageResourceType = typeof(Resources.Register),ErrorMessageResourceName = "SSNRegularExpr")]
    [StringLength(4, ErrorMessageResourceType = typeof(Resources.Register),ErrorMessageResourceName = "SSNRegularExpr")]
    [ScriptIgnore]
    public string SSN { get; set; }
}

I want to make ensure at least either of the 3 (Account Number,SSN and BirthDate) is entered.For which ValidateMustHaveAtleastOne class is witten below.But I want to do the validation only if PasswordCreationStatus is false.

And the ValidateMustHaveAtLeastOne class looks like this:

[AttributeUsage(AttributeTargets.Property)]
  public class ValidateMustHaveAtLeastOne : ValidationAttribute, IClientValidatable {

    #region Construnctor
    public ValidateMustHaveAtLeastOne(string groupName, string validationMessage) {
      GroupName = groupName;
      ValidationMessage = validationMessage;
    }
    #endregion

    #region Properties
    /// <summary>
    /// Name of the group of the properties
    /// </summary>
    public string GroupName { get; private set; }

    /// <summary>
    /// Vaidation message
    /// </summary>
    public string ValidationMessage { get; private set; }

    #endregion

    #region Public overrides
    /// <summary>
    /// Validates the group of properties.
    /// </summary>
    /// <param name="value">The value to validate.</param>
    /// <param name="context">The context information about the validation operation.</param>
    /// <returns>An instance of the ValidationResult class.</returns>
    protected override ValidationResult IsValid(object value, ValidationContext context) {
      foreach (var property in GetGroupProperties(context.ObjectType)) {
        var propertyValue = property.GetValue(context.ObjectInstance, null);
        if (propertyValue != null) {
          return null;
        }
      }
      return new ValidationResult(ValidationMessage);
    }
    #endregion

    #region Implementation of IClientValidateable
    /// <summary>
    /// To enable client side implementation of same validtion rule.
    /// </summary>
    /// <param name="metadata">The model metadata.</param>
    /// <param name="context">The controller context.</param>
    /// <returns>The client validation rules for this validator.</returns>

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context) {
      var groupProperties = GetGroupProperties(metadata.ContainerType);
      List<string> propertyNames = new List<string>();
      foreach (var property in groupProperties) {
        propertyNames.Add(property.Name);
      }
      var rule = new ModelClientValidationRule {
        ErrorMessage = this.ValidationMessage
      };
      rule.ValidationType = string.Format("group", GroupName.ToLower());
      rule.ValidationParameters["propertynames"] = string.Join(",", propertyNames);
      yield return rule; // yield key word is used as return type is  declared as IEnumerable<ModelClientValidationRule>
    }
    #endregion

        /// Returns the group of properties that implements this attribute
    /// </summary>
    /// <param name="type">Type of the Model</param>
    /// <returns>List of properties</returns>
    private IEnumerable<PropertyInfo> GetGroupProperties(Type type) {
      var propertyInfo = new List<PropertyInfo>();
      foreach (PropertyInfo property in type.GetProperties()) {
        if (property.GetCustomAttributes(typeof(ValidateMustHaveAtLeastOne), false).GetLength(0) > 0) {
          propertyInfo.Add(property);
        }
      }
      return propertyInfo;
    }


  }

I think its possible with minimum enhancement. Any help will be highly appreciated. Thanks in advance.

Updated your Validation to validate if the dependent property is matched with target value then do the validation.

[AttributeUsage(AttributeTargets.Property)]
public class ValidateMustHaveAtLeastOne : ValidationAttribute, IClientValidatable {

#region Construnctor
public ValidateMustHaveAtLeastOne(string groupName, string validationMessage, string dependentProperty, object targetValue) {
  GroupName = groupName;
  ValidationMessage = validationMessage;

  DependentProperty = dependentProperty;
  TargetValue = targetValue;
}
#endregion

#region Properties
/// <summary>
/// Name of the group of the properties
/// </summary>
public string GroupName { get; private set; }

/// <summary>
/// Vaidation message
/// </summary>
public string ValidationMessage { get; private set; }

/// <summary>
/// Gets or sets the dependent property.
/// </summary>
/// <value>
/// The dependent property.
/// </value>
public string DependentProperty { get; set; }

/// <summary>
/// Gets or sets the target value.
/// </summary>
/// <value>
/// The target value.
/// </value>
public object TargetValue { get; set; }

#endregion

#region Public overrides
/// <summary>
/// Validates the group of properties.
/// </summary>
/// <param name="value">The value to validate.</param>
/// <param name="context">The context information about the validation operation.</param>
/// <returns>An instance of the ValidationResult class.</returns>
protected override ValidationResult IsValid(object value, ValidationContext context) {
    var containerType = validationContext.ObjectInstance.GetType();
    var field = containerType.GetProperty(this.DependentProperty);
    if (field != null)
    {
        // get the value of the dependent property
        var dependentvalue = field.GetValue(validationContext.ObjectInstance, null);

        // compare the value against the target value
        if ((dependentvalue == null && this.TargetValue == null)
            || (dependentvalue != null && dependentvalue.Equals(this.TargetValue)))
        {
            foreach (var property in GetGroupProperties(context.ObjectType)) {
            var propertyValue = property.GetValue(context.ObjectInstance, null);
            if (propertyValue != null) {
              return null;
                }
          }
          return new ValidationResult(ValidationMessage);
        }
    }

  return ValidationResult.Success;
}
#endregion

#region Implementation of IClientValidateable
/// <summary>
/// To enable client side implementation of same validtion rule.
/// </summary>
/// <param name="metadata">The model metadata.</param>
/// <param name="context">The controller context.</param>
/// <returns>The client validation rules for this validator.</returns>

public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context) {
  var groupProperties = GetGroupProperties(metadata.ContainerType);
  List<string> propertyNames = new List<string>();
  foreach (var property in groupProperties) {
    propertyNames.Add(property.Name);
  }
  var rule = new ModelClientValidationRule {
    ErrorMessage = this.ValidationMessage
  };
  rule.ValidationType = string.Format("group", GroupName.ToLower());
  rule.ValidationParameters["propertynames"] = string.Join(",", propertyNames);

  string depProp = this.BuildDependentPropertyId(metadata, context as ViewContext);
  // find the value on the control we depend on; if it's a bool, format it javascript style
  string targetValue = (this.TargetValue ?? string.Empty).ToString();
  if (this.TargetValue.GetType() == typeof(bool))
  {
     targetValue = targetValue.ToLower();
  }

  rule.ValidationParameters["dependentproperty"] = depProp;
  rule.ValidationParameters["targetvalue"] = targetValue;
  yield return rule; // yield key word is used as return type is  declared as IEnumerable<ModelClientValidationRule>
}
#endregion

    /// Returns the group of properties that implements this attribute
/// </summary>
/// <param name="type">Type of the Model</param>
/// <returns>List of properties</returns>
private IEnumerable<PropertyInfo> GetGroupProperties(Type type) {
  var propertyInfo = new List<PropertyInfo>();
  foreach (PropertyInfo property in type.GetProperties()) {
    if (property.GetCustomAttributes(typeof(ValidateMustHaveAtLeastOne), false).GetLength(0) > 0) {
      propertyInfo.Add(property);
    }
  }
  return propertyInfo;
}

 private string BuildDependentPropertyId(ModelMetadata metadata, ViewContext viewContext)
{
    string depProp = viewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(this.DependentProperty);

    // This will have the name of the current field appended to the beginning, because the TemplateInfo's context has had this fieldname appended to it.
    var thisField = metadata.PropertyName + "_";
    if (depProp.StartsWith(thisField, StringComparison.OrdinalIgnoreCase))
    {
        depProp = depProp.Substring(thisField.Length);
    }

    return depProp;
} 

}

Usage

 [ValidateMustHaveAtLeastOne("AtLeastOneShoudlbeThere", "", "PasswordCreationStatus", false)]

You may use an already done (tested) library for validation with some conditions.

There is a lot of libraries for this, but maybe the most famous one is foolproof

in the above link you could see the usage and documentation for this.

I think the following validation attribute is suitable for your case.

[RequiredIfTrue("PasswordCreationStatus")]

You can add a calculated property that is going to be valid only when at least one of the properties is provided:

public class PasswordResetModel 
{

    public bool PasswordCreationStatus { get; set; }

    [Display(Name = "AcctNumber", ResourceType = typeof(Resources.Register))]        
    //Account number is numeric
    [RegularExpression(@"^[0-9]+$", ErrorMessageResourceType = typeof(Resources.Register),
      ErrorMessageResourceName = "AccountNumberRegularExpr")]
    [ScriptIgnore]
    public int? AccountNumber { get; set; }

    [Display(Name = "BirthDate", ResourceType = typeof(Resources.Register))]        
    [ScriptIgnore]
    public string BirthDate { get; set; }

    [Display(Name = "SSN", ResourceType = typeof(Resources.Register))]      
    //Accepts only four digits
    [RegularExpression(@"^\d{4}$", ErrorMessageResourceType = typeof(Resources.Register),ErrorMessageResourceName = "SSNRegularExpr")]
    [StringLength(4, ErrorMessageResourceType = typeof(Resources.Register),ErrorMessageResourceName = "SSNRegularExpr")]
    [ScriptIgnore]
    public string SSN { get; set; }

    [Required(ErrorMessage = "Must Have At Least One of the properties")]
    public string MustHaveAtLeastOneIsValid {
       get{
           return  this.SSN != null || this.BirthDate != null || this.AccountNumber.HasValue ? "valid": null;
       }
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM