Client side validation not working with reused and

2019-07-02 15:15发布

问题:

I have an asp.net MVC 5 application in which I tried to re-use a nested complex view model class in different places in an .cshtml file. The reused complex view model is named as SchoolPersonViewModel that has many properties, and the Phone and Email properties are validated like "If Phone is not provided, then Email must be provided. If Phone is provided, then Email is optional input". I wrote a custom server and client side validation but it works with server side. But the client side validation is not working properly. For example Email text box prompts to be filled even though the associated Phone text box is filled. Please see the attachment. For example. Please help. Thank you beforehand.

I know the problem comes from the error: there are 3 validation attributes for the 3 Email text boxes with same value as data-val-emailrequired-stringphoneprop="Phone" . The Phone value at run time causes the ambiguity (it is the uniqueness is missing) for the jQuery validation machine but I do not how to solve it. Please see the rendered attributes below. Please help. Thank you in advance.

Details about my codes:

On my cshtml view, I call the view model complex class (SchoolPersonViewModel) 3 times: one for Student, one for Father and one for Mother.

C# MVC model classes

public class SchoolPersonViewModel
{
    [DisplayName("Phone")]
    public string Phone { get; set; }
    [DisplayName("Email")]
    [EmailRequired(StringPhonePropertyName = "Phone", ErrorMessage = "Email is required if Phone is not provided")]
    public string Email { get; set; }
    .... // other properties
}

public class StudentEnrollViewModel
{
    public SchoolPersonViewModel Student { get; set; }
    public SchoolPersonViewModel Father { get; set; }
    public SchoolPersonViewModel Mother { get; set; }
}

Validation attribute

// If Phone is not input then Email is required -- server and client side validation
public class EmailRequiredAttribute : ValidationAttribute, IClientValidatable
{
    public string StringPhonePropertyName { get; set; }

    protected override ValidationResult IsValid(object value, System.ComponentModel.DataAnnotations.ValidationContext validationContext)
    {
        var phone = ValidatorCommon.GetValue<string>(validationContext.ObjectInstance, StringPhonePropertyName);
        var email = (string)value;
        if (!string.IsNullOrWhiteSpace(phone) || (string.IsNullOrWhiteSpace(phone) && !string.IsNullOrWhiteSpace(email)))
        {
            return ValidationResult.Success;
        }
        if (string.IsNullOrWhiteSpace(phone) && string.IsNullOrWhiteSpace(email))
        {
            return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
        }
        return ValidationResult.Success;
    }

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

        modelClientValidationRule.ValidationParameters.Add("stringphoneprop", StringPhonePropertyName);
        yield return modelClientValidationRule;
    }
}

Container view model and the jQuery validation codes:

@model ExpandoObjectSerializeDeserialize.Web.Models.StudentEnrollViewModel
....
@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    ....
    @Html.EditorFor(m => m.Student)
    ....
    @Html.EditorFor(m => m.Father)
    ....
    @Html.EditorFor(m => m.Mother)

    <input type="submit" value="Create" class="btn btn-default" />
}

@section scripts {
    <script type="text/javascript">
        jQuery.validator.addMethod('emailrequired', function(value, element, params) {
            var phoneTextboxId = $(element).attr('data-val-emailrequired-stringphoneprop');
            var phoneTextboxValue = $('#' + phoneTextboxId).val();
            // empty string is evaluated as ‘false’ and non-empty, non-null string is evaluated as ‘true’ in JavaScript
            return phoneTextboxValue || value;
        });

        jQuery.validator.unobtrusive.adapters.add('emailrequired', {}, function(options) {
            options.rules['emailrequired'] = true;
            options.messages['emailrequired'] = options.message;
        });
    </script>
}

The above view is rendered as follows at running time:

<div class="col-md-4">
    <input class="form-control text-box single-line" id="Father_Phone" name="Father.Phone" type="text" value="">
    <span class="field-validation-valid text-danger" data-valmsg-for="Father.Phone" data-valmsg-replace="true"></span>
</div>
<div class="col-md-4">
    <input class="form-control text-box single-line input-validation-error" data-val="true" data-val-emailrequired="Email is required if Phone is not provided" data-val-emailrequired-stringphoneprop="Phone" id="Student_Email" name="Student.Email" type="text" value="">
    <span class="text-danger field-validation-error" data-valmsg-for="Student.Email" data-valmsg-replace="true"><span for="Student_Email" class="">Email is required if Phone is not provided</span></span>
</div>
<div class="col-md-4">
    <input class="form-control text-box single-line" id="Mother_Phone" name="Mother.Phone" type="text" value="">
    <span class="field-validation-valid text-danger" data-valmsg-for="Mother.Phone" data-valmsg-replace="true"></span>
