1

I have an address class like so:

public class CustomerAddress
{
    [Required]
    public string Line1 { get; set; }

    public string Line2 { get; set; }

    [Required]
    public string Town { get; set; }

    [Required]
    public string Postcode { get; set; }
}

And I have a view model like so:

public class CheckoutViewModel
{
    [Required]
    public string Name { get; set; }

    //... etc

    public bool DeliverySameAsBilling { get; set; }

    public CustomerAddress BillingAddress { get; set; }

    public CustomerAddress DeliveryAddress { get; set; }
 }

I only want the delivery address to validate when DeliverySameAsBilling is false, and I can see from this that IValidatableObject might be the way to go.

That example imposes more strict criteria on the model than the attributes do though; in my case I want to optionally ignore the [Required] attributes in the CustomerAddress class. How do I do this? How would I wire up the appropriate client side validation?

Alternatively I could use a custom attribute like this on each of BillingAddress and DeliveryAddress and then maybe the client side validation would be more easily handled; however I still don't know how to effectively "cancel" the validation for the property if DeliverySameAsBilling is true.

Which is best?

Community
  • 1
  • 1
sjmeverett
  • 1,277
  • 1
  • 11
  • 23

1 Answers1

3

Take a look at this.

http://blogs.msdn.com/b/simonince/archive/2010/06/04/conditional-validation-in-mvc.aspx

Create a RequiredIfAttribute

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;

namespace ConditionalValidation.Validation
{
    public class RequiredIfAttribute : ValidationAttribute
    {
        // Note: we don't inherit from RequiredAttribute as some elements of the MVC
        // framework specifically look for it and choose not to add a RequiredValidator
        // for non-nullable fields if one is found. This would be invalid if we inherited
        // from it as obviously our RequiredIf only applies if a condition is satisfied.
        // Therefore we're using a private instance of one just so we can reuse the IsValid
        // logic, and don't need to rewrite it.
        private RequiredAttribute innerAttribute = new RequiredAttribute();
        public string DependentProperty { get; set; }
        public object TargetValue { get; set; }

        public RequiredIfAttribute(string dependentProperty, object targetValue)
        {
            this.DependentProperty = dependentProperty;
            this.TargetValue = targetValue;
        }

        public override bool IsValid(object value)
        {
            return innerAttribute.IsValid(value);
        }
    }
}

Then create a RequiredIfValidator

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace ConditionalValidation.Validation
{
    public class RequiredIfValidator : DataAnnotationsModelValidator<RequiredIfAttribute>
    {
        public RequiredIfValidator(ModelMetadata metadata, ControllerContext context, RequiredIfAttribute attribute)
            : base(metadata, context, attribute)
        {
        }

        public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
        {
            // no client validation - I might well blog about this soon!
            return base.GetClientValidationRules();
        }

        public override IEnumerable<ModelValidationResult> Validate(object container)
        {
            // get a reference to the property this validation depends upon
            var field = Metadata.ContainerType.GetProperty(Attribute.DependentProperty);

            if (field != null)
            {
                // get the value of the dependent property
                var value = field.GetValue(container, null);

                // compare the value against the target value
                if ((value == null && Attribute.TargetValue == null) ||
                    (value.Equals(Attribute.TargetValue)))
                {
                    // match => means we should try validating this field
                    if (!Attribute.IsValid(Metadata.Model))
                        // validation failed - return an error
                        yield return new ModelValidationResult { Message = ErrorMessage };
                }
            }
        }
    }
}

And use it in the model

namespace ConditionalValidation.Models
{
    public class Person
    {
        [HiddenInput(DisplayValue = false)]
        public int Id { get; set; }

        [StringLength(10)]
        [RequiredIf("City", null)]
        public string Name { get; set; }

        [RequiredIf("IsUKResident", true, ErrorMessage = "You must specify the City if UK resident")]
        public string City { get; set; }

        [RequiredIf("IsUKResident", false, ErrorMessage = "You must specify the country if not UK resident")]
        [RegularExpression("^(\\w)+$", ErrorMessage = "Only letters are permitted in the Country field")]
        public string Country { get; set; }

        // this field is last in the class - therefore any RequiredAttribute validation that occurs
        // on fields before it don't guarantee this field's value is correctly set - see my blog post 
        // if that doesn't make sense!
        [DisplayName("UK Resident")]
        public bool IsUKResident { get; set; }
    }
}
Marco Staffoli
  • 2,475
  • 2
  • 27
  • 29
  • Thanks for such a detailed answer. You'll notice that BillingAddress and DeliveryAddress don't actually have required attributes, but the model validation will fail anyway because the sub properties have required attributes. Without putting RequiredIf attributes into the CustomerAddress class, how do I use your code to selectively *ignore* the address field on the view model level (rather than selectively require)? Thanks! – sjmeverett Oct 26 '11 at 16:15
  • for Billing+Delivery address what I do is disable the fields in JS - that way they're not validated client side. Then I remove the modelstate serverside using this http://stackoverflow.com/questions/14008561/is-there-a-strongly-named-way-to-remove-modelstate-errors-in-asp-net-mvc – Simon_Weaver Feb 21 '13 at 22:34