Data validation in WPF
Una necessità comune per ogni applicazione con interfaccia grafica che accetti input da parte dell'utente, è quella di validare le informazioni immesse per assicurare che queste siano nel formato e nel tipo atteso. In questo articolo vedremo come funziona la validazione in WPF ed le diverse opzioni di validazione disponibili compresa l'implementazione di ValidationRules personalizzate ed utilizzando le interfacce IDataErrorInfo e INotifyErrorDataError che sono state introdotte nel Framework .NET 4.5. Questo articolo contienie inoltre un esempio che mostra come validare i dati utilizzando le data annotations.
Data binding
In una tipica applicazione WPF che utilizza il Design Pattern MVVM (Model-View-View-Model), una DependencyProperty utilizza il DataBinding per associare (to bind) alcuni dati ad una proprietà CLR del modello nell'interfaccia. Se l'associazione è correttamente impostata ed il modello implementa l'interfaccia System.ComponentModel.INotifyPropertyChanged per fornire la notifica quando i dati cambiano, le modifiche sono automaticamente riflesse nell'elemento della vista a cui è associata. Viceversa, un dato impostato nel viewmodel viene automaticamente aggiornato quando l'utente modifica il valore nell'interfaccia grafica.
Appuranto che il ViewModel ha una proprietà chiamata "Name", viene bindata/accoppiata alla proprietà Text del TextBox nell'XAML in questo modo:
< TextBox Text = "{Binding Path=Name}" /> <!-- equivalent to <TextBox Text="{Binding Name}"/> --> |
Sorgente
Inoltre il Path che specifica il nome della proprietà da associare/bindare, l'associazione deve avere un oggetto sorgente. Se non viene espressamente specificato impostando la proprietà Source del binding, eredita il DataContext dall'elemento genitore/parent come sorgente. In applicazioni MVVM WPF, il ModelView agisce da finestra DataContext:
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); this .DataContext = new ViewModel(); } } |
Questo significa che tutti i controlli all'interno della finestra ereditano il DataContext a meno che qualche elemento padre di un controllo non lo sovrascriva impostando la propria proprietà DataContext. Oltre ad ereditare ed impostare direttamente la proprietà DataContext di un elemento, è possibile anche specificare la sorgente di accoppiamento/binding utilizzando la proprietà ElementName, utilizzata quando si vuole bindare/accoppiare qualche altro elemento, o la proprietà RelativeSource. Quest'ultimo può essere molto utile per associare/bindare Styles e ControlTemplates e quando si svuole bindare/associare alcune proprietà in un elemento padre.
Mode
La proprietà Mode della classe System.Windows.Data.Binding permette di controllare la direzione del flusso dei dati, per esempio quando l'associazione/binding deve aggiornare solamente l'interfaccia utente, la proprietà source del DataContext o entrambi, come definito nell'enumerato System.Windows.Data.BindingMode:
- OneWay: Solo il valore della DependencyProperty dell'elemento dell'UI viene aggiornata quando la proprietà source cambia. Viene utilizzata in scenari read-only.
- TwoWay: Entrambe le proprietà dell'elemento dell'UI ed la proprietà source vengono aggiornate quando entrambi i valori vengono aggiornati. Spesso utilizzato per controlli interattivi come TextBox.
- OneTime: Solamente la proprietà dell'elemento UI viene aggiornata e viene aggiornata solamente quando l'applicazione si avvia o quando il DataContext è sottoposto a cambiamento. Utilizzato quando i dati mostrati sono veramente STATIC.
- OneWayToSource: Solamente la proprietà source viene aggiornata quando la proprietà dell'elemento UI è modificato. Il contrario di OneWay.
Esiste anche l'opzione Default che ritorna la metodologia di accoppiamento/binding standard della dependency property. Per la proprietà Text del TextBox, l'opzione default è TwoWay ma varia per ogni dependency property.
UpdateSourceTrigger
Per i binding TwoWay e OneWayToSource esiste un'ulteriore proprietà nella classe Binding chiamata UpdateSourceTrigger che specifica che trigger viene usato per aggiornare la proprietà source. Se UpdateSourceTrigger è impostato a LostFocus, che è il valore di default per la proprietà Text del controllo TextBox, il testo che viene digitato nel TextBox non aggiorna la sorgente fino a quando il controllo non "perde il focus" (che succede quando si clicca al di fuori del controllo stesso). Se si necessita che la source venga aggiornata, ad esempio quando il setter per la proprieà bound del DataContext viene chiamato, come se l'utente stesse digitando all'interno del TextBox, si deve settare la proprietà UpdateSourceTrigger a PropertyChanged:
< TextBox Text = "{Binding Name, UpdateSourceTrigger=PropertyChanged}" /> |
Inoltre l'opzione Default che lavora similmente all'enumeratoBindingMode, esiste anche un'opzione chiamata Explicit definita nell'enumerato System.Windows.Data.Binding.UpdateSourceTrigger. Impostando la proprietà a questo valore significa che il valore dalla proprietà sorgente riceve gli aggiornamenti solamente quando viene espressamente chiamato il metodo BindingExpression.UpdateSource() method in code. Tipicamente non viene mai utilizzato questo approccio in una applicazione MVVM.
Data type conversion
Se si vuole bindare/associare/bindare la proprietà di un ViewModel ad uno specifico tipo di una DependencyProperty nella vista di un tipo differente, bisogna implementare una classe Custom Converter implementando l'interfaccia System.Windows.Data.IValueConverter e settando la proprietà Converter dell'associazione/binding ad una istanza di questo. Una classe Converter converte i dati da un tipo ad un altro durante l'associazione/binding implementando i metodi Convert e ConvertBack della menzionata interfaccia. Una volta creata la classe Converter viene tipicamente aggiunta come risorsa all'XAML con un univoco attributo x:Key ed infine referenziarlo nel binding come StaticResource:
< Window x:Class = "WpfDataValidation.MainWindow" xmlns:local = "clr-namespace:MyApplication" Title = "MainWindow" Height = "350" Width = "525" > < Window.Resources > < local:MyCustomConverter x:Key = "myCustomConverter" /> </ Window.Resources > < Grid > < TextBox Text = "{Binding Name, Converter={StaticResource myCustomConverter}}" /> </ Grid > </ Window > |
Comunque, quando si associano/binding dati di tipo diverso da System.String ad una DependencyProperty di tipo stringa, non è necessario utilizzare un converter in quando viene automaticamente convertito applicando il metodo ToString() del valore della proprietà sorgente.
Questo significa che non si deve utilizzare un converter per mostrare un valore di System.Int32 (int) in una TextBox:
public class ViewModel { public ViewModel() { /* Set default age */ this .Age = 30; } public int Age { get ; set ; } } |
< TextBox Text = "{Binding Age, UpdateSourceTrigger=PropertyChanged}" /> |
Data validation
Se un utente inserisce un valore non valido che non può essere convertito in int e deve essere settato come proprietà Age nel ViewModel nell'esempio precedente, un errore di validazione viene sollevato ed un messaggio di ritorno viene proposto all'utente per indicare l'errore. Di default si vedrà un bordo rosso attorno all'elemento UI quando questo si presenta, ad esempio se viene digitata una lettera, attorno alla TextBox che è associata ad una proprietà di tipo int:
Il messaggio che descrive l'errore è archiviato nella proprietà ErrorContent dell'oggetto System.Windows.Controls.ValidationError che è aggiunto alla collezione Validation.Errors dell'elemento associato/bindato a runtime. Quando la proprietà Validation.Errors ha al suo interno un oggetto ValidationError, un'altra proprietà chiamata Validation.HasError ritorna true.
ErrorTemplate
Per poter vedere il messaggio di errore nella View, si può sostituire il template standard che disegna il bordo rosso attorno all'elemento con un control template personalizzato impostando la proprietà Validation.ErrorTemplate al controllo:
< TextBox Text = "{Binding Age, UpdateSourceTrigger=PropertyChanged}" > < Validation.ErrorTemplate > < ControlTemplate > < StackPanel > <!-- Placeholder for the TextBox itself --> < AdornedElementPlaceholder x:Name = "textBox" /> < TextBlock Text = "{Binding [0].ErrorContent}" Foreground = "Red" /> </ StackPanel > </ ControlTemplate > </ Validation.ErrorTemplate > </ TextBox > |
Da notare che Validation.ErrorTemplate viene mostrato nell'AdornerLayer. Gli elemnti nell'AdornerLayer vengono renderizzati/disegnati in alto rispetto al resto dell'elemento e non vengono considerati quando il motore di renderizzazione misura e dispone i controlli attorno all'AdornerLayer. L'Adorned Element, in questo caso, è il controllo TextBox stesso e bisogna includere un AdornedElementPlaceholder nel control template dove si vuole lasciare lo spazio per il messaggio. Il template precedente, permette di visualizzare il messaggio di errore sotto il TextBox. Si noti che TextBlock apparirà sopra qualsiasi elemento che si trovano proprio sotto il TextBox in quanto gli adornatori sono sempre visivamente sopra.
ValidationRule
Ora, che è possibile visualizzare l'attuale messaggio di errore, che riporta "Il valore .... non può essere convertito", quando la conversione di un int in una stringa, si vedrà come personalizzarlo. Si può fare implementando una Validation Rule (Regola di Validazione) ed associarla all'oggetto Binding. Una regola di validazione personalizzata è una classe che deriva dalla classe astratta System.Windows.Controls.ValidationRule e ne implementa il metodo Validate(). Ha una proprietà chiamata ValidationStep che controlla quando il motore di associazione/binding esegue il metodo Validate. L'enumerato System.Windows.Controls.ValidationStep ha le seguenti opzioni:
- RawProposedValue: La regola di validazione è eseguita prima che sia effettuata la conversione del valore. Questa è l'opzione di default.
- ConvertedProposedValue: La regola di validazione viene eseguita dopo la conversione ma prima che il valore venga richiamato il setting del valore nella proprietà della source.
- UpdatedValue: La regola di validazione è eseguita dopo che la proprietà della source è stata aggiornata.
- CommittedValue: La regola di validazione è eseguita dopo che il valore è stato committato alla source.
Di seguito si può vedere come implementare una regola di validazione personalizzata che verifichi quando la stringa possa essere convertita in intero ed imposta la proprietà ErrorContent dell'oggetto ValidationError nella collezione Validation.Errors collection. Notare che la proprietà ValidationStep necessita di essere settata a RawProposedValue, sia esplicitamente che implicitamente utilizzando il valore di default, perché la regola sia applicata prima che la conversione standard sia chiamata.
public class StringToIntValidationRule : ValidationRule { public override ValidationResult Validate( object value, System.Globalization.CultureInfo cultureInfo) { int i; if ( int .TryParse(value.ToString(), out i)) return new ValidationResult( true , null ); return new ValidationResult( false , "Please enter a valid integer value." ); } } |
< Window x:Class = "WpfDataValidation.MainWindow" xmlns:local = "clr-namespace:WpfDataValidation" Title = "MainWindow" Height = "175" Width = "400" > < StackPanel Margin = "50" > < TextBox > < TextBox.Text > < Binding Path = "Age" UpdateSourceTrigger = "PropertyChanged" > < Binding.ValidationRules > < local:StringToIntValidationRule ValidationStep = "RawProposedValue" /> </ Binding.ValidationRules > </ Binding > </ TextBox.Text > ... </ TextBox > </ StackPanel > </ Window > |
Validation process
Se il metodo Validata di un oggetto ValidationRule ritorna un ValidationResult invalido, la procedura di validazione del motore di associazione/binding elencata di seguito si ferma. Se l'oggetto ritornato ha la proprietà IsValid impostata a true, il processo di validazione continua al passaggio successivo.
- Il metodo Validate di tutti gli oggetti ValidationRule che sono associati con il binding e la proprietà ValidationStep è impostata a RawProposedValue è eseguito finché uno di questi ritorni un invalid ValidationResult o finché tutti siano passati.
- Se l'associazione/binding ha un converter/convertitore, il metodo CoverBack viene chiamato.
- Il motore di associazione/binding prova a convertire il valore ritornato dal metodo ConverterBack del convertitore, assumendo che ci sia un convertitore associato con l'associazione/binding, o il valore della DependencyProperty al tipo della proprietà source.
- Viene chiamato il setter della proprietà source.
- Tutti i metodi Validate degli oggetti ValidationRule objects con la proprietà ValidationStep impostata a UpdatedValue vengono valutati nel medesimo modo descritto nel primo step.
- Come nello step precedente, per tutti gli oggetti ValidationRule con la proprietà ValidationStep impostata a CommittedValue
Prima che il metodo Validate degli oggetti ValidationRule venga eseguito, a qualunque step, ogni errore che viene aggiunto alla proprietà Validation.Errors dell'associazione/binding dell'elemento durante lo step che in una precedente procedura di validazione era stato rimosso. La collection di Validation.Errors viene "pulita" anche quando si presenta una validazione a buon fine.
ExceptionValidationRule
WPF fornisce due concrete implementazioni built-in della classe ValidationRule. La classe System.Windows.Controls.ExceptionValidationRule aggiunge un oggetto ValidationError alla collezione Validation.Errors quando un'eccezione è sollevata nel processo di setting della proprietà source. Per esempio, può essere utile se la proprietà Age del ViewModel fosse forzata ad accettare valori compresi tra 10 e 100 e sollevi un'eccezione se il valore non fosse compreso in questo range:
private int _age; public int Age { get { return _age; } set { if (value < 10 || value > 100) throw new ArgumentException( "The age must be between 10 and 100" ); _age = value; } } |
< TextBox > < TextBox.Text > < Binding Path = "Age" UpdateSourceTrigger = "PropertyChanged" > < Binding.ValidationRules > < ExceptionValidationRule /> </ Binding.ValidationRules > </ Binding > </ TextBox.Text > </ TextBox > |
Una sintassi alternativa per aggiungere questa regola esplicitamente alla collezione di ValidationRules è quella di settare la proprietà ValidatesOnExceptions a true:
< TextBox Text = "{Binding Path=Age, UpdateSourceTrigger=PropertyChanged, ValidatesOnExceptions=True}" /> |
IDataErrorInfo
L'altra regola validazione built-in è la classe System.Windows.Controls.DataErrorValidationRule. Questa controlla se errori di validazione sono sollevati dall'interfaccia. Questa interfaccia definisce due proprietà the ritornano una stringa che indica cosa è sbagliato assieme all'oggetto ed alcune proprietà rispettivamente dell'oggetto. Di seguito viene mostrato come ViewModel deve implementare l'interfaccia IDataErrorInfo per validare la proprietà Age in accordo con le stesse regole di prima, ma senza sollevare alcuna eccezione::
public class ViewModel : System.ComponentModel.IDataErrorInfo { public ViewModel() { /* Set default age */ this .Age = 30; } public int Age { get ; set ; } public string Error { get { return null ; } } public string this [ string columnName] { get { switch (columnName) { case "Age" : if ( this .Age < 10 || this .Age > 100) return "The age must be between 10 and 100" ; break ; } return string .Empty; } } } |
WPF identifica automaticamente gli oggetti source che implementano questa interfaccia per fornire un modo di visualizzare errori personalizzati. Basta ricordarsi di associare DataErrorValidationRule con l'associazione/binding nella View, sia aggiungendolo alla collezione di ValidationRule del binding, oppure settando la proprietà ValidatesOnDataErrors del binding a true:
< TextBox Text = "{Binding Path=Age, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" /> |
INotifyDataErrorInfo
Il Framework 4.5 ha introdotto una nuova interfaccia System.ComponentModel.INotifyDataErrorInfo - la stessa intefaccia è già presente in Silverlight dalla versione 4 - che permette di sviluppare validazioni server-side asincronomamente e notificarle alla View mediante l'evento ErrorsChanged una volta che la validazione è completata. Similmente rende possibile di invalidare una proprietà quando viene settata un'altra proprietà e supporta il setting di multierrori per proprietà ed oggetti errori personalizzati di altri tipi oltre System.String (string).
/* The built-in System.ComponentModel.INotifyDataErrorInfo interface */ public interface INotifyDataErrorInfo { bool HasErrors { get ; } event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged; IEnumerable GetErrors( string propertyName); } |
Multiple errors per property
Il metodo GetErrors dell'interfaccia ritorna un IEnumerable che contiene gli errori di validazione per una specifica proprietà o per tutta l'entità. Si deve sempre richiamare l'evento ErrorsChanged quando la collezione ritornata del metodo GetErrors è modificata. Se la source del binding TwoWay implementa interfaccia INotifyDataErrorInfo ed la proprietà ValidatesOnNotifyDataErrors del binding è impostata a true (come di default), il motore di WPF 4.5 automaticamente monitora l'evento ErrorsChanged e chiama il metodo GetErrors per aggiornare l'elenco degli errori una volta che l'evento è invocato dall'oggetto source che ha la proprietà HasErrors impostata a true.
Di seguito c'è un esempio di un semplice servizio con un singolo metodo che valida un username prima interrogando il DataBase per determinare se è già stato usato o no e successivamente ne verifica la lunghezza e finalmente determina se contiene caratteri illegati utilizzando una RegularExpression. Il metodo ritorna vero o falso a seconda che la validazione sia andata a buon fine o no ed inoltra ritorna un elenco di messaggi di errore come parametro out. Dichiarare un argomento come out è utile quando si vuole che il metodo ritorni più valori.
public interface IService { bool ValidateUsername( string username, out ICollection< string > validationErrors); } public class Service : IService { public bool ValidateUsername( string username, out ICollection< string > validationErrors) { validationErrors = new List< string >(); int count = 0; using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings[0].ConnectionString)) { SqlCommand cmd = new SqlCommand( "SELECT COUNT(*) FROM [Users] WHERE Username = @Username" , conn); cmd.Parameters.Add( "@Username" , SqlDbType.VarChar); cmd.Parameters[ "@Username" ].Value = username; conn.Open(); count = ( int )cmd.ExecuteScalar(); } if (count > 0) validationErrors.Add( "The supplied username is already in use. Please choose another one." ); /* Verifying that length of username */ if (username.Length > 10 || username.Length < 4) validationErrors.Add( "The username must be between 4 and 10 characters long." ); /* Verifying that the username contains only letters */ if (!Regex.IsMatch(username, @"^[a-zA-Z]+$" )) validationErrors.Add( "The username must only contain letters (a-z, A-Z)." ); return validationErrors.Count == 0; } } |
Asynchronous validation
L'implementazione della seguente View dell'interfaccia INotifyDataErrorInfo utilizza questo servizio per fornire una validazione asincrona. Accanto una referenza dello stesso servizio, è presente un System.Collections.Generic.Dictionary<string, System.Collections.Generic.ICollection<string>> dove la chiave rappresenta il nome di una proprietà ed il valore rappresenta una collezione di validation errors corrispondenti alla proprietà.
public class ViewModel : INotifyDataErrorInfo { private readonly IService _service; private readonly Dictionary< string , ICollection< string >> _validationErrors = new Dictionary< string , ICollection< string >>(); public ViewModel(IService service) { _service = service; } ... #region INotifyDataErrorInfo members public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged; private void RaiseErrorsChanged( string propertyName) { if (ErrorsChanged != null ) ErrorsChanged( this , new DataErrorsChangedEventArgs(propertyName)); } public System.Collections.IEnumerable GetErrors( string propertyName) { if ( string .IsNullOrEmpty(propertyName) || !_validationErrors.ContainsKey(propertyName)) return null ; return _validationErrors[propertyName]; } public bool HasErrors { get { return _validationErrors.Count > 0; } } #endregion } |
Il setter di una proprietà Username di un ViewModel viene definito utilizzando un metodo privato che chiama il servizio in modo asincrono utilizzando async-await, che sono state introdotte nel Framework .NET 4.5 per semplificare l'approccio alla programmazione asincrona, ed aggiorna il dictionary con i risultati della validazione:
private string _username; public string Username { get { return _username; } set { _username = value; ValidateUsername(_username); } } private async void ValidateUsername( string username) { const string propertyKey = "Username" ; ICollection< string > validationErrors = null ; /* Call service asynchronously */ bool isValid = await Task< bool >.Run(() => { return _service.ValidateUsername(username, out validationErrors); }) .ConfigureAwait( false ); if (!isValid) { /* Update the collection in the dictionary returned by the GetErrors method */ _validationErrors[propertyKey] = validationErrors; /* Raise event to tell WPF to execute the GetErrors method */ RaiseErrorsChanged(propertyKey); } else if (_validationErrors.ContainsKey(propertyKey)) { /* Remove all errors for this property */ _validationErrors.Remove(propertyKey); /* Raise event to tell WPF to execute the GetErrors method */ RaiseErrorsChanged(propertyKey); } } |
Perché la View sia in grado di mostrare più di un singolo messaggio di errore, bisogna apportare una modifica al Validation.ErrorTemplate del controller associato/bindato. Solitamente si utilizza un ItemsControl per mostrare una collezione di oggetti in XAML:
< TextBox Text = "{Binding Username, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}" > < Validation.ErrorTemplate > < ControlTemplate > < StackPanel > <!-- Placeholder for the TextBox itself --> < AdornedElementPlaceholder x:Name = "textBox" /> < ItemsControl ItemsSource = "{Binding}" > < ItemsControl.ItemTemplate > < DataTemplate > < TextBlock Text = "{Binding ErrorContent}" Foreground = "Red" /> </ DataTemplate > </ ItemsControl.ItemTemplate > </ ItemsControl > </ StackPanel > </ ControlTemplate > </ Validation.ErrorTemplate > </ TextBox > |
Custom error objects
Come accennato, il metodo GetErrors può fornire qualunque tipo di errore sottoforma di oggetti di qualunque tipo e può essere molto utile quando si vogliono presentare errori personalizzati nella View. Conderare il seguente tipo di esempio che ha una proprietà string che descrive l'errore di validazione ed una proprietà in più di tipo enumerato che specifica la gravità dell'errore:
public class CustomErrorType { public CustomErrorType( string validationMessage, Severity severity) { this .ValidationMessage = validationMessage; this .Severity = severity; } public string ValidationMessage { get ; private set ; } public Severity Severity { get ; private set ; } } public enum Severity { WARNING, ERROR } public class Service : IService { /* The service method modifed to return objects of type CustomErrorType instead of System.String */ public bool ValidateUsername( string username, out ICollection<CustomErrorType> validationErrors) { validationErrors = new List<CustomErrorType>(); int count = 0; /* query database as before */ ... if (count > 0) validationErrors.Add( new CustomErrorType( "The supplied username is already in use. Please choose another one." , Severity.ERROR)); /* Verifying that length of username */ if (username.Length > 10 || username.Length < 4) validationErrors.Add( new CustomErrorType( "The username should be between 4 and 10 characters long." , Severity.WARNING)); /* Verifying that the username contains only letters */ if (!Regex.IsMatch(username, @"^[a-zA-Z]+$" )) validationErrors.Add( new CustomErrorType( "The username must only contain letters (a-z, A-Z)." , Severity.ERROR)); return validationErrors.Count == 0; } } |
Se si utilizza lo stesso ErrorTemplate come mostrato in precedenza per presentare gli errori di validazione del tipo precedente, si può vedere la rappresentazione ToString() quando l'errore viene intercettato. Si può scegliere se sovrascrivere il metodo ToString() per ritornare un messaggio di errore o semplicemente correggere il template per personalizzare il tipo personalizzato. Di seguito un esempio di come è possibile modificare il colore dell'errore di validazione in base alla proprietà Severtity dell'oggetto CustomErrorType che viene ritornato dalla proprietà ErrorContent dell'oggetto ValidationError nella collezione Validation.Errors:
< Validation.ErrorTemplate > < ControlTemplate xmlns:local = "clr-namespace:WpfApplication1" > < StackPanel > <!-- Placeholder for the TextBox itself --> < AdornedElementPlaceholder x:Name = "textBox" /> < ItemsControl ItemsSource = "{Binding}" > < ItemsControl.ItemTemplate > < DataTemplate > < TextBlock Text = "{Binding ErrorContent.ValidationMessage}" > < TextBlock.Style > < Style TargetType = "{x:Type TextBlock}" > < Setter Property = "Foreground" Value = "Red" /> < Style.Triggers > < DataTrigger Binding = "{Binding ErrorContent.Severity}" Value = "{x:Static local:Severity.WARNING}" > < Setter Property = "Foreground" Value = "Orange" /> </ DataTrigger > </ Style.Triggers > </ Style > </ TextBlock.Style > </ TextBlock > </ DataTemplate > </ ItemsControl.ItemTemplate > </ ItemsControl > </ StackPanel > </ ControlTemplate > </ Validation.ErrorTemplate > |
Cross-property errors
Così come il metodo GetErrors ritorna una collezione di errori di validazione per una data proprietà, si può facilmente sviluppare una validazione cross-property - per casi in cui un cambio del valore di una proprietà può causare un errore in un'altra proprietà - aggiungendo un appropriato errore al dictionary, o qualunque collezione si stia utilizzando per archiviare gli errori di validazione, ed infine comunicare al motore di accoppiamento/binding di richiamare questo metodo per intercettare l'evento ErrorsChanged.
Nell'esempio di seguito, la proprietà Interest è obbligatoria solamente quando la proprietà Type ha un certo valore e la validazione della proprietà Interest deve essere verificata solo quando entrambe le proprietà sono settate.
public class ViewModel : INotifyDataErrorInfo { private readonly Dictionary< string , ICollection< string >> _validationErrors = new Dictionary< string , ICollection< string >>(); private Int16 _type; public Int16 Type { get { return _type; } set { _type = value; ValidateInterestRate(); } } private decimal ? _interestRate; public decimal ? InterestRate { get { return _interestRate; } set { _interestRate = value; ValidateInterestRate(); } } private const string dictionaryKey = "InterestRate" ; private const string validationMessage = "You must enter an interest rate." ; private void ValidateInterestRate() { /* The InterestRate property must have a value only if the Type property is set to 1 */ if (_type.Equals(1) && !_interestRate.HasValue) { if (_validationErrors.ContainsKey(dictionaryKey)) _validationErrors[dictionaryKey].Add(validationMessage); else _validationErrors[dictionaryKey] = new List< string > { validationMessage }; RaiseErrorsChanged( "InterestRate" ); } else if (_validationErrors.ContainsKey(dictionaryKey)) { _validationErrors.Remove(dictionaryKey); RaiseErrorsChanged( "InterestRate" ); } } #region INotifyDataErrorInfo members ... #endregion } |
Mentre l'interfaccia IDataErrorInfo che era fornita fino al Framework .NET 3.5, basicamente forniva solamente la capacità di ritornare una stringa che specifica cos'è sbagliato di una data proprietà, la nuova interfaccia INotifyDataErrorInfo fornisce una maggiore flessibilità e viene generalmente utilizzata quando si implementano nuove classi.
Data annotations
In ASP.NET MVC il modello standard di accoppiamento/binding supporta la validazione delle proprietà utilizzando gli attributi DataAnnotations. DataAnnotations riferiscono ad un set di attributi nel namespace System.ComponentModel.DataAnnotations (definito in System.ComponentModel.DataAnnotations.dll) che si possono applicare ad una classe o ai suoi membri per specificare le regole di validazione, come i dati vengono mostrati e le relazioni tra le classi. Fondamentalmente permette di spostare la logica di validazione dal controller al model (o nel model binder) che effettivamente rende più semplice scrivere unit tests per le action dei controller.
In WPF si possono generare questo genere dei validazioni manualmente ed esiste la classe statica System.ComponentModel.DataAnnotations.Validator che può essere usata apposta per questo scopo. Questa classe espone alcune sovrascritture di metodi che permettono di validare un oggetto intero o una singola proprietà di un oggetto.
Di seguito un esempio di classe model con due proprietà che vengono decorate con attributi DataAnnotations. Sul Sito Microsoft è possibile trovare la lista degli attributi built-in forniti da MSDN, ma è possibile definire i propri creando una classe che erediti dalla classe astratta System.ComponentModel.DataAnnotations.ValidationAttribute.
public class Model { [Required(ErrorMessage = "You must enter a username." )] [StringLength(10, MinimumLength = 4, ErrorMessage = "The username must be between 4 and 10 characters long" )] [RegularExpression( @"^[a-zA-Z]+$" , ErrorMessage = "The username must only contain letters (a-z, A-Z)." )] public string Username { get ; set ; } [Required(ErrorMessage = "You must enter a name." )] public string Name { get ; set ; } } |
Il View Model seguente infine implementa l'interfaccia INotifyDataErrorInfo ed utilizza il metodo TryValidateProperty della classe Validator per eseguire le regole di validazione specificate nei Data Annotations nella classe model. L'overload del metodo utilizzato che prende un'istanza dell'oggetto da validare, un oggetto System.ComponentModel.DataAnnotations.ValidationContext che descrive il context nel quale il controllo di validazione deve lavorare, una collezione per contenere la descrizione di ogni validazione fallita e un valore Boolean per specificare quando tutte le proprietà sono state validate. Da notare che l'esempio di validazione di seguito fornisce metodi per validazioni singole o complete dell'oggetto Model.
public class ViewModel : INotifyDataErrorInfo { private readonly Dictionary< string , ICollection< string >> _validationErrors = new Dictionary< string , ICollection< string >>(); private readonly Model _user = new Model(); public string Username { get { return _user.Username; } set { _user.Username = value; ValidateModelProperty(value, "Username" ); } } public string Name { get { return _user.Name; } set { _user.Name = value; ValidateModelProperty(value, "Name" ); } } protected void ValidateModelProperty( object value, string propertyName) { if (_validationErrors.ContainsKey(propertyName)) _validationErrors.Remove(propertyName); ICollection<ValidationResult> validationResults = new List<ValidationResult>(); ValidationContext validationContext = new ValidationContext(_user, null , null ) { MemberName = propertyName }; if (!Validator.TryValidateProperty(value, validationContext, validationResults)) { _validationErrors.Add(propertyName, new List< string >()); foreach (ValidationResult validationResult in validationResults) { _validationErrors[propertyName].Add(validationResult.ErrorMessage); } } RaiseErrorsChanged(propertyName); } /* Alternative solution using LINQ */ protected void ValidateModelProperty_( object value, string propertyName) { if (_validationErrors.ContainsKey(propertyName)) _validationErrors.Remove(propertyName); PropertyInfo propertyInfo = _user.GetType().GetProperty(propertyName); IList< string > validationErrors = ( from validationAttribute in propertyInfo.GetCustomAttributes( true ).OfType<ValidationAttribute>() where !validationAttribute.IsValid(value) select validationAttribute.FormatErrorMessage( string .Empty)) .ToList(); _validationErrors.Add(propertyName, validationErrors); RaiseErrorsChanged(propertyName); } protected void ValidateModel() { _validationErrors.Clear(); ICollection<ValidationResult> validationResults = new List<ValidationResult>(); ValidationContext validationContext = new ValidationContext(_user, null , null ); if (!Validator.TryValidateObject(_user, validationContext, validationResults, true )) { foreach (ValidationResult validationResult in validationResults) { string property = validationResult.MemberNames.ElementAt(0); if (_validationErrors.ContainsKey(property)) { _validationErrors[property].Add(validationResult.ErrorMessage); } else { _validationErrors.Add(property, new List< string > { validationResult.ErrorMessage }); } } } /* Raise the ErrorsChanged for all properties explicitly */ RaiseErrorsChanged( "Username" ); RaiseErrorsChanged( "Name" ); } #region INotifyDataErrorInfo members /* Same implementation as above */ #endregion } |