Convalida corretta con MVVM

Avvertenza: post molto lungo e dettagliato.

Va bene, la convalida in WPF quando si utilizza MVVM. Ho letto molte cose ora, ho letto molte domande su SO e ho provato molti approcci, ma a un certo punto tutto sembra un po ‘hacky e non sono sicuro di come farlo nel modo giusto ™.

Idealmente, voglio che tutte le validazioni avvengano nel modello della vista usando IDataErrorInfo ; quindi è quello che ho fatto. Esistono tuttavia diversi aspetti che rendono questa soluzione non una soluzione completa per l’intero argomento di convalida.

La situazione

Prendiamo la seguente forma semplice. Come puoi vedere, non è niente di speciale. Abbiamo solo due caselle di testo che si legano a una string e proprietà int nel modello di vista ciascuna. Inoltre abbiamo un pulsante associato a un ICommand .

Forma semplice con solo una stringa e un input intero

Quindi per la validazione ora abbiamo due scelte:

  1. Possiamo eseguire la convalida automaticamente ogni volta che cambia il valore di una casella di testo. In quanto tale, l’utente ottiene una risposta istantanea quando inserisce qualcosa di non valido.
    • Possiamo fare un ulteriore passo avanti per disabilitare il pulsante in caso di errori.
  2. Oppure possiamo eseguire la validazione solo esplicitamente quando viene premuto il pulsante, quindi mostriamo tutti gli errori se applicabili. Ovviamente non possiamo disabilitare il pulsante sugli errori qui.

Idealmente, voglio implementare la scelta 1. Per i normali binding di dati con ValidatesOnDataErrors triggersto questo è il comportamento predefinito. Pertanto, quando il testo cambia, il binding aggiorna l’origine e triggers la convalida IDataErrorInfo per quella proprietà; gli errori sono segnalati alla vista. Fin qui tutto bene.

Stato di convalida nel modello di vista

Il bit interessante è lasciare che il modello di vista, o il pulsante in questo caso, sappia se ci sono errori. Il modo in cui IDataErrorInfo funziona, è principalmente lì per riportare gli errori alla vista. Quindi la vista può facilmente vedere se ci sono errori, visualizzarli e persino mostrare annotazioni usando Validation.Errors . Inoltre, la convalida avviene sempre guardando una singola proprietà.

Quindi avere il modello di vista sapere quando ci sono errori, o se la convalida è riuscita, è difficile. Una soluzione comune è semplicemente triggersre la convalida IDataErrorInfo per tutte le proprietà nel modello di vista stesso. Questo viene spesso eseguito utilizzando una proprietà IsValid separata. Il vantaggio è che questo può anche essere facilmente utilizzato per disabilitare il comando. Lo svantaggio è che questo potrebbe eseguire la convalida su tutte le proprietà un po ‘troppo spesso, ma la maggior parte delle convalide dovrebbe essere semplicemente sufficiente a non danneggiare le prestazioni. Un’altra soluzione potrebbe essere quella di ricordare quali proprietà hanno prodotto errori utilizzando la validazione e controllare solo quelle, ma ciò sembra un po ‘complicato e non necessario per la maggior parte delle volte.

La linea di fondo è che questo potrebbe funzionare bene. IDataErrorInfo fornisce la convalida di tutte le proprietà e possiamo semplicemente utilizzare quell’interfaccia nel modello di visualizzazione stesso per eseguire la convalida anche lì per l’intero object. Presentazione del problema:

Eccezioni vincolanti

Il modello di vista usa tipi reali per le sue proprietà. Quindi, nel nostro esempio, la proprietà intero è un int reale. La casella di testo utilizzata nella vista tuttavia supporta solo il testo in modo nativo. Pertanto, quando si esegue il binding a int nel modello di visualizzazione, il motore di associazione dati eseguirà automaticamente conversioni di tipo, o almeno ci proverà. Se puoi inserire del testo in una casella di testo destinata ai numeri, è molto probabile che non ci siano sempre numeri validi all’interno: così il motore di associazione dei dati non riuscirà a convertire e lanciare un FormatException .

