views:

119

answers:

3

I am trying to validate that either of 2 textbox fields in my view have a value supplied. I made this Model Validator:

public class RequireEitherValidator : ModelValidator
{
    private readonly string compareProperty;
    private readonly string errorMessage;

    public RequireEitherValidator(ModelMetadata metadata,
    ControllerContext context, string compareProperty, string errorMessage)
        : base(metadata, context)
    {
        this.compareProperty = compareProperty;
        this.errorMessage = errorMessage;
    }

    public override IEnumerable<ModelValidationResult> Validate(object container)
    {
        if (Metadata.Model == null)
            yield break;
        var propertyInfo = container.GetType().GetProperty(compareProperty);
        if (propertyInfo == null)
            throw new InvalidOperationException("Unknown property:" + compareProperty);

        string valueToCompare = propertyInfo.GetValue(container, null).ToString();

        if (string.IsNullOrEmpty(Metadata.Model.ToString()) && string.IsNullOrEmpty(valueToCompare))
            yield return new ModelValidationResult
            {
                Message = errorMessage
            };
    }
}

This validation logic never gets hit and I think it's because no value gets supplied to the textboxes.

In case you need it, here's the provider and attribute I created along with the attribute usage:

public class MyValidatorProvider : AssociatedValidatorProvider
{
    protected override IEnumerable<ModelValidator> GetValidators(
        ModelMetadata metadata, ControllerContext context,
        IEnumerable<Attribute> attributes)
    {
        foreach (var attrib in attributes.OfType<RequireEitherAttribute>())
            yield return new RequireEitherValidator(metadata, context,
            attrib.CompareProperty, attrib.ErrorMessage);
    }
}

public class RequireEitherAttribute : Attribute
{
    public readonly string CompareProperty;
    public string ErrorMessage { get; set; }

    public RequireEitherAttribute(string compareProperty)
    {
        CompareProperty = compareProperty;
    }
}

public class StudentLogin
{
    [DisplayName("Last Name")]
    [Required(ErrorMessage = "You must supply your last name.")]        
    public string LastName { get; set; }

    [DisplayName("Student ID")]
    [RegularExpression(@"^\d{1,8}$", ErrorMessage = "Invalid Student ID")]
    [RequireEither("SSN", ErrorMessage = "You must supply your student id or social security number.")]        
    public int? StudentId { get; set; }

    [DisplayName("Social Security Number")]
    [RegularExpression(@"^\d{9}|\d{3}-\d{2}-\d{4}$", ErrorMessage = "Invalid Social Security Number")]
    public string SSN { get; set; }
}

My view:

 <%Html.BeginForm(); %>
    <p>
        Please supply the following information to login:</p>
    <ol class="standard">
        <li>
            <p>
                <%=Html.LabelFor(x => x.LastName) %><br />
                <%=Html.TextBoxFor(x => x.LastName)%>
                <%=Html.ValidationMessageFor(x => x.LastName) %></p>
        </li>
        <li>
            <p>
                <%=Html.LabelFor(x => x.StudentId) %><br />
                <%=Html.TextBoxFor(x => x.StudentId) %>
                <%=Html.ValidationMessageFor(x => x.StudentId) %></p>
            <p style="margin-left: 4em;">
                - OR -</p>
            <p>
                <%=Html.LabelFor(x => x.SSN)%><br />
                <%=Html.TextBoxFor(x => x.SSN) %>
                <%=Html.ValidationMessageFor(x => x.SSN) %>
            </p>
        </li>
    </ol>
    <%=Html.SubmitButton("submit", "Login") %>
    <%Html.EndForm(); %>
+1  A: 

One way to approach this is not just create a ValidationAttribute and apply this at the class level.

[RequireEither("StudentId", "SSN")]
public class StudentLogin

The error message will automatically show up in the Validation Summary. The attribute would look something like this (I've drastically simplified the validation logic inside IsValid() by treating everything as strings just for brevity:

public class RequireEither : ValidationAttribute
{
    private string firstProperty;
    private string secondProperty;

    public RequireEither(string firstProperty, string secondProperty)
    {
        this.firstProperty = firstProperty;
        this.secondProperty = secondProperty;
    }

    public override bool IsValid(object value)
    {
        var firstValue = value.GetType().GetProperty(this.firstProperty).GetValue(value, null) as string;
        var secondValue = value.GetType().GetProperty(this.secondProperty).GetValue(value, null) as string;

        if (!string.IsNullOrWhiteSpace(firstValue))
        {
            return true;
        }

        if (!string.IsNullOrWhiteSpace(secondValue))
        {
            return true;
        }
        // neither was supplied so it's not valid
        return false;
    }
}

Note that in this case the object passed to IsValid() is the instance of the class itself rather than the property.

Steve Michelotti
I like it. I'll try it out.
Ronnie Overby
What about client-side validation?
Ronnie Overby
This would definitely need custom client validation (well known example here: http://haacked.com/archive/2009/11/19/aspnetmvc2-custom-validation.aspx). not sure how well it's going to work with class-level validation though.
Steve Michelotti
It doesn't work. I tried that already.
Ronnie Overby
A: 

Used Steve's suggestion with just some slight changes:

public class RequireEitherAttribute : ValidationAttribute
{
    private string firstProperty;
    private string secondProperty;

    public RequireEitherAttribute(string firstProperty, string secondProperty)
    {
        this.firstProperty = firstProperty;
        this.secondProperty = secondProperty;
    }

    public override bool IsValid(object value)
    {
        object firstValue = value.GetType().GetProperty(firstProperty).GetValue(value, null);
        object secondValue = value.GetType().GetProperty(secondProperty).GetValue(value, null);

        return InputSupplied(firstValue) || InputSupplied(secondValue);
    }

    private bool InputSupplied(object obj)
    {
        if (obj == null)
            return false;

        if (obj is string)
        {
            string str = (string)obj;

            if (str.Trim() == string.Empty)
                return false;
        }

        return true;
    }
}

Since this isn't a property level validation, I had to add a validation summary to the view.

I'm still curious about how to hook this in with client side validation.

Ronnie Overby
What if the user types a space? Then the simple null check will not be sufficient.
Steve Michelotti
Thanks. Fixed, I think.
Ronnie Overby
+1  A: 

I like Steve and Ronnie solutions, and although the custom attribute that they created may be used for other classes/property-pairs, I dislike the "magic strings" and reflection for such a simple case, and I usually create a validation which justs suits the scenario in hand.

For example, in this case I would have created something like:

[AttributeUsage(AttributeTargets.Class)]
public class RequireStudentInfoAttribute : ValidationAttribute
{
    public override bool IsValid(object value)
    {
        var student = value as StudentLogin;
        if(student == null)
        {
            return false;
        }

        if (student.StudentId.HasValue || !string.IsNullOrEmpty(student.SSN))
        {
            return true;
        }

        return false;
    }
}

And just apply it on the StudentLogin class like:

[RequireStudentInfo]
public class StudentLogin

Regarding client-side validation, I usually go for http://xval.codeplex.com/, as it integrates really well with Data Annotations

psousa