1

I have a registration page that asks for your given Country and State. This is all fine and dandy when you are from a Country that has states like in the U.S, but when you are from a country that doesn't have states I would like to leave the state field blank and still allow you to register and submit the page still.

In my DB table for Country I have a column set boolean true if they have states and false if they don't. This way when someone selects a country with states it loads the states in the drop down menu.

I have the model set up like such.

      [Required(ErrorMessage = "State is required.")]                
      public string StateId { get; set; }

I tried to make the value nullable but that seem to have no effect. So Is it even possible to do validation like this using DataAnnotations?

whisk
  • 713
  • 1
  • 10
  • 34
  • 1
    In addition to the below, there are a few nuget packages which add ready made extra validation attributes including rules like `RequiredIf` and similar, which sound like it would suit your needs. – ADyson Sep 15 '20 at 20:50
  • 1
    This is a good and generalizable question but regarding the specific example of States, there are analogous Concepts, geographically speaking, in many countries. So you might just want to localize your form – Aluan Haddad Sep 15 '20 at 20:52
  • @AluanHaddad I agree. I will have to adjust for that is the near future – whisk Sep 15 '20 at 20:58

2 Answers2

2

This question is very similar to this:

Custom Validation Attributes: Comparing two properties in the same model

Basically, you create a custom data attribute. In your case (pseudo code)

public class MyViewModel
{
    [RequiredIfHasState("End", ErrorMessage = "State is required.")]
    public string StateId { get; set; }

    public bool HasState { get; set; }
}

public class RequiredIfHasStateAttribute : ValidationAttribute
{
    private readonly string _comparisonProperty;

    public RequiredIfHasStateAttribute(string comparisonProperty)
    {
        _comparisonProperty = comparisonProperty;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        ErrorMessage = ErrorMessageString;
        var currentValue = (bool)value;

        var property = validationContext.ObjectType.GetProperty(_comparisonProperty);

        if (property == null)
            throw new ArgumentException("Property with this name not found");

        var comparisonValue = (string)property.GetValue(validationContext.ObjectInstance);

        if (currentValue && string.IsNullOrEmpty(comparisonValue))
            return new ValidationResult(ErrorMessage);

        return ValidationResult.Success;
    }
}
r3plica
  • 13,017
  • 23
  • 128
  • 290
  • 2
    Very nice and clean. You could even generalize this to a `[RequiredWhenMember(nameof(HasState), Value = true)]` sort of attribute base class – Aluan Haddad Sep 15 '20 at 21:14
1

As r3plica said, you could create a custom validation method to check the state. In the custom validation method, you could query the database and check whether the country has state and then return the validation result. Code as below:

public class DeveloperViewModel
{
    public int Id { get; set; }
    [Required(ErrorMessage = "Name is Required")]
    public string Name { get; set; }
    [Required(ErrorMessage ="Country is Required")]
    public string Country { get; set; }
    [RequiredIfHasState("Country", ErrorMessage ="State is Required")]
    public string State { get; set; }
}

public class RequiredIfHasStateAttribute : ValidationAttribute
{
    private readonly string _comparisonProperty;

    public RequiredIfHasStateAttribute(string comparisonProperty)
    {
        _comparisonProperty = comparisonProperty;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        ErrorMessage = ErrorMessageString;

        //get entered state value
        var stateValue = (string)value;
        var property = validationContext.ObjectType.GetProperty(_comparisonProperty);
        if (property == null)
            throw new ArgumentException("Property with this name not found");
        //get the country value
        var countryValue = (string)property.GetValue(validationContext.ObjectInstance);
        //get the current dbcontext
        var _context = (MvcMovieContext)validationContext.GetService(typeof(MvcMovieContext));
        //query the database and check whether the country has state.
        if (_context.Countries.Where(c => c.CountryCode == countryValue).Select(c => c).FirstOrDefault().HasState)
        {
            if(stateValue == null)
            { 
                //if country has state and the state is null. return error message
                return new ValidationResult(ErrorMessage);
            }
            else
            {
                //if country has state and the state is not found.
                if(!_context.Countries.Where(c => c.CountryCode == countryValue).Any(c => c.States.Any(e => e.StateName == stateValue)))
                {
                    return new ValidationResult("State not found");
                }
            }
        }   
        return ValidationResult.Success;
    }
}

The screenshot as below (GB country doesn't contain state, the US country has states):

enter image description here

Zhi Lv
  • 18,845
  • 1
  • 19
  • 30