Oggi sono rimasto off-line tutto il tempo, una vera
tortura per qualsiasi bloggatore come me, e come tutti voi. Questa sera voglio
recuperare, proponendo il mio metodo di validazione che ho deciso di
adottare nel mio software di fatturazione. Il progetto è ovviamente di tipo
Windows Forms. Mi interessava la Janky.Validation, perchè ne ho discusso con Giancarlo che me ne ha sempre parlato bene, ed effettivamente da
quando mi ha passato i sorgenti, ho cercato di usarla con regolarità. Il
risultato ottenuto è davvero ottimo, e sono qui a parlarvi proprio di questo.
Non voglio scendere troppo nel dettaglio, perciò arrivo al dunque.
Per adesso, basta sapere che sul click di
ogni pulsante Salva istanzio un oggetto
ValidationContext ed aggiungo tutte le
RuleBase necessarie alla validazione. Se la validazione ha
successo, allora procedo con il salvataggio dell'oggetto. Altrimenti gestisco le
BrokenRules che, come descritto da Giancarlo (Janky) Sudano nel suo post,
ritornano essenzialmente le regole non validate: per ogni BrokenRule infranta,
ho un object (che rappresenta il
controllo) ed una string (che
rappresenta il messaggio di errore) che uso per piegare ai miei voleri
l'ErrorProvider.
Ma c'era comunque qualcosa che volevo migliorare.
Non voglio referenziare la Janky.Validation nella
UI
Facendo così, la validazione avviene nel layer UI. In questo
layer, ho referenziato la Janky.Validation. Sul click ho istanziato un
ValidationContext. Questa cosa non mi piaceva molto. Io volevo raggiungere i
seguenti obiettivi:
- Slegare il più possibile la UI dal meccanismo di validazione. Oggi uso la
Janky.Validation, cosa succede se un domani uso il
Validation Block? Oppure altro? Io nella UI non voglio
saperne di librerie esterne, voglio un qualcosa che nel modo più ".NET
possibile" mi dica se ci sono errori oppure no.
- Passo seguente: voglio un modo più "intelligente" per capire a quale
controllo fa riferimento ogni errore. Non mi piace usare l'enum, perchè sono
costretto a fare uno switch per ogni BrokenRules, e dedurre da questo il
Control. Non è per niente bello, su, da bravi, ripetetelo con me: non è bello,
non è bello, non è bello.
- Passo seguente: ho costruito una classe che mi istanzia un
oggetto xyzValidator, dove xyz sta per un oggetto del mio
domain model. Quindi, avrò un ArticoloValidator, un FatturaValidator, un
ClienteValidator, etc.
- Questa classe accede al meccanismo sottostante esposto dalla
Janky.Validation. Essa non fa nient'altro che ritornarmi un
IDictionary<object, string>, che mappa 1:1 le BrokenRules infrante.
Stessa cosa di prima, per il resto: object contiene il controllo per
questo errore, mentre string è il messaggio d'errore.
- Sul click del pulsante Salva, adesso, istanzio questa
classe wrapper. Gestisco l'IDictionary di ritorno e manipolo l'ErrorProvider
visualizzando eventuali errori.
Tutto questo è convertito in righe di codice. Eccovele. Innanzitutto ho
dichiarato una classe Validator astratta.
public abstract class Validator
{
protected Dictionary<object, string> _dict = new Dictionary<object, string>();
protected ValidationContext ctx;
protected bool initialized = false;
protected object[] _controls;
protected abstract void Initialize();
public Validator()
{ ctx = new ValidationContext(); }
public object[] Controls
{
get { return _controls; }
set { _controls = value; }
}
public Dictionary<object, string> IsValid()
{
if (!initialized) Initialize();
if (!ctx.IsValid())
{
foreach (RuleBase rb in ctx.BrokenRules)
_dict.Add(rb.Key, rb.Message);
}
return (_dict);
}
La classe Validator contiene una proprietà _dict, che
verrà ritornata al chiamante. Contiene un ValidationContext, che conterrà tutte
le RuleBase. Contiene un bool di utility che mi serve per capire quando il
Validator è a posto. Poi c'è una cosa che devo migliorare: l'array di controlli,
a cui ogni possibile errore fa riferimento. Il metodo
Initialize è astratto, perchè va specializzato dalla classe
reale. Essendo la Validator astratta, ho creato 3 classi
Validator reali (Cliente, Articolo e Fattura). Ne faccio vedere solo uno, quello
dell'articolo.
public class ArticoloValidator : Validator
{
private Articolo _articolo;
public ArticoloValidator(Articolo Articolo, object[] UIControls)
{
_articolo = Articolo;
_controls = UIControls;
}
protected override void Initialize()
{
ctx = new ValidationContext();
ctx.Rules.Add(new IsValidStringLengthRule(_controls[0], _articolo.Codice, 1, 10, "Inserire un codice da 1 a 10 caratteri!"));
ctx.Rules.Add(new IsValidStringLengthRule(_controls[1], _articolo.Denominazione, 1, 255, "La denominazione è obbligatoria!"));
ctx.Rules.Add(new IsValueInRangeRule(_controls[2], (double)_articolo.CostoUnitario, 1, 5000, "Il costo unitario è obbligatorio!"));
ctx.Rules.Add(new IsValueInRangeRule(_controls[3], (double)_articolo.Iva, 20, 20, "Secondo le leggi in vigore, l'IVA è sempre al 20%!"));
}
}
Il costruttore di ArticoloValidator prende in ingresso un'istanza di
Articolo ed un array di controlli. L'override di Initialize(), veramente brutta
così com'è, riempie il ValidationContext definito nella classe base, aggiunge le
RuleBase specifiche che mi servono ed il gioco è fatto. Il
risultato è che adesso quando chiamo il metodo IsValid della
classe Validator, mi viene ritornato un IDictionary<object, string>, come
dicevo prima.
Nel layer UI, per ogni WF ho creato una function che ritorna un
bool.
private bool notifyErrors()
{
errNotifica.Clear();
ArticoloValidator validator = new ArticoloValidator(currentEntity,
new object[] { txtCodice, txtDenominazione, txtCostoUnitario, txtIva });
IDictionary<object, string> dict = validator.IsValid();
if (dict.Count == 0) return (false);
IEnumerator<KeyValuePair<object, string>> cycle = dict.GetEnumerator();
while (cycle.MoveNext())
{
if (cycle.Current.Key != null)
{
errNotifica.SetIconAlignment((Control)cycle.Current.Key, ErrorIconAlignment.TopLeft);
errNotifica.SetError((Control)cycle.Current.Key, cycle.Current.Value);
}
}
return(true);
}
Questa function ritorna true se ci sono uno o più
errori. Ritorna false se invece è tutto ok. Notare che il
costruttore riceve un oggetto currentEntity (di tipo Articolo) ed un array di
Controls.
Da notare inoltre che nel blocco di codice riportato qui sopra
non si parla assolutamente di
Janky.Validation. Infatti, tale
libreria non è assolutamente referenziata nella UI.
Non ci sono istruzioni using nel codice della
WF. E' tutto molto pulito. Non faccio altro che chiedere alla
classe ArticoloValidator un qualche feedback sulla validazione, che mi viene
ritornato da un IDictionary. Se un giorno volessi cambiare metodo di
validazione, modificherei l'implementazione di ArticoloValidator, e niente di
più.
Ultimo appunto, ma che reputo importante. Dopo aver ottenuto un IEnumerator,
ciclo tranquillamente l'IDictionary. Sono sicuro che la Key
contiene il Control, e il Value contiene il messaggio di
errore. Si può migliorare il casting, si possono migliorare un po' di cose, ma
per adesso i miei obiettivi li ho raggiunti. Evviva!