Search code examples
validationunobtrusive-validationasp.net-core-3.0client-side-validationcustom-attribute

How to set up Client side validation for a custom RequiredIf attribute for Asp.NET Core 3.0


I was not able to get my client side validation method to fire after setting up the validation according to various sources. After lots of struggle I found that changing the order of when the scripts were loaded resolved the issue. I have provided an answer to show a complete setup for 'RequiredIf' custom attribute for asp.net core 3.0 MVC. Hopefully it will save other people precious time.


Solution

  • Make a new class inheriting ValidationAttribute and IClientModelValidator:

        public class RequiredIfAttribute : ValidationAttribute, IClientModelValidator
        {
            private string PropertyName { get; set; }
    
            private object DesiredValue { get; set; }
    
            public RequiredIfAttribute(string propertyName, object desiredvalue)
            {
                PropertyName = propertyName;
                DesiredValue = desiredvalue;
            }
    
            protected override ValidationResult IsValid(object value, ValidationContext context)
            {
                object instance = context.ObjectInstance;
                Type type = instance.GetType();
                object propertyvalue = type.GetProperty(PropertyName).GetValue(instance, null);
    
                if ((value == null && propertyvalue == DesiredValue) || (value == null && propertyvalue != null && propertyvalue.Equals(DesiredValue)))
                {
                    return new ValidationResult(ErrorMessage);
                }
    
                return ValidationResult.Success;
            }
    
            public void AddValidation(ClientModelValidationContext context)
            {
                MergeAttribute(context.Attributes, "data-val", "true");
                var errorMessage = FormatErrorMessage(context.ModelMetadata.GetDisplayName());
                MergeAttribute(context.Attributes, "data-val-requiredif", errorMessage);
                MergeAttribute(context.Attributes, "data-val-requiredif-otherproperty", PropertyName);
                MergeAttribute(context.Attributes, "data-val-requiredif-otherpropertyvalue", DesiredValue == null? "": DesiredValue.ToString());
            }
    
            private bool MergeAttribute(IDictionary<string, string> attributes, string key, string value)
            {
                if (attributes.ContainsKey(key))
                {
                    return false;
                }
                attributes.Add(key, value);
                return true;
            }
        }
    }

    Apply the attribute in your model as an annotation above your property:

    [Display(Name = "Effective Date Column Name")]
    [RequiredIf("EffectiveDate", null, ErrorMessage = "Effective Date Column Name is required or Enter an Effective Date.")]
    public string ColumnNameEffectiveDate { get; set; }
    
    [Display(Name = "Enter Effective Date")]
    public DateTime? EffectiveDate { get; set; }

    Add the validation elements to your html:

    <fieldset>
        <legend class="w-auto">Step 4: Set Effective Date</legend>
        <div class="form-row">
            <div class="form-group col-12">
                <small class="form-text">Select the name of the date column to import, or enter a date.</small>
            </div>
            <div class="form-group col-12 col-lg-4 columnheader">
                <label asp-for="@Model.ColumnNameEffectiveDate" class="slightlyBold"></label>
                <select class="form-control selectpicker"
                        asp-for="@Model.ColumnNameEffectiveDate">
                    <option value="">Nothing selected</option>
                </select>
                <span asp-validation-for="@Model.ColumnNameEffectiveDate" class="text-danger"></span> <==== HERE
            </div>
            <div class="form-group col-12 col-lg-2 text-center">
                <label class="slightlyBold pt-4">OR</label>
            </div>
            <div class="form-group col-12 col-lg-4">
                <label asp-for="@Model.EffectiveDate" class="slightlyBold">Enter Effective Date:</label>
                <input type="text" class="datepicker form-control" asp-for="@Model.EffectiveDate">
            </div>
        </div>
    </fieldset>

    At runtime the HTML will change to include the validation tags:

    <select class="form-control selectpicker" data-val="true" data-val-requiredif="Effective Date Column Name is required or Enter an Effective Date." data-val-requiredif-otherproperty="EffectiveDate" data-val-requiredif-otherpropertyvalue="" id="ColumnNameEffectiveDate" name="ColumnNameEffectiveDate" disabled="disabled">
        <option value="">Nothing selected</option>
    </select>

    Make a javascript file that will add the new rule to the unobtrusive adapter (I called my file "customValidationRules.js"):

    $(function () {
    
        jQuery.validator.unobtrusive.adapters.add("requiredif", ["otherproperty", "otherpropertyvalue"],
            function (options) {
                options.rules["requiredif"] = options.params;
                options.messages["requiredif"] = options.message
            });
    
    }(jQuery));

    and a second file for the method to run for that rule (I called my file "customValidationMethods.js"):

    (function ($) {
        jQuery.validator.addMethod("requiredif",
            function (value, element, parameters) {
    
                var targetId = parameters.otherproperty;
                var targetValue = parameters.otherpropertyvalue;
    
                var otherpropertyvalue = (targetValue == null || targetValue == undefined ? "" : targetValue).toString();
    
                var otherpropertyElement = $('#' + targetId);
    
                if (!value.trim() && otherpropertyElement.val() == otherpropertyvalue) {
                    var isValid = $.validator.methods.required.call(this, value, element, parameters);
                    return isValid;
                }
    
                return true;
            }
        );
    })(jQuery);

    Ensure to reference the needed validation scripts for the page. The order the scripts is what made my client side validation start firing for the custom attribute:

    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/jquery-ajax-unobtrusive/dist/jquery.unobtrusive-ajax.js"></script>
    @*These 4 validation scripts must not be changed*@
    <script src="~/lib/jquery-validation/dist/jquery.validate.js"></script> <==== HERE
    <script src="~/js/customValidationMethods.js"></script> <==== HERE
    <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script> <==== HERE
    <script src="~/js/customValidationRules.js"></script> <==== HERE
    @*These 4 validation scripts must not be changed*@
    <script src="~/lib/popper.js/umd/popper.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>