MVC3验证 - 从组中需要一个

Sha*_*awn 36 asp.net-mvc jquery-validate unobtrusive-validation asp.net-mvc-3

给定以下viewmodel:

public class SomeViewModel
{
  public bool IsA { get; set; }
  public bool IsB { get; set; }
  public bool IsC { get; set; } 
  //... other properties
}
Run Code Online (Sandbox Code Playgroud)

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

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; }
}
Run Code Online (Sandbox Code Playgroud)

我希望在表单提交之前在客户端验证表单更改中的值,这就是为什么我更愿意在可能的情况下避免使用类级别属性.

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

Dar*_*rov 75

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

[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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

    [HttpPost]
    public ActionResult Index(SomeViewModel model)
    {
        return View(model);
    }
}
Run Code Online (Sandbox Code Playgroud)

和一个观点:

@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" />
}
Run Code Online (Sandbox Code Playgroud)

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

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;
}, '');
Run Code Online (Sandbox Code Playgroud)

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

  • 到目前为止,对我收到的问题的最好回应之一.不仅是为了迅速而明确的答案,而且是为了提供一个我可以学习的功能性例子.我希望我能多次将你的回答标记为答案. (4认同)
  • 这是一个很好的回应(+1).不过,我还有一个小问题.RequireAtLeastOneOfGroup属性应用于三个不同的字段,生成三个客户端规则,在摘要中显示三个错误.有没有办法将规则分组到单个客户端规则中,以便验证状态一次涵盖所有三个字段? (2认同)

Cod*_*und 5

使用require_from_group从jQuery的验证团队:

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

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

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

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

此方法的文档解释了这一点:

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

最终结果是这些输入都没有:

...除非其中至少一个被填充,否则将进行验证。

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

options[0]: 组中必须填写的字段数 options 2 : CSS 选择器,用于定义条件需要的字段组

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

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

partnumber:     {require_from_group: [2,".productinfo"]},
category:       {require_from_group: [2,".productinfo"]},
description:    {require_from_group: [2,".productinfo"]}
Run Code Online (Sandbox Code Playgroud)

我创建了两个类RequireFromGroupAttributeRequireFromGroupFieldAttribute它们将帮助您进行服务器端和客户端验证

RequireFromGroupAttribute 类定义

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

[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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

在您的模型中,如何使用它:

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; }
}
Run Code Online (Sandbox Code Playgroud)

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

[RequireFromGroup(GroupOne, Number = 2)]
public class SomeViewModel
{
    //...
}
Run Code Online (Sandbox Code Playgroud)

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

@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" />
}
Run Code Online (Sandbox Code Playgroud)

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

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

让我们谈谈客户端验证。

如果您检查类中的GetClientValidationRules实现,RequireFromGroupFieldAttribute您将看到我使用的是字符串,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;
});
Run Code Online (Sandbox Code Playgroud)

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

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

希望有帮助!