Chi si accinge alla stesura di programmi in Visual Basic, specie di tipo gestionale, si accorge ben
presto che il semplice gesto di trascinare una TextBox sopra un Form, non e` seguito da un'altrettanta
facilita` di validazione del testo inserito. Tutto e` semplice finche` si usano CheckBox,
OptionButton ecc., ma quando siamo in presenza di campi a testo libero per acquisire nomi, indirizzi e cosi` via,
le cose sono piu` complicate. Vediamo alcune possibili soluzioni.
La prima idea che viene in mente a chi affronta il problema per la prima volta,
e` quella di porre il codice di validazione nell'evento LostFocus,
cosi` da controllare se il testo o il valore inserito risponde ai
criteri voluti nel momento in cui l'utente abbandona la TextBox
(o altro Control similare).
Qualora l'utente digiti qualcosa che non va bene, riportiamo il cursore sul testo errato
tramite Text1.Setfocus.
Chi tenta questa strada pero`, s'imbattera` presto in un serio problema: supponiamo di
avere una Text1 ed una Text2. Nella Text1 e` stato inserito un valore errato.
In conseguenza dell'evento LostFocus di Text1, il codice di validazione riscontra
il problema e cerca di riportare il fuoco su Text1.
A questo punto, pero`, per tornare alla Text1 necessita soddisfare la regola imposta nel LostFocus di Text2 e magari proprio questa TextBox non accetta valori Null !!! Succede cosi` che prima di tornare alla Text1 dove abbiamo digitato un testo o un valore non corretto, dobbiamo per forza inserire qualcosa di valido nella Text2; solo allora saremmo proiettati nuovamente nella Text1 dal nostro precedente SetFocus. Immaginate cosa succederebbe se di TextBox ce ne fossero una decina e nessuno volesse valori Null, a zero o comunque non quelli di default! Il fuoco diventerebbe una patata bollente che nessuno vuole e si entrerebbe in loop con il cursore che corre veloce da una TextBox all'altra! Qualcuno potrebbe anche pensare di ovviare al problema usando DoEvents qua e la` ma il risultato varierebbe di poco, forse invece di avere un loop infinito se ne avrebbe uno che termina con ôOut of stack spaceö!
Vediamo allora cos'altro si potrebbe inventare.
Altre soluzioni potrebbero essere l'uso dell'evento Keypress o KeyDown per riconoscere
un tasto particolare, ad esempio il mitico Enter e solo allora controllare il
testo inserito, ma che succede se uno si sposta col mouse?
E cosa succede se viene usato il taglia/incolla?
No, proviamo un'altra strada. Si potrebbe usare l'evento Change e controllare la
stringa man mano che si forma, ovvero mentre l'utente digita il testo.
Questa potrebbe essere una valida soluzione, pero` e` applicabile solo in particolari casi.
Uno di questi potrebbe essere la ricerca di un nome in una piccola rubrica,
con l'artistico effetto di visione immediata di tutti i nomi che corrispondono al
testo digitato fino a quel momento.
In un sistema di rete e/o su un archivio di grandi dimensioni e` pero`
una cosa impensabile.
Figuriamoci che i professionisti consigliano di usare meno cursori
di database possibili, figuriamoci a proporre una cosa del genere!
Che dire poi se il controllo dovesse essere fatto solo quando l'utente ha
terminato di digitare e non prima, per evitare di fare controlli inutili
o addirittura visualizzare errori che non esistono?
Come si capisce che l'utente ha terminato l'input?
Se si usa il LostFocus siamo daccapo; se si usa il KeyPress siamo al punto precedente.
Un'altra soluzione potrebbe essere validare tutti i campi solo alla fine della
compilazione del form, quando l'utente preme un pulsante di OK.
Questo e` un sistema consigliato anche nel Mastering Visual Basic 5
for experienced developers della Microsoft ed usato anche dalla casa
medesima in molti suoi prodotti.
Ma anche qui si pone un dilemma: mettiamo di avere un Form con il quale
si acquisiscono i dati di un'anagrafica clienti e, com'e` d'obbligo,
vogliamo evitare all'utente di dover digitare CAP, Citta`, numero di telefono ecc.
qualora da nome e cognome si stabilisca tramite una query su database
che il cliente e` gia` esistente.
Come impostiamo il form? Mettiamo un pulsante da premere dopo l'inserimento
di nome e cognome e se non risulta nessun cliente lasciamo che
l'utente continui l'inserimento?
Non ci piace! Inoltre al termine
serve comunque un altro pulsante per fare la verifica di tutti i dati
inseriti e resta quindi un secondo problema: il nervosismo degli
utenti che si trovano a premere OK convinti di aver finito di compilare
la lunga lista di input e si ritrovano invece con una sfilza di
MessageBox che informano di questo e quel campo da dover ridigitare.
E` meglio evitare.
Si potrebbe anche controllare a tempo, tramite un Timer, tutti i campi
del Form cosi` da accorgersi prima della pressione del pulsante OK che
qualche campo contiene valori non validi e porvi di nuovo il fuoco.
Se il Timer fa un controllo ogni 300mS si ha poco consumo di tempo macchina
e per l'utente il controllo sembra immediato, senza che abbia
il tempo di digitare qualcosa in una nuova TextBox prima della
validazione della precedente. Non e` male. Pero` questo sistema asincrono
puo` complicare un po' la vita. Se facciamo un controllo cosi` nudo
e crudo possiamo essere avvertiti di errori inesistenti, solo perche`
non abbiamo ancora finito di completare l'input. Ad esempio, se usiamo una
TextBox per accettare codici di articoli che non devono essere puramente
numerici, potremmo essere avvisati di errore quando scriviamo 123,
perche` il meccanismo di validazione non puo` sapere che stavamo
per scrivere 123ab. Dobbiamo quindi inserire un filtro per effettuare
il controllo solo sui campi senza fuoco. Non e` un problema grazie alla
collection Controls. Resta pero` ancora qualche problemino.
Se riprendiamo l'esempio precedente dove tramite nome e cognome
vogliamo controllare in un database l'esistenza di un cliente e
compilare il resto delle TextBox in automatico, vediamo che mettendo
nell'evento LostFocus del cognome la query al database, rischiamo
di effettuarla anche con valori errati perche` quando il timer
si accorge del problema, per veloce che sia, e` ormai gia` avvenuto
il LostFocus e quindi la query. Mmmh non siamo certo sulla via
della semplicita`. L'unica cosa positiva di questo sistema potrebbe essere
la centralizzazione di tutto il codice di validazione dentro
il timer o in una Sub.
Se invece lasciassimo andare il fuoco dove vuole e avvertissimo solamente
l'utente di quale campo non e` valido? Si potrebbe far cambiare
il colore della TextBox incriminata o robe simili e lasciare che
sia l'utente a tornare sul luogo del misfatto.
Se non lo fa non si attiva il pulsante OK. Non mi sembra male.
Teniamola in considerazione.
Un altro sistema potrebbe essere creare degli ActiveX su misura.
Non e` una cosa tanto banale, ma si puo` fare. In questo caso pero`
e` bene mettere in preventivo un bel consumo di risorse di sistema.
Se ne puo` riparlare.
A questo punto viene spontanea una domanda: ma la Microsoft come fa?
Pensavo di carpire qualche idea analizzando il comportamento di alcuni
suoi programmi famosi, ma mi sono reso conto che anche mamma
Microsoft non e` poi cosi` brillante sotto questo aspetto.
Provate a dare un'occhiata al funzionamento della rubrica
di Exchange o di Outlook: vi siete mai accorti che non c'e`
nessun controllo e possono essere inserite anche lettere nel
campo telefono o numeri come nome e cognome? Anche in programmi
piu` sostanziosi come Access non e` meglio: i controlli vengono effettuati
tutti assieme al momento del salvataggio del record (eccetto alcuni casi)
e non e` raro ritrovarsi con una sfilza di messaggi di errore
uno dietro l'altro, spesso senza neanche l'indicazione di quale
campo genera il problema.
Anche nella Mastering Microsoft Series gli unici consigli proposti sono:
(1) restringere il numero di opzioni
(2) usare il piu` possibile Controls che offrano solo scelte obbligate
(3) usare MaxLenght per limitare la lunghezza del testo inserito
(4) usare KeyPress, KeyDown e KeyUp per avvertire immediatamente
se si digita un numero in un campo solo testo o viceversa, oppure per
convertire da maiuscolo a minuscolo ecc.
(5) validare tutti i campi tramite un tasto OK che si attiva solo quando
tutti i campi obbligatori sono pieni.
Ringrazio di tutti questi ottimi consigli offerti, ma restiamo
sempre al punto di partenza.
Se certi metodi di programmazione sono comodi a chi sviluppa software,
certo non portano vantaggi a chi dovra` usarlo come utente finale.
Vediamo quindi un metodo per fare contente entrambi le parti e
casomai integrare a questo sistema di validazione anche i consigli Microsoft.
Controllo centralizzato
Il metodo che sto per illustrare, non e` niente di strabiliante,
forse e` l'uovo di Colombo, ma e` abbastanza comodo da realizzare e
mantenere dato che il codice di validazione dei campi e` raggruppato
tutto insieme e non c'e` bisogno di scorrere il programma su e giu`
alla ricerca dei nostri controlli di validazione.
Consuma pochissimo tempo macchina e risorse.
Tengo a precisare che e` solo un esempio di una tecnica e prescinde
dall'uso specifico che se ne vuole fare: chi usa il Jet Database Engine
puo` voler usare il piu` possibile le validation rules imposte ai fields ed evitare l'imposizione di regole nell'interfaccia utente. Cio` non toglie che, nei casi in cui non e` possibile applicare solo tale tecnica (e sono moltissimi), non possa essere usato il controllo centralizzato in esame. Magari qualcuno potrebbe fare anche un ibrido per un doppio controllo. Morale: fateci quello che vi pare!
Ma veniamo al sodo; il concetto di funzionamento e` questo: ogni campo di
acquisizione, che nel programma chiamo UIF (User Input Field), quando acquista
il fuoco, quindi nell'evento GotFocus, setta una variabile pubblica di nome
UIFActive di tipo Control come ActiveControl (set UIFActive=Me.ActiveControl).
Questa variabile viene usata in un Select Case nella sub UIFValidation per sapere
quale Control validare. Non puo` infatti essere usata direttamente la proprieta`
ActiveControl perche` prima di arrivare al Select Case il Control attivo e` gia`
un altro, ovvero quello che ha causato il LostFocus del campo da validare.
Al momento del LostFocus si richiama la Sub UIFValidation la quale impone
validazioni diverse per ogni Control.
Il listato 1 mostra l'implementazione base del concetto appena illustrato
applicando la validazione a due TextBox.
Passiamo alla pratica
Il programma allegato UIFValidation.vbp e` composto da cinque TextBox
a cui vengono applicate validazioni diverse, come riportato sopra il campo
stesso. Avviando il programma, Text1 prende il fuoco e si ha l'evento
GotFocus che setta UIFActive per contenere Text1 come oggetto.
Inseriamo in Text1 un valore non corretto e proviamo a spostarci su un altro
campo con il mouse o con il tasto Tab. Qui si verifica una cosa che
inizialmente lascia un po' perplessi. Abbiamo dapprima l'evento LostFocus
di Text1 che richiama UIFValidation. Entrati in questa Sub, L'activeControl
e` gia` Text2, nonostante debba ancora verificarsi il suo evento GotFocus.
La Sub, tramite UIFActive puo` comunque effettuare la validazione sul Control
corretto e si accorge che in Text1 sono stati inseriti valori errati.
Impone quindi Text1.SetFocus e ci avviamo ad uscire dalla Sub.
Il bello pero` arriva adesso: ad Exit Sub ci aspetta come un'avvoltoio
il GotFocus della Text2 che carica in UIFActive l'oggetto TextBox che ha il fuoco
e indovinate chi e`?
Ci si aspetterebbe fosse Text2 dato che siamo nel suo GotFocus.
Se fosse cosi` pero`, sarebbe la fine perche` al momento in cui
LostFocus richiama inevitabilmente UIFValidation per controllare Text2
saremmo nella condizione identica all'uso diretto della validazione nel
LostFocus di cui abbiamo parlato ad inizio articolo.
Invece nonostante sia in corso di esecuzione l'evento GotFocus di Text2,
l'ActiveControl e` gia` su Text1!
Questo avviene grazie ad UIFActive.SetFocus eseguito dalla Sub UIFValidation
invocata dal LostFocus di Text1.
Ma non e` tutto; se nella UIFValidation inseriamo delle MessageBox per
notificare all'utente la ragione della non validazione, non si ha per
niente l'esecuzione del codice nell'evento GotFocus della Text2,
quindi UIFActive resta Text1. Per di piu`, nonostante non si verifichi
il GotFocus di Text2, abbiamo invece il suo LostFocus e questo richiama
nuovamente UIFValidation. Grazie alla linea If Me.ActiveControl
Is UIFActive Then Exit Sub si impedisce la visualizzazione della stessa
MessageBox di errore sul campo Text1 due volte consecutive.
Provate a toglierla per constatare di persona il problema.
La sub UIFValidation include anche un piccolo controllo sulle
validazione richieste dai campi, ma non implementate per dimenticanza.
Il nome del Form e del campo incriminato vengono in questo caso mostrate
all'utente.
E` tutto. Non e` semplice come sistema?
Forse e` piu` complicata la spiegazione di quanto non lo sia nella realta`!
Analizzando l'esempio dovrebbe risultare tutto chiaro.
Provate a mettere dei Debug.Print in ogni GotFocus e LostFocus delle
TextBox o usare dei Watch in modo da vedere in diretta il succedersi degli eventi
e verificare quanto sopra.
Forse ci siamo
Abbiamo cosi` realizzato un sistema per avere inputs secondo un certo
criterio semplice e funzionale.
A questo punto possiamo includere anche altri controlli come quelle
indicati dalla Microsoft, ad esempio nell'evento Keypress.
Se pensate che potrebbe valere la pena disabilitare per esempio
la digitazione dei numeri in un campo Cognome invece di attendere
che l'utente termini la compilazione per mostrare il messaggio di errore,
basta un istruzione:
If IsNumeric(Chr$(KeyAscii)) then KeyAscii = 0.
Ricordate solo di mettere questi controlli come optional e di
includere comunque sempre la verifica in UIFValidation altrimenti
avreste sicuramente problemi quando un testo viene introdotto col taglia/incolla.
Questi mini controlli portano a decentralizzare un po' la validazione,
ma e` pur sempre tutto abbastanza compatto e quel che conta di piu`, ottimo per l'utente.
E` altresi` valida l'idea di disabilitare il pulsante OK finche`
non sono stati compilati i campi obbligatori, anzi lo ritengo proprio
indispensabile per l'effettuazione del prossimo passo:
la gestione dei campi obbligatori.
Gestione dei campi obbligatori
Come si nota dal programma di esempio, possiamo imporre che certi campi
non accettino valori Null, quindi il problema di rendere obbligatori certi campi
sembrerebbe risolto. E` bene pero` riservare tale caratteristica per casi particolari.
I campi che non accettano Null possono infatti essere un po' noiosi perche`
una volta che hanno il fuoco non e` piu` possibile andarsene senza
inserire qualcosa di valido (vedi Text2 e Text4 nel file allegato).
Cosi`, se mi posiziono sopra un ipotetico campo Partita I.V.A.,
poi mi accorgo che non la conosco e per adesso voglio inserire solo nome e cognome
non posso piu` spostarmi prima di aver inserito una partita I.V.A. valida.
Conviene quindi adottare una gestione dei campi obbligatori vera e propria.
Per realizzarla possiamo scegliere almeno tre strade:
1) Lasciare abilitato il pulsante OK ed inserire del codice
nell'evento Click che controlli se qualche campo obbligatorio
e` rimasto vuoto. Quando l'utente preme OK riceve un messaggio
e viene spostato sul campo da compilare.
Premendo di nuovo OK, se ve ne sono altri succede la stessa
cosa fino al completamento.
2) Porre il codice di test dei campi obbligatori dentro
la UIFValidation e farlo eseguire ad ogni sua invocazione.
3) Creare una apposita Sub da richiamare dall'evento
Change di ogni TextBox.
La prima soluzione non mi aggrada molto, e` ancora troppo irritante
per l'utente, meno funzionale e piu` difficile da implementare
delle altre due.
La seconda strada e` ottima. Ha un consumo di tempo di CPU ridotto
all'osso: solo quando si conferma l'inserimento spostandoci su
un altro campo si ha la validazione del campo attuale piu`
il controllo dei campi obbligatori. Unico neo risiede
nell'abilitazione del pulsante OK:
supponendo di aver appena terminato di riempire l'ultima delle
TextBox obbligatorie, il suddetto pulsante non si attivera` finche`
non ci spostiamo su un altro Control a scelta, perche` solo allora
si verifica il LostFocus e l'invocazione di UIFValidation.
Non e` un grave problema, tuttavia se puo` sembrare
brutto c'e` sempre il terzo metodo, che guarda caso e`
quello che ho usato nel file di esempio
UIFVAlidation.vbp e riportato nel Listato 2.
Il consumo di tempo di CPU e` maggiore, ma non di molto.
A differenza della seconda soluzione vengono controllati al volo tutti i
campi che non devono restare vuoti, ogni volta che si digita un carattere.
Dando un'occhiata alla Sub di nome UIFEmpty si puo` notare comunque come
sia scheletrica. Pensando a quali mattoni di programmi girano sulle
povere macchine d'oggi si puo` certo chiudere un occhio!
Il funzionamento e` semplicissimo: alla pressione di un tasto
o comunque quando si verifica l'evento Change di una TextBox,
viene richiamata UIFEmpty. Dentro la sub vengono controllati uno dopo
l'altro tutti i campi obbligatori. Appena ne viene incontrato uno vuoto
la sub termina. Se invece vengono verificati positivamente tutti
i campi desiderati, si raggiunge l'ultima linea della sub che
abilita il pulsante OK. Piu` semplice di cosi` non saprei cosa inventare.
Dobbiamo solo ricordare di richiamare UIFEmpty dall'evento Change
di ogni campo che definiamo come obbligatorio. Abbiamo cosi`
risposto alla richiesta degli utenti, abbiamo una gestione semplice
delle validazioni ed abbiamo fatto contento il processore.
Questo sdoppiamento fra validazione di input e campo obbligatorio
dona piena liberta` di movimento in fase di inserimento dati.
Abbiamo ora dei campi di acquisizione che accettano valori
Null o meno (ricordarsi di usare Trim$ se si desidera considerare
Null anche un campo riempito a soli spazi), gestione dei campi obbligatori,
notifica di errore immediato e al contrario di altri metodi,
funziona anche il taglia/incolla e spostamenti con tastiera o mouse.
A questo punto lascio a voi integrare altri controlli di validita`
piu` o meno interattivi ed abbellire il tutto. Tanto per imboccare qualcosa,
si potrebbe iniziare indicando con un asterisco o con un diverso colore
i campi obbligatori. Una volta imparato il meccanismo l'utente sara`
spinto automaticamente a controllare i campi contrassegnati senza che neanche
gli sfiori il pensiero di provare a premere OK in anticipo.
Si potrebbe anche selezionare in automatico tutto il testo della TextBox tramite
l'evento GotFocus inserendo qualcosa di simile:
ActiveControl.SelSart=0
ActiveControl.SelLenght=Len(ActiveControl)
Volendo si potrebbe anche centralizzare tutto dentro un
Class Module ed usare RaiseEvent per sollevare un evento di errore
della validazione che ritorni anche il testo da mostrare all'utente magari
in un prossimo articolo.