Le avventure in VB.Net di un principiante ex-VB6 - 9
a cura di Oscar Zanin e Diego Cattaruzza (requisiti: Visual Basic Express e SqlServer Express )Premessa
C'è stato un lungo intervallo tra questa puntata e quella precedente.
Soprattutto a causa dello stile di programmazione un po' disordinato di Oscar e della eccessiva rigidità mentale di Diego (se non gli dici le cose in modo preciso, è più stolido di un computer).Argomento di questa puntata sarebbero dovute essere le due classi principali che costituiscono l'interfaccia tra il programma e il database.
Invece ne verrà presentata solo una e si parlerà dell'importanza di scrivere codice in un modo che possa essere compreso da altri (e anche da noi stessi, passato un certo lasso di tempo).Inoltre, si esporrà (senza approfondirne più di tanto alcuni aspetti) il problema dei tipi di dato del database contrapposti ai tipi di dato .Net.
Introduzione
Per interagire con i dati del database, è opportuno predisporre delle classi che fungano da interfaccia tra il programma e il database. In altre parole, bisogna implementare il Data Layer della nostra applicazione, che verrà utilizzato dal Business Layer (le form con le regole e le azioni).Queste classi espongono un insieme di metodi che, lavorando opportunamente fra loro e interagendo con le form chiamanti, si occupano della scrittura, modifica, cancellazione e spostamento di record di una o più tabelle del database. Queste classi, Tabella e DatiMasterDetails, che verranno illustrate in questo articolo, rappresentano forse il punto nevralgico della porzione di programma che stiamo realizzando. Sono lo snodo obbligatorio dal quale passeranno tutte le form del programma che dovranno interagire con i dati del database SQL.
Nello specifico, la classe Tabella viene sfruttata dove una form è legata a un'unica tabella del database (per esempio la form dell'anagrafica degli Stati), mentre la DatiMasterDetails viene usata quando in una form debbono essere gestite più tabelle relazionate fra loro in un rapporto master-details (ne è un esempio la form dei clienti che coinvolge la tabella clienti che è una master, e le tabelle sedi e riferimenti che sono dei details: la classe DatiMasterDetails si occupa di entrambe le tabelle dettaglio, in riferimento alla tabella master).
[Diego] Questa era l'introduzione di Oscar. E questo è un inciso che volendo potete saltare, ma che vi prego di leggere, poiché parla dell'importanza di usare nomi appropriati a quanto scriviamo nel codice.
I nomi delle due classi originali, DatiMaster e DatiMasterDetails, mi hanno tratto in inganno e hanno generato un primo equivoco sul quale si sono avvitati altri equivoci, che hanno rallentato moltissimo la mia comprensione sia degli appunti che del codice di Oscar
Solo dopo un bel po', mi sono reso conto che gli appunti descrivevano una cosa, ma che i nomi usati nel codice facevano pensare a tutt'altro.
Nella classe DatiMasterDetails, tutte le tabelle venivano istanziate come... DatiMaster, e io non capivo come alcune di esse potessero 'diventare' dettagli.
Si tenga anche ben presente che il tutto sta funzionando benissimo a regime in alcune applicazioni sviluppate da Oscar. Non è che i nomi influiscano sul funzionamento del programma. Influiscono solo sul funzionamento dei due neuroni di Diego, nel senso che li ingrippano tra loro.
L'importanza dei nomi
Nella realtà del codice, la classe DatiMaster è una generica classe Tabella, senza alcunché di particolare che la definisca 'master'. Ma io sono stato in grado di rendermene conto solo dopo un lungo lavoro di analisi. Se poi si considera che mi occupo di questa serie di articoli nei ritagli di tempo...Dando i nomi giusti alla classe e ai suoi membri, cioè facendo un po' di refactoring sul codice di Oscar, questo è diventato molto più comprensibile ai pochi neuroni di Diego. Perché vi possiate rendere conto dell'importanza dei nomi, espongo la tabella dei membri coi nomi originali, una breve descrizione di ciascuno e i nomi nuovi. E' piuttosto lunga, ma consiglio vivamente di esaminarla, cercando di comprendere (dovrebbe essere intuitivo) il motivo del cambiamento del nome del membro, se c'è stato.
Nome originale Descrizione Nome nuovo DatiMaster classe Tabella Campi adp DataAdapter adp cbl CommandBuilder cbl cnn Connection mConnessione ColonneChiave vettore dei nomi delle colonne chiave mColonneChiave dst DataSet mDataSet indiceRecord indice di un record mIndiceRecord myColonnaModifica nome del campo 'modifica' mColonnaModifica myOrderBy clausola Order By per la query di selezione della tabella mOrderBy myQueryComando query di selezione della tabella mQuerySelezione myStringaConnessione stringa di connessione per l'oggetto Connection mStringaConnessione ValoriColonneChiave vettore dei valori delle colonne chiave, convertiti string mValoriColonneChiave Proprietà Connessione Connection Connessione ConnStringa stringa di connessione per l'oggetto Connection StringaConnessione Modifica nome del campo 'modifica' NomeCampoModifica NumRighe numero record contenuti in tabella NumeroRighe Ordinamento clausola Order By per la query di selezione della tabella Ordinamento Query query di selezione della tabella QuerySelezione Tabella nome della tabella NomeTabella Metodi AddChiavi aggiunge una chiave al vettore dei nomi delle colonne chiave AggiungiChiave AddValoriChiavi aggiunge un valore al vettore dei valori delle colonne chiave AggiungiValoreChiave AggiornaDB aggiorna il database AggiornaDatabase CancellaRiga cancella un record CancellaRiga CaricaDati legge i dati in una DataTabel del DataSet CaricaDati ControlloModifica controlla in base al campo 'modifica' se il record è modificabile o no Modificabile ImpostaColonnaModifica imposta a vero o falso il valore del campo 'modifica' del record corrente ImpostaColonnaModifica ImpostaPosizione legge i valori delle colonne chiave di un record LeggiRecord ImpostaRigaNuova aggiunge un record alla tabella AggiungiRigaNuova ModificaRecord imposta a vero il valore del campo 'modifica' del record corrente ImpostaModificaRecord MoveFirst, MoveLast, MoveNext, MovePrev navigazione MoveFirst, MoveLast, MoveNext, MovePrev PosizionaRecord trova l'indice del record con i dati valori delle colonne chiave TrovaRecord ResetValoriChiavi svuota il vettore dei valori delle colonne chiave ResettaValoriChiavi RitornaRigaCorrente restituisce un puntatore al record corrente RigaCorrente RitornaRigaNuova restituisce un puntatore a un nuovo record RigaNuova Se avete avuto la pazienza di arrivare sin qui, forse siete anche giunti alla conclusione che quella di Diego non è sempre pedanteria. Immaginate di rileggere il codice con i vecchi nomi a distanza di qualche mese e chiedetevi se sareste in grado di capire al volo, senza analizzare il codice, cosa è questo o quel membro della classe.
Qui finisce l'inciso sulla forma del codice e ci si può dedicare alla prima delle due classi del Data Layer del nostro programma.La classe Tabella
Si occupa della gestione di una singola tabella del database SQL.
Aggiungiamo la classe al progetto APP.Data (il nome del file rimane DatiMaster.vb) e digitiamo le direttive:Imports APP.UI Imports System.Data.SqlClient Public Class TabellaLa prima direttiva serve per abbreviare il codice per i metodi della classe Messaggi, la seconda... vi lascio indovinare, mentre introduco i campi (ne manca uno che aggiungeremo in seguito, quando ne parleremo):
Private adp As SqlDataAdapter Private mIndiceRecord As Integer = 0 Private mColonneChiave As New List(Of String) Private mValoriColonneChiave As List(Of String)Il DataAdapter servirà (vi ricordate la puntata scorsa?) per interfacciare il DataSet e il database relativamente a questa tabella; mIndiceRecord servirà per il corretto posizionamento e lo spostamento fra i record della tabella; gli altri due campi sono due vettori per i nomi e i valori delle colonne chiave della tabella: in sostanza, servono a individuare un preciso record.
Nello sviluppo di una classe, può accadere che si preveda di usarla anche in modi diversi di come si intende fare nella situazione contingente. A esempio, mentre stiamo pensando di usare un particolare costruttore, che per la nostra applicazione va benissimo, implementiamo qualche altro costruttore "caso mai ce ne capitasse la necessità".
E' quello che ha fatto Oscar: ci sono alcuni costruttori che nella nostra applicazione non vengono usati (quello che useremo verrà illustrato in seguito):
Public Sub New() ' End Sub Public Sub New(ByVal stringaConnessione As String) mStringaConnessione = stringaConnessione End Sub Public Sub New(ByVal stringaConnessione As String, ByVal querySelezione As String) mStringaConnessione = stringaConnessione mQuerySelezione = querySelezione End SubA essi si passano dei parametri per valorizzare dei campi relativi ad altrettante proprietà. E' opportuno, a questo punto, elencare proprietà e campi relativi (si omette il codice al loro interno):
Private mConnessione As SqlConnection Public Property Connessione() As SqlConnection Private mStringaConnessione As String Public Property StringaConnessione() As String Private mDataSet As DataSet Public Property DataSet() As DataSet Private mNomeTabella As String Public Property NomeTabella() As String Private mQuerySelezione As String Public Property QuerySelezione() As String Private mOrderBy As String Public Property Ordinamento() As String Private mColonnaModifica As String Public Property NomeCampoModifica() As String Public ReadOnly Property NumeroRighe() As Integer Get Return mDataSet.Tables(0).Rows.Count End Get End PropertyTranne l'ultima, NumeroRighe, che è di sola lettura e che restituisce il numero dei record della tabella nel Dataset di riferimento, e NomeTabella, di cui parleremo nel seguito, le proprietà sono implementate con la solita forma Get-Set, nel senso che non hanno ulteriore codice di comportamento: ricevono o restituiscono un valore.
- Connessione è un oggetto SqlConnection, cioè una connessione a un database Sqlserver;
- StringaConnessione è la stringa di una connessione da creare dall'interno di questa classe (nella nostra applicazione non viene usata);
- DataSet è l'oggetto DataSet che contiene la nostra Tabella, e anche eventuali altre, come si vedrà quando sarà illustrata la classe DatiMasterdetails;
- NomeTabella è il nome della tabella che intendiamo gestire, è ovviamente la proprietà più importante;
- QuerySelezione è l'espressione query per caricare i dati della tabella dal database a una DataTable;
- Ordinamento è l'eventuale clausola Order By della query di selezione con l'elenco dei campi per i quali ordinare la tabella;
- NomeCampoModifica è il nome del campo speciale che si occupa dello stato del record: serve per gestire correttamente l'accesso simultaneo a un record nel caso di multiutenza - Da come avete visto costruendo il database SQL, al momento in tutte le tabelle tale campo si chiama Modifica;
Quelli che seguono, invece, sono i metodi per gestire le colonne chiave della tabella:
Public Sub AggiungiChiave(ByVal chiave As String) mColonneChiave.Add(chiave) End Sub Public Sub AggiungiValoreChiave(ByVal valore As String) mValoriColonneChiave.Add(valore) End Sub Public Sub ResettaValoriChiavi() mValoriColonneChiave = New List(Of String) End Sub Private Sub RiempiValoriColonneChiave(ByVal indice As Integer) mValoriColonneChiave = New List(Of String) For Each nomeColonna As String In mColonneChiave mValoriColonneChiave.Add((mDataSet.Tables(0).Rows(indice)(nomeColonna)).ToString) Next nomeColonna End SubCom'è intuibile, i nomi dei campi e i relativi valori trovano corrispondenza grazie alla loro posizione ordinale all'interno dei rispettivi vettori. L'ultimo metodo legge i valori delle colonne chiave traendoli da un record della tabella di cui viene passato l'indice.
Con il codice scritto fino a questo punto, siamo in grado di fornire a un oggetto di classe Tabella tutte le informazioni che servono, come nell'esempio che segue:
Dim tb As New Tabella With tb .Connessione = oSqlConnection .NomeTabella = "Clienti" .QuerySelezione = "SELECT * FROM clienti" .Ordinamento = "cliente" .AggiungiChiave("idcliente") End WithQuesto ci obbliga a conoscere la struttura della tabella - e fin qui va bene, perché è perfettamente logico - ma anche a ricordarcela 'sempre', dato che dobbiamo anche digitare espressamente i nomi dei campi chiave, con i possibili rischi di errore.
Sarebbe più pratico che se ne occupi la classe, di ricavare la propria struttura. Ecco quindi un altro costruttore, quello soprannunciato:Public Sub New(ByVal connessione As SqlConnection, ByVal tabella As String) mConnessione = connessione Me.NomeTabella = tabella End SubA esso passiamo un oggetto Connection e il nome della tabella. Modifichiamo inoltre il codice della proprietà NomeTabella:
Public Property NomeTabella() As String Get Return mNomeTabella End Get Set(ByVal value As String) mNomeTabella = value GetSchema() End Set End PropertyNel metodo GetSchema ottengo l'insieme dei campi chiave e popolo il relativo vettore:
Private Sub GetSchema() ' apro la connessione mConnessione.Open() ' imposto la query mQuerySelezione = "SELECT * FROM " & mNomeTabella ' istanzio l'adattatore adp = New SqlDataAdapter(mQuerySelezione, mConnessione.ConnectionString) ' e il dataset, riempiendolo coi dati mDataSet = New DataSet adp.Fill(mDataSet, "Dati") ' punto la tabella Dim tb As New DataTable tb = mDataSet.Tables(0) ' ne ottengo lo schema in un'altra datatable Dim sc As New DataTable sc = adp.FillSchema(tb, SchemaType.Source) ' di questo schema ottengo l'insieme delle chiavi Dim pk As DataColumn() = sc.PrimaryKey ' se ce ne sono If pk.Length > 0 Then For Each dc As DataColumn In pk ' aggiungo il nome AggiungiChiave(dc.ColumnName) Next End If mConnessione.Close() End SubL'uso del metodo FillSchema del DataAdapter è risolutivo.
La babele dei tipi
A cosa servono i vettori (nomi e valori) delle colonne chiave? A costruire le clausole Where nelle query parametriche che individuano il record su cui si deve agire. Si creano dei comandi con queste query e si popola l'insieme Parameters con oggetti Parameter relativi alle colonne chiave.A esempio, per controllare il campo 'modifica' di un record, si può costruire un Command aggiungendo parametri con il metodo AddWithValue:
cmd.Parameters.AddWithValue("@" + nomeColonna.ToString, _ mDataSet.Tables(0).Rows(mIndiceRecord)(nomeColonna.ToString))Però il metodo AddWithValue non permette di precisare il tipo del campo, il che espone il codice a qualche rischio. Sarebbe meglio usare il metodo Add, che ha un parametro 'tipo di campo'.
[Diego] Attenzione: nei paragrafi che seguono, non ho potuto trovare valide alternative ai termini 'tipo', 'parametro', 'schema' che non generassero confusione. Troverete quindi questi termini ripetuti molto spesso nella stessa frase ma riferiti a entità diverse. Leggete con calma, anche più volte, perché se perdete il filo non potete comprendere.
Qui ci si trova però di fronte al problema dei tipi, anzi: del tipo dei tipi. Il tipo del tipo-di-campo richiesto dal metodo Add non è del tipo System.Type, quello usato da .Net, ma del tipo "Database".Type, cioè quello specifico usato dal Provider di dati (Sqlserver, OleDb, Oracle...).
La DataColumn usata nel ciclo che scandisce l'insieme PrimaryKey, nel metodo GetSchema mostrato sopra, espone un DataType che corrisponde a un System.Type. Il metodo SqlCommand.Parameters.Add usa l'enumerato SqlDbType. I due tipi non sono convertibili tra loro.
Quindi bisogna ricorrere a un altro sistema per leggere lo schema della nostra tabella: si usa un Datareader richiedendo la tabella dello schema della tabella, e dei record di questa 'tabella dello schema' si leggono i campi che ci interessano, quelli del nome e del tipo del campo della tabella. Queste due informazioni vanno a formare un vettore dei tipi delle colonne chiave, che abbiamo volutamente omesso di mettere tra i campi, all'inizio di questo articolo, in previsione di questa spiegazione. Se avessimo cercato di spiegare allora la ragione dell'esistenza del vettore dei tipi avremmo certamente confuso le idee.
Aggiungiamo quindi tra i campi il campo mTipiColonne:Private mTipiColonne As New Dictionary(Of String, SqlDbType)Potete notare anzittuto che si tratta di un dizionario, non di una lista, in modo da poter individuare l'elemento che serve in base alla chiave, che è il nome del campo, per ottenere il valore dell'elemento, che è il tipo del campo; inoltre vengono inventariati tutti i campi della tabella, non solo quelli chiave, dato che servono anch'essi.
Il metodo GetSchema, quindi, è come segue:Private Sub GetSchema() ' apro la connessione mConnessione.Open() ' imposto la query mQuerySelezione = "SELECT * FROM " & mNomeTabella ' imposto un Command Dim cmd As New SqlCommand(mQuerySelezione, mConnessione) ' ne ottengo un datareader 'speciale' Dim rd As SqlDataReader = cmd.ExecuteReader(CommandBehavior.SchemaOnly) ' del quale ottengo la tabella dello schema della mNomeTabella Dim dt As DataTable = rd.GetSchemaTable ' scorro la tabella e popolo il dizionario For Each r As DataRow In dt.Rows mTipiColonne.Add(r("ColumnName").ToString, CType(r("ProviderType"), SqlDbType)) NextCome potete vedere, è necessario indicare 'per nome esteso' le colonne che ci interessano (andrebbe bene anche il numero, ma non vale la pena rendere meno leggibile il codice e andarsi a cercare qual numero mettere). Per conoscere il nome delle colonne dello schema di ciascun campo, ho eseguito un ciclo come questo (attenzione: non fa parte del codice del metodo GetSchema, c'è stato solo per il tempo necessario ad acquisire le informazioni):
For Each r As DataRow In dt.Rows For Each c As DataColumn In dt.Columns Debug.Print("{0} ({1}): {2}", c.ColumnName, c.DataType.ToString, r(c).ToString) Next NextSe si vuole verificare il contenuto del dizionario:
For Each kvp As KeyValuePair(Of String, SqlDbType) In mTipiColonne Debug.Print("{0}: {1}", kvp.Key, kvp.Value.ToString) NextAvrete inoltre notato la tipizzazione del valore della colonna ProviderType, che è Integer, nel tipo SqlDbType.
Il codice del metodo GetSchema continua con la parte già digitata prima:' istanzio l'adattatore adp = New SqlDataAdapter(mQuerySelezione, mConnessione.ConnectionString) ' e il dataset, riempiendolo coi dati mDataSet = New DataSet adp.Fill(mDataSet, "Dati") ' punto la tabella Dim tb As New DataTable tb = mDataSet.Tables(0) ' ne ottengo lo schema in un'altra datatable Dim sc As New DataTable sc = adp.FillSchema(tb, SchemaType.Source) ' di questo schema ottengo l'insieme delle chiavi Dim pk As DataColumn() = sc.PrimaryKey ' se ce ne sono If pk.Length > 0 Then For Each dc As DataColumn In pk ' aggiungo il nome AggiungiChiave(dc.ColumnName) Next End If mConnessione.Close() End SubAdesso è possibile usare il metodo Add per predisporre adeguatamente un Command parametrico, laddove ci servirà.
Ci siamo limitati, al momento, a rilevare solo il tipo del campo, ma è ovvio che con lo stesso sistema potremo rilevare anche altre proprietà, come Size, Scale e Precision, preparando così, più che un dizionario di tipi di colonne, un insieme completo di oggetti Parameter.I metodi di gestione e navigazione
Risolti i problemi di definizione della tabella, non resta che implementare i vari metodi per la gestione dei dati e della navigazione tra i record della tabella (dobbiamo tenere presente che sulle form ci saranno appositi pulsanti per eseguire le tipiche operazioni).Il metodo CaricaDati si occupa di caricare i dati prelevandoli dalla tabella SQL in una DataTable "Dati" del DataSet.
Public Sub CaricaDati() Try Dim cmd As New SqlCommand cmd.Connection = mConnessione If mOrderBy.Trim <> "" Then cmd.CommandText = mQuerySelezione & " ORDER BY " & mOrderBy Else cmd.CommandText = mQuerySelezione End If adp = New SqlDataAdapter(cmd) Dim cbl As New SqlCommandBuilder(adp) mDataSet = New DataSet adp.Fill(mDataSet, "Dati") Dim colonneDatiChiave(mColonneChiave.Count - 1) As DataColumn For Each nomeColonna In mColonneChiave colonneDatiChiave(mColonneChiave.IndexOf(nomeColonna)) = mDataSet.Tables(0).Columns(nomeColonna) Next nomeColonna mDataSet.Tables(0).PrimaryKey = colonneDatiChiave mIndiceRecord = 0 If mDataSet.Tables(0).Rows.Count = 0 Then mIndiceRecord = -1 End If Catch ex As Exception 'Errori non gestiti Messaggi.Errore(mClassName, System.Reflection.MethodBase.GetCurrentMethod(), ex) End Try End SubNel codice, si istanzia e si predispone un SqlCommand, fornendogli la connessione e la query di selezione, con o senza l'ordinamento; se ne ottiene il DataAdapter da passare a un CommandBuilder.
L'oggetto CommandBuilder non è stato descritto in precedenza e si riporta quanto dice MSDN: "Consente la generazione automatica di comandi di tabella singola per risolvere le modifiche apportate a un oggetto DataSet con il database SQL Server associato". Crea cioè automaticamente i comandi di Insert, Update e Delete.
Il DataAdapter serve anche, come al solito, per creare la DataTable "Dati" nel nostro DataSet; di questa tabella viene anche impostata la proprietà PrimaryKey, tramite un vettore di DataColumn valorizzato scandendo il vettore dei nomi delle colonne chiave; infine viene adeguatamente inizializzato mIndicerecord.
Il metodo Modificabile verifica se un dato record è modificabile, controllando il valore del campo 'modifica':
Public Function Modificabile() As Boolean Static cmd As SqlCommand TryIl Command parametrico è sempre lo stesso, strutturalmente, quindi se ne fa una variabile statica, per non doverlo re-impostare ogni volta.
If cmd Is Nothing Then ' predispongo il command, se non è ancora stato inizializzato Dim filtro As String = "" 'Costruisco il filtro sui campi chiave For Each nomeColonna As String In mColonneChiave If filtro <> "" Then filtro += " AND " filtro += nomeColonna.ToString + " = @" + nomeColonna.ToString Next cmd = New SqlCommand("SELECT " & mColonnaModifica & " FROM " & mNomeTabella & _ " WHERE " + filtro, mConnessione) ' preparo l'insieme Parameters, usando il metodo Add For Each nomeColonna As String In mColonneChiave cmd.Parameters.Add("@" + nomeColonna.ToString, mTipiColonne(nomeColonna.ToString)) Next nomeColonna End IfSe il codice viene eseguito per la prima volta, il Command non è inizializzato e viene predisposto, appunto, una volta per tutte. Prima si costruisce, scandendo la lista dei nomi delle colonne chiave, la stringa per la clausola Where che completerà la query di selezione del comando; query mirata solamente sul campo 'modifica'. Poi, con identica scansione, si prepara l'insieme dei Parameters, usando il metodo Add (è questa la riga per la quale abbiamo escogitato la maniera di sfuggire alla 'babele dei tipi').
' valorizzo l'insieme dei parametri For Each nomeColonna As String In mColonneChiave cmd.Parameters("@" + nomeColonna.ToString).Value = _ mDataSet.Tables(0).Rows(mIndiceRecord)(nomeColonna.ToString) Next mConnessione.Open() Dim risultato As Object = cmd.ExecuteScalar() mConnessione.Close() If risultato Is DBNull.Value Then Return True End If If Convert.ToBoolean(risultato) Then Messaggi.Errore("Record già in modifica !", "Attenzione") Return False End If Return TrueA questo punto il Command è preparato e bisogna solo valorizzare, tramite il solito ciclo, i parametri con i valori contenuti nei campi relativi della DataRow con indice mIndiceRecord (record corrente), dell'insieme Rows della DataTable "Dati" del DataSet. Quindi si apre la connessione e si esegue il metodo ExecuteScalar del Command, che restituisce in risultato il valore del primo campo della prima riga restituita dalla selezione, che è il campo 'modifica'. In base a questo valore si restituisce che il record è modificabile oppure no, con eventuale messaggio all'utente. (si omette la parte di codice che gestisce le eccezioni).
Il metodo CancellaRiga si occupa di eliminare il record visualizzato sulla form chiamante dalla DataTable (prima) e dalla tabella del database (poi).
Il metodo richiede un parametro che indica se lo stesso è richiamato per la cancellazione di record da una tabella master o detail. Questo è dovuto al fatto che la classe che stiamo descrivendo (Tabella) è sfuttata più volte (tante quante sono le tabelle master o details contemplate nella form) dalla classe DatiMasterDetails che ne richiama e sfrutta i metodi. Come avrete sicuramente intuito, se questa classe si occupa di una tabella alla volta, la classe DatiMasterDetails si occuperà invece della gestione di più tabelle in rapporto relazionale fra di loro di master-details.Public Function CancellaRiga(ByVal tabellaMaster As Boolean) As Boolean Try If tabellaMaster = True Then mDataSet.Tables(0).Rows(mIndiceRecord).Delete() AggiornaDataBase() Else If mDataSet.Tables(0).Rows.Count > 0 Then For indice As Integer = 0 To mDataSet.Tables(0).Rows.Count - 1 mDataSet.Tables(0).Rows(indice).Delete() Next AggiornaDataBase() End If End If Return TrueSe si deve cancellare una riga da una tabella Master, si applica il metodo Delete solo su quella riga. Se invece si devono cancellare righe da una tabella Detail, ammesso che ce ne siano di correlate a quella della tabella Master, il metodo Delete viene applicato a tutte. Sì, tutte: se elimino un Cliente, devo eliminare tutte le sedi e i riferimenti a esso associati.
Catch ex As DBConcurrencyExceptionQui cerchiamo per la prima volta di intercettare un'eccezione precisa. In questo caso si tratta di un'eccezione di concorrenza di accesso ai dati. Nello specifico il programma genererà questa eccezione se il record che si tenta di cancellare è in fase di modifica da parte di un altro utente oppure è stato già cancellato in precedenza da un altro utente.
CaricaDati() If Not TrovaRecord() Then Messaggi.Errore("Record già cancellato da un altro utente !", "Attenzione") Return False End If mDataSet.Tables(0).Rows(mIndiceRecord).Delete() AggiornaDataBase() Return True Catch ex As Exception 'Errori non gestiti Messaggi.Errore(mClassName, System.Reflection.MethodBase.GetCurrentMethod(), ex) Return False End Try End FunctionSe si incorre in questa eccezione si ricaricano i dati dalla tabella tramite il metodo CaricaDati descritto in precedenza. Quindi si cerca di posizionarsi sul record da cancellare tramite il metodo TrovaRecord (che vedremo in seguito).
Se il record non viene trovato significa che è già stato cancellato da un altro utente: si visualizza un avviso e si restituisce il fallimento dell'operazione. Se il record viene trovato significa che è in fase di modifica presso un altro utente, quindi si ritenta la cancellazione e si restituisce il successo dell'operazione.Il metodo RigaNuova non fa altro che restituire una nuova DataRow vuota e tipizzata sulla struttura attuale della DataTable.
Public Function RigaNuova() As DataRow Return mDataSet.Tables(0).NewRow End FunctionIl metodo AggiungiRigaNuova si occupa di inserire nella DataTable una DataRow, che riceve come parametro, salvando inoltre i nuovi valori dei campi chiave nella relativa lista. Si intercetta e si gestisce una eventuale eccezione di valore chiave duplicato.
Public Function AggiungiRigaNuova(ByRef riga As DataRow) As Boolean Try 'Memorizzo i valori dei campi chiave mValoriColonneChiave = New List(Of String) For Each nomeColonna As String In mColonneChiave mValoriColonneChiave.Add(riga(nomeColonna).ToString) Next 'Aggiungo la nuova riga al DataSet mDataSet.Tables(0).Rows.Add(riga) Catch ex As ConstraintException 'Errore di chiave primaria duplicata Messaggi.Errore(ex.Message, "Attenzione") Return False Catch ex As Exception 'Errori non gestiti Messaggi.Errore(mClassName, System.Reflection.MethodBase.GetCurrentMethod(), ex) Return False End Try Return True End FunctionIl metodo AggiornaDataBase sincronizza il database con la tabella.
Public Sub AggiornaDataBase() Try adp.Update(mDataSet, "Dati") Catch ex As Exception Throw ex End Try End SubIn una riga tanta sostanza. Come avete letto nei cenni su ADO.NET, sfruttando il metodo Update del DataAdapter, si applicano alla tabella nel database inserimenti, modifiche e cancellazioni di record corrispondenti a inserimenti, modifiche e cancellazioni di DataRow della nostra tabella.
I parametri passati al metodo Update sono il DataSet e il nome della DataTable (che non è quello della tabella, a meno di coincidenze).
Stavolta, come vedete, l'eventuale eccezione è rilanciata al chiamante, poiché l'aggiornamento è un'operazione il cui fallimento può portare conseguenze gravi.Il metodo ImpostaModificaRecord imposta a Vero lo stato del campo 'modifica' per la gestione della concorrenza in multiutenza. Nella form viene chiamato quando l'utente preme il pulsante di modifica record.
Public Function ImpostaModificaRecord() As Boolean Try RiempiValoriColonneChiave(mIndiceRecord) 'Forzatura del campo di gestione dello stato di modifica a vero mDataSet.Tables(0).Rows(mIndiceRecord)(mColonnaModifica) = True AggiornaDataBase() Return True Catch ex As DBConcurrencyException ' si ricaricano i dati e si tenta di trovare il record da modificare CaricaDati() If Not TrovaRecord() Then Messaggi.Errore("Impossibile modificare. Record cancellato da un altro utente. " & _ "Cliccare sul pulsante Aggiorna per allineare nuovamente i propri dati " & _ "con quelli del database.", "Attenzione") Return False End If 'Cerco di forzare nuovamente il campo di gestione dello stato di modifica mDataSet.Tables(0).Rows(mIndiceRecord)(mColonnaModifica) = True AggiornaDataBase() Return True Catch ex As Exception 'Errori non gestiti Messaggi.Errore(mClassName, System.Reflection.MethodBase.GetCurrentMethod(), ex) Return False End Try End FunctionDopo aver depositato i dati delle colonne chiave, in vista di una possibile ricerca del record, si imposta il campo 'modifica' a vero e si aggiorna il database. Se si verifica un'eccezione di accesso concorrente, si ricaricano i dati e si ricerca il record da modificare. Se la ricerca non ha successo, il record è stato cancellato da un altro utente: si visualizza un avviso e si restituisce il fallimento dell'operazione. Se la ricerca ha successo, si ritenta la valorizzazone del campo 'modifica' e si aggiorna il database, restituendo il successo dell'operazione.
Il metodo ImpostaColonnaModifica valorizza il campo 'modifica' con il valore del parametro che riceve. Notate che il metodo precedente forzava a Vero lo stesso campo, mentre con questo metodo è possibile impostarlo anche a Falso.
Il metodo precedente, nella form, viene richiamato quanto l'utente inizia la fase di modifica del record visualizzato, mentre questo metodo viene richiamato quando l'utente annulla l'operazione in corso oppure quando preme il "pulsante di emergenza" per lo sblocco del record (vedremo in seguito a cosa serve).Public Sub ImpostaColonnaModifica(ByVal flag As Boolean) If mIndiceRecord < 0 Then Exit Sub End If Try 'Cambio il valore del campo di gestione dello stato di modifica mDataSet.Tables(0).Rows(mIndiceRecord)(mColonnaModifica) = flag 'Allineo il DataSet con il database AggiornaDataBase() Catch ex As Exception 'Errori non gestiti Messaggi.Errore(mClassName, System.Reflection.MethodBase.GetCurrentMethod(), ex) End Try End SubIl metodo RigaCorrente soddisfa due esigenze:
- controlla se esistono DataRow nella DataTable altrimenti restituisce il fallimento dell'operazione.
Può accadere quando il metodo viene richiamato in fase di visualizzazione dei dati sulla form.- se esiste una DataRow corrente, ne riporta i dati nella DataRow passata per riferimento e restituisce il successo dell'operazione.
Può accadere quando l'utente, nella form, entra in fase di modifica di un record.Public Function RigaCorrente(ByRef riga As DataRow) As Boolean If mDataSet.Tables(0).Rows.Count = 0 Or mIndiceRecord < 0 Then Return False End If riga = mDataSet.Tables(0).Rows(mIndiceRecord) Return True End FunctionIl metodo TrovaRecord cerca di trovare il record corrispondente ai valori contenuti nel vettore dei valori delle colonne chiave. Se riesce valorizza il campo mIndiceRecord sulla nuova posizione restituendo il successo dell'operazione, altrimenti restituisce il fallimento dell'operazione, indicando così che il record non è più presente.
Public Function TrovaRecord() As Boolean Dim filtroValoriColonneChiave(mValoriColonneChiave.Count - 1) As Object For Each valore As String In mValoriColonneChiave filtroValoriColonneChiave(mValoriColonneChiave.IndexOf(valore)) = CType(valore, Object) Next valore Dim dv As New DataView(mDataSet.Tables(0)) dv.Sort = mOrderBy Dim indiceTrovato As Integer = dv.Find(filtroValoriColonneChiave) If indiceTrovato < 0 Then Return False Else mIndiceRecord = indiceTrovato Return True End If End FunctionNel codice, dopo aver costruito il filtro dei valori delle colonne chiave, lo si passa al metodo Find di una DataView costruita sulla tabella ed eventualmente ordinata, e si imposta mIndicerecord di conseguenza. Si tenga presente che se non viene trovata una DataRow corrispondente ai criteri di ricerca, il metodo Find restituisce -1. Inoltre si può notare come il vettore da passare a tale metodo debba essere di Object, richiedendo la tipizzazione a Object dell'elemento String della lista dei valori delle colonne chiave (boxing).
Il metodo LeggiRecord (con un overload) usa il metodo RiempiValoriColonneChiave per popolare la lista relativa con i valori delle colonne chiave della riga corrente o di quella indicata:
Public Sub LeggiRecord() If mDataSet.Tables(0).Rows.Count <= 0 Then Exit Sub End If RiempiValoriColonneChiave(mIndiceRecord) End Sub Public Sub LeggiRecord(ByVal indice As Integer) RiempiValoriColonneChiave(indice) End SubCi (e vi) risparmiamo l'illustrazione dei metodi Movexxxx, certi che la loro comprensione non risulterà ardua, leggendone il codice nell'allegato.
Errata Corrige
Nel codice della FrmMain Diego è incappato in una svista, gentilmente segnalata da Mauro Marini (che ringrazio).
Il metodo FormFiglia_ActiveControlChanged va corretto come segue:Private Sub FormFiglia_ActiveControlChanged(ByVal sender As Object, _ ByVal e As ActiveControlChangedEventArgs) Me.InfoMessage = e.MessaggioInfo End SubPrima veniva modificato il campo mInfoMessage e non la proprietà, con la conseguenza che non veniva eseguito il codice con cui si visualizza il messaggio nella barra di stato.
Conclusione
In questa puntata è stata illustrata la classe Tabella, prima delle due classi fondamentali per il Data Layer della nostra applicazione PrimiPassi.
Si è anche sottolineata, con qualche speranza di risultare utili, l'importanza di una denominazione congrua dei membri di una classe.
Inoltre è stato usato un interessante metodo per risolvere il problema delle discrepanze tra i tipi di dato .Net e quelli di database.Il codice a corredo di questo articolo è 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.Arrivederci alla prossima puntata, in cui verrà illustrata la classe DatiMasterDetails.