Convalida MVC3: richiede uno dal gruppo

Data la seguente viewmodel:

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

Desidero creare un attributo personalizzato che convalidi che almeno una delle proprietà disponibili sia vera. Immagino di poter colbind un attributo a una proprietà e assegnare un nome a un gruppo in questo modo:

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

Vorrei convalidare sul lato client prima dell’invio del modulo come valori nella modifica del modulo, motivo per cui preferisco evitare un attributo a livello di class se ansible.

Ciò richiederebbe sia la convalida lato server sia quella lato client per individuare tutte le proprietà con valori di nome gruppo identici passati come parametro per l’attributo personalizzato. È ansible? Qualsiasi guida è molto apprezzata.

    Ecco un modo per procedere (ci sono altri modi, sto solo illustrando uno che corrisponde al modello di visualizzazione così com’è):

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

    Ora, definiamo un controller:

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

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

    L’ultima parte rimasta sarebbe quella di registrare gli adattatori per la convalida del lato client:

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

    In base alle tue esigenze specifiche, il codice potrebbe essere adattato.

    Uso di require_from_group dal team di validazione jquery:

    jQuery-validation project ha una sottocartella nella cartella src chiamata additional . Puoi verificarlo qui .

    In quella cartella abbiamo molti metodi di convalida aggiuntivi che non sono comuni, motivo per cui non vengono aggiunti per impostazione predefinita.

    Come vedi in quella cartella esistono tanti metodi che devi scegliere scegliendo il metodo di validazione di cui hai effettivamente bisogno.

    In base alla tua domanda, il metodo di convalida necessario è denominato require_from_group dalla cartella aggiuntiva. Basta scaricare il file associato che si trova qui e inserirlo nella cartella dell’applicazione Scripts .

    La documentazione di questo metodo spiega questo:

    Diciamo che “almeno gli ingressi X che corrispondono al selettore Y devono essere riempiti”.

    Il risultato finale è che nessuno di questi input:

    … verrà convalidato a meno che almeno uno di essi sia stato riempito.

    numero parte: {require_from_group: [1, “. productinfo”]}, descrizione: {require_from_group: [1, “. productinfo”]}

    opzioni [0]: numero di campi che devono essere compilati nelle opzioni di gruppo 2 : selettore CSS che definisce il gruppo di campi richiesti condizionatamente

    Perché è necessario scegliere questa implementazione:

    Questo metodo di validazione è generico e funziona per ogni input (testo, checkbox, radio ecc.), textarea e select . Questo metodo consente anche di specificare il numero minimo di input richiesti che devono essere riempiti, ad es

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

    Ho creato due classi RequireFromGroupAttribute e RequireFromGroupFieldAttribute che ti aiuteranno sia su validazioni lato server che lato client

    RequireFromGroupAttribute definizione della class

    RequireFromGroupAttribute deriva solo da Attribute . La class è utilizzata solo per la configurazione, ad esempio l’impostazione del numero di campi che devono essere riempiti per la convalida. È necessario fornire a questa class la class del selettore CSS che verrà utilizzata dal metodo di convalida per ottenere tutti gli elementi nello stesso gruppo. Poiché il numero predefinito di campi richiesti è 1, questo attributo viene utilizzato solo per decorare il modello se il requisito minimo nel gruppo specificato è maggiore del numero predefinito.

     [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 definizione della class

    RequireFromGroupFieldAttribute che deriva da ValidationAttribute e implementa IClientValidatable . È necessario utilizzare questa class su ogni proprietà del modello che partecipa alla convalida del gruppo. Devi passare la class del selettore 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; } } 

    Come usarlo nel tuo modello di vista?

    Nel tuo modello ecco come 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; } } 

    Per impostazione predefinita, non è necessario decorare il modello con RequireFromGroupAttribute perché il numero predefinito di campi richiesti è 1. Ma se si desidera che un numero di campi obbligatori sia diverso da 1, è ansible effettuare le seguenti operazioni:

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

    Come si usa nel codice di visualizzazione?

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

    Nota che il selettore di gruppo che hai specificato quando usi RequireFromGroupField attributo RequireFromGroupField è usato nella tua vista specificandolo come una class in ogni input coinvolto nei tuoi gruppi.

    Questo è tutto per la validazione lato server.

    Parliamo della convalida del lato client.

    Se si controlla l’implementazione RequireFromGroupFieldAttribute nella class RequireFromGroupFieldAttribute , si vedrà che sto usando la stringa requirefromgroup e non require_from_group come nome del metodo per la proprietà ValidationType . Questo perché ASP.Net MVC consente solo al nome del tipo di convalida di contenere caratteri alfanumerici e non deve iniziare con un numero. Quindi è necessario aggiungere il seguente 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 è davvero semplice perché nell’implementazione della funzione adaptater è sufficiente debind la convalida al metodo require_from_group corretto.

    Poiché funziona con ogni tipo di input , area di testo e elementi select , potrei pensare che questo modo sia più generico.

    Spero possa aiutare!

    Ho implementato la fantastica risposta di Darin nella mia applicazione, tranne che l’ho aggiunta per le stringhe e non per i valori booleani. Questo era per cose come nome / azienda, o telefono / email. L’ho amato tranne che per un nocciolo minore.

    Ho provato a inviare il mio modulo senza telefono di lavoro, telefono cellulare, telefono di casa o e-mail. Ho ricevuto quattro errori di validazione separati lato client. Questo va bene per me perché permette agli utenti di sapere esattamente quali campi possono essere compilati per far sparire l’errore.

    Ho digitato un indirizzo email. Ora la singola convalida sotto e-mail è andata via, ma i tre sono rimasti sotto i numeri di telefono. Anche questi non sono più errori.

    Quindi, ho riassegnato il metodo jQuery che verifica la convalida per tener conto di ciò. Codice sotto. Spero che aiuti qualcuno.

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

    So che questo è un vecchio thread, ma mi sono imbattuto nello stesso scenario e ho trovato alcune soluzioni e ne ho visto uno che risolve la domanda di Matt sopra, così ho pensato di condividere per chi incontra questa risposta. Check out: MVC3 gruppo di input di validazione non invadente