Il motore di associazione dei dati genera un'eccezione e viene visualizzata nella vista

Dal lato della vista, possiamo facilmente vederlo. Le eccezioni dal motore di associazione vengono automaticamente catturate da WPF e vengono visualizzate come errori: non è nemmeno necessario abilitare Binding.ValidatesOnExceptions che sarebbe necessario per le eccezioni generate nel setter. I messaggi di errore hanno comunque un testo generico, quindi potrebbe essere un problema. Ho risolto questo problema utilizzando un gestore Binding.UpdateSourceExceptionFilter , Binding.UpdateSourceExceptionFilter l’eccezione generata e osservando la proprietà di origine e generando invece un messaggio di errore meno generico. Tutto ciò si è accumulato nella mia estensione di markup Binding, quindi posso avere tutti i valori predefiniti di cui ho bisogno.

Quindi la vista va bene. L’utente commette un errore, visualizza un feedback di errore e può correggerlo. Il modello di visualizzazione tuttavia è perso . Poiché il motore di associazione ha generato l’eccezione, la fonte non è mai stata aggiornata. Quindi il modello di vista è ancora sul vecchio valore, che non è ciò che viene visualizzato all’utente, e ovviamente la convalida IDataErrorInfo non si applica.

Quel che è peggio, non c’è un buon modo per il modello di visualizzazione di saperlo. Almeno, non ho ancora trovato una buona soluzione per questo. Ciò che sarebbe ansible è che la vista restituisca al modello di vista un errore. Questo può essere fatto collegando i dati della proprietà Validation.HasError al modello di vista (che non è ansible direttamente), quindi il modello di visualizzazione potrebbe controllare prima lo stato della vista.

Un’altra opzione potrebbe essere quella di Binding.UpdateSourceExceptionFilter l’eccezione gestita in Binding.UpdateSourceExceptionFilter al modello di visualizzazione, in modo che venga notificata anche a esso. Il modello di visualizzazione potrebbe anche fornire un’interfaccia per il binding per segnalare queste cose, consentendo messaggi di errore personalizzati invece di quelli generici per tipo. Ma ciò creerebbe un accoppiamento più forte dalla vista al modello di vista, che in genere voglio evitare.

Un’altra “soluzione” sarebbe quella di eliminare tutte le proprietà tipizzate, utilizzare le proprietà di string semplice e invece eseguire la conversione nel modello di visualizzazione. Ciò ovviamente sposterebbe tutte le convalide sul modello della vista, ma implicherebbe anche un’incredibile quantità di duplicati di cose di cui il motore di binding dei dati di solito si prende cura. Inoltre cambierebbe la semantica del modello di vista. Per me, una vista è costruita per il modello di vista e non il contrario – naturalmente il design del modello di vista dipende da ciò che immaginiamo sia la vista, ma c’è ancora libertà generale come la vista lo fa. Quindi il modello di vista definisce una proprietà int perché c’è un numero; la vista ora può usare una casella di testo (che consente tutti questi problemi), o usare qualcosa che funziona nativamente con i numeri. Quindi no, cambiare i tipi di proprietà in string non è un’opzione per me.

Alla fine, questo è un problema della vista. La vista (e il relativo motore di associazione dei dati) è responsabile di fornire i valori appropriati del modello di visualizzazione con cui lavorare. Ma in questo caso, sembra che non ci sia un buon modo per dire al modello di vista che dovrebbe invalidare il valore della vecchia proprietà.

BindingGroups

I gruppi vincolanti sono un modo in cui ho cercato di affrontarlo. I gruppi vincolanti hanno la capacità di raggruppare tutte le convalide, incluso IDataErrorInfo e le eccezioni generate. Se disponibili per il modello di visualizzazione, hanno anche un mezzo per controllare lo stato di convalida di tutte quelle fonti di convalida, ad esempio utilizzando CommitEdit .

