Validação MVC3 – Exigir um do grupo

Dado o seguinte viewmodel:

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

Desejo criar um atributo personalizado que valide pelo menos uma das propriedades disponíveis como verdadeiras. Eu imagino ser capaz de append um atributo a uma propriedade e atribuir um nome de grupo da seguinte forma:

 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; } } 

Eu gostaria de validar no lado do cliente antes de enviar o formulário como valores na alteração de formulário, e é por isso que eu prefiro evitar um atributo de nível de class, se possível.

Isso exigiria a validação do lado do servidor e do lado do cliente para localizar todas as propriedades com valores de nome de grupo idênticos passados ​​como o parâmetro para o atributo customizado. Isso é possível? Qualquer orientação é muito apreciada.

Aqui está uma maneira de prosseguir (há outras maneiras, estou apenas ilustrando uma que correspondesse ao seu modelo de visão como está):

 [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 GetGroupProperties(Type type) { return from property in type.GetProperties() where property.PropertyType == typeof(bool) let attributes = property.GetCustomAttributes(typeof(RequireAtLeastOneOfGroupAttribute), false).OfType() where attributes.Count() > 0 from attribute in attributes where attribute.GroupName == GroupName select property; } public IEnumerable 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; } } 

Agora, vamos definir um controlador:

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

e uma visão:

 @model SomeViewModel   @using (Html.BeginForm()) { @Html.EditorFor(x => x.IsA) @Html.ValidationMessageFor(x => x.IsA) 
@Html.EditorFor(x => x.IsB)
@Html.EditorFor(x => x.IsC)
@Html.EditorFor(x => x.IsY) @Html.ValidationMessageFor(x => x.IsY)
@Html.EditorFor(x => x.IsZ)
}

A última parte restante seria registrar os adaptadores para a validação do lado do cliente:

 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; }, ''); 

Com base em seus requisitos específicos, o código pode ser adaptado.

Uso de require_from_group da equipe de validação do jquery:

Projeto de validação jQuery tem uma sub-pasta na pasta src chamada adicional . Você pode conferir aqui .

Nessa pasta, temos muitos methods de validação adicionais que não são comuns e é por isso que eles não são adicionados por padrão.

Como você vê nessa pasta, existem tantos methods que você precisa escolher escolhendo qual método de validação você realmente precisa.

Com base na sua pergunta, o método de validação necessário é denominado require_from_group da pasta adicional. Basta baixar este arquivo associado que está localizado aqui e colocá-lo na pasta do aplicativo Scripts .

A documentação deste método explica isso:

Permite que você diga “pelo menos X inputs que correspondem ao seletor Y devem ser preenchidas”.

O resultado final é que nenhuma dessas inputs:

… validará a menos que pelo menos um deles esteja preenchido.

partnumber: {require_from_group: [1, “. productinfo”]}, description: {require_from_group: [1, “. productinfo”]}

options [0]: número de campos que devem ser preenchidos nas opções do grupo 2 : Seletor CSS que define o grupo de campos obrigatórios

Por que você precisa escolher esta implementação:

Este método de validação é genérico e funciona para cada input (texto, checkbox de seleção, rádio etc), textarea e select . Este método também permite especificar o número mínimo de inputs necessárias que precisam ser preenchidas, por exemplo

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

Eu criei duas classs RequireFromGroupAttribute e RequireFromGroupFieldAttribute que irão ajudá-lo nas validações do lado do servidor e do lado do cliente

RequireFromGroupAttribute class RequireFromGroupAttribute

RequireFromGroupAttribute deriva apenas do Attribute . A class é usada apenas para configuração, por exemplo, definindo o número de campos que precisam ser preenchidos para a validação. Você precisa fornecer a essa class a class do seletor CSS que será usada pelo método de validação para obter todos os elementos no mesmo grupo. Como o número padrão de campos obrigatórios é 1, esse atributo é usado apenas para decorar seu modelo se o requisito mínimo no grupo especificado for maior que o número padrão.

 [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().SingleOrDefault(a => a.Selector == selector); return requiredFromGroupAttribute?.Number ?? DefaultNumber; } } 

