6

I'm developing an ASP.NET MVC 5.2.3 custom data annotation for validation in Visual Studio 2015. It needs to take any number of fields and ensure that if one has a value, they all must have a value; if they're all null/blank, it should be okay.

A few examples have helped:

However, I'm not sure how to do the client-side validation where you have an unknown number of fields being validated.

How do you pass that to the client using the implementation of the GetClientValidationRules() method of the IClientValidatable interface?

Also, how do I apply this new data annotation to the properties on my view model? Would it look like this?

[MultipleRequired("AppNumber", "UserId", /* more fields */), ErrorMessage = "Something..."]
[DisplayName("App #")]
public int AppNumber { get; set; }

[DisplayName("User ID")]
public int UserId { get; set; }

Here's as far as I could get with the MultipleRequiredAttribute custom data annotation class:

public class MultipleRequiredAttribute : ValidationAttribute, IClientValidatable
{
    private readonly string[] _fields;
    public MultipleRequiredAttribute(params string[] fields)
    {
        _fields = fields;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        // If any field has value, then all must have value
        var anyHasValue = _fields.Any(f => !string.IsNullOrEmpty(f));

        if (!anyHasValue) return null;

        foreach (var field in _fields)
        {
            var property = validationContext.ObjectType.GetProperty(field);
            if (property == null)
                return new ValidationResult($"Property '{field}' is undefined.");

            var fieldValue = property.GetValue(validationContext.ObjectInstance, null);

            if (string.IsNullOrEmpty(fieldValue?.ToString()))
                return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
        }

        return null;
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        yield return new ModelClientValidationRule
        {
            ErrorMessage = ErrorMessage,
            ValidationType = "multiplerequired"
        };
    }
}

Thank you.

Community
  • 1
  • 1
Alex
  • 34,699
  • 13
  • 75
  • 158
  • you build a custom function for jquery Validate js plugin on client side – Steve Feb 03 '17 at 20:55
  • 2
    Start by reading [The Complete Guide To Validation In ASP.NET MVC 3 - Part 2](http://www.devtrends.co.uk/blog/the-complete-guide-to-validation-in-asp.net-mvc-3-part-2). In your `GetClientValidationRules()` method, you add a `ModelClientValidationRule` where you can pass a (say) comma separated list of the property names - i.e. your `fields` values - which can be parsed and used in the client side scripts (if your having issues, let me know and I'll add an answer but wont get a chance for a few hours) –  Feb 03 '17 at 21:03
  • Thanks, @StephenMuecke! One of my issues was how to pass the values to the client. – Alex Feb 03 '17 at 21:17
  • 1
    You question states _if one has a value, they all must have a value_ but your code is not validating that (and you would also need to apply the attribute to all properties if that is the case) –  Feb 04 '17 at 00:32
  • 1
    Also your `return new ValidationResult($"Property '{field}' is undefined.");` does not really makes sense (displaying that message in the view would be meaningless and confusing to the user) - either ignore it, or check in in the constructor and throw an exception –  Feb 04 '17 at 00:35
  • Thanks, @StephenMuecke. Copied code from one of the examples and have not yet run it. – Alex Feb 04 '17 at 00:38

1 Answers1

3

In order to get client side validation, you need to pass the values of the 'other properties' in the ModelClientValidationRule by using the .Add() method of the rules ValidationParameters property, and then write the client side scripts to add the rules to the $.validator.

But first there are a few other issues to address with your attribute. First you should execute your foreach loop only if the value of the property you applied the attribute is null. Second, returning a ValidationResult if one of the 'other properties' does not exist is confusing and meaningless to a user and you should just ignore it.

The attribute code should be (note I changed the name of the attribute)

public class RequiredIfAnyAttribute : ValidationAttribute, IClientValidatable
{
    private readonly string[] _otherProperties;
    private const string _DefaultErrorMessage = "The {0} field is required";

    public RequiredIfAnyAttribute(params string[] otherProperties)
    {
        if (otherProperties.Length == 0) // would not make sense
        {
            throw new ArgumentException("At least one other property name must be provided");
        }
        _otherProperties = otherProperties;
        ErrorMessage = _DefaultErrorMessage;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        if (value == null) // no point checking if it has a value
        {
            foreach (string property in _otherProperties)
            {
                var propertyName = validationContext.ObjectType.GetProperty(property);
                if (propertyName == null)
                {
                    continue;
                }
                var propertyValue = propertyName.GetValue(validationContext.ObjectInstance, null);
                if (propertyValue != null)
                {
                    return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
                }
            }
        }
        return ValidationResult.Success;
    }
    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        var rule = new ModelClientValidationRule
        {
            ValidationType = "requiredifany",
            ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()),
        };
        / pass a comma separated list of the other propeties
        rule.ValidationParameters.Add("otherproperties", string.Join(",", _otherProperties));
        yield return rule;
    }
}