Per impostazione predefinita, i gruppi vincolanti implementano la scelta 2 dall’alto. Rendono esplicitamente l’aggiornamento dei binding, aggiungendo essenzialmente uno stato non vincolante aggiuntivo. Pertanto, quando si fa clic sul pulsante, il comando può confermare tali modifiche, triggersre gli aggiornamenti di origine e tutte le convalide e ottenere un risultato singolo se ha avuto successo. Quindi l’azione del comando potrebbe essere questa:

  if (bindingGroup.CommitEdit()) SaveEverything(); 

CommitEdit restituirà true solo se tutte le convalide sono riuscite. Prenderà IDataErrorInfo in considerazione e controllerà anche le eccezioni di binding. Questa sembra essere una soluzione perfetta per la scelta 2. L’unica cosa che è un po ‘una seccatura è la gestione del gruppo vincolante con i binding, ma ho costruito me stesso qualcosa che si occupa principalmente di questo ( correlato ).

Se è presente un gruppo di rilegatura per un’associazione, l’associazione utilizzerà automaticamente un UpdateSourceTrigger esplicito. Per implementare la scelta 1 dall’alto utilizzando i gruppi di associazione, in pratica dobbiamo modificare il trigger. Poiché ho un’estensione di binding personalizzata comunque, è piuttosto semplice, l’ho appena impostato su LostFocus per tutti.

Quindi ora i binding verranno aggiornati ogni volta che un campo di testo cambia. Se l’origine potrebbe essere aggiornata (il motore di associazione non fa eccezione), IDataErrorInfo verrà eseguito normalmente. Se non può essere aggiornato, la vista è ancora in grado di vederlo. E se clicchiamo sul nostro pulsante, il comando sottostante può chiamare CommitEdit (anche se non è necessario eseguire alcun commit) e ottenere il risultato della convalida totale per vedere se può continuare.

Potremmo non essere in grado di disabilitare il pulsante facilmente in questo modo. Almeno non dal modello di vista. Controllare la convalida ripetutamente non è una buona idea solo per aggiornare lo stato del comando, e il modello di vista non viene notificato quando viene comunque lanciata un’eccezione del motore di associazione (che dovrebbe quindi disabilitare il pulsante) – o quando si passa a abilita nuovamente il pulsante. Potremmo ancora aggiungere un trigger per disabilitare il pulsante nella vista utilizzando Validation.HasError modo che non sia imansible.

Soluzione?

Quindi, nel complesso, questa sembra essere la soluzione perfetta. Qual è il mio problema con esso però? Ad essere onesti, non ne sono del tutto sicuro. I gruppi vincolanti sono una cosa complessa che sembra essere solitamente utilizzata in gruppi più piccoli, possibilmente con più gruppi di rilegatura in un’unica vista. Usando un grande gruppo di rilegatura per l’intera visione solo per garantire la mia convalida, mi sembra di abusarne. E continuo a pensare che ci dev’essere un modo migliore per risolvere l’intera situazione, perché sicuramente non posso essere l’unico ad avere questi problemi. E finora non ho visto molte persone utilizzare i gruppi vincolanti per la convalida con MVVM, quindi sembra strano.

Quindi, qual è esattamente il modo corretto di eseguire la convalida in WPF con MVVM mentre è ansible verificare le eccezioni del motore di associazione?


La mia soluzione (/ hack)

Prima di tutto, grazie per il tuo contributo! Come ho scritto sopra, sto usando IDataErrorInfo già per fare la convalida dei miei dati e personalmente ritengo che sia l’utilità più comoda per fare il lavoro di validazione. Sto usando utility simili a quelle suggerite da Sheridan nella sua risposta qui sotto, quindi anche il mantenimento funziona bene.

Alla fine, il mio problema si è ridotto al problema dell’eccezione di binding, in cui il modello di visualizzazione non avrebbe saputo quando è successo. Mentre potevo gestire questo problema con gruppi vincolanti come descritto sopra, ho comunque deciso di non farlo perché non mi sentivo a mio agio. Quindi cosa ho fatto invece?

