Le avventure in VB.Net di un principiante ex-VB6 - 17
a cura di Oscar Zanin e Diego Cattaruzza (requisiti: Visual Basic Express e SqlServer)Premessa
Nell'articolo precedente di questa serie erano state omesse alcune parti di codice, lasciate come 'compito a casa'; in questo articolo si farà una specie di 'correzione in classe', illustrando qualche pezzo di codice che potrebbe essere sfuggito.
Quindi si porrà rimedio a un bug segnalato da uno dei nostri affezionati 'alunni'. Non solo: verrà corretto anche un altro bug scoperto da Oscar. Questa seconda correzione introduce il problema dei valori nulli, per risolvere il quale sono state implementate due classi che verranno illustrate nella seconda parte dell'articolo.Correzione dei compiti a casa
Per coloro che hanno diligentemente tentato di fare i 'compiti', sviluppando da soli le form 'Unità di misura' e 'Prodotti', non sarà particolarmente difficile confrontare il loro codice con il nostro. Molto probabimente troveranno la novità cui non hanno pensato: stavolta sono stati implementati i metodi di convalida, nella FrmProdotti:#Region "Metodi di convalida" Private Sub TxtPrezAcq_Validating(ByVal sender As System.Object, _ ByVal e As System.ComponentModel.CancelEventArgs) _ Handles TxtPrezAcq.Validating If TxtPrezAcq.Text = String.Empty Then Exit Sub End If If Convalida(TxtPrezAcq.Text, "Prezzo di acquisto", _ TipoConv.Valuta_maggiore_di_zero, False) = False Then Messaggi.Avviso("Il campo Prezzo di acquisto se valorizzato deve essere numerico !", _ "Attenzione") e.Cancel = True Exit Sub End If TxtPrezAcq.Text = Formatta(TxtPrezAcq.Text, TipoFor.Valuta) End Sub Private Sub TxtPrezVen_Validating(ByVal sender As System.Object, _ ByVal e As System.ComponentModel.CancelEventArgs) _ Handles TxtPrezVen.Validating If TxtPrezVen.Text = String.Empty Then Exit Sub End If If Convalida(TxtPrezVen.Text, "Prezzo di vendita", _ TipoConv.Valuta_maggiore_di_zero, False) = False Then Messaggi.Avviso("Il campo Prezzo di vendita se valorizzato deve essere numerico !", _ "Attenzione") e.Cancel = True Exit Sub End If TxtPrezVen.Text = Formatta(TxtPrezVen.Text, TipoFor.Valuta) End Sub Private Sub TxtQuanMag_Validating(ByVal sender As System.Object, _ ByVal e As System.ComponentModel.CancelEventArgs) _ Handles TxtQuanMag.Validating If TxtQuanMag.Text = String.Empty Then Exit Sub End If If Convalida(TxtQuanMag.Text, "Quantità a magazzino", _ TipoConv.Migliaia_con_decimali_maggiore_o_uguale_a_zero, False) = False Then Messaggi.Avviso("Il campo Quantità a magazzino se valorizzato deve essere numerico !", _ "Attenzione") e.Cancel = True Exit Sub End If TxtQuanMag.Text = Formatta(TxtQuanMag.Text, TipoFor.Percentuale) End Sub #End RegionStavolta è stato il caso di implementare la validazione dei dati immessi, se immessi (altrove, con DatiValidi, si controlla per lo più se il dato obbligatorio è stato immesso o meno, non la sua validità di tipo e di formato ai fini della valorizzazione del campo nel database). Di per sé, il codice dovrebbe essere sufficientemente autoesplicativo, ma ci sembrava utile puntualizzare la differenza tra questi metodi, che sono metodi di evento, e il metodo DatiValidi, che non lo è ed ha altri scopi, come già spiegato.
Correzione del metodo PopolaGriglia della FrmRicerca
In seguito alla segnalazione di un lettore andiamo a correggere l'eccezione che sorge in una situazione ben precisa: l'utente clicca sul pulsante di filtro senza avere selezionato alcun criterio nei vari ComboBox. In questo caso devono essere restituiti tutti i record relativi al tipo di ricerca in essere.La correzione da effettuare è semplice e consta di due passaggi:
1. Spostare la riga di codice
' aggiungo la clausola WHERE stringaSQL &= " WHERE "all'interno del ciclo, quando si valuta se si sta gestendo il primo criterio valorizzato:
If piuDiUnParametro Then stringaSQL &= " AND " Else piuDiUnParametro = True ' aggiungo la clausola WHERE stringaSQL &= " WHERE " End If2 - Aggiungere uno spazio prima della clausola ORDER BY. La seguente riga:
' completo la stringa sql con l'ordinamento stringaSQL &= "ORDER BY " & mInfoRicerca.Ordinamentodiventa:
' completo la stringa sql con l'ordinamento stringaSQL &= " ORDER BY " & mInfoRicerca.OrdinamentoQuesto evita un errore di sintassi nella compilazione della espresione query
Effettuando personalmente dei test di ricerca nelle form dei prodotti e dei tipi di pagamento (non ancora illustrata nella serie) Oscar si è accorto di un altro errore che ora andremo a sistemare.
Nel caso in cui una delle colonne della griglia contenesse un valore NULL veniva generata un eccezione. Inoltre ci si è resi conto che gestire la tipologia del dato nella compilazione del vettore di valori per le celle portava più problemi che vantaggi. E' uno di quei casi, come li definisce Diego, di babele dei dati.
Pertanto il codice seguente, successivo al ciclo di costruzione della query di selezione:
While dtr.Read() ' compilo un vettore di valori per le celle Dim arrayValori(mInfoRicerca.Colonne.Count) As Object For c As Integer = 0 To mInfoRicerca.Colonne.Count - 1 ' a seconda del tipo di campo Select Case mInfoRicerca.Colonne(c).TipoCampo Case SqlDbType.Char, SqlDbType.NChar, SqlDbType.NText, SqlDbType.NVarChar, _ SqlDbType.Text, SqlDbType.VarChar 'Testi arrayValori(c) = dtr.GetString(c) Case SqlDbType.BigInt, SqlDbType.Int, SqlDbType.SmallInt 'Numeri senza decimali arrayValori(c) = dtr.GetInt32(c) Case SqlDbType.Decimal, SqlDbType.Float, SqlDbType.Money, SqlDbType.Real, _ SqlDbType.SmallMoney 'Numeri con decimali arrayValori(c) = dtr.GetDecimal(c) Case SqlDbType.Date, SqlDbType.DateTime, SqlDbType.DateTime2, SqlDbType.SmallDateTime, _ SqlDbType.Time 'Date arrayValori(c) = dtr.GetDateTime(c) Case SqlDbType.Bit 'Booleani arrayValori(c) = dtr.GetBoolean(c) End Select Next ' preparo la riga nuova e la aggiungo Dim rigaNuova As New DataGridViewRow rigaNuova.CreateCells(DgvDati, arrayValori) DgvDati.Rows.Add(rigaNuova) End Whileviene sostituito dal codice seguente:
While dtr.Read() 'Compilo un vettore di valori per le celle Dim arrayValori(mInfoRicerca.Colonne.Count) As Object For c As Integer = 0 To mInfoRicerca.Colonne.Count - 1 If dtr(c) Is DBNull.Value Then arrayValori(c) = Nothing Else arrayValori(c) = dtr.GetValue(c) End If Next 'Preparo la riga nuova e la aggiungo Dim rigaNuova As New DataGridViewRow rigaNuova.CreateCells(DgvDati, arrayValori) DgvDati.Rows.Add(rigaNuova) End WhileCome potete notare, si controlla l'eventuale nullità del valore del campo: se è nullo, si assegna Nothing all'elemento dell'array, viceversa si assegna il valore ottenuto dal metodo GetValue.
Viene completamente eliminato il costrutto Select, sostituito dall'unica riga:arrayValori(c) = dtr.GetValue(c)dov'è il metodo GetValue a recuperare il tipo opportuno del campo. Questo è stato un 'incidente' di porgrammazione che ci spinge a parlare adesso della classe ValoreCampo, che è una versione leggermente modificata della classe FieldsValue presente nell'articolo "Guarda senza mani - Quarta parte". Le spiegazioni che seguiranno saranno pertanto quelle fornite a suo tempo dai due autori.
Classe ValoreCampo
Questa classe serve per la gestione dei fastidiosi quanto necessari DbNull. Una strategia, usata da molti programmatori per liberarsi dal problema dei DbNull, consiste nel fare in modo che tutti i campi di una tabella debbano per forza essere valorizzati, ma, senza DbNull, non possiamo gestire relazioni, senza relazioni, addio all'integrità referenziale; e allora, che diamine lavorano a fare i programmatori che creano gli RDBMS, se poi noi usiamo il 5% del loro lavoro?Noi siamo fra coloro che non hanno paura dei DbNull, ma questo non toglie che non sia necessario fare qualche acrobazia nel codice visto che DbNull non è null e neppure Nothing. Questa classe piccola, ma molto importante, è stata realizzata per evitare i controlli da farsi su ogni metodo, funzione, classe che abbia a che vedere con i dati quando si tratta di verificare o assegnare il valore di un campo database a una variabile. Per realizzare tutto questo, si sono usati i Generics e, ancora una volta, le classi e i metodi statici.
Questa è la struttura esterna della classe:Imports System Imports System.Collections.Generic Imports System.Text Public Class ValoreCampo(Of T As IComparable) #Region "Variabili private" 'Nome della classe usato per debug nella generazione delle eccezioni Private Shared ReadOnly mClassName As String = _ System.Reflection.MethodBase.GetCurrentMethod.ReflectedType.Name #End Region End ClassSi può subito notare una particolarità: l'uso, dopo la dichiarazione del nome, della clausola (Of T As IsComparable). Cosa significa? Significa che la nostra classe è un tipo Generic, cui però non è possibile assegnare qualsiasi oggetto: i soli oggetti ammessi sono quelli che implementano l'interfaccia IComparable, cioè quelli che possono essere direttamente comparati.
Confusi? Vediamo allora il codice che contiene, così la confusione aumenterà (ma poi tutto sembrerà semplice quando la classe verrà usata).Aggiungiamo il primo metodo che verrà esposto dalla classe:
Public Shared Function Value(ByVal dato As Object, ByVal defaultValue As T) As T Try Dim outValue As T = defaultValue Dim nullo As DBNull = TryCast(dato, DBNull) If nullo Is Nothing Then outValue = DirectCast(dato, T) End If Return outValue Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." + _ System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End FunctionLa prima versione del metodo Value. Il parametro dato è l'oggetto proveniente dal database (cioè un generico object) e defaultValue è il valore che vogliamo sia restituito se il valore di dato è DbNull. Questo metodo è generico, ci permette di processare in modo tipizzato un dato - di qualsiasi tipo, purché aderente a IComparable - e verificare se questo dato è DbNull, nel qual caso è possibile assegnargli il valore di default indicato. In altre parole, se a questo metodo viene passato un oggetto del tipo specificato, viene restituito il suo valore, se invece il dato fosse DbNull, verrebbe restituito il valore indicato come default.
A esempio, con:Dim name As String = ValoreCampo(Of String).Value(row("CAMPO_DB"), String.Empty)informiamo la classe che ci aspettiamo che il campo CAMPO_DB sia una stringa, e che vogliamo ci venga restituito il valore String.Empty, se l'oggetto che gli passiamo, su cui deve fare il controllo, è DbNull.
Aggiungiamo alla classe il seguente overload del metodo precedente:
Public Shared Function Value(ByVal dato As Object) As T Try Return ValoreCampo(Of T).Value(dato, Nothing) Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." + _ System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End FunctionLa seconda versione, tanto per rendere le cose più vivaci, è implementata in modo tale da non necessitare del valore di default. Infatti, utilizza una delle funzionalità aggiunte alla specifica 2.0 del Framework che inizializza qualsiasi tipo con il valore di default imposto dal suo costruttore di base senza parametri. Pertanto, per la String sarà Nothing, per l'intero e tutti i numerici sarà 0, e così via.
Ora implementeremo anche i metodi complementari, che restituiscono un object con valore DbNull se il valore del dato passato è Nothing o uguale al valore che viene ritenuto pari a Nothing (per regole di business, ad esempio).
Quindi implementiamo un primo metodo che prende un valore da un controllo e lo converte in un valore adatto al database oppure in DbNull:Public Shared Function TryParseToDBNull(ByVal dato As T, ByVal dbNullValue As T) As Object Try Dim objRet As Object = Nothing If dbNullValue.Equals(False) And dato.Equals(False) Then Return False End If If dbNullValue Is Nothing AndAlso dato Is Nothing OrElse dato.CompareTo(dbNullValue) = 0 Then objRet = DBNull.Value Else objRet = dato End If Return objRet Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." + _ System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End FunctionIn questo codice, se entrambi i parametri passati hanno valore Nothing , oppure il valore da noi considerato equivalente a Nothing, viene restituito un Object DbNull, altrimenti viene restituito un Object comunque valido per il database.
In questo metodo è presente l'unica piccola modifica apportata al codice originale. Sono state infatti aggiunte le seguenti righe di codice per potere gestire anche le CheckBox:If dbNullValue.Equals(False) And dato.Equals(False) Then Return False End IfAnche questo metodo ha il suo overload:
Public Shared Function TryParseToDBNull(ByVal dato As T) As Object Try Return ValoreCampo(Of T).TryParseToDBNull(dato, Nothing) Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." + _ System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End FunctionLa seconda versione del metodo utilizza la prima passando il valore di default del tipo di dato da esaminare.
Classe ConversioneTipo
Questa classe lavorerà in simbiosi con la precedente. Infatti, ci permetterà di tipizzare e valorizzare correttamente i dati contenuti nelle nostre TextBox, quando utilizzeremo il metodo TryParseToDBNull.
La classe sarà composta da tanti metodi quanti sono i tipi di conversione che vogliamo gestire.
Questa è la struttura iniziale, con il primo metodo:Public Class ConversioneTipo #Region "Variabili private" 'Nome della classe usato per debug nella generazione delle eccezioni Private Shared ReadOnly mClassName As String = _ System.Reflection.MethodBase.GetCurrentMethod.ReflectedType.Name #End Region #Region "Metodi pubblici" Public Shared Function StringToInteger(ByVal dato As String) As Integer If String.IsNullOrEmpty(dato) Then Return Integer.MaxValue Else Return Integer.Parse(dato) End If End FunctionLa logica di implementazione è la stessa per tutti i metodi: il parametro dato viene valorizzato con il valore di una TextBox.Text. Se il dato passato è nullo o stringa vuota, viene restituito il valore massimo per il tipo, altrimenti viene restituito il dato convertito nel tipo corretto.
E' opportuno un esempio di utilizzo di questo metodo in simbiosi con il metodo TryParseToDBNull della classe precedente. Supponiamo che la TextBox contenga il numero 15. Il campo del database sarà così valorizzato:row("CAMPO_DB") = ValoreCampo(Of Integer).TryParseToDBNull(StringToInteger(TxtCampo.Text), _ Integer.MaxValue)Si noti che il secondo parametro - che, ricordo, è il valore che noi consideriamo pari a Nothing, è valorizzato con Tipo.MaxValue. Quindi, in questo caso, il metodo StringToInteger ci restituirà un Integer pari a 15 e altrettanto farà TryParseToDBNull.
Se, invece di contenere 15, la nostra TextBox fosse stata vuota, StringToInteger ci avrebbe restituito Integer.MaxValue, che, passato a TryParseToDBNull nel parametro dato e in essa confrontato con il valore che abbiamo assegnato al secondo parametro (ripeto, il valore che noi consideriamo pari a Nothing) ci avrebbe restituito un DBNull.Value.Essendo gli altri metodi del tutto simili al primo, si omette di inserirli nell'articolo.
Se qualcuno si chiedesse come ci si può assicurare che una TextBox contenga un valore convertibile correttamente nel tipo richiesto, gli potremmo rispondere: attraverso la classe ConvalidaDati, di cui abbiamo già visto qualche esempio d'uso.
Conclusione
Dopo la correzione dei 'compiti a casa', con l'illustrazione di qualche novità nel codice della FrmProdotti, è stata illustrata la correzione di due bug (uno segnalato da un lettore e l'altro scoperto da Oscar), e l'implementazione di due nuove classi, destinate alla gestione dei valori nulli.Il codice di PrimiPassi sviluppato fino a questo momento è come al solito disponibile in area download.
Anche per questa puntata, Diego mette a disposizione nel suo blog un post cui scrivere critiche, suggerimenti, richieste di chiarimento.