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
相关产品推荐
相关产品推荐