Come accennato in precedenza, UpdateSourceExceptionFilter eccezioni di binding sul lato della vista ascoltando l’ UpdateSourceExceptionFilter un binding. Qui, posso ottenere un riferimento al modello di vista dal DataItem dell’espressione di DataItem . Quindi ho un’interfaccia IReceivesBindingErrorInformation che registra il modello di visualizzazione come un ansible ricevitore per informazioni sugli errori di binding. Quindi lo uso per passare il percorso di associazione e l’eccezione al modello di visualizzazione:

 object OnUpdateSourceExceptionFilter(object bindExpression, Exception exception) { BindingExpression expr = (bindExpression as BindingExpression); if (expr.DataItem is IReceivesBindingErrorInformation) { ((IReceivesBindingErrorInformation)expr.DataItem).ReceiveBindingErrorInformation(expr.ParentBinding.Path.Path, exception); } // check for FormatException and produce a nicer error // ... } 

Nel modello di visualizzazione, quindi, ricordo ogni volta che mi viene notificata l’espressione di bind del percorso:

 HashSet bindingErrors = new HashSet(); void IReceivesBindingErrorInformation.ReceiveBindingErrorInformation(string path, Exception exception) { bindingErrors.Add(path); } 

E ogni volta che IDataErrorInfo riconvalida una proprietà, so che l’associazione ha funzionato, e posso cancellare la proprietà dal set di hash.

Nel modello di visualizzazione I, quindi, è ansible verificare se il set di hash contiene elementi e interrompere qualsiasi azione che richieda la convalida completa dei dati. Potrebbe non essere la soluzione più bella a causa dell’accoppiamento dalla vista al modello di vista, ma usando quell’interfaccia è almeno un po ‘meno un problema.

Attenzione: risposta lunga anche

Uso l’interfaccia IDataErrorInfo per la convalida, ma l’ho personalizzato in base alle mie esigenze. Penso che troverai che risolve anche alcuni dei tuoi problemi. Una differenza rispetto alla tua domanda è che la implemento nella mia class di tipi di dati di base.

Come hai sottolineato, questa interfaccia si occupa solo di una proprietà alla volta, ma chiaramente in questo giorno ed età, non va bene. Quindi ho appena aggiunto una proprietà della raccolta da utilizzare invece:

 protected ObservableCollection errors = new ObservableCollection(); public virtual ObservableCollection Errors { get { return errors; } } 

Per risolvere il tuo problema di non essere in grado di visualizzare errori esterni (nel tuo caso dalla vista, ma nella mia dal modello di visualizzazione), ho semplicemente aggiunto un’altra proprietà di raccolta:

 protected ObservableCollection externalErrors = new ObservableCollection(); public ObservableCollection ExternalErrors { get { return externalErrors; } } 

Ho una proprietà HasError che guarda la mia collezione:

 public virtual bool HasError { get { return Errors != null && Errors.Count > 0; } } 

Ciò mi consente di associarlo a Grid.Visibility utilizzando un BoolToVisibilityConverter personalizzato, ad es. per mostrare una Grid con un controllo di raccolta all’interno che mostra gli errori quando ce ne sono. Mi permette anche di cambiare un Brush in Red per evidenziare un errore (usando un altro Converter ), ma credo che tu abbia capito l’idea.

Quindi in ogni tipo di dati, o class del modello, sovrascrivo la proprietà Errors e implemento l’indicizzatore Item (semplificato in questo esempio):

 public override ObservableCollection Errors { get { errors = new ObservableCollection(); errors.AddUniqueIfNotEmpty(this["Name"]); errors.AddUniqueIfNotEmpty(this["EmailAddresses"]); errors.AddUniqueIfNotEmpty(this["SomeOtherProperty"]); errors.AddRange(ExternalErrors); return errors; } } public override string this[string propertyName] { get { string error = string.Empty; if (propertyName == "Name" && Name.IsNullOrEmpty()) error = "You must enter the Name field."; else if (propertyName == "EmailAddresses" && EmailAddresses.Count == 0) error = "You must enter at least one e-mail address into the Email address(es) field."; else if (propertyName == "SomeOtherProperty" && SomeOtherProperty.IsNullOrEmpty()) error = "You must enter the SomeOtherProperty field."; return error; } } 

Il metodo AddUniqueIfNotEmpty è un metodo di extension personalizzato e “fa quello che viene detto sul barattolo”. Nota come chiamerà a turno ciascuna proprietà che voglio convalidare e compilare una raccolta da loro, ignorando gli errori duplicati.

Usando la collezione ExternalErrors , posso convalidare cose che non posso validare nella class data:

 private void ValidateUniqueName(Genre genre) { string errorMessage = "The genre name must be unique"; if (!IsGenreNameUnique(genre)) { if (!genre.ExternalErrors.Contains(errorMessage)) genre.ExternalErrors.Add(errorMessage); } else genre.ExternalErrors.Remove(errorMessage); } 

Per indirizzare il tuo punto riguardo la situazione in cui un utente inserisce un carattere alfabetico in un campo int , io tendo ad usare una IsNumeric AttachedProperty personalizzata per il TextBox , ad es. Non li lascio fare questi tipi di errori. Sento sempre che è meglio fermarlo, piuttosto che lasciarlo accadere e poi sistemarlo.

Complessivamente sono davvero contento della mia capacità di convalida in WPF e non mi manca affatto.

Per finire con e per completezza, ho sentito che dovrei INotifyDataErrorInfo del fatto che ora INotifyDataErrorInfo un’interfaccia INotifyDataErrorInfo che include alcune di queste funzionalità aggiunte. Puoi trovare ulteriori informazioni dalla pagina Interfaccia INotifyDataErrorInfo su MSDN.


AGGIORNAMENTO >>>

Sì, la proprietà ExternalErrors mi consente di aggiungere errori relativi a un object dati esterno a quell’object … scusa, il mio esempio non è stato completato … se avessi mostrato il metodo IsGenreNameUnique , avresti visto che utilizza LinQ su tutti gli elementi di dati di Genre nella raccolta per determinare se il nome dell’object è univoco o meno:

 private bool IsGenreNameUnique(Genre genre) { return Genres.Where(d => d.Name != string.Empty && d.Name == genre.Name).Count() == 1; } 

Per quanto riguarda il tuo problema int / string , l’unico modo in cui posso vedere che ricevi quegli errori nella tua class di dati è se dichiari tutte le proprietà come object , ma poi avresti un sacco di casting da fare. Forse potresti raddoppiare le tue proprietà in questo modo:

 public object FooObject { get; set; } // Implement INotifyPropertyChanged public int Foo { get { return FooObject.GetType() == typeof(int) ? int.Parse(FooObject) : -1; } } 

Quindi se Foo stato utilizzato nel codice e FooObject stato utilizzato in Binding , puoi farlo:

 public override string this[string propertyName] { get { string error = string.Empty; if (propertyName == "FooObject" && FooObject.GetType() != typeof(int)) error = "Please enter a whole number for the Foo field."; ... return error; } } 

In questo modo potresti soddisfare le tue esigenze, ma avrai un sacco di codice extra da aggiungere.

Lo svantaggio è che questo potrebbe eseguire la convalida su tutte le proprietà un po ‘troppo spesso, ma la maggior parte delle convalide dovrebbe essere semplicemente sufficiente a non danneggiare le prestazioni. Un’altra soluzione potrebbe essere quella di ricordare quali proprietà hanno prodotto errori utilizzando la validazione e controllare solo quelle, ma ciò sembra un po ‘complicato e non necessario per la maggior parte delle volte.

Non è necessario tenere traccia di quali proprietà hanno errori; devi solo sapere che gli errori esistono. Il modello di visualizzazione può mantenere un elenco di errori (utile anche per visualizzare un riepilogo degli errori) e la proprietà IsValid può semplicemente riflettere se l’elenco ha qualcosa. Non è necessario controllare tutto ogni volta IsValid viene chiamato IsValid , a condizione che il riepilogo degli errori sia aggiornato e IsValid sia aggiornato ogni volta che viene modificato.


Alla fine, questo è un problema della vista. La vista (e il relativo motore di associazione dei dati) è responsabile di fornire i valori appropriati del modello di visualizzazione con cui lavorare. Ma in questo caso, sembra che non ci sia un buon modo per dire al modello di vista che dovrebbe invalidare il valore della vecchia proprietà.

È ansible ascoltare gli errori all’interno del contenitore associato al modello di visualizzazione:

 container.AddHandler(Validation.ErrorEvent, Container_Error); ... void Container_Error(object sender, ValidationErrorEventArgs e) { ... } 

Questo ti avvisa quando vengono aggiunti o rimossi degli errori ed è ansible identificare le eccezioni di binding in base e.Error.Exception , in modo che la vista possa mantenere un elenco di eccezioni di binding e informare il modello di visualizzazione di esso.

Ma qualsiasi soluzione a questo problema sarà sempre un trucco, perché la vista non sta riempiendo correttamente il suo ruolo, il che sta dando all’utente un mezzo per leggere e aggiornare la struttura del modello di visualizzazione. Questo dovrebbe essere visto come una soluzione temporanea finché non si presenta correttamente all’utente un qualche tipo di “casella intera ” invece di una casella di testo .

Secondo me, il problema sta nella convalida che accade in troppi luoghi. Desideravo anche scrivere tutti i miei dati di accesso per la convalida in ViewModel ma tutti quei numeri vincenti stavano facendo impazzire il mio ViewModel .

Ho risolto questo problema creando un binding che non fallisce mai. Ovviamente, se un’associazione ha sempre successo, il tipo stesso deve gestire le condizioni di errore con garbo.

Tipo di valore disponibile

Ho iniziato con la creazione di un tipo generico che supportava con garbo le conversioni non riuscite:

 public struct Failable { public T Value { get; private set; } public string Text { get; private set; } public bool IsValid { get; private set; } public Failable(T value) { Value = value; try { var converter = TypeDescriptor.GetConverter(typeof(T)); Text = converter.ConvertToString(value); IsValid = true; } catch { Text = String.Empty; IsValid = false; } } public Failable(string text) { Text = text; try { var converter = TypeDescriptor.GetConverter(typeof(T)); Value = (T)converter.ConvertFromString(text); IsValid = true; } catch { Value = default(T); IsValid = false; } } } 

Si noti che anche se il tipo non riesce a inizializzare a causa di una stringa di input non valida (secondo costruttore), memorizza in modo silenzioso lo stato non valido e anche il testo non valido . Questo è necessario per supportare il round-trip di binding anche in caso di input errati .

Convertitore di valore generico

Un convertitore di valore generico può essere scritto utilizzando il tipo sopra riportato:

 public class StringToFailableConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value.GetType() != typeof(Failable)) throw new InvalidOperationException("Invalid value type."); if (targetType != typeof(string)) throw new InvalidOperationException("Invalid target type."); var rawValue = (Failable)value; return rawValue.Text; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { if (value.GetType() != typeof(string)) throw new InvalidOperationException("Invalid value type."); if (targetType != typeof(Failable)) throw new InvalidOperationException("Invalid target type."); return new Failable(value as string); } } 

