Convalida condizionale ASP.NET MVC


129

Come utilizzare le annotazioni dei dati per eseguire una convalida condizionale sul modello?

Ad esempio, supponiamo di avere il seguente modello (Persona e Senior):

public class Person
{
    [Required(ErrorMessage = "*")]
    public string Name
    {
        get;
        set;
    }

    public bool IsSenior
    {
        get;
        set;
    }

    public Senior Senior
    {
        get;
        set;
    }
}

public class Senior
{
    [Required(ErrorMessage = "*")]//this should be conditional validation, based on the "IsSenior" value
    public string Description
    {
        get;
        set;
    }
}

E la seguente vista:

<%= Html.EditorFor(m => m.Name)%>
<%= Html.ValidationMessageFor(m => m.Name)%>

<%= Html.CheckBoxFor(m => m.IsSenior)%>
<%= Html.ValidationMessageFor(m => m.IsSenior)%>

<%= Html.CheckBoxFor(m => m.Senior.Description)%>
<%= Html.ValidationMessageFor(m => m.Senior.Description)%>

Vorrei essere il campo obbligatorio della proprietà "Senior.Description" in base alla selezione della proprietà "IsSenior" (true -> richiesto). Come implementare la convalida condizionale in ASP.NET MVC 2 con annotazioni di dati?


1
Recentemente ho posto una domanda simile: stackoverflow.com/questions/2280539/…
Darin Dimitrov

Non ho capito bene. Un Senioroggetto è sempre un anziano, quindi perché IsSenior può essere falso in quel caso. Non hai solo bisogno che la proprietà 'Person.Senior' sia nulla quando Person.IsSeniorè falsa. Oppure, perché non implementare la IsSeniorproprietà come segue: bool IsSenior { get { return this.Senior != null; } }.
Steven

Steven: "IsSenior" si traduce nel campo della casella di controllo nella vista. Quando l'utente controlla la casella di controllo "IsSenior", il campo "Senior.Description" diventa obbligatorio.
Peter Stegnar,

Darin Dimitrov: Beh, un po ', ma non del tutto. Vedete, come otterreste che il messaggio di errore è appent al campo specifico? Se si convalida a livello di oggetto, viene visualizzato un errore a livello di oggetto. Ho bisogno di un errore a livello di proprietà.
Peter Stegnar,

Risposte:


150

C'è un modo molto migliore per aggiungere regole di convalida condizionale in MVC3; fai ereditare il tuo modello IValidatableObjecte implementa il Validatemetodo:

public class Person : IValidatableObject
{
    public string Name { get; set; }
    public bool IsSenior { get; set; }
    public Senior Senior { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) 
    { 
        if (IsSenior && string.IsNullOrEmpty(Senior.Description)) 
            yield return new ValidationResult("Description must be supplied.");
    }
}

Maggiori informazioni su Presentazione di ASP.NET MVC 3 (anteprima 1) .


se la proprietà è di tipo "int", che richiede valore, se si riempie quel campo, Convalida non funziona ..
Jeyhun Rahimov,

2
Sfortunatamente, Microsoft ha inserito questo livello errato: la convalida è una logica aziendale e questa interfaccia è nella DLL System.Web. Per usarlo, devi dare al tuo livello aziendale una dipendenza da una tecnologia di presentazione.
NightOwl888,

7
lo fai se lo implementi - vedi l'esempio completo su falconwebtech.com/post/…
viperguynaz

4
falconwebtech.com/post/… - @viperguynaz questo non funziona
Smit Patel

1
@RayLoveless dovresti chiamare ModelState.IsValid- non chiamare Convalida direttamente
viperguynaz

63

Ho risolto questo problema gestendo il dizionario "ModelState" , che è contenuto dal controller. Il dizionario ModelState include tutti i membri che devono essere convalidati.

Ecco la soluzione:

