Why does Validator.TryValidateObject does not validate class if I have validation attribute in a property?
Asked Answered
P

1

11

I created a custom ValidationAttribute that targets a class. This validates correctly whenever I try to call the Validator.TryValidateObject. But when I have other ValidationAttribute in the properties inside my class, the validation results does not contain the result for the class level validation.

Here's a sample code:

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class IsHelloWorldAttribute : ValidationAttribute
{
    public object _typeId = new object();
    public string FirstProperty { get; set; }
    public string SecondProperty { get; set; }

    public IsHelloWorldAttribute(string firstProperty, string secondProperty)
    {
        this.FirstProperty = firstProperty;
        this.SecondProperty = secondProperty; 
    }

    public override bool IsValid(object value)
    {
        PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(value);
        string str1 = properties.Find(FirstProperty, true).GetValue(value) as string;
        string str2 = properties.Find(SecondProperty, true).GetValue(value) as string;

        if (string.Format("{0}{1}", str1,str2) == "HelloWorld")
            return true;
        return false;
    }

    public override object TypeId
    {
        get
        {
            return _typeId;
        }
    }
}

Here's the code of the class that I need to validate

[IsHelloWorld("Name", "Code", ErrorMessage="Is not Hello World")]
public class MyViewModel : BaseViewModel
{
    string name;
    string code;

    [Required]
    public string Name
    {
        get { return model.Name; }
        set
        {
            if (model.Name != value)
            {
                model.Name = value;
                base.RaisePropertyChanged(() => this.Name);
            }
        }
    }        

    public string Code
    {
        get { return code; }
        set
        {
            if (code != value)
            {
                code = value;
                base.RaisePropertyChanged(() => this.Code);
            }
        }
    }
}

Here's how I call the TryValidateObject method:

            var validationContext = new ValidationContext(this, null, null);               
            var validationResults = new List<ValidationResult>();               
            Validator.TryValidateObject(this, validationContext, validationResults, true);

Now, if I have the [Required] attribute in the Name property and I tried to call the Validator.TryValidateObject, the validation result is only one, that's the result for the Required validation. But when I removed the [Required] attribute from the Name and left the IsHellowWorld attribute then called the TryValidateObject, it will give me one result and that's the HellowWorldValidation's result.

What I need to do is to get all the validation on the class level and on the properties level. Can I achieve this without implementing my own TryValidateObject method?

Puree answered 5/2, 2014 at 7:31 Comment(2)
Class-level validation won't fire if properties are invalid - is it your case?Rabinowitz
Alright, I understand. Thanks Lanorkin.Puree
A
11

This is because the check is short circuited if property errors are detected. This makes sense since class level validation can be more expensive potentially involving call backs, other data source calls, etc.

If a property error has been detected, then the logic as it stands simply stands down.

Within System.ComponentModel.DataAnnotations.Validator source code:

public static bool TryValidateObject(object instance, ValidationContext validationContext, 
  ICollection<ValidationResult> validationResults, bool validateAllProperties)
{
  if (instance == null)
    throw new ArgumentNullException("instance");
  if (validationContext != null && instance != validationContext.ObjectInstance)
    throw new ArgumentException(DataAnnotationsResources.
    Validator_InstanceMustMatchValidationContextInstance, "instance");
  bool flag = true;
  bool breakOnFirstError = validationResults == null;
  foreach (Validator.ValidationError validationError in 
    Validator.GetObjectValidationErrors(instance, validationContext, 
    validateAllProperties, breakOnFirstError))
  {
    flag = false;
    if (validationResults != null)
      validationResults.Add(validationError.ValidationResult);
  }
  return flag;
}

Note the call to Validator.GetObjectValidationErrors which in turn is defined as:

private static IEnumerable<Validator.ValidationError> GetObjectValidationErrors(
  object instance, ValidationContext validationContext, 
  bool validateAllProperties, bool breakOnFirstError)
{
  if (instance == null)
    throw new ArgumentNullException("instance");
  if (validationContext == null)
    throw new ArgumentNullException("validationContext");
  List<Validator.ValidationError> list = new List<Validator.ValidationError>();

  //Check for property errors here
  list.AddRange(Validator.GetObjectPropertyValidationErrors(instance, validationContext,
    validateAllProperties, breakOnFirstError));

  // Short circuits here if any found
  if (Enumerable.Any<Validator.ValidationError>(
    (IEnumerable<Validator.ValidationError>) list))
    return (IEnumerable<Validator.ValidationError>) list;

  // Class level validation occurs below this point
  IEnumerable<ValidationAttribute> validationAttributes = 
    Validator._store.GetTypeValidationAttributes(validationContext);
  list.AddRange(Validator.GetValidationErrors(instance, validationContext, 
    validationAttributes, breakOnFirstError));
  if (Enumerable.Any<Validator.ValidationError>(
    (IEnumerable<Validator.ValidationError>) list))
    return (IEnumerable<Validator.ValidationError>) list;
  IValidatableObject validatableObject = instance as IValidatableObject;
  if (validatableObject != null)
  {
    foreach (ValidationResult validationResult in 
      Enumerable.Where<ValidationResult>(validatableObject.Validate(validationContext), 
      (Func<ValidationResult, bool>) (r => r != ValidationResult.Success)))
      list.Add(new Validator.ValidationError((ValidationAttribute) null, instance, 
        validationResult));
  }
  return (IEnumerable<Validator.ValidationError>) list;
}
Addendum answered 11/8, 2015 at 7:50 Comment(1)
For me it was the validateAllProperties flag. By default it's false which means only Required field attributes are checked. Setting this flag to true will cause the validation to also occur on other DataAnnotation attributes.Zebra

© 2022 - 2024 — McMap. All rights reserved.