【问题标题】:MVC3 Validation - Require One From GroupMVC3 验证 - 需要组中的一个
【发布时间】:2016-08-13 02:02:30
【问题描述】:

给定以下视图模型:

public class SomeViewModel
{
  public bool IsA { get; set; }
  public bool IsB { get; set; }
  public bool IsC { get; set; } 
  //... other properties
}

我希望创建一个自定义属性来验证至少一个可用属性是否为真。我设想能够将属性附加到属性并分配一个组名,如下所示:

public class SomeViewModel
{
  [RequireAtLeastOneOfGroup("Group1")]
  public bool IsA { get; set; }

  [RequireAtLeastOneOfGroup("Group1")]
  public bool IsB { get; set; }

  [RequireAtLeastOneOfGroup("Group1")]
  public bool IsC { get; set; } 

  //... other properties

  [RequireAtLeastOneOfGroup("Group2")]
  public bool IsY { get; set; }

  [RequireAtLeastOneOfGroup("Group2")]
  public bool IsZ { get; set; }
}

我想在表单提交之前在客户端进行验证,因为表单中的值发生了变化,这就是为什么我希望尽可能避免使用类级属性。

这将需要服务器端和客户端验证来定位具有作为自定义属性参数传入的相同组名值的所有属性。这可能吗?非常感谢任何指导。