</div>
<div class="col-md-4">
    <input class="form-control text-box single-line input-validation-error" data-val="true" data-val-emailrequired="Email is required if Phone is not provided" data-val-emailrequired-stringphoneprop="Phone" id="Father_Email" name="Father.Email" type="text" value="">
    <span class="text-danger field-validation-error" data-valmsg-for="Father.Email" data-valmsg-replace="true"><span for="Father_Email" class="">Email is required if Phone is not provided</span></span>
</div>
<div class="col-md-4">
    <input class="form-control text-box single-line" id="Mother_Phone" name="Mother.Phone" type="text" value="">
    <span class="field-validation-valid text-danger" data-valmsg-for="Mother.Phone" data-valmsg-replace="true"></span>
</div>      
<div class="col-md-4">
    <input class="form-control text-box single-line input-validation-error" data-val="true" data-val-emailrequired="Email is required if Phone is not provided" data-val-emailrequired-stringphoneprop="Phone" id="Mother_Email" name="Mother.Email" type="text" value="">
    <span class="text-danger field-validation-error" data-valmsg-for="Mother.Email" data-valmsg-replace="true"><span for="Mother_Email" class="">Email is required if Phone is not provided</span></span>
</div>

回答1:

The first poor design decision is that you have written a ValidationAttribute which is too specific to a particular scenario. Your attribute should be a simple RequiredIfAttribute that can be used in any scenario where the value of a property (not specifically your 'Email` property) is dependent on the value of another property.

In your case, the attribute would be used as

[RequiredIf("Phone", null, ErrorMessage = "...")]
public string Email { get; set; }

The next issue you have is the client side script and the fact you do not get the value of the dependent property. Your

var phoneTextboxId = $(element).attr('data-val-emailrequired-stringphoneprop');

returns "Phone", and therefore the following line

var phoneTextboxValue = $('#' + phoneTextboxId).val();

returns undefined, (the elements you want have id="Father_Phone" etc) which means that return phoneTextboxValue || value; always returns false.

foolproof provides a library of many common conditional validation attributes, including a [RequiredIf] and a [RequiredIfEmpty], both of which would be suitable for your case.

But if you want to write your own, then I recommend The Complete Guide To Validation In ASP.NET MVC 3 - Part 2 as a good guide.

The code for a RequiredIfAttribute would be

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class RequiredIfAttribute : ValidationAttribute, IClientValidatable
{

    #region .Declarations 

    private const string _DefaultErrorMessage = "Please enter the {0}.";
    private readonly string _PropertyName;
    private readonly object _Value;

    public RequiredIfAttribute(string propertyName, object value)
    {
        if (string.IsNullOrEmpty(propertyName))
        {
            throw new ArgumentNullException("propertyName");
        }
        _PropertyName = propertyName;
        _Value = value;
        ErrorMessage = _DefaultErrorMessage;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        if (value == null)
        {
            var property = validationContext.ObjectInstance.GetType().GetProperty(_PropertyName);
            var propertyValue = property.GetValue(validationContext.ObjectInstance, null);
            if (propertyValue != null && propertyValue.Equals(_Value))
            {
                return new ValidationResult(string.Format(ErrorMessageString, validationContext.DisplayName));
            }
        }
        return ValidationResult.Success;
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        var rule = new ModelClientValidationRule
        {
            ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()),
            ValidationType = "requiredif",
        };
        rule.ValidationParameters.Add("dependentproperty", _PropertyName);
        rule.ValidationParameters.Add("targetvalue", _Value);
        yield return rule;
    }
}

and the associated scripts

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.addMethod("requiredif", function (value, element, params) {
    if ($(element).val() != '') {
        // The element has a value so its OK
        return true;
    }
    if (!params.dependentelement) {
        return true;
    }
    var dependentElement = $(params.dependentelement);
    if (dependentElement.is(':checkbox')) {
        var dependentValue = dependentElement.is(':checked') ? 'True' : 'False';
        return dependentValue != params.targetvalue;
    } else if (dependentElement.is(':radio')) {
        // If its a radio button, we cannot rely on the id attribute
        // So use the name attribute to get the value of the checked radio button
        var dependentName = dependentElement[0].name;
        dependentValue = $('input[name="' + dependentName + '"]:checked').val();
        return dependentValue != params.targetvalue;
    }
    return dependentElement.val() !== params.targetvalue;
});

$.validator.unobtrusive.adapters.add("requiredif", ["dependentproperty", "targetvalue"], function (options) {
    var element = options.element;
    var dependentproperty = options.params.dependentproperty;
    var dependentElement = sandtrapValidation.getDependentElement(element, dependentproperty);
    options.rules['requiredif'] = {
        dependentelement: dependentElement,
        targetvalue: options.params.targetvalue
    };
    options.messages['requiredif'] = options.message;
});