MVC3 ModelClientValidationRule compare 2 different

2019-05-26 19:02发布

问题:

I am trying to create a custom validation using IClientValidatable

I have 2 fields PhoneNumber and Mobile. I want the user to either or both. Only 1 is required but at least one must be provided.

I have managed to get to this so far

 public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        var rule = new ModelClientValidationRule
                       {
                           ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()),
                           ValidationType = "required",
                       };


        rule.ValidationParameters.Add("phone", "PhoneNumber");
        rule.ValidationParameters.Add("mobile", "MobileNumber");

        yield return rule;
    }

This applies the validation to the outputted html elements

<input data-val="true" data-val-length="Mobile number must be a maximum length of 14." data-val-length-max="14" data-val-required="Landline or mobile phone number is needed." data-val-required-required="MobileNumber" id="MobileNumber" name="MobileNumber" type="text" value="">
<input data-val="true" data-val-length="Landline number must be a maximum length of 14." data-val-length-max="14" data-val-required="Landline or mobile phone number is needed." data-val-required-required="PhoneNumber" id="PhoneNumber" name="PhoneNumber" type="text" value="">

Now I know that's not everything done. but if I try to hit the submit button the validation kicks in a shows 2 validation errors within my summary.

I am a little stuck on how to add the validator adapter.

So far....any I know its wrong but

 jQuery.validator.unobtrusive.adapters.add('required',
        [$("#PhoneNumber").val(), $("#MobileNumber").val()],
        function(options) {
                    options.rules['required'] = options.params;
                    options.messages['required'] = options.message;
        });

回答1:

First, you will need to create your own validation attribute in a similar way than the Compare attribute.

In this attribute you will specify the other dependent property, and the error message will be formated to take into account the properties display names.

The attribute will look like this (I am not too proud about its name and default error message!):

public class SelectOneValidationAttribute : ValidationAttribute, IClientValidatable
{
    private const string DefaultErrorMessage = "Please enter '{0}' or '{1}'.";
    private string _otherFieldName;

    public SelectOneValidationAttribute(String otherFieldName)
        : base(DefaultErrorMessage)
    {
        _otherFieldName = otherFieldName;
    }

    public override string FormatErrorMessage(string name)
    {
        return String.Format(CultureInfo.CurrentCulture, ErrorMessageString, name, _otherFieldName);
    }

    protected override DataAnnotationsValidationResult IsValid(object value, ValidationContext validationContext)
    {
        PropertyInfo otherPropertyInfo = validationContext.ObjectType.GetProperty(_otherFieldName);
        if (otherPropertyInfo == null)
        {
            return new DataAnnotationsValidationResult("Unknown property " + _otherFieldName);
        }


        string strValue = value == null ? null : value.ToString();
        if(!String.IsNullOrEmpty(strValue))
            //validation succeeds as this field has been populated
            return null;

        object otherPropertyValue = otherPropertyInfo.GetValue(validationContext.ObjectInstance, null);

        string strOtherPropertyValue = otherPropertyValue == null ? null : otherPropertyValue.ToString();
        if (!String.IsNullOrEmpty(strOtherPropertyValue))
            //validation succeeds as the other field has been populated
            return null; 
        else
            //validation failed, none of the 2 fields were populated
            return new DataAnnotationsValidationResult(DefaultErrorMessage);
    }


    //Create the data attributes for the client to use
    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        var rule = new ModelClientValidationRule
        {
            ValidationType = "selectone",
            ErrorMessage = FormatErrorMessage(metadata.GetDisplayName())
        };

        rule.ValidationParameters["otherfield"] = FormatPropertyForClientValidation(_otherFieldName);

        yield return rule;
    }

    public static string FormatPropertyForClientValidation(string property)
    {
        return "*." + property;
    }
}

So you can use it in your models as in:

public class YourModel
{
    [SelectOneValidation("Phone")]
    public string Mobile { get; set; }

    [SelectOneValidation("Mobile")]
    public string Phone { get; set; }
}

With that code, the error messages "Please enter 'Mobile' or 'Phone'." and "Please enter 'Phone' or 'Mobile'." will be displayed when the server side validations fails. (You could set the same error message on both like "Please enter one..." )

In order to add the client side validation, you will need to create the adaptor for the unobtrusive validation. (Make sure you add it somewhere before the unobtrusive validation parses the document, otherwise you will need to manually parse it):

//Add an adaptor for our new jquery validator, that builds the optional parameters 
//for our validation code (the other field to look at)
$.validator.unobtrusive.adapters.add("selectone", ["otherfield"], function (options) {
    var prefix = getModelPrefix(options.element.name),
        other = options.params.otherfield,
        fullOtherName = appendModelPrefix(other, prefix),
        element = $(options.form).find(":input[name=" + fullOtherName + "]")[0];

    setValidationValues(options, "selectone", element);
});

This is using a few of the utility functions defined in the unobtrusive validation library like:

function setValidationValues(options, ruleName, value) {
    options.rules[ruleName] = value;
    if (options.message) {
        options.messages[ruleName] = options.message;
    }
}

function getModelPrefix(fieldName) {
    return fieldName.substr(0, fieldName.lastIndexOf(".") + 1);
}

function appendModelPrefix(value, prefix) {
    if (value.indexOf("*.") === 0) {
        value = value.replace("*.", prefix);
    }
    return value;
}

Finally I have created a new validation rule. This is because although the required validation allows setting a filtering expression so the field is flagged only as valid when that expression is evaluated as true, see required validation, we need to apply a similar fix for onBlur validations than for the equalto rule.

The validation method is applying the said fix from the equalto validation, and then just calling the required rule with a dependent selector on the other related field, so the required validation returns false only when the field is blank and the dependent has not been filled.

$.validator.addMethod("selectone", function (value, element, param) {
    // bind to the blur event of the target in order to revalidate whenever the target field is updated
    // TODO find a way to bind the event just once, avoiding the unbind-rebind overhead
    var otherField = $(param);
    if (this.settings.onfocusout) {
        otherField.unbind(".validate-selectone").bind("blur.validate-selectone", function () {
            $(element).valid();
        });
    }

    var otherFieldBlankFilter = ":input[name=" + otherField[0].name + "]:blank";
    return $.validator.methods.required.call(this, value, element, otherFieldBlankFilter);
});

If you don't mind about that, as if you are validating only when the form is submitted, you could just write an adaptor that directly uses the required rule:

$.validator.unobtrusive.adapters.add("selectone", ["otherfield"], function (options) {
    var prefix = getModelPrefix(options.element.name),
        other = options.params.otherfield,
        fullOtherName = appendModelPrefix(other, prefix),
        element = $(options.form).find(":input[name=" + fullOtherName + "]")[0];

    setValidationValues(options, "required", ":input[name=" + fullOtherName + "]:blank");
});

With all of this in place, your validation should work. If both are empty and you try to submit, error messages will be displayed for both fields. If you then enter something into one of them and tab out, both error messages should be removed.

You should also be able to adapt this to your needs, as you can modify the validation attribute, unobtrusive adaptor and validation rule.