You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C#自定义ValidationAttribute无法阻止表单提交问题求助

解决ASP.NET Core自定义验证特性无法阻止表单提交的问题

问题场景

我有一个Membership类,包含4个属性,要求DaysActive、MonthsActive、YearsActive三者无需全部填写,但至少要填其中一个。自定义了RequiredIfAttribute验证特性,调试确认后端能返回错误信息,但UI端无法阻止三个字段都为空时的表单提交。不能给三个字段都加Required特性,否则会强制用户全部填写。

相关代码如下:

Membership类

public class Membership
{
    [Display(ResourceType = typeof(MembershipResources), Name = nameof(Name))]
    public string Name { get; set; }

    [Display(ResourceType = typeof(MembershipResources), Name = nameof(DaysActive))]
    [RequiredIf(nameof(MonthsActive), nameof(YearsActive), ErrorMessageResourceType = typeof(MembershipResources), ErrorMessageResourceName = "ErrorDays")]
    public int? DaysActive { get; set; }

    [Display(ResourceType = typeof(MembershipResources), Name = nameof(MonthsActive))]
    [RequiredIf(nameof(DaysActive), nameof(YearsActive), ErrorMessageResourceType = typeof(MembershipResources), ErrorMessageResourceName = "ErrorMonths")]
    public int? MonthsActive { get; set; }

    [Display(ResourceType = typeof(MembershipResources), Name = nameof(YearsActive))]
    [RequiredIf(nameof(DaysActive), nameof(MonthsActive), ErrorMessageResourceType = typeof(MembershipResources), ErrorMessageResourceName = "ErrorYears")]
    public int? YearsActive { get; set; }
}

自定义验证特性

public class RequiredIfAttribute : ValidationAttribute
{
    private readonly string[] dependentProperties;

    public RequiredIfAttribute(params string[] dependentProperties)
    {
        this.dependentProperties = dependentProperties;
    }

    protected override ValidationResult IsValid(object? value, ValidationContext validationContext)
    {
        if (value == null)
        {
            var notNullProps = 0;
            foreach (var dependentProperty in dependentProperties)
            {
                var dependentPropertyValue = validationContext.ObjectInstance?.GetType()?
                    .GetProperty(dependentProperty)?
                    .GetValue(validationContext.ObjectInstance);

                if (!string.IsNullOrEmpty(dependentPropertyValue?.ToString()))
                {
                    notNullProps++;
                }
            }

            if (notNullProps == 0)
            {
                if (ErrorMessageResourceType != null && ErrorMessageResourceName != null) 
                {
                    var errorProperty = ErrorMessageResourceType.GetProperty(ErrorMessageResourceName);
                    if (errorProperty != null)
                    {
                        return new ValidationResult((string?)errorProperty.GetValue(null, null));
                    }
                }
            }
        }

        return ValidationResult.Success;
    }
}

视图模型

public class CreateMembershipViewModel
{
    public Membership Membership { get; set; }
}

表单视图

<form asp-controller="Membership" asp-action="Add" method="post" enctype="multipart/form-data">
    <div asp-validation-summary="All" class="text-danger"></div>
    <div class="card-body">
        <div class="form-group">
            <label asp-for="Membership.Name"></label>
            <input asp-for="Membership.Name" class="form-control" required />
            <span asp-validation-for="Membership.Name" class="text-danger"></span>
        </div>

        <div class="form-group">
            <label asp-for="Membership.DaysActive"></label>
            <input asp-for="Membership.DaysActive" class="form-control" type="number" />
            <span asp-validation-for="Membership.DaysActive" class="text-danger"></span>
        </div>

        <div class="form-group">
            <label asp-for="Membership.MonthsActive"></label>
            <input asp-for="Membership.MonthsActive" class="form-control" type="number" />
            <span asp-validation-for="Membership.MonthsActive" class="text-danger"></span>
        </div>

        <div class="form-group">
            <label asp-for="Membership.YearsActive"></label>
            <input asp-for="Membership.YearsActive" class="form-control" type="number" />
            <span asp-validation-for="Membership.YearsActive" class="text-danger"></span>
        </div>
    </div>

    <div class="card-footer">
        <div class="form-group">
            <button class="btn btn-primary" type="submit">@CommonResources.Add</button>
        </div>
    </div>
</form>

问题原因

ASP.NET Core的客户端非侵入式验证仅默认支持内置验证特性(如Required、StringLength),自定义的ValidationAttribute如果没有实现客户端验证逻辑,只会在服务器端触发验证,客户端不会拦截表单提交。

解决方案

方案一:给自定义特性添加客户端验证支持

修改RequiredIfAttribute,让它实现IClientModelValidator接口,生成客户端验证规则:

public class RequiredIfAttribute : ValidationAttribute, IClientModelValidator
{
    private readonly string[] dependentProperties;

    public RequiredIfAttribute(params string[] dependentProperties)
    {
        this.dependentProperties = dependentProperties;
    }