The scripts will then be

sandtrapValidation = {
    getDependentElement: function (validationElement, dependentProperty) {
        var dependentElement = $('#' + dependentProperty);
        if (dependentElement.length === 1) {
            return dependentElement;
        }
        var name = validationElement.name;
        var index = name.lastIndexOf(".") + 1;
        var id = (name.substr(0, index) + dependentProperty).replace(/[\.\[\]]/g, "_");
        dependentElement = $('#' + id);
        if (dependentElement.length === 1) {
            return dependentElement;
        }
        // Try using the name attribute
        name = (name.substr(0, index) + dependentProperty);
        dependentElement = $('[name="' + name + '"]');
        if (dependentElement.length > 0) {
            return dependentElement.first();
        }
        return null;
    }
}

$.validator.unobtrusive.adapters.add("requiredifany", ["otherproperties"], function (options) {
    var element = options.element;
    var otherNames = options.params.otherproperties.split(',');
    var otherProperties = [];
    $.each(otherNames, function (index, item) {
        otherProperties.push(sandtrapValidation.getDependentElement(element, item))
    });
    options.rules['requiredifany'] = {
        otherproperties: otherProperties
    };
    options.messages['requiredifany'] = options.message;
});

$.validator.addMethod("requiredifany", function (value, element, params) {
    if ($(element).val() != '') {
        // The element has a value so its OK
        return true;
    }
    var isValid = true;
    $.each(params.otherproperties, function (index, item) {
        if ($(this).val() != '') {
            isValid = false;
        }
    });
    return isValid;
});
  • Thanks, @StephenMuecke. I'm lost with the logic of the `sandtrapValidation` code; do you mind explaining that a bit? – Alex Feb 06 '17 at 14:03
  • 1
    Its a general purpose function for finding an associated element in the DOM. You may have a model containing properties which are complex objects or collections, so it might be rendering as inputs with (say) `name="Employees[0].FirstName" id="Employees_0__FirstName"` and `name="Employees[0].LastName" id="Employees_0__LastName"`. So assume you want to validate the `LastName` is required if `FirstName` is provided. All you can pass in the `GetClientValidationRules()` method is the name of the other property i.e. `LastName` –  Feb 06 '17 at 21:29
  • 1
    The method first checks if the DOM includes an element with `id="LastName"`. For a simple object, that will return an element. But in this case it wont, so the next part of the function gets the name of the current element (which is `name="Employees[0].FirstName"`) and gets the part to the left of the last dot (`Employees[0]`) and appends to other property to generate `Employees[0].LastName`. Because searching by `id` is faster than by `name` attribute, the `.replace()` generates `Employees_0__LastName` and a search for that element is performed. –  Feb 06 '17 at 21:33
  • 1
    In most cases that will find what you want, but some users override the `id` attribute so nothing would be found, and the final check if based on finding the element based on the `name` attribute. –  Feb 06 '17 at 21:36
  • 1
    As a side note, I simplified that method a bit and omitted some code related to elements that are checkboxes and radio buttons which need to be handled a bit differently, but I don't think that would be applicable in your case –  Feb 06 '17 at 21:42
  • Thanks, @StephenMuecke. Actually, I've got checkbox lists in my web app. And I'm doing some code acrobatics to handle those on the client. How do you detect those using the `sandTrapValidation`? And why is that an alias for the `getDependentElement` function? – Alex Feb 07 '17 at 14:12
  • 1
    The 'alias' is just namespacing the function (its part of a my jquery plugin that includes numerous validation functions that are not handles by the built in validation attributes) and just prevents any possible (albeit unlikely) conflicts with plugins –  Feb 08 '17 at 00:55
  • 1
    Without seeing how what you validating in your checkboxlist, and the 'code acrobatics' its a bit hard to comment. –  Feb 08 '17 at 00:58