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

Posted

技术标签:

【中文标题】客户端验证不适用于重用和嵌套的复杂属性【英文标题】:Client side validation not working with reused and nested complex property 【发布时间】:2018-09-10 09:10:44 【问题描述】:

我有一个 asp.net MVC 5 应用程序,我尝试在 .cshtml 文件的不同位置重用嵌套的复杂视图模型类。重用的复杂视图模型命名为SchoolPersonViewModel,有很多属性,PhoneEmail 属性验证为“如果不提供电话,则必须提供电子邮件。如果提供电话,则电子邮件是可选输入”。我编写了一个自定义服务器和客户端验证,但它适用于服务器端。但客户端验证无法正常工作。例如,Email 文本框提示填写,即使关联的 Phone 文本框已填写。请参阅附件。例如请帮忙。提前谢谢你。

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

关于我的代码的详细信息:

在我的cshtml 视图中,我调用视图模型复杂类 (SchoolPersonViewModel) 3 次:一次为Student,一次为Father,一次为Mother

C# MVC 模型类

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>

上面的视图在运行时渲染如下:

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

第一个糟糕的设计决定是您编写了一个ValidationAttribute,它对特定场景过于具体。您的属性应该是一个简单的RequiredIfAttribute,可以在属性值(不是特别是您的“电子邮件”属性)依赖于另一个属性值的任何情况下使用。

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

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

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

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

返回"Phone",因此返回以下行

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

返回undefined,(你想要的元素有id="Father_Phone"等)这意味着return phoneTextboxValue || value;总是返回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;
);

【讨论】:

感谢您的回答。顺便问一下,在什么情况下,html标签(元素)的name属性是用字符[或]构建的?我通过我的代码示例了解点 (.) 字符。您的 JavaScript 代码行: var id = (name.substr(0, index) +dependentProperty).replace(/[\.[]]/g, "_");使用正则表达式提取想要的子字符串。 它是为收藏而生成的。例如@for(int i = 0; i &lt; Model.MyCollection.MyProperty.Count; i++) @Html.TextBoxFor(m =&gt; m.MyCollection[i].MyProperty) 生成&lt;input name=MyCollection[0].MyProperty" id="MyCollection_0__MyProperty" ... /&gt;

以上是关于客户端验证不适用于重用和嵌套的复杂属性的主要内容,如果未能解决你的问题,请参考以下文章

带有客户端证书身份验证的 Wcf 不适用于 soapui

Yii 客户端验证不适用于 ajax 加载的表单

Ldap 身份验证不适用于 Spring Boot

嵌套 JWS + JWE 与具有身份验证加密的 JWE

JWT 身份验证适用于 $http.get 但不适用于 $http.post

Asp.Net MVC 5 Jquery 验证不适用于带有提交事件的 ajax 帖子,显示为有效表单