【问题讨论】:

    标签: asp.net-mvc asp.net-mvc-3 jquery-validate unobtrusive-validation


    【解决方案1】:

    这是一种继续进行的方法(还有其他方法,我只是举例说明一种与您的视图模型相匹配的方法):

    [AttributeUsage(AttributeTargets.Property)]
    public class RequireAtLeastOneOfGroupAttribute: ValidationAttribute, IClientValidatable
    {
        public RequireAtLeastOneOfGroupAttribute(string groupName)
        {
            ErrorMessage = string.Format("You must select at least one value from group \"{0}\"", groupName);
            GroupName = groupName;
        }
    
        public string GroupName { get; private set; }
    
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            foreach (var property in GetGroupProperties(validationContext.ObjectType))
            {
                var propertyValue = (bool)property.GetValue(validationContext.ObjectInstance, null);
                if (propertyValue)
                {
                    // at least one property is true in this group => the model is valid
                    return null;
                }
            }
            return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
        }
    
        private IEnumerable<PropertyInfo> GetGroupProperties(Type type)
        {
            return
                from property in type.GetProperties()
                where property.PropertyType == typeof(bool)
                let attributes = property.GetCustomAttributes(typeof(RequireAtLeastOneOfGroupAttribute), false).OfType<RequireAtLeastOneOfGroupAttribute>()
                where attributes.Count() > 0
                from attribute in attributes
                where attribute.GroupName == GroupName
                select property;
        }
    
        public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
        {
            var groupProperties = GetGroupProperties(metadata.ContainerType).Select(p => p.Name);
            var rule = new ModelClientValidationRule
            {
                ErrorMessage = this.ErrorMessage
            };
            rule.ValidationType = string.Format("group", GroupName.ToLower());
            rule.ValidationParameters["propertynames"] = string.Join(",", groupProperties);
            yield return rule;
        }
    }
    

    现在,让我们定义一个控制器:

    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            var model = new SomeViewModel();
            return View(model);        
        }
    
        [HttpPost]
        public ActionResult Index(SomeViewModel model)
        {
            return View(model);
        }
    }
    

    还有一个观点:

    @model SomeViewModel
    
    <script src="@Url.Content("~/Scripts/jquery.validate.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.js")" type="text/javascript"></script>
    
    @using (Html.BeginForm())
    {
        @Html.EditorFor(x => x.IsA)
        @Html.ValidationMessageFor(x => x.IsA)
        <br/>
        @Html.EditorFor(x => x.IsB)<br/>
        @Html.EditorFor(x => x.IsC)<br/>
    
        @Html.EditorFor(x => x.IsY)
        @Html.ValidationMessageFor(x => x.IsY)
        <br/>
        @Html.EditorFor(x => x.IsZ)<br/>
        <input type="submit" value="OK" />
    }
    

    剩下的最后一部分是为客户端验证注册适配器:

    jQuery.validator.unobtrusive.adapters.add(
        'group', 
        [ 'propertynames' ],
        function (options) {
            options.rules['group'] = options.params;
            options.messages['group'] = options.message;
        }
    );
    
    jQuery.validator.addMethod('group', function (value, element, params) {
        var properties = params.propertynames.split(',');
        var isValid = false;
        for (var i = 0; i < properties.length; i++) {
            var property = properties[i];
            if ($('#' + property).is(':checked')) {
                isValid = true;
                break;
            }
        }
        return isValid;
    }, '');
    

    根据您的具体要求,可能会调整代码。

    【讨论】:

    • 到目前为止,这是我收到的问题的最佳回答之一。不仅是为了迅速而明确的答案,而且是为了提供一个我可以从中学习的有效示例。我希望我可以多次将您的回复标记为答案。
    • 这行jQuery代码中的一个小修正var properties = params.propertynames.split(',');应该是var properties = params.properties.split(',');
    • 这是一个很好的回应 (+1)。然而,我还有一个小问题。 RequireAtLeastOneOfGroup 属性应用于三个不同的字段,生成三个客户端规则,在摘要中显示三个错误。有没有办法将规则分组为单个客户端规则,以便验证状态一次涵盖所有三个字段?
    • @MattHamsmith 您可以通过使用每个组的一个成员的单独验证消息而不是摘要 @Html.ValidationMessageFor(x =&gt; x.OneMemberOfTheGroup) 来解决此问题,然后您可以在摘要调用中添加 true 以排除属性错误.我知道不理想,但它是解决问题的一种方法
    • 你有一行写着rule.ValidationType = string.Format("group", GroupName.ToLower());,这似乎不对,因为组名不会在任何地方进入字符串。那应该是什么?
    【解决方案2】:

    使用来自 jquery-validation 团队的require_from_group

    jQuery-validation 项目在 src 文件夹中有一个名为 additional 的子文件夹。 您可以查看here

    在该文件夹中,我们有许多不常见的额外验证方法,这就是默认情况下不添加它们的原因。

    正如您在该文件夹中看到的那样,它存在很多方法,您需要通过选择您实际需要的验证方法来选择。

    根据您的问题,您需要的验证方法从附加文件夹命名为 require_from_group。 只需下载位于here 的相关文件并将其放入您的Scripts 应用程序文件夹即可。

    此方法的文档对此进行了说明:

    让您说“必须填充与选择器 Y 匹配的至少 X 个输入。”

    最终结果是这些输入都不是:

    ...除非其中至少有一个被填满,否则将生效。

    部件号:{require_from_group: [1,".productinfo"]}, 描述:{require_from_group: [1,".productinfo"]}

    options[0]:组中必须填写的字段数 options2: CSS 选择器,定义了一组有条件的必填字段

    为什么你需要选择这个实现:

    此验证方法是通用的,适用于每个input(文本、复选框、单选等)、textareaselect此方法还允许您指定需要填写的所需输入的最小数量 例如

    partnumber:     {require_from_group: [2,".productinfo"]},
    category:       {require_from_group: [2,".productinfo"]},
    description:    {require_from_group: [2,".productinfo"]}
    

    我创建了两个类 RequireFromGroupAttributeRequireFromGroupFieldAttribute,它们将在服务器端和客户端验证方面为您提供帮助

    RequireFromGroupAttribute类定义

    RequireFromGroupAttribute 仅派生自 Attribute。该类仅用于配置,例如设置验证需要填写的字段数。您需要向此类提供 CSS 选择器类,验证方法将使用该选择器类来获取同一组中的所有元素。因为必填字段的默认数量为 1,所以此属性仅在指定组中的最低要求大于默认数量时用于装饰您的模型。

    [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
    public class RequireFromGroupAttribute : Attribute
    {
        public const short DefaultNumber = 1;
    
        public string Selector { get; set; }
    
        public short Number { get; set; }
    
        public RequireFromGroupAttribute(string selector)
        {
            this.Selector = selector;
            this.Number = DefaultNumber;
        }
    
        public static short GetNumberOfRequiredFields(Type type, string selector)
        {
            var requiredFromGroupAttribute = type.GetCustomAttributes<RequireFromGroupAttribute>().SingleOrDefault(a => a.Selector == selector);
            return requiredFromGroupAttribute?.Number ?? DefaultNumber;
        }
    }
    

    RequireFromGroupFieldAttribute 类定义

    RequireFromGroupFieldAttribute 派生自 ValidationAttribute 并实现 IClientValidatable。您需要在模型中参与组验证的每个属性上使用此类。您必须通过 css 选择器类。

    [AttributeUsage(AttributeTargets.Property)]
    public class RequireFromGroupFieldAttribute : ValidationAttribute, IClientValidatable
    {
        public string Selector { get; }
    
        public bool IncludeOthersFieldName { get; set; }
    
        public RequireFromGroupFieldAttribute(string selector)
            : base("Please fill at least {0} of these fields")
        {
            this.Selector = selector;
            this.IncludeOthersFieldName = true;
        }
    
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            var properties = this.GetInvolvedProperties(validationContext.ObjectType); ;
            var numberOfRequiredFields = RequireFromGroupAttribute.GetNumberOfRequiredFields(validationContext.ObjectType, this.Selector);
    
            var values = new List<object> { value };
            var otherPropertiesValues = properties.Where(p => p.Key.Name != validationContext.MemberName)
                                                  .Select(p => p.Key.GetValue(validationContext.ObjectInstance));
            values.AddRange(otherPropertiesValues);
    
            if (values.Count(s => !string.IsNullOrWhiteSpace(Convert.ToString(s))) >= numberOfRequiredFields)
            {
                return ValidationResult.Success;
            }
    
            return new ValidationResult(this.GetErrorMessage(numberOfRequiredFields, properties.Values), new List<string> { validationContext.MemberName });
        }
    
        public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
        {
            var properties = this.GetInvolvedProperties(metadata.ContainerType);
            var numberOfRequiredFields = RequireFromGroupAttribute.GetNumberOfRequiredFields(metadata.ContainerType, this.Selector);
            var rule = new ModelClientValidationRule
            {
                ValidationType = "requirefromgroup",
                ErrorMessage = this.GetErrorMessage(numberOfRequiredFields, properties.Values)
            };
            rule.ValidationParameters.Add("number", numberOfRequiredFields);
            rule.ValidationParameters.Add("selector", this.Selector);
    
            yield return rule;
        }
    
        private Dictionary<PropertyInfo, string> GetInvolvedProperties(Type type)
        {
            return type.GetProperties()
                       .Where(p => p.IsDefined(typeof(RequireFromGroupFieldAttribute)) &&
                                   p.GetCustomAttribute<RequireFromGroupFieldAttribute>().Selector == this.Selector)
                       .ToDictionary(p => p, p => p.IsDefined(typeof(DisplayAttribute)) ? p.GetCustomAttribute<DisplayAttribute>().Name : p.Name);
        }
    
        private string GetErrorMessage(int numberOfRequiredFields, IEnumerable<string> properties)
        {
            var errorMessage = string.Format(this.ErrorMessageString, numberOfRequiredFields);
            if (this.IncludeOthersFieldName)
            {
                errorMessage += ": " + string.Join(", ", properties);
            }
    
            return errorMessage;
        }
    }
    

    如何在你的视图模型中使用它?

    在你的模型中是如何使用它:

    public class SomeViewModel
    {
        internal const string GroupOne = "Group1";
        internal const string GroupTwo = "Group2";
    
        [RequireFromGroupField(GroupOne)]
        public bool IsA { get; set; }
    
        [RequireFromGroupField(GroupOne)]
        public bool IsB { get; set; }
    
        [RequireFromGroupField(GroupOne)]
        public bool IsC { get; set; }
    
        //... other properties
    
        [RequireFromGroupField(GroupTwo)]
        public bool IsY { get; set; }
    
        [RequireFromGroupField(GroupTwo)]
        public bool IsZ { get; set; }
    }
    

    默认情况下,您不需要使用RequireFromGroupAttribute 装饰模型,因为默认必填字段数为 1。但如果您希望必填字段的数量不同于 1,您可以执行以下操作:

    [RequireFromGroup(GroupOne, Number = 2)]
    public class SomeViewModel
    {
        //...
    }
    

    如何在你的视图代码中使用它?

    @model SomeViewModel
    
    <script src="@Url.Content("~/Scripts/jquery.validate.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/require_from_group.js")" type="text/javascript"></script>
    
    @using (Html.BeginForm())
    {
        @Html.CheckBoxFor(x => x.IsA, new { @class="Group1"})<span>A</span>
        @Html.ValidationMessageFor(x => x.IsA)
        <br />
        @Html.CheckBoxFor(x => x.IsB, new { @class = "Group1" }) <span>B</span><br />
        @Html.CheckBoxFor(x => x.IsC, new { @class = "Group1" }) <span>C</span><br />
    
        @Html.CheckBoxFor(x => x.IsY, new { @class = "Group2" }) <span>Y</span>
        @Html.ValidationMessageFor(x => x.IsY)
        <br />
        @Html.CheckBoxFor(x => x.IsZ, new { @class = "Group2" })<span>Z</span><br />
        <input type="submit" value="OK" />
    }
    

    请注意,您在使用 RequireFromGroupField 属性时指定的组选择器在您的视图中使用,方法是将其指定为组中涉及的每个输入中的类。

    这就是服务器端验证的全部内容。

    我们来谈谈客户端验证。

    如果您检查RequireFromGroupFieldAttribute 类中的GetClientValidationRules 实现,您将看到我使用字符串requirefromgroup 而不是require_from_group 作为ValidationType 属性的方法名称。这是因为 ASP.Net MVC 只允许验证类型的名称包含字母数字字符,并且不能以数字开头。所以你需要添加以下javascript:

    $.validator.unobtrusive.adapters.add("requirefromgroup", ["number", "selector"], function (options) {
        options.rules["require_from_group"] = [options.params.number, options.params.selector];
        options.messages["require_from_group"] = options.message;
    });
    

    javascript 部分非常简单,因为在适配器函数的实现中,我们只是将验证委托给正确的require_from_group 方法。

    因为它适用于所有类型的inputtextareaselect 元素,我可能认为这种方式更通用。

    希望有帮助!

    【讨论】:

    • 仅供参考,我认为您可以通过将属性的 Selector 属性更改为 GroupIdentifier 属性来避免向标记中的输入添加类。然后在 GetClientValidationRules 你可以添加这个: rule.ValidationParameters.Add("group-identifier", this.GroupIdentifier); rule.ValidationParameters.Add("selector", "[data-group-identifier=" + this.GroupIdentifier + "]");
    【解决方案3】:

    我在我的应用程序中实现了 Darin 的绝妙答案,除了我为字符串而不是布尔值添加了它。这是用于名称/公司或电话/电子邮件之类的东西。我喜欢它,除了一个小小的挑剔。

    我尝试在没有工作电话、手机、家庭电话或电子邮件的情况下提交表单。我在客户端收到了四个单独的验证错误。这对我来说很好,因为它让用户确切地知道可以填写哪些字段以使错误消失。

    我输入了一个电子邮件地址。现在电子邮件下的单一验证消失了,但三个仍然在电话号码下。这些也不再是错误。

    所以,我重新分配了检查验证的 jQuery 方法来解决这个问题。代码如下。希望它可以帮助某人。

    jQuery.validator.prototype.check = function (element) {
    
       var elements = [];
       elements.push(element);
       var names;
    
       while (elements.length > 0) {
          element = elements.pop();
          element = this.validationTargetFor(this.clean(element));
    
          var rules = $(element).rules();
    
          if ((rules.group) && (rules.group.propertynames) && (!names)) {
             names = rules.group.propertynames.split(",");
             names.splice($.inArray(element.name, names), 1);
    
             var name;
             while (name = names.pop()) {
                elements.push($("#" + name));
             }
          }
    
          var dependencyMismatch = false;
          var val = this.elementValue(element);
          var result;
    
          for (var method in rules) {
             var rule = { method: method, parameters: rules[method] };
             try {
    
                result = $.validator.methods[method].call(this, val, element, rule.parameters);
    
                // if a method indicates that the field is optional and therefore valid,
                // don't mark it as valid when there are no other rules
                if (result === "dependency-mismatch") {
                   dependencyMismatch = true;
                   continue;
                }
                dependencyMismatch = false;
    
                if (result === "pending") {
                   this.toHide = this.toHide.not(this.errorsFor(element));
                   return;
                }
    
                if (!result) {
                   this.formatAndAdd(element, rule);
                   return false;
                }
             } catch (e) {
                if (this.settings.debug && window.console) {
                   console.log("Exception occurred when checking element " + element.id + ", check the '" + rule.method + "' method.", e);
                }
                throw e;
             }
          }
          if (dependencyMismatch) {
             return;
          }
          if (this.objectLength(rules)) {
             this.successList.push(element);
          }
       }
    
       return true;
    };
    

    【讨论】:

      【解决方案4】:

      我知道这是一个旧线程,但我只是遇到了同样的情况,并找到了一些解决方案,并看到了一个解决了上面 Matt 问题的解决方案,所以我想我会分享给那些遇到这个答案的人。签出:MVC3 unobtrusive validation group of inputs

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-10-01
        • 1970-01-01
        • 1970-01-01
        • 2017-05-24
        • 2016-06-11
        相关资源
        最近更新 更多