RequireFromGroupFieldAttribute class RequireFromGroupFieldAttribute

RequireFromGroupFieldAttribute que deriva de ValidationAttribute e implementa IClientValidatable . Você precisa usar essa class em cada propriedade em seu modelo que participa da validação do seu grupo. Você deve passar pela class do seletor 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 { 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 { validationContext.MemberName }); } public IEnumerable 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 GetInvolvedProperties(Type type) { return type.GetProperties() .Where(p => p.IsDefined(typeof(RequireFromGroupFieldAttribute)) && p.GetCustomAttribute().Selector == this.Selector) .ToDictionary(p => p, p => p.IsDefined(typeof(DisplayAttribute)) ? p.GetCustomAttribute().Name : p.Name); } private string GetErrorMessage(int numberOfRequiredFields, IEnumerable properties) { var errorMessage = string.Format(this.ErrorMessageString, numberOfRequiredFields); if (this.IncludeOthersFieldName) { errorMessage += ": " + string.Join(", ", properties); } return errorMessage; } } 

Como usá-lo no seu modelo de visualização?

Em seu modelo aqui está como usá-lo:

 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; } } 

Por padrão, você não precisa decorar seu modelo com RequireFromGroupAttribute porque o número padrão de campos obrigatórios é 1. Mas se você quiser que um número de campos obrigatórios seja diferente de 1, você pode fazer o seguinte:

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

Como usá-lo em seu código de visão?

 @model SomeViewModel    @using (Html.BeginForm()) { @Html.CheckBoxFor(x => x.IsA, new { @class="Group1"})A @Html.ValidationMessageFor(x => x.IsA) 
@Html.CheckBoxFor(x => x.IsB, new { @class = "Group1" }) B
@Html.CheckBoxFor(x => x.IsC, new { @class = "Group1" }) C
@Html.CheckBoxFor(x => x.IsY, new { @class = "Group2" }) Y @Html.ValidationMessageFor(x => x.IsY)
@Html.CheckBoxFor(x => x.IsZ, new { @class = "Group2" })Z
}

Observe que o seletor de grupo que você especificou ao usar o atributo RequireFromGroupField é usado em sua visualização, especificando-o como uma class em cada input envolvida em seus grupos.

Isso é tudo para a validação do lado do servidor.

Vamos falar sobre a validação do lado do cliente.

Se você verificar a implementação GetClientValidationRules na class RequireFromGroupFieldAttribute , verá que estou usando a cadeia requirefromgroup e não require_from_group como o nome do método para a propriedade ValidationType . Isso ocorre porque o ASP.Net MVC permite apenas que o nome do tipo de validação contenha caracteres alfanuméricos e não inicie com um número. Então você precisa adicionar o seguinte 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; }); 

A parte javascript é realmente simples porque na implementação da function adaptadora nós apenas delegamos a validação para o método require_from_group correto.

Porque funciona com todo tipo de input , textarea e select elements, eu acho que dessa forma é mais genérico.

Espero que ajude!

Eu implementei a resposta incrível de Darin no meu aplicativo, exceto que eu adicionei para strings e não valores booleanos. Isso era para coisas como nome / empresa ou telefone / email. Eu adorei, exceto por um pequeno erro.

Tentei enviar meu formulário sem um telefone comercial, telefone celular, telefone residencial ou e-mail. Eu tenho quatro erros de validação separados do lado do cliente. Isso é bom para mim porque permite que os usuários saibam exatamente quais campos podem ser preenchidos para fazer com que o erro desapareça.

Eu digitei um endereço de e-mail. Agora a única validação sob e-mail foi embora, mas os três permaneceram sob os números de telefone. Estes também não são mais erros.

Então, eu reatribui o método jQuery que verifica a validação para considerar isso. Código abaixo. Espero que ajude alguém.

 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; }; 

Eu sei que este é um tópico antigo, mas acabei de encontrar o mesmo cenário e encontrei algumas soluções e vi uma que resolveu a questão de Matt acima, então pensei em compartilhar com aqueles que se deparam com essa resposta. Check out: grupo de validação discreto MVC3 de inputs