XAML Handy Converters

Dal momento che creare e utilizzare istanze di generici fa pena in XAML, consente di creare istanze statiche di convertitori comuni:

 public static class Failable { public static StringToFailableConverter Int32Converter { get; private set; } public static StringToFailableConverter DoubleConverter { get; private set; } static Failable() { Int32Converter = new StringToFailableConverter(); DoubleConverter = new StringToFailableConverter(); } } 

Altri tipi di valore possono essere estesi facilmente.

uso

L’utilizzo è piuttosto semplice, basta cambiare il tipo da int a Failable :

ViewModel

 public Failable NumberValue { //Custom logic along with validation //using IsValid property } 

XAML

  

In questo modo, è ansible utilizzare lo stesso meccanismo di convalida ( IDataErrorInfo o INotifyDataErrorInfo o qualsiasi altra cosa) in ViewModel controllando la proprietà IsValid . Se IsValid è true, puoi utilizzare direttamente il Value .

Ok, credo di aver trovato la risposta che stavi cercando …
Non sarà facile da spiegare – ma ..
Molto facile da capire una volta spiegato …
Penso che sia il più preciso / “certificato” per MVVM visto come “standard” o al minimo tentativo standard.

Ma prima di iniziare … devi cambiare un concetto a cui ti sei abituato riguardo MVVM:

“Inoltre cambierebbe la semantica del modello di vista: per me, una vista è costruita per il modello di vista e non il contrario – naturalmente il design del modello di vista dipende da cosa immaginiamo la vista da fare, ma c’è ancora un generale libertà come la vista lo fa ”

Quel paragrafo è la fonte del tuo problema .. – perché?

Perché stai affermando che View-Model non ha alcun ruolo per adattarsi alla vista.
Questo è sbagliato in molti modi – come ti dimostrerò molto semplicemente ..

Se hai una proprietà come:

public Visibility MyPresenter { get...

Qual è la Visibility se non qualcosa che serve alla vista?
Il tipo stesso e il nome che verrà dato alla proprietà è sicuramente costituito per la vista.

Esistono due categorie View-Models distinguibili in MVVM in base alla mia esperienza:

  • Presenter View Model – che deve essere agganciato a pulsanti, menu, elementi Tab, ecc ….
  • Entity View Model – che deve essere impegnato nei controlli che portano i dati dell’ quadro sullo schermo.

Questi sono due diversi: preoccupazioni completamente diverse.

E ora alla soluzione:

 public abstract class ViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public void RaisePropertyChanged([CallerMemberName] string propertyName = null) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } 

 public class VmSomeEntity : ViewModelBase, INotifyDataErrorInfo { //This one is part of INotifyDataErrorInfo interface which I will not use, //perhaps in more complicated scenarios it could be used to let some other VM know validation changed. public event EventHandler ErrorsChanged; //will hold the errors found in validation. public Dictionary ValidationErrors = new Dictionary(); //the actual value - notice it is 'int' and not 'string'.. private int storageCapacityInBytes; //this is just to keep things sane - otherwise the view will not be able to send whatever the user throw at it. //we want to consume what the user throw at us and validate it - right? :) private string storageCapacityInBytesWrapper; //This is a property to be served by the View.. important to understand the tactic used inside! public string StorageCapacityInBytes { get { return storageCapacityInBytesWrapper ?? storageCapacityInBytes.ToString(); } set { int result; var isValid = int.TryParse(value, out result); if (isValid) { storageCapacityInBytes = result; storageCapacityInBytesWrapper = null; RaisePropertyChanged(); } else storageCapacityInBytesWrapper = value; HandleValidationError(isValid, "StorageCapacityInBytes", "Not a number."); } } //Manager for the dictionary private void HandleValidationError(bool isValid, string propertyName, string validationErrorDescription) { if (!string.IsNullOrEmpty(propertyName)) { if (isValid) { if (ValidationErrors.ContainsKey(propertyName)) ValidationErrors.Remove(propertyName); } else { if (!ValidationErrors.ContainsKey(propertyName)) ValidationErrors.Add(propertyName, validationErrorDescription); else ValidationErrors[propertyName] = validationErrorDescription; } } } // this is another part of the interface - will be called automatically public IEnumerable GetErrors(string propertyName) { return ValidationErrors.ContainsKey(propertyName) ? ValidationErrors[propertyName] : null; } // same here, another part of the interface - will be called automatically public bool HasErrors { get { return ValidationErrors.Count > 0; } } } 

E ora da qualche parte nel tuo codice – il tuo comando pulsante ‘CanExecute’ può aggiungere alla sua implementazione una chiamata a VmEntity.HasErrors.

E la pace potrebbe essere sul tuo codice per quanto riguarda la convalida d’ora in poi 🙂