views:

48

answers:

3

Does anybody know of a good algorithm to mutually exclusively check two properties using a ModelValidator?

Something like:

    [EitherPropertyRequired("BuildingNumber","BuildingName"]
    public class Address{
       public int BuildingNumber { get; set; }
       public string BuildingName { get; set; }
   }
+1  A: 
[AttributeUsage(AttributeTargets.Class)]
public class EitherPropertyRequiredAttribute : ValidationAttribute
{
    public override bool IsValid(object value)
    {
        // value will be the model

        Address address = (Address)value;

        // TODO: Check the properties of address here and return true or false

        return true;
    }
}

You could make this more generic by avoiding it casting to Address and using attribute properties and reflection.

Darin Dimitrov
Darin, how do I make the above solution work with client side validation?
byte
@byte, you need to [implement a custom DataAnnotationsModelValidator](http://haacked.com/archive/2009/11/19/aspnetmvc2-custom-validation.aspx)
Darin Dimitrov
+1  A: 

I ended up creating an attribute and manually checking it with a custom ModelValidator. This custom model validator is checked using an AssociatedValidatorProvider which is registered in Application_Start().

protected void Application_Start()
{
    ModelValidatorProviders.Providers.Add(new ZipValidationProvider());
}

public class ZipValidationProvider:AssociatedValidatorProvider
{
    protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
    {
        foreach (var attribute in attributes.OfType<EitherPropertyRequiredAttribute>())
        {
            yield return new EitherPropertyRequiredValidator(metadata,
                context, attribute.FirstProperty, attribute.SecondProperty, attribute.Message);
        }
    }
}

[AttributeUsage(AttributeTargets.Class)]
public class EitherPropertyRequiredAttribute : Attribute
{
    public readonly string FirstProperty;
    public readonly string SecondProperty;
    public readonly string Message;

    public EitherPropertyRequiredAttribute(string firstProperty, string secondProperty, 
        string message)
    {
        FirstProperty = firstProperty;
        SecondProperty = secondProperty;
        Message = message;
    }
}

public class EitherPropertyRequiredValidator:ModelValidator
{
    private readonly string firstProperty;
    private readonly string secondProperty;
    private readonly string message;

    public EitherPropertyRequiredValidator(ModelMetadata metadata,
                                    ControllerContext context,
        string firstProperty, 
        string secondProperty,
        string message)
        :base(metadata,context)
    {
        this.firstProperty = firstProperty;
        this.secondProperty = secondProperty;
        this.message = message;
    }

    private PropertyInfo GetPropertyInfoRecursive(Type type, string property)
    {
        var prop = type.GetProperty(property);
        if (prop != null) return prop;

        foreach (var p in type.GetProperties())
        {
            if (p.PropertyType.Assembly == typeof (object).Assembly)
                continue;

            return GetPropertyInfoRecursive(p.PropertyType, property);
        }

        return null;
    }

    private object GetPropertyValueRecursive(object obj, PropertyInfo propertyInfo)
    {
        Type objectType = obj.GetType();

       if(objectType.GetProperty(propertyInfo.Name) != null)
            return propertyInfo.GetValue(obj, null);

        foreach (var p in objectType.GetProperties())
        {
            if (p.PropertyType.Assembly == typeof(object).Assembly)
                continue;

            var o = p.GetValue(obj,null);
            return GetPropertyValueRecursive(o, propertyInfo);
        }

        return null;
    }

    public override IEnumerable<ModelValidationResult> Validate(object container)
    {
        if (Metadata.Model == null)
            yield break;

        var firstPropertyInfo = GetPropertyInfoRecursive(Metadata.Model.GetType(),firstProperty);
        if(firstPropertyInfo == null)
            throw new InvalidOperationException("Unknown property:" + firstProperty);

        var secondPropertyInfo = GetPropertyInfoRecursive(Metadata.Model.GetType(),secondProperty);
        if(secondPropertyInfo == null)
            throw new InvalidOperationException("Unknown property:" + secondProperty);

        var firstPropertyValue = GetPropertyValueRecursive(Metadata.Model, firstPropertyInfo);
        var secondPropertyValue = GetPropertyValueRecursive(Metadata.Model, secondPropertyInfo);

        bool firstPropertyIsEmpty = firstPropertyValue == null ||
                                    firstPropertyValue.ToString().Length == 0;

        bool secondPropertyIsEmpty = secondPropertyValue == null ||
                                     secondPropertyValue.ToString().Length == 0;

        if (firstPropertyIsEmpty && secondPropertyIsEmpty)
        {
                yield return new ModelValidationResult
                {
                    MemberName = firstProperty,
                    Message = message
                };
        }
    }
}
David Neale
David, I have implemented a couple of validators similar to one that you posted above. One of the problem that I encountered was with nested classes in ViewModels when doing client side input validation.
byte
Do you mean if you used the above validation attribute on an actual object representing a nested class or when looping through the properties of the view model?
David Neale