Se è necessario implementare una convalida condizionale basata su alcuni campi (ad es. Se A = vero, allora è richiesto B), mantenendo al contempo la messaggistica di errore a livello di proprietà (ciò non è vero per i validatori personalizzati che si trovano a livello di oggetto) è possibile ottenere questo gestendo "ModelState", semplicemente rimuovendo le convalide indesiderate da esso.

... In qualche classe ...

public bool PropertyThatRequiredAnotherFieldToBeFilled
{
  get;
  set;
}

[Required(ErrorMessage = "*")] 
public string DepentedProperty
{
  get;
  set;
}

... la lezione continua ...

... In alcune azioni del controller ...

if (!PropertyThatRequiredAnotherFieldToBeFilled)
{
   this.ModelState.Remove("DepentedProperty");
}

...

Con questo otteniamo la convalida condizionale, lasciando tutto il resto uguale.


AGGIORNARE:

Questa è la mia implementazione finale: ho usato un'interfaccia sul modello e l'attributo action che convalida il modello che implementa detta interfaccia. L'interfaccia prescrive il metodo Validate (ModelStateDictionary modelState). L'attributo on action chiama semplicemente Validate (modelState) su IValidatorSomething.

Non volevo complicare questa risposta, quindi non ho menzionato i dettagli finali di implementazione (che, alla fine, contano nel codice di produzione).


17
Lo svantaggio è che una parte della logica di convalida si trova nel modello e l'altra parte nei controller.
Kristof Claes l'

Beh, ovviamente questo non è necessario. Mostro solo l'esempio più semplice. Ho implementato questo con l'interfaccia sul modello e con l'attributo di azione che convalida il modello che implementa l'interfaccia menzionata. L'interfaccia diffonde il metodo Validate (ModelStateDictionary modelState). Quindi finalmente fai TUTTA la validazione nel modello. Comunque, buon punto.
Peter Stegnar,

Mi piace la semplicità di questo approccio nel frattempo fino a quando il team MVC non costruisce qualcosa di meglio fuori dagli schemi. Ma la tua soluzione funziona con la convalida lato client abilitata ??
Aaron,

2
@Aaron: Sono contento che ti piaccia la soluzione, ma sfortunatamente questa soluzione non funziona con la convalida lato client (poiché ogni attributo di convalida necessita dell'implementazione JavaScript). Potresti aiutarti con l'attributo "Remoto", quindi solo la chiamata Ajax verrà emessa per convalidarlo.
Peter Stegnar,

Sei in grado di espandere questa risposta? Questo ha un certo senso, ma voglio assicurarmi di esserlo. Sono di fronte a questa situazione esatta e voglio risolverla.
Richard B,

36

Ho avuto lo stesso problema ieri, ma l'ho fatto in modo molto pulito che funziona sia per la convalida lato client che lato server.

Condizione: in base al valore di altre proprietà nel modello, si desidera rendere necessaria un'altra proprietà. Ecco il codice

public class RequiredIfAttribute : RequiredAttribute
{
    private String PropertyName { get; set; }
    private Object DesiredValue { get; set; }

    public RequiredIfAttribute(String propertyName, Object desiredvalue)
    {
        PropertyName = propertyName;
        DesiredValue = desiredvalue;
    }

    protected override ValidationResult IsValid(object value, ValidationContext context)
    {
        Object instance = context.ObjectInstance;
        Type type = instance.GetType();
        Object proprtyvalue = type.GetProperty(PropertyName).GetValue(instance, null);
        if (proprtyvalue.ToString() == DesiredValue.ToString())
        {
            ValidationResult result = base.IsValid(value, context);
            return result;
        }
        return ValidationResult.Success;
    }
}

Qui PropertyName è la proprietà sulla quale si desidera rendere la propria condizione DesiredValue è il valore particolare del PropertyName (proprietà) per il quale è necessario convalidare l'altra proprietà per

Di 'che hai il seguente

public class User
{
    public UserType UserType { get; set; }

    [RequiredIf("UserType", UserType.Admin, ErrorMessageResourceName = "PasswordRequired", ErrorMessageResourceType = typeof(ResourceString))]
    public string Password
    {
        get;
        set;
    }
}

Infine, ma non meno importante, registra l'adattatore per il tuo attributo in modo che possa eseguire la convalida lato client (l'ho messo in global.asax, Application_Start)

 DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(RequiredIfAttribute),typeof(RequiredAttributeAdapter));

