Validación de MVC3: Requerir uno del grupo

Dado el siguiente modelo de vista:

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

Deseo crear un atributo personalizado que valida que al menos una de las propiedades disponibles sea verdadera. Me imagino poder adjuntar un atributo a una propiedad y asignarle un nombre de grupo así:

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

Me gustaría validar en el lado del cliente antes de la presentación del formulario como valores en el cambio de formulario, por lo que prefiero evitar un atributo de nivel de clase si es posible.

Esto requeriría tanto la validación del lado del servidor como del lado del cliente para localizar todas las propiedades que tengan valores de nombre de grupo idénticos pasados ​​como el parámetro para el atributo personalizado. es posible? Cualquier orientación es muy apreciada.

Esta es una forma de proceder (hay otras formas, solo estoy ilustrando una que coincidiría con su modelo de vista tal 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; } } 

Ahora, definamos un controlador:

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

y una vista:

 @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)
}

La última parte que queda sería registrar adaptadores para la validación del lado del 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; }, ''); 

En función de sus requisitos específicos, el código podría adaptarse.

Uso de require_from_group del equipo jquery-validation:

El proyecto jQuery-validation tiene una subcarpeta en la carpeta src llamada adicional . Puedes consultarlo aquí .

En esa carpeta tenemos muchos métodos de validación adicionales que no son comunes, por eso no se agregan por defecto.

Como puede ver en esa carpeta, existen tantos métodos que debe elegir eligiendo qué método de validación realmente necesita.

Según su pregunta, el método de validación que necesita se llama require_from_group de la carpeta adicional. Simplemente descargue este archivo asociado que se encuentra aquí y póngalo en su carpeta de aplicaciones de Scripts .

La documentación de este método explica esto:

Le permite decir que “al menos X entradas que coinciden con el selector Y deben llenarse”.

El resultado final es que ninguna de estas entradas:

… validará a menos que al menos uno de ellos esté lleno.

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

opciones [0]: número de campos que deben rellenarse en las opciones de grupo 2 : selector de CSS que define el grupo de campos condicionalmente requeridos

Por qué necesita elegir esta implementación:

Este método de validación es genérico y funciona para cada input (texto, checkbox, radio, etc.), área de textarea y select . Este método también le permite especificar el número mínimo de entradas requeridas que deben llenarse, por ejemplo

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

RequireFromGroupAttribute dos clases, RequireFromGroupAttribute y RequireFromGroupFieldAttribute que te ayudarán tanto en las validaciones del lado del servidor como del lado del cliente

RequireFromGroupAttribute clase RequireFromGroupAttribute

RequireFromGroupAttribute solo se deriva de Attribute . La clase se usa solo para configuración, por ejemplo, establecer la cantidad de campos que se deben llenar para la validación. Debe proporcionar a esta clase la clase de selector de CSS que usará el método de validación para obtener todos los elementos en el mismo grupo. Debido a que el número predeterminado de campos obligatorios es 1, este atributo solo se usa para decorar su modelo si el requisito mínimo en el grupo spcefied es mayor que el número predeterminado.

 [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 clase RequireFromGroupFieldAttribute

RequireFromGroupFieldAttribute que se deriva de ValidationAttribute e implementa IClientValidatable . Debe usar esta clase en cada propiedad de su modelo que participe en la validación de su grupo. Debe pasar la clase de selector 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; } } 

¿Cómo usarlo en su modelo de vista?

En su modelo aquí está cómo usarlo:

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

De forma predeterminada, no necesita decorar su modelo con RequireFromGroupAttribute porque la cantidad predeterminada de campos obligatorios es 1. Pero si desea que un número de campos obligatorios sea diferente a 1, puede hacer lo siguiente:

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

¿Cómo usarlo en tu código de vista?

 @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 el selector de grupo que especificó cuando usa el atributo RequireFromGroupField se usa en su vista al especificarlo como una clase en cada entrada involucrada en sus grupos.

Eso es todo por la validación del lado del servidor.

Hablemos de la validación del lado del cliente.

Si comprueba la implementación de RequireFromGroupFieldAttribute en la clase RequireFromGroupFieldAttribute , verá que estoy usando la cadena requirefromgroup y no require_from_group como el nombre del método para la propiedad ValidationType . Eso es porque ASP.Net MVC solo permite que el nombre del tipo de validación contenga caracteres alfanuméricos y no debe comenzar con un número. Por lo tanto, debe agregar el siguiente 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; }); 

La parte javascript es realmente simple porque en la implementación de la función de adaptación simplemente delegamos la validación al método correcto require_from_group .

Debido a que funciona con todo tipo de input , área de textarea y elementos select , puedo pensar que esta forma es más genérica.

¡Espero que ayude!

Implementé la impresionante respuesta de Darin en mi aplicación, excepto que la agregué para cadenas y no para valores booleanos. Esto fue para cosas como nombre / compañía, o teléfono / correo electrónico. Me encantó, excepto por un pequeño detalle.

Traté de enviar mi formulario sin un teléfono de trabajo, teléfono móvil, teléfono residencial o correo electrónico. Tengo cuatro errores de validación por separado del lado del cliente. Esto está bien para mí porque les permite a los usuarios saber exactamente qué campo (s) pueden rellenarse para que desaparezca el error.

Ingresé una dirección de correo electrónico. Ahora la validación individual en el correo electrónico desapareció, pero las tres permanecieron bajo los números de teléfono. Estos ya no son más errores.

Entonces, reasigné el método jQuery que verifica la validación para dar cuenta de esto. Código a continuación. Espero que ayude a alguien.

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

Sé que este es un hilo viejo, pero me encontré con el mismo escenario y encontré algunas soluciones y vi una que resuelve la pregunta de Matt anterior, así que pensé que la compartiría para aquellos que encuentren esta respuesta. Check out: MVC3 grupo de validación discreto de entradas