客户端验证不适用于重复使用和嵌套的复合体 属性

Client side validation not working with reused and nested complex property

我有一个 asp.net MVC 5 应用程序,我在其中尝试在 .cshtml 文件的不同位置重新使用嵌套的复杂视图模型 class。重用的复杂视图模型被命名为 SchoolPersonViewModel,它有很多属性,PhoneEmail 属性被验证为 "If Phone is not provided, then Email must be provided. If Phone is provided, then Email is optional input"。我写了一个自定义服务器和客户端验证,但它适用于服务器端。但是 客户端验证 无法正常工作。例如 Email 文本框提示填写,即使关联的 Phone 文本框已填写。请参阅附件。例如。请帮忙。预先谢谢你。

我知道问题来自错误:3 个电子邮件文本框有 3 个验证属性,其值与 data-val-emailrequired-stringphoneprop="Phone" 相同。 运行 时的 Phone 值导致歧义 jQuery 验证机(缺少唯一性)但我不知道如何解决它。请查看下面呈现的属性。请帮忙。提前谢谢你。

我的代码详情:

在我的 cshtml 视图中,我调用视图模型复杂 class (SchoolPersonViewModel) 3 次:一次用于 Student,一次用于 Father和一个 Mother.

C# MVC 模型 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; }
}

验证属性

// 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;
    }
}

容器视图模型和 jQuery 验证码:

@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>
}

以上视图在运行ning时间渲染如下:

<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>

第一个糟糕的设计决定是您编写的 ValidationAttribute 对特定场景过于具体。您的属性应该是一个简单的 RequiredIfAttribute,可以在 属性 的值(不是特别是您的“电子邮件” 属性)的值取决于另一个 属性.

在您的情况下,该属性将用作

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

您遇到的下一个问题是客户端脚本以及您没有获得从属 属性 的值这一事实。你的

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

returns "Phone",因此下面一行

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

returns undefined,(你想要的元素有 id="Father_Phone" 等)这意味着 return phoneTextboxValue || value; 总是 returns false.

foolproof 提供了一个包含许多常见条件验证属性的库,包括 [RequiredIf][RequiredIfEmpty],两者都适合您的情况。

但如果您想自己编写,那么我推荐 The Complete Guide To Validation In ASP.NET MVC 3 - Part 2 作为一个很好的指南。

RequiredIfAttribute 的代码为

[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;
    }
}

和关联的脚本

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;
});