    protected override ValidationResult IsValid(object? value, ValidationContext validationContext)
    {
        if (value == null)
        {
            var notNullProps = 0;
            foreach (var dependentProperty in dependentProperties)
            {
                var dependentPropertyValue = validationContext.ObjectInstance?.GetType()?
                    .GetProperty(dependentProperty)?
                    .GetValue(validationContext.ObjectInstance);

                if (dependentPropertyValue != null && !string.IsNullOrEmpty(dependentPropertyValue.ToString()))
                {
                    notNullProps++;
                }
            }

            if (notNullProps == 0)
            {
                string errorMsg = string.Empty;
                if (ErrorMessageResourceType != null && ErrorMessageResourceName != null) 
                {
                    var errorProperty = ErrorMessageResourceType.GetProperty(ErrorMessageResourceName);
                    if (errorProperty != null)
                    {
                        errorMsg = (string?)errorProperty.GetValue(null, null) ?? "至少需要填写一个活跃时长字段";
                    }
                }
                else
                {
                    errorMsg = ErrorMessage ?? "至少需要填写一个活跃时长字段";
                }
                return new ValidationResult(errorMsg);
            }
        }

        return ValidationResult.Success;
    }

    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-dependentprops", string.Join(",", dependentProperties));
    }

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

        attributes.Add(key, value);
        return true;
    }
}

然后在视图中添加客户端验证脚本,确保引入jQuery验证相关文件后,添加自定义验证方法:

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
    <script>
        // 自定义验证方法
        $.validator.addMethod("requiredif", function (value, element, params) {
            // 检查当前字段是否有值
            if (value && value.trim() !== "") {
                return true;
            }

            // 拆分依赖属性列表
            var dependentProps = params.split(',');
            var hasAnyValue = false;

            // 遍历依赖属性,检查是否有值
            dependentProps.forEach(function(propName) {
                // 构造表单字段选择器(因属性嵌套在Membership下,需加前缀)
                var fieldSelector = '[name="Membership.' + propName + '"]';
                var dependentValue = $(fieldSelector).val();
                if (dependentValue && dependentValue.trim() !== "") {
                    hasAnyValue = true;
                }
            });

            // 若当前字段为空且所有依赖字段也为空,验证失败
            return hasAnyValue;
        });

        // 适配非侵入式验证
        $.validator.unobtrusive.adapters.add("requiredif", ["dependentprops"], function (options) {
            options.rules["requiredif"] = options.params.dependentprops;
            options.messages["requiredif"] = options.message;
        });
    </script>
}

方案二:使用模型级验证(IValidatableObject)

如果跨字段验证逻辑复杂,更推荐在模型级别实现IValidatableObject接口,统一处理验证逻辑:

修改Membership类:

public class Membership : IValidatableObject
{
    [Display(ResourceType = typeof(MembershipResources), Name = nameof(Name))]
    public string Name { get; set; }

    [Display(ResourceType = typeof(MembershipResources), Name = nameof(DaysActive))]
    public int? DaysActive { get; set; }

    [Display(ResourceType = typeof(MembershipResources), Name = nameof(MonthsActive))]
    public int? MonthsActive { get; set; }

    [Display(ResourceType = typeof(MembershipResources), Name = nameof(YearsActive))]
    public int? YearsActive { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        // 检查三个字段是否都为空
        if (!DaysActive.HasValue && !MonthsActive.HasValue && !YearsActive.HasValue)
        {
            // 返回错误信息,指定关联字段
            yield return new ValidationResult(
                MembershipResources.ErrorAtLeastOneActiveField, // 需在资源文件中定义该错误信息
                new[] { nameof(DaysActive), nameof(MonthsActive), nameof(YearsActive) });
        }
    }
}

这种方式下,客户端验证仍需手动编写脚本,逻辑类似方案一:

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
    <script>
        $(function() {
            $('form').submit(function(e) {
                var days = $('[name="Membership.DaysActive"]').val();
                var months = $('[name="Membership.MonthsActive"]').val();
                var years = $('[name="Membership.YearsActive"]').val();

                // 检查三个字段是否都为空
                if ((!days || days.trim() === "") && 
                    (!months || months.trim() === "") && 
                    (!years || years.trim() === "")) {
                    e.preventDefault();
                    // 显示错误信息(可以更新验证摘要或字段提示)
                    $('.text-danger[asp-validation-summary]').text(MembershipResources.ErrorAtLeastOneActiveField);
                    // 也可以给对应字段添加错误样式
                    $('[name="Membership.DaysActive"], [name="Membership.MonthsActive"], [name="Membership.YearsActive"]').addClass('is-invalid');
                }
            });
        });
    </script>
}

注意事项

  • 确保视图中已引入jQuery、jquery.validate.js和jquery.validate.unobtrusive.js(_ValidationScriptsPartial通常包含这些文件)。
  • 若属性嵌套在视图模型的子对象中(如Membership属性),客户端脚本中字段名称需加上前缀(如Membership.DaysActive)。

内容的提问来源于stack exchange,提问作者EasyPeasy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 17:57:33