Questo è stato il punto di partenza originale miroprocessordev.blogspot.com/2012/08/…
Dan Hunex

Esiste una soluzione equivalente in asp.net mvc2? Le classi ValidationResult, ValidationContext non sono disponibili in asp.net mvc2 (.net framework 3.5)
User_MVC

2
Funziona solo sul lato server come afferma il blog collegato
Pakman,

2
Sono riuscito a farlo funzionare sul lato client con MVC5, ma nel client avvia la convalida, indipendentemente da cosa sia DesiredValue.
Geethanga,

1
@Dan Hunex: in MVC4, non sono riuscito a funzionare correttamente sul lato client e si avvia la convalida, indipendentemente da cosa sia DesiredValue. Qualsiasi aiuto pls?
Jack

34

Ho usato questo fantastico nuget che fa annotazioni dinamiche ExpressiveAnnotations

Puoi convalidare qualsiasi logica tu possa sognare:

public string Email { get; set; }
public string Phone { get; set; }
[RequiredIf("Email != null")]
[RequiredIf("Phone != null")]
[AssertThat("AgreeToContact == true")]
public bool? AgreeToContact { get; set; }

3
La libreria ExpressiveAnnotation è la soluzione più flessibile e generica di tutte le risposte qui. Grazie per la condivisione!
Sudhanshu Mishra,

2
Mi sono sbattuto la testa cercando di trovare una soluzione per una giornata solida. ExpressiveAnnotations sembra essere la soluzione per me!
Caverman,

La libreria ExpressiveAnotation è fantastica!
Doug Knudsen,

1
Ha anche il supporto lato client!
Nattrass,

1
Nessun supporto per .NET Core però, e non sembra che accadrà.
gosr,

18

È possibile disabilitare i validatori in modo condizionale rimuovendo gli errori da ModelState:

ModelState["DependentProperty"].Errors.Clear();


6

Ora esiste un framework che esegue immediatamente questa convalida condizionale (tra le altre utili convalide di annotazione dei dati): http://foolproof.codeplex.com/

In particolare, dai un'occhiata al validatore [RequiredIfTrue ("IsSenior")]. Lo metti direttamente sulla proprietà che desideri convalidare, in modo da ottenere il comportamento desiderato dell'errore di convalida associato alla proprietà "Senior".

È disponibile come pacchetto NuGet.


3

Devi convalidare a livello di Persona, non a livello di Senior, oppure Senior deve avere un riferimento alla sua persona madre. Mi sembra che tu abbia bisogno di un meccanismo di auto-validazione che definisce la validazione sulla Persona e non su una delle sue proprietà. Non ne sono sicuro, ma non credo che DataAnnotations lo supporti immediatamente. Quello che puoi fare crea il tuo da Attributecui derivaValidationAttribute cui può essere decorato a livello di classe e successivamente creare un validatore personalizzato che consenta anche l'esecuzione di quei validatori a livello di classe.

So che Validation Application Block supporta l'auto-validazione immediata, ma VAB ha una curva di apprendimento piuttosto ripida. Tuttavia, ecco un esempio usando VAB:

[HasSelfValidation]
public class Person
{
    public string Name { get; set; }
    public bool IsSenior { get; set; }
    public Senior Senior { get; set; }

    [SelfValidation]
    public void ValidateRange(ValidationResults results)
    {
        if (this.IsSenior && this.Senior != null && 
            string.IsNullOrEmpty(this.Senior.Description))
        {
            results.AddResult(new ValidationResult(
                "A senior description is required", 
                this, "", "", null));
        }
    }
}

