Utente, perchè sei tu… utente!
Il problema è la validazione dell’input dell’utente, vediamo da dove sono partito:
La situazione è decisamente banale, classicissima Window (Wpf) per l’editing di una Entity (Person), il tutto basato strettamente su M-V-VM, quindi è un “requisito” che nel code-behind della Window non ci finisca nulla, o xaml o ViewModel. Il problema è la vadilazione dell’input, diamo un paio di regole di business che “incriccano” la cosa:
- Rule A: Person.FirstName non deve essere vuoto;
- Rule B: Person.LastName non deve essere vuoto;
- Evidenziare ogni singolo controllo in uno stato non valido;
- All’avvio della Window non visualizzare i controlli come invalidi anche se lo sono;
- Visualizzare l’elenco dei problemi solo ed esclusivamente nel momento in cui l’utente cerca di salvare;
ValidationRule
Il motore di Data Binding di Wpf ha un suo motore di validazioni dei dati che generalmente utilizziamo senza rendercene conto; quando scriviamo una cosa del tipo:
Chiediamo al motore di binding di intercettare le exception e visualizzare il controllo in stato di errore. La sintassi è esattamente equivalente alla seguente:<TextBox Text="{Binding Path=MyProperty, ValidatesOnExceptions=True, NotifyOnValidationError=True}" />
che mette ben in evidenza il concetto di ValidationRule. Una validation rule è una regola che viene valutata per decidere se il dato che sta viaggiando dal controllo verso la entity, o viceversa, debba essere considerato valido, scrivere una ValidationRule è un’operazione banale e rende il vostro processo di validazione decisamente flessibile perchè avete la possibilità di istruire il motore di binding su dove (durante la pipeline di binding) utilizzare la vostra validation rule: potete scegliere in quale direzione intervenire e se intervenire prima o dopo il set del valore. Tutto molto interessante ma:<TextBox> <TextBox.Text> <Binding Path="MyProperty"> <Binding.ValidationRules> <ExceptionValidationRule /> Binding.ValidationRules> Binding> TextBox.Text> TextBox>
- In un’applicazione, dal mediamente grande in su, avete l’inghippo che decentralizzate la validazione su ogni singola Window/Controllo, se quindi lo stesso dato viene visualizzato/editato in punti diversi l’onere della manutenzione comincia a diventare pesante;
- Spostare la logica di validazione sulla View secondo me è, in ottica M-V-VM, sbagliato perchè da responsabilità, alla View, che non dovrebbe avere;
- Se nel team c’è anche un designer (solo designer, che usa Blend) obbligate il designer a conoscere le regole di business/validazione del dato;
- Infine, per come è pensata una ValidationRule avete il problema che non siete in grado in maniera semplice, dall’interno della rule, di accedere al contesto generale: quello che succede è che la rule ha in mano il dato/valore ma nulla di più quindi si deve limitare a valutare il dato e non lo può fare contestualizzandolo;
Exception Driven Validation
La nostra TextBox resta invariata e nel ViewModel possiamo scrivere qualcosa del tipo:
Il tutto funziona, producendo questo a runtime senza far schiantare l’applicazione (il che è già qualcosa :-D):public String FirstName { get { return this.dataSource.FirstName; } set { if( value != this.FirstName ) { if( String.IsNullOrEmpty(value) ) { throw new ArgumentException( "Il nome non può essere vuoto." ); } this.dataSource.FirstName = value; } } }
Ci sono però una serie di effetti collaterali:
- Utilizzate le eccezioni per “guidare” la logica di business e non sarebbe cosa buona e giusta;
- Dovete inventarvi un sistema per estrarre la vera eccezione perchè come si vede dall’immagine la vostra ArgumentException diventa la InnerException di una TargetInvocationException;
- Non avete la possibilità di valutare il dato nel suo insieme: se le regole per un singolo dato sono più di una la prima che fallisce genera un’eccezione e verrà visualizzata solo quella;
IDataErrorInfo
Il framework 3.5 aggiunge a Wpf la nozione di IDataErrorInfo, interfaccia storica, offrendo quello che secondo me è il miglior compromesso in termini di validazione dei dati inseriti dell’utente. Sia chiaro si poteva fare anche prima dell’introduzione del fx 3.5 perchè quello che è stato aggiunto è:
- una ValidationRule: DataErrorValidationRule, che altro non fa che controllare se il target del data binding è IDataErrorInfo e nel caso delegare la validazione del dato dopo averlo scritto;
- Una nuova versione della classe/markup extension Binding con una proprietà ValidatesOnDataErrors;
<TextBox Text="{Binding Path=MyProperty, ValidatesOnDataErrors=True, NotifyOnValidationError=True}" />Nel nostro ViewModel avremo invece una cosa del tipo:
Il motore di binding chiamerà, dopo il set della proprietà, quella bruttura* passandovi il nome della proprietà che deve essere validata. La bruttura deve tornare o un messaggio di errore o una stringa nulla per identificare che non ci sono errori.public String this[ String propertyName ] { get { /* * Perform validation and return the * error message for the given property, * otherwise return null. */ return null; } }
Tutto ciò ha però un “difetto” la validazione viene fatta, decisamente comodo e vedremo tra poco perchè, in maniera “bidirezionale” quindi il motore di binding chiama il vostro sistema (la bruttura) di validazione dopo aver fatto il push del dato dalla UI alla entity e viceversa dopo aver fatto anche il pull del dato dalla entity alla UI. Questo ha quindi un effetto collaterale:
La validazione viene effettuata anche la prima volta che viene inizializzato il binding di ogni singolo controllo; il rischio è quindi:la cosa è facilmente aggirabile, fortunatamente:
Il tutto in termini di UX non è proprio consistente in quanto l’utente non ha ancora fatto nulla, quindi è poco sensato dirgli che ha già sbagliato.
- l’utente chiede di creare una nuova Person;
- la Window per l’inserimento dei dati appare e purtroppo appare con già visualizzati tutti gli errori;
Ci limitiamo a tener traccia di tutte le proprietà che sono state validate almeno una volta, supponendo che la prima volta sia quella che del binding iniziale.readonly HashSet<String> propertiesValidatedAtLeastOnce = new HashSet<String>(); public String this[ String propertyName ] { get { if( !this.propertiesValidatedAtLeastOnce.Contains( propertyName ) ) { this.propertiesValidatedAtLeastOnce.Add( propertyName ); return null; } /* * Validation for the given property * has been already requested once, * proceed with validation */ return null; } }
Prima di capire come soddisfare l’ultimo requisito, la visualizzazione di un “sommario” degli errori, introduciamo rapidamente il sistema di validazione usato:
Enterprise Library: Validation Application Block
Perchè reinventare la ruota? abbiamo un motore di validazione che funziona egregiamente, quindi perchè non usare quello?
Passando oltre… quello che faccio è una cosa del tipo, sulla entity (con la benedizione del capo):
il capo direbbe che ci sono fior di motivi per inventarne uno nuovo… :-D
Definisco le regole di validazione a cui la entity deve sempre sottostare, quindi non quelle contestuali all’uso.class Person { [NotNullValidator( MessageTemplate = "FirstName non può essere nullo." )]
[StringLengthValidator( 0, RangeBoundaryType.Exclusive, 0, RangeBoundaryType.Ignore, MessageTemplate="FirstName è obbligatorio." )] public String FirstName { get; set; } [StringLengthValidator( 0, RangeBoundaryType.Exclusive, 0, RangeBoundaryType.Ignore, MessageTemplate = "LastName è obbligatorio." )] public String LastName { get; set; } }
Il motore di validazione (la bruttura) nel ViewModel diventa:
Creo un validatore “statico”, la creazione di un validatore è la parte veramente onerosa in termini prestazionali della validazione, ad ogni richiesta eseguo la validazione dell’intera entity e mi salvo il risultato, quindi cerco all’interno dei risultati se ci sono degli errori per la proprietà che stiamo validando, nel caso aggrego i messaggi di errore: in questo modo al client tornano tutti gli errori per quella proprietà e non solo il primo.ValidationResults results; readonly Validator<Person> validator = ValidationFactory.CreateValidator<Person>(); readonly HashSet<String> propertiesValidatedAtLeastOnce = new HashSet<String>(); public String this[ String propertyName ] { get { if( !this.propertiesValidatedAtLeastOnce.Contains( propertyName ) ) { this.propertiesValidatedAtLeastOnce.Add( propertyName ); return null; } this.results = this.validator.Validate( this.dataSource ); if( !results.IsValid ) { var error = results.Where( err => err.Key == propertyName ) .Select( err => err.Message ) .Aggregate( String.Empty, ( a, e ) => { a += e + Environment.NewLine; return a; } ) .TrimEnd( Environment.NewLine.ToCharArray() ); return error; } return null; } }
Intermezzo
Nel frattempo vediamo anche come visualizzare l’errore all’utente: con Wpf nulla di più semplice! Ogni controllo ha una serie di attached property che servono per stabilire cosa e come fare in caso di errori di validazione:
Validation.ErrorTemplate
L’ErrorTemplate di un controllo è quella proprietà che ci permette di personalizzare il template che vogliamo assegnare ad un controllo quando il suo stato è “sono in errore”, possiamo quindi definire nelle risorse un ControlTemplate:
che definisce come vogliamo visualizzare l’errore, ed infine associarlo al nostro controllo:<ControlTemplate x:Key="errorTemplate"> <DockPanel> <AdornedElementPlaceholder x:Name="elementPlaceHolder" /> <TextBlock Foreground="Red" ToolTip="{Binding ElementName=elementPlaceHolder, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}" FontSize="20">*TextBlock> DockPanel> ControlTemplate>
Possiamo anche definire uno stile per cambiare, ad esempio il background:<TextBox Validation.ErrorTemplate="{StaticResource errorTemplate}" ... />
e “triggherare” il colore di background quando l’altra proprietà findamentale del motore di validazione cambia di valore: Validation.HasError è l’attached property che ci fornisce lo stato di un controllo.<Style x:Key="textBoxInError" TargetType="{x:Type TextBox}"> <Style.Triggers> <Trigger Property="Validation.HasError" Value="True"> <Setter Property="Background" Value="#EFB8B8" /> Trigger> Style.Triggers> Style>
ErrorSummary
Ci restano ancora un paio di nodi da scogliere:
- Vogliamo visualizzare, ad esempio al tentativo di salvataggio, un elenco di tutti gli errori;
- Vogliamo, quando viene visualizzato il sommario, anche triggherare lo stato invalido sugli eventuali controlli che non sono in stato invalido ma sono invalidi… un esempio chiarificatore:
- l’utente avvia la creazione di una nuova Person;
- la Window non è ancora in stato non valido perchè l’utente non ha fatto nulla per ora;
- l’utente cerca di pigiare il bottone Salva;
- Visualizziamo l’ErrorSummary;
- Vogliamo fare in modo che tutti i controlli sulla Window, se non validi, visualizzino il loro stato;
Abbiamo una semplice lista di stringhe che rappresenta tutti gli errori, al tentativo di salvataggio:public ObservableCollection<String> Errors { get; private set; }void TrySave() { this.Errors.Clear(); this.results = this.validator.Validate( this.dataSource ); if( !this.results.IsValid ) { foreach( var err in results ) { if( this.PropertyChanged != null ) { this.PropertyChanged( this, new PropertyChangedEventArgs( err.Key ) ); } this.Errors.Add( err.Message ); } } }
- Ci liberiamo di ogni errore precedente;
- Eseguiamo nuovamente la validazione della entity;
- Se non è valida:
- Aggiungiamo ogni errore alla lista degli errori;
- Scateniamo un evento PropertyChanged per ognuna delle proprietà non valide;
Sulla Window mi sono limitato ad aggiungere una ListBox in binding con la collection Errors.
la form all’avvio… e:
la form dopo aver solamente premuto il pulsante “Salva”.
N.D.R.
Se il form che state utilizzando è pregno di controlli attenzione che le validazioni che vengono triggherate possono diventare migliaia nel giro di pochissimo tempo e quindi le performance della UI diventare un problema percepibile anche dall’utente.
Il codice di esempio è semplicemente un esempio volutamente semplice, offre parecchi (s)punti di ottimizzazione per cercare di limare il numero di validazioni che vengono eseguite.
E adesso?
Il prossimo passaggio, materia di un altro post, è quello di avere un sistema che in automatico analizzi le regole di validazione del modello con cui siamo in binding e visualizzi all’utente, di fianco ad ogni controllo, i requisiti per cui quel controllo sia valido. Visualizzando ad esempio, per impostazione predefinita, i controlli obbligatori e a richiesta le regole a cui ogni singolo controllo verrà assoggettato in fase di validazione.
.m
* mi piacerebbe guardare in faccia quello che ha pensato una proprietà indicizzata invece che un metodo…