Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom validation not firing client-side

I am writing a custom attribute to require a property in a viewmodel if another property has a specified value.

I used this post for reference: RequiredIf Conditional Validation Attribute

But have been encountering issues with the .NET Core revisions for IClientModelValidator. Specifically, the server side validation works as expected with ModelState.IsValid returning false, and ModelState errors containing my custom error codes. I feel that I am missing something when translating between the differing versions of validator.

The old (working) solution has the following:

public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata,
    ControllerContext context)
{
    var rule = new ModelClientValidationRule
    {
        ErrorMessage = ErrorMessageString,
        ValidationType = "requiredif",
    };
    rule.ValidationParameters["dependentproperty"] =
        (context as ViewContext).ViewData.TemplateInfo.GetFullHtmlFieldId(PropertyName);
    rule.ValidationParameters["desiredvalue"] = DesiredValue is bool
        ? DesiredValue.ToString().ToLower()
        : DesiredValue;

    yield return rule;
}

Based on the changes to IClientModelValidator outlined here: https://github.com/aspnet/Announcements/issues/179 I have written the following methods:

    public void AddValidation(ClientModelValidationContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(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-dependentproperty", PropertyName);

        var desiredValue = DesiredValue.ToString().ToLower();
        MergeAttribute(context.Attributes, "data-val-requiredif-desiredvalue", desiredValue);
    }

    private bool MergeAttribute(
        IDictionary<string, string> attributes,
        string key,
        string value)
    {
        if (attributes.ContainsKey(key))
        {
            return false;
        }
        attributes.Add(key, value);
        return true;
    }

These are being called as expected, and values are properly populated, yet the following JS is ignored. Leaving me to suspect I am missing something between the two.

    $.validator.addMethod("requiredif", function (value, element, parameters) {
        var desiredvalue = parameters.desiredvalue;
        desiredvalue = (desiredvalue == null ? "" : desiredvalue).toString();
        var controlType = $("input[id$='" + parameters.dependentproperty + "']").attr("type");
        var actualvalue = {}
        if (controlType === "checkbox" || controlType === "radio") {
            var control = $("input[id$='" + parameters.dependentproperty + "']:checked");
            actualvalue = control.val();
        } else {
            actualvalue = $("#" + parameters.dependentproperty).val();
        }
        if ($.trim(desiredvalue).toLowerCase() === $.trim(actualvalue).toLocaleLowerCase()) {
            var isValid = $.validator.methods.required.call(this, value, element, parameters);
            return isValid;
        }
        return true;
    });
    $.validator.unobtrusive.adapters.add("requiredif", ["dependentproperty", "desiredvalue"], function (options) {
        options.rules["requiredif"] = options.params;
        options.messages["requiredif"] = options.message;
    });

Any ideas?

EDIT: Just to erase doubt that the server side is working properly and the issue almost certainly lies client side, here is a snip of the generated HTML for a decorated field:

<input class="form-control" type="text" data-val="true" data-val-requiredif="Profession Other Specification is Required" data-val-requiredif-dependentproperty="ProfessionTypeId" data-val-requiredif-desiredvalue="10" id="ProfessionOther" name="ProfessionOther" value="" placeholder="Please Specify Other">
like image 203
Kristi Kozak Avatar asked Dec 22 '16 19:12

Kristi Kozak


1 Answers

So I had the same setup and same result as the original questioner. By stepping through a project where custom validators were being fired and where they weren't, I was able to determine that when the page is initially loaded, jquery.validate.js attaches a validator object to the form. The validator for the working project contained the key for the custom validator I had created. The validator for the one that did not work was missing that key (which was later added and available at the time I was posting my form).

Unfortunately, as the validator object had already been created and attached to the form without my custom validator, it never reached that function. The key to solving this issue was to move my two JS functions outside of the jQuery ready function, as close to the top of my main script as possible (just after I set my jQuery validator defaults). I hope this helps someone else!

My project is written in TypeScript, so my structure is a bit different but the JavaScript for actually adding the validator remains unchanged.

Here is the code for my "SometimesRequired" validator Typescript class:

export class RequiredSometimesValidator {
    constructor() {
        // validator code starts here
        $.validator.addMethod("requiredsometimes", function (value, element, params) {
            var $prop = $("#" + params);
            // $prop not found; search for a control whose Id ends with "_params" (child view)
            if ($prop.length === 0) 
                $prop = $("[id$='_" + params + "']");

            if ($prop.length > 0) {
                var ctrlState = $prop.val();
                if (ctrlState === "EditableRequired" && (value === "" || value === "Undefined"))
                    return false;
            }
            return true;
        });

        $.validator.unobtrusive.adapters.add("requiredsometimes", ["controlstate"], function (options) {
            options.rules["requiredsometimes"] = options.params["controlstate"];
            options.messages["requiredsometimes"] = options.message;
        });
        // validator code stops here
    }
}

Then in my boot-client.ts file (the main file which powers my application's JavaScript), I instantiate a new copy of the validator above (thus calling the constructor which adds the custom validator to the validator object in memory) outside of document.ready:

export class Blueprint implements IBlueprint {
    constructor() {
        // this occurs prior to document.ready
        this.initCustomValidation();

        $(() => { 
            // document ready stuff here
        });
    }
    private initCustomValidation = (): void => {
        // structure allows for load of additional client-side validators
        new RequiredSometimesValidator();
    }
}

As a very simple example not using TypeScript, you should be able to do this:

<script>
    $.validator.addMethod("requiredsometimes", function (value, element, params) {
        var $prop = $("#" + params);
        // $prop not found; search for a control whose Id ends with "_params" (child view)
        if ($prop.length === 0) 
            $prop = $("[id$='_" + params + "']");

        if ($prop.length > 0) {
            var ctrlState = $prop.val();
            if (ctrlState === "EditableRequired" && (value === "" || value === "Undefined"))
                return false;
        }
        return true;
    });

    $.validator.unobtrusive.adapters.add("requiredsometimes", ["controlstate"], function (options) {
        options.rules["requiredsometimes"] = options.params["controlstate"];
        options.messages["requiredsometimes"] = options.message;
    });

    $(function() {
        // document ready stuff
    });

</script>
like image 83
Loni2Shoes Avatar answered Oct 16 '22 23:10

Loni2Shoes