"Devi convalidare a livello di Persona, non a livello di Senior" Sì, questa è un'opzione, ma perdi la capacità che l'errore venga aggiunto a un determinato campo, richiesto nell'oggetto Senior.
Peter Stegnar,

3

Ho avuto lo stesso problema, avevo bisogno di una modifica dell'attributo [Richiesto] - crea campo richiesto in dipendenza della richiesta http. La soluzione era simile alla risposta di Dan Hunex, ma la sua soluzione non funzionava correttamente (vedi commenti). Non utilizzo una convalida discreta, ma solo MicrosoftMvcValidation.js pronto all'uso. Ecco qui. Implementa il tuo attributo personalizzato:

public class RequiredIfAttribute : RequiredAttribute
{

    public RequiredIfAttribute(/*You can put here pararmeters if You need, as seen in other answers of this topic*/)
    {

    }

    protected override ValidationResult IsValid(object value, ValidationContext context)
    {

    //You can put your logic here   

        return ValidationResult.Success;//I don't need its server-side so it always valid on server but you can do what you need
    }


}

Quindi devi implementare il tuo provider personalizzato per usarlo come adattatore nel tuo global.asax

public class RequreIfValidator : DataAnnotationsModelValidator <RequiredIfAttribute>
{

    ControllerContext ccontext;
    public RequreIfValidator(ModelMetadata metadata, ControllerContext context, RequiredIfAttribute attribute)
       : base(metadata, context, attribute)
    {
        ccontext = context;// I need only http request
    }

//override it for custom client-side validation 
     public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
     {       
               //here you can customize it as you want
         ModelClientValidationRule rule = new ModelClientValidationRule()
         {
             ErrorMessage = ErrorMessage,
    //and here is what i need on client side - if you want to make field required on client side just make ValidationType "required"    
             ValidationType =(ccontext.HttpContext.Request["extOperation"] == "2") ? "required" : "none";
         };
         return new ModelClientValidationRule[] { rule };
      }
}

E modifica il tuo global.asax con una linea

DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(RequiredIfAttribute), typeof(RequreIfValidator));

ed eccolo qui

[RequiredIf]
public string NomenclatureId { get; set; }

Il vantaggio principale per me è che non devo codificare un validatore client personalizzato come in caso di convalida discreta. funziona esattamente come [Richiesto], ma solo nei casi desiderati.


La parte sull'estensione DataAnnotationsModelValidatorera esattamente quello che dovevo vedere. Grazie.
twip


0

Utilizzo tipico per la rimozione condizionale dell'errore dallo stato del modello:

  1. Rendi condizionale la prima parte dell'azione del controller
  2. Eseguire la logica per rimuovere l'errore da ModelState
  3. Esegui il resto della logica esistente (in genere la convalida dello stato del modello, quindi tutto il resto)

Esempio:

public ActionResult MyAction(MyViewModel vm)
{
    // perform conditional test
    // if true, then remove from ModelState (e.g. ModelState.Remove("MyKey")

    // Do typical model state validation, inside following if:
    //     if (!ModelState.IsValid)

    // Do rest of logic (e.g. fetching, saving

Nel tuo esempio, mantieni tutto così com'è e aggiungi la logica suggerita all'azione del tuo controller. Suppongo che il ViewModel passato all'azione del controller abbia gli oggetti Person e Senior Person con i dati popolati in essi dall'interfaccia utente.


0

Sto usando MVC 5 ma potresti provare qualcosa del genere:

public DateTime JobStart { get; set; }

[AssertThat("StartDate >= JobStart", ErrorMessage = "Time Manager may not begin before job start date")]
[DisplayName("Start Date")]
[Required]
public DateTime? StartDate { get; set; }

Nel tuo caso diresti qualcosa del tipo "IsSenior == true". Quindi devi solo controllare la convalida sulla tua azione post.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.