Le avventure in VB.Net di un principiante ex-VB6 - 10
a cura di Oscar Zanin e Diego Cattaruzza (requisiti: Visual Basic Express e SqlServer Express )

Premessa
Dopo l'illustrazione, nell'articolo precedente, della classe Tabella - nella quale si è parlato, tra l'altro, dell'importanza di denominare in modo congruo metodi proprietà e variabili nel nostro codice, in modo da essere in grado di capirlo a distanza di qualche tempo, caso mai dovessimo applicare qualche modifica, o da porre altri in grado di comprenderne la struttura (nel caso di lavori di squadra, come in questa serie scritta a quattro mani da Diego e Oscar) - è adesso la volta di illustrare l'altra classe fondamentale per il Data Layer della nostra applicazione Primi Passi: la classe DatiMasterDetails.

Stavolta si parlerà, partendo dal codice originale di Oscar, del modo in cui (secondo Diego) si concepisce una classe. Se ne è accennato anche in occasione della illustrazione della classe Tabella, ma ne sono stati esposti solo i risultati pratici, non i principi teorici.

Questo articolo si propone di esporre il codice originale, valido e funzionante, criticandone le impostazioni concettuali per arrivare a proporre in alternativa codice altrettanto valido e altrettando funzionante, ma più corrispondente al concetto di classe.

Il codice di Oscar
[Diego] Dopo aver chiarito l'equivoco relativo alla parola 'DatiMaster', nell'analisi della classe Tabella esposta nel precedente articolo di questa serie, la comprensione della classe DatiMasterDetails è diventata facile. Naturalmente, anche in questo caso c'erano molte denominazioni incongrue, ma stavolta la correzione è risultata semplice. Allo scopo didattico di far (nuovamente, repetita iuvant) comprendere l'importanza di dare i nomi giusti, si riporta anche per questa classe la tabella denominazioni vecchie e denominazioni nuove:

Nome originale Descrizione Nome nuovo
DatiMasterDetails classe DatiMasterDetails
Campi
datiMaster La tabella Master, di tipo Tabella mMaster
datiDetails il vettore delle tabelle di dettaglio, di tipo Tabella mDetails
colonneMaster vettore di liste di stringhe per i campi della master usati nelle relazioni tra le tabelle mColonneMaster
colonneDettaglio vettore di liste di stringhe per i campi delle tabelle dettaglio usati nelle relazioni tra le tabelle mColonneDettaglio
(si omettono i campi di riferimento per le 'quasi' omonime proprietà)
Proprietà
Tabella nome della tabella Master NomeTabellaMaster
Query query di selezone per la tabella Master QuerySelezioneMaster
Ordinamento clausola Order By per la query di selezione della tabella Master OrdinamentoMaster
Modifica nome del campo 'modifica' NomeCampoModificaMaster
Cnn Connection Connessione
NumeroDettagli numero delle tabelle di dettaglio NumeroDettagli
NumRigheMaster numero delle righe nella tabella Master NumeroRigheMaster
DescrizioneErrore implementata per eventuale uso futuro di descrizioni delle eccezioni DescrizioneErrore
Metodi
AddChiavi aggiunge una chiave al vettore dei nomi delle colonne chiave della tabella Master AggiungiChiaveMaster
AddValoriChiavi aggiunge un valore al vettore dei valori delle colonne chiave della tabella Master AggiungiValoreChiaveMaster
ResetValoriChiavi svuota il vettore dei valori delle colonne chiave della tabella Master ResettaValoriChiaviMaster
SvuotaDataSetDettagli svuota tutte le tabelle di dettaglio SvuotaDettagli
ImpostaDet_Query imposta la query di selezione per una tabella di dettaglio ImpostaQuerySelezioneDettaglio
ImpostaDet_Tabella imposta il nome di una tabella di dettaglio ImpostaNomeTabellaDettaglio
ImpostaDet_Orderby imposta la clausola Order By per la query di selezione di una tabella di dettaglio ImpostaOrdinamentoDettaglio
ImpostaDet_CampiChiave aggiunge una chiave al vettore dei nomi delle colonne chiave di una tabella dettaglio AggiungiChiaveDettaglio
RitornaDet_DataSet restituisce il dataset di una tabella di dettaglio DataSetDettaglio
RitornaRigacorrente restituisce un puntatore al record corrente della tabella Master RigaCorrenteMaster
ImpostaDet_ColonneMaster Aggiunge una chiave di relazione lato Master AggiungiChiaveRelazioneMaster
ImpostaDet_ColonneDettaglio Aggiunge una chiave di relazione lato dettaglio AggiungiChiaveRelazioneDettaglio
RitornaRigaNuova restituisce un puntatore a un nuovo record nella tabella Master RigaNuovaMaster
RitornaDettRigaNuova restituisce un puntatore a un nuovo record in una tabella Dettaglio RigaNuovaDettaglio
ImpostaRigaNuova aggiunge un record alla tabella Master AggiungiRigaNuovaMaster
ImpostaDettRigaNuova aggiunge un record a una tabella dettaglio AggiungiRigaNuovaDettaglio
CaricaDati legge i dati sia della tabella Master che delle tabelle di dettaglio CaricaDati
CancellaRiga elimina una riga della tabella Master e le righe collegate nelle tabelle dettaglio CancellaRiga
ModificaRecord controlla in base al campo 'modifica' se il record corrente della Master è modificabile o no ImpostaModificaRecordMaster
ImpostaColonnaModifica imposta a vero o falso il valore del campo 'modifica' del record corrente della tabella Master ImpostaColonnaModificaMaster
AggiornaDB Aggiorna il database AggiornaDataBase
ImpostaFiltroDettagli Imposta il filtro per le tabelle di dettaglio in modo relativo al record corrente puntato dalla tabella Master ImpostaFiltroDettagli
MoveFirst, MoveLast, MoveNext, MovePrev navigazione nella tabella Master (e di conseguenza nelle tabelle di dettaglio) MoveFirst, MoveLast, MoveNext, MovePrev
PosizionaRecord trova l'indice del record della tabella Master con i dati valori delle colonne chiave (e di conseguenza nelle tabelle di dettaglio) TrovaRecord
ImpostaPosizione legge i valori delle colonne chiave di un record della tabella Master LeggiRecordMaster

Il difetto concettuale più evidente (anche se non grave) di questa classe DatiMasterDetails è lo stesso della classe Tabella: entrambe le classi espongono proprietà che sono indispensabili al loro funzionamento, ma che non vengono richieste dai costruttori originali, che magari richiedono parametri del tutto non indispensabili.

A esempio, per un oggetto di tipo Tabella, prima dell'implementazione del costruttore che richiede connessione e nome della tabella, bisogna impostare - a parte, cioè successivamente all'istanza - la connessione, il nome della tabella, la query di selezione, le sue chiavi. Il nome della tabella è evidentemente un dato indispensabile, come la connessione, mentre la query di selezione, poiché essa è (finora) sempre nel formato canonico ("SELECT * FROM tabella"), richiede lavoro superfluo da parte del programmatore, dato che deve essere impostata dall'esterno della classe, mentre potrebbe tranquillamente essere impostata dall'interno, visto che l'unica informazione che varia è il nome della tabella. Anche l'aggiunta delle chiavi è lavoro superfluo da parte di chi usa la classe, visto che è possibile ricavare le chiavi di una tabella conoscendone solo il nome (e la connessione, ovviamente).

Analogamente, per un oggetto di tipo DatiMasterDetails, si può - nel codice di Oscar - fornire al costruttore originale solo il numero delle tabelle di dettaglio, che è un'informazione per niente indispensabile, mentre le informazioni davvero necessarie (i nomi della tabella master, delle tabelle di dettaglio e la connessione devono essere impostate dopo aver istanziato la classe.

Implementate in questo modo, con questi limiti, le due classi non sono molto diverse da moduli in cui ritrovare i metodi che servono funzionalità comuni: l'unico vantaggio è costituito dalla possibile presenza di più oggetti dello stesso tipo (Tabella), per ciascuno dei quali funziona lo stesso codice definito dalla classe.

Anche le relazioni tra le tabelle non sono state definite, nella classe DatiMasterDetails, in modo da risparmiare lavoro al programmatore che usa la classe: sono rappresentate da due matrici distinte, mColonneMaster e mColonneDettaglio, e il programmatore deve ricordarsi di aggiungere le chiavi nello stesso ordine. Invece, se ci si astraesse dal 'pensare a come scrivere codice' e ci si concentrasse sul 'pensare a cosa definire', cioè sull'entità che si sta descrivendo, verrebbe del tutto naturale creare una struttura Relazione e usare, nella classe DatiMasterDetails, un solo vettore (di oggetti Relazione, appunto) richiedendo al programmatore di 'pensare' solo alla relazione come 'unicuum', non di fare attenzione a non scordarsi uno degli elementi della relazione. E' più facile pensare "AggiungiRelazione(campoChiaveMaster, campoChiaveDettaglio)" che doversi ricordare "AggiungiCampoChiaveMaster" senza dimenticare "AggiungiCampoChiaveDettaglio".

Impostare, da bel principio, il numero delle tabelle di dettaglio che faranno parte di una classe DatiMasterDetails è inutile, perché la cosa più naturale non è quella di dire alla classe di far spazio a un numero stabilito di tabelle, ma di aggiungere una tabella alla volta, lasciando alla classe il compito di farle spazio e di tenere nota del numero delle tabelle di dettaglio. Tecnicamente, a chi usa la classe, non deve essere necessario sapere quante tabelle sono di dettaglio. E' una informazione che serve solo alla classe, non al codice che ne userà un oggetto:

  For indiceDettagli As Integer = 0 To mNumeroDettagli - 1

Codice come questo dovrebbe trovarsi solo nella classe DatiMasterDetails, non nel codice che la usa.

C'è poi da considerare che, conseguentemente a una non ottimale progettazione della classe Tabella, ogni oggetto Tabella ha il suo DataSet, il che costituisce obiettivamente uno spreco di risorse, ma questo è un difetto che ci teniamo, per il momento.

Elementi da togliere, da aggiungere, da sostituire
Come avrete intuito dalla critica appena letta, diversi membri della classe DatiMasterDetails sono diventati inutili, specie in seguito all'implementazione del codice che legge lo schema della tabella direttamente dal database, mentre altri risultano utili (a esempio un metodo AggiungiTabella), altri ancora vanno sostituiti (i vettori con i campi chiave di relazione), qualcun altro infine si rende necessario (una nuova classe Relazione, un tipo enumerato).

Per far comprendere il senso e la portata delle modifiche, si mostrerà qualche esempio del codice che usa questa classe, 'prima e dopo la cura'.

I campi
Aggiungiamo la classe DatiMasterDetails al progetto APP.Data e imponiamo le direttive:

Imports APP.Base
Imports APP.UI
Imports System.Data.SqlClient

Quindi ragioniamo sui campi, che sono, originariamente:

  Private mMaster As Tabella
  Private mDetails(0) As Tabella

  Private mColonneMaster(0) As List(Of String)
  Private mColonneDettaglio(0) As List(Of String)

Partendo dall'assunto che esista sicuramente, oltre alla tabella master, almeno una tabella di dettaglio e quindi almeno una relazione, vengono impostate le dimensioni iniziali di tre vettori, uno per le tabelle di dettaglio gli altri per le colonne chiave di relazione. L'idea è di aggiungere elementi man mano che se ne presenta la necessità, un po' come era stato fatto anche per la classe Tabella. E' meglio allora, quando si prevede un simile scenario, scegliere oggetti diversi dal semplice Array, che non impongano un ridimensionamento in occasione di ogni aggiunta di elementi. Nel nostro caso, ricorreremo a List(Of T).

Inoltre, invece di avere due distinti vettori per le colonne chiave di relazione, che costringono a preoccuparsi dell'allineamento relativo tra loro (per ciascun elemento di un vettore deve esistere un elemento nell'altro vettore, e nella stessa posizione ordinale) è opportuno avere un solo insieme, che esponga la coppia di elementi correlati.
Per giunta, l'uso di vettori costringe il programmatore a ricorrere a un indice numerico per indicare la tabella di dettaglio cui si riferisce un elemento, nel codice in cui usa la classe DatiMasterDetails.
A esempio, per impostare la relazione tra le tabelle Clienti e Clienti_Sedi:

      Dati.AggiungiChiaveRelazioneMaster(0, "Codice")
      Dati.AggiungiChiaveRelazioneDettaglio(0, "Codice")

Dove Dati è un oggetto DatiMasterDetails, di cui vengono usati i due metodi AggiungiChiaveRelazione... passando come parametro l'indice della tabella di riferimento (zero), e il nome del campo chiave ("Codice"). Bisogna non solo ricordarsi di usare entrambi i metodi, ma che l'indice zero corrisponde alla tabella Clienti_Sedi. Per quest'ultimo difetto, senza complicarsi troppo la vita, si potrebbe definire un enumerato privato, ma vedremo come fare ancora meglio in seguito.

Per la coppia di metodi, invece, è senz'altro opportuno fare in modo di usarne uno solo, pensando alla relazione tra i campi, piuttosto che ai campi coinvolti nella relazione - ho fatto apposta un gioco di parole, per rimarcare la differenza concettuale.
Il modo più facile per cavarsela è creare una nuova struttura, Relazione, che esponga i campi chiave che la compongono e un costruttore che li richieda:

  Public Structure Relazione

    Private mNomeCampoMaster As String
    Public Property NomeCampoMaster() As String
      Get
        Return mNomeCampoMaster
      End Get
      Set(ByVal value As String)
        mNomeCampoMaster = value
      End Set
    End Property


    Private mNomeCampoDettaglio As String
    Public Property NomeCampoDettaglio() As String
      Get
        Return mNomeCampoDettaglio
      End Get
      Set(ByVal value As String)
        mNomeCampoDettaglio = value
      End Set
    End Property

    Public Sub New(ByVal nomeCampoMaster As String, ByVal nomeCampoDettaglio As String)
      Me.NomeCampoDettaglio = nomeCampoDettaglio
      Me.NomeCampoMaster = nomeCampoMaster
    End Sub
  End Structure

Si sceglie una struttura perché. sostanzialmente, si tratta solo di una coppia di valori, senza comportamenti particolari (anche se tali valori sono di tipo String, cioè di un tipo-riferimento e non di un tipo-valore, come Integer o Date).
Così, si può sostituire i campi originali con:

  Private mMaster As Tabella
  Private mDetails As New List(Of Tabella)
  Private mRelazioni As New List(Of List(Of Relazione))

Come si può vedere, mRelazioni è un insieme di insiemi. Secondo logica, dovrebbe avere le stesse dimensioni di mDetails, e i suoi elementi sono liste di oggetti Relazione, poiché è possibile che due tabelle siano correlate attraverso più di un campo chiave.
Si può inoltre sostituire i due metodi originali:

  Public Sub AggiungiChiaveRelazioneMaster(ByVal indiceTabella As Integer, ByVal chiave As String)
    mColonneMaster(indiceTabella).Add(chiave)
  End Sub

  Public Sub AggiungiChiaveRelazioneDettaglio(ByVal indiceTabella As Integer, ByVal chiave As String)
    mColonneDettaglio(indiceTabella).Add(chiave)
  End Sub

     con uno solo:

  Public Sub AggiungiRelazione(ByVal indiceTabella As Integer, ByVal campoMaster As String, _
                               ByVal campoDettaglio As String)
    mRelazioni(indiceTabella).Add(New Relazione(campoMaster, campoDettaglio))
  End Sub

da usare in questo modo:

      Dati.AggiungiRelazione(mIndiceClientiSedi, "Codice", "Codice")

dove la costante zero, usata nel codice originale, è sostituita da una variabile mIndiceClientiSedi valorizzata al momento di aggiungere la tabella di dettaglio Clienti_Sedi (come vedremo nel seguito). In questo modo, il programmatore non deve preoccuparsi di scrivere il valore giusto per l'indice della tabella di dettaglio, né di non dimenticare una delle chiavi di relazione.

Il costruttore
Quello originale è questo:

  Public Sub New(ByVal numeroTabelleDettaglio As Integer)

    'Imposto il numero delle tabelle di dettaglio e ridimensiono di conseguenza gli array
    mNumeroDettagli = numeroTabelleDettaglio

    ReDim mColonneMaster(mNumeroDettagli)
    ReDim mColonneDettaglio(mNumeroDettagli)

    mMaster = New Tabella

    'Imposto le colonne chiave della tabella Master e di quelle dei Dettagli
    For indice As Integer = 0 To mNumeroDettagli - 1
      mDetails.Add(New Tabella)
      mColonneMaster(indice) = New List(Of String)
      mColonneDettaglio(indice) = New List(Of String)
    Next indice

  End Sub

Questo costruttore richiede il numero delle tabelle di dettaglio e si limita a ridimensionare i vettori e a popolarli con oggetti non inizializzati, che dovranno essere valorizzati in seguito. Come già rilevato in precedenza, il numero delle tabelle di dettaglio non è una informazione che deve venire dall'esterno della DatiMasterDetails, è un valore usato all'interno e ricavabile da informazioni contenute nella classe stessa.
Questo è il costruttore che Diego ritiene più adatto:

  Public Sub New(ByVal connessione As SqlConnection, ByVal nomeTabellaMaster As String)
    mConnessione = connessione
    mMaster = New Tabella(connessione, nomeTabellaMaster)
  End Sub

Esso richiede le cose davvero indispensabili: la connessione e il nome della tabella master. Come appare logico, non può esserci una DatiMasterDetails senza connessione e senza una tabella master, quindi è opportuno pretenderle, evitando al programmatore il rischio di dimenticare qualcosa per distrazione, con la conseguente perdita di tempo al momento in cui si verifica l'errore.
Passando gli stessi parametri al costruttore dell'oggetto Tabella mMaster, verrano automaticamente impostate le colonne chiave, la query di selezione, eccetera (vedi articolo precedente).

Le proprietà relative alla tabella master
A questo punto, molte delle proprietà presenti nel codice originale sono superflue. Mi limito a elencarle, risparmiandovi il codice che non troverete più: NomeTabellaMaster, QuerySelezioneMaster, Connessione - che vengono impostate al momento della creazione dell'oggetto DatimasterDetails - NumeroDettagli, che sono valori ricavabili all'interno della classe e del tutto non usati all'esterno. Rimangono le seguenti, che fanno tutte riferimento alle omologhe proprietà della Tabella master, già illustrate nell'articolo precedente:

  Public ReadOnly Property NumeroRigheMaster() As Integer
    Get
      Return mMaster.NumeroRighe
    End Get
  End Property
  
  Private mOrdinamentoMaster As String
  Public Property OrdinamentoMaster() As String
    Get
      Return mOrdinamentoMaster
    End Get
    Set(ByVal value As String)
      mOrdinamentoMaster = value
      mMaster.Ordinamento = mOrdinamentoMaster
    End Set
  End Property

  Private mNomeCampoModificaMaster As String
  Public Property NomeCampoModificaMaster() As String
    Get
      Return mNomeCampoModificaMaster
    End Get
    Set(ByVal value As String)
      mNomeCampoModificaMaster = value
      mMaster.NomeCampoModifica = mNomeCampoModificaMaster
    End Set
  End Property

  Private mDescrizioneErrore As String
  Public Property DescrizioneErrore() As String
    Get
      Return mDescrizioneErrore
    End Get
    Set(ByVal value As String)
      mDescrizioneErrore = value
    End Set
  End Property

Questo significa che codice come questo:

      Dati = New DatiMasterDetails(2)

      'Impostazione Master
      Dati.Connessione = ConnessioneDatabase
      Dati.NomeCampoModificaMaster = "Modifica"
      Dati.QuerySelezioneMaster = "SELECT * FROM Clienti"
      Dati.NomeTabellaMaster = "Clienti"
      Dati.OrdinamentoMaster = "Codice"
      Dati.AggiungiChiaveMaster("Codice")

Viene rimpiazzato da:

      Dati = New DatiMasterDetails(ConnessioneDatabase, "Clienti")
      Dati.NomeCampoModificaMaster = "Modifica"
      Dati.OrdinamentoMaster = "Codice"

I metodi relativi alla tabella Master
Si intende i metodi che non fanno altro che usare gli omologhi metodi della classe Tabella, relativamente alla tabella master. Sono stati illustrati nell'articolo precedente:

  Public Sub AggiungiChiaveMaster(ByVal chiave As String)
    mMaster.AggiungiChiave(chiave)
  End Sub

  Public Sub AggiungiValoreChiaveMaster(ByVal valore As String)
    mMaster.AggiungiValoreChiave(valore)
  End Sub

  Public Sub ResettaValoriChiaviMaster()
    mMaster.ResettaValoriChiavi()
  End Sub

  Public Function RigaCorrenteMaster(ByRef riga As DataRow) As Boolean
    Return mMaster.RigaCorrente(riga)
  End Function

  Public Function RigaNuovaMaster() As DataRow
    Return mMaster.RigaNuova()
  End Function

  Public Function AggiungiRigaNuovaMaster(ByVal riga As DataRow) As Boolean
    Return mMaster.AggiungiRigaNuova(riga)
  End Function

  Public Function ImpostaModificaRecordMaster() As Boolean
    If Not mMaster.Modificabile() Then
      Return False
    End If

    Return mMaster.ImpostaModificaRecord
  End Function

  Public Sub ImpostaColonnaModificaMaster(ByVal flag As Boolean)
    mMaster.ImpostaColonnaModifica(flag)
  End Sub

I metodi relativi alle tabelle di dettaglio
Nel codice originale di Oscar era previsto, si ricorda, un vettore di tabelle, delle quali bisognava impostare tutte le proprietà, a cominciare dal nome:

  Public Sub ImpostaNomeTabellaDettaglio(ByVal indiceTabella As Integer, ByVal nomeTabella As String)
    mDetails(indiceTabella).NomeTabella = nomeTabella
  End Sub

Ma, poiché la classe Tabella è (adesso) impostata affinché trovi da sé le informazioni che le servono, poiché non vogliamo che il programmatore che usa la classe DatiMasterDetails debba ricordarsi l'indice della tabella di dettaglio, poiché adesso le tabelle di dettaglio sono un insieme di tipo List(Of Tabella), ecco il nuovo metodo, che si occupa anche di aggiungere un elemento alla lista delle relazioni. Avranno quindi sempre lo stesso indice, indipendentemente dal fatto di conoscere o meno il suo valore, che viene opportunamente restituito al chiamante:

  Public Function AggiungiTabellaDettaglio(ByVal nomeTabella As String) As Integer
    mDetails.Add(New Tabella(mConnessione, nomeTabella))
    mRelazioni.Add(New List(Of Relazione))
    Return mDetails.Count - 1
  End Function

In questo modo, si può sostituire nella FrmClienti questo codice:

      Dati.ImpostaQuerySelezioneDettaglio(0, "SELECT * FROM Clienti_Sedi")
      Dati.ImpostaNomeTabellaDettaglio(0, "Clienti_Sedi")
      Dati.ImpostaOrdinamentoDettaglio(0, "Codice, Nome, Citta")
      Dati.AggiungiChiaveDettaglio(0, "Codice")
      Dati.AggiungiChiaveDettaglio(0, "Nome")
      Dati.AggiungiChiaveDettaglio(0, "Citta")

 con questo:

      mIndiceClientiSedi = Dati.AggiungiTabellaDettaglio("Clienti_Sedi")
      Dati.ImpostaOrdinamentoDettaglio(mIndiceClientiSedi, "Codice, Nome, Citta")

 dove mIndiceClientiSedi è una variabile dichiarata a livello di modulo della FrmClienti:

  Dim mIndiceClientiSedi As Integer

 e può essere usata ovunque serviva (prima) il valore zero. Non serve più ricordarsi il valore dell'indice della tabella Clienti_Sedi nell'insieme delle tabelle di dettaglio. E' questo un esempio di incapsulamento.

Si possono così togliere i metodi AggiungiChiaveRelazioneMaster, AggiungiChiaveRelazioneDettaglio - sostituiti da AggiungiRelazione, già illustrata in precedenza - ImpostaQuerySelezioneDettaglio (superflua grazie agli automatismi implementati nella classe Tabella) e ImpostaNomeTabellaDettaglio (appena sostituita dal metodo AggiungiTabellaDettaglio).

Gli altri metodi relativi alle tabelle di dettaglio sono:

  Public Sub SvuotaDettagli()
    For Each tb As Tabella In mDetails
      tb.DataSet.Tables(0).Rows.Clear()
    Next
  End Sub

  Public Sub ImpostaOrdinamentoDettaglio(ByVal indiceTabella As Integer, ByVal ordinamento As String)
    mDetails(indiceTabella).Ordinamento = ordinamento
  End Sub

  Public Sub AggiungiChiaveDettaglio(ByVal indiceTabella As Integer, ByVal Valore As String)
    mDetails(indiceTabella).AggiungiChiave(Valore)
  End Sub

  Public Function RigaNuovaDettaglio(ByVal indiceTabella As Integer) As DataRow
    Return mDetails(indiceTabella).RigaNuova
  End Function

  Public Function AggiungiRigaNuovaDettaglio(ByVal indiceTabella As Integer, ByVal riga As DataRow) As Boolean
    Return mDetails(indiceTabella).AggiungiRigaNuova(riga)
  End Function

  Public Function DataSetDettaglio(ByVal indiceTabella As Integer) As DataSet
    Return mDetails(indiceTabella).DataSet
  End Function

Sono tutti, tranne SvuotaDettagli, metodi che riprendono i loro omologhi della classe Tabella, già illustrati nell'articolo precedente.

I metodi che interagiscono col database
In questa sezione vengono illustrati i metodi che interessano la classe nel suo complesso (tabella master e tabelle di dettaglio).

  Public Sub CaricaDati()

    mMaster.CaricaDati()

    ImpostaFiltroDettagli()

    For indice As Integer = 0 To mNumeroDettagli - 1
      mDetails(indice).CaricaDati()
    Next indice
  End Sub

Questo metodo legge i dati dal database, sfruttando gli omologhi metodi degli oggetti Tabella coinvolti. Prima di caricare i dati delle tabelle di dettaglio, viene usato il metodo ImpostaFiltroDettagli (di cui nel seguito) per filtrare solo i record di dettaglio correlati col record corrente della tabella master.

  Public Function CancellaRiga() As Boolean
    'Controllo lo stato di modifica del record Master
    If Not mMaster.Modificabile() Then
      Return False
    End If

Questo metodo si occupa di eliminare un record dalla tabella master e i record a esso correlati dalle tabelle di dettaglio.
Dapprima si controlla se l'operazione è lecita, tramite l'apposito metodo della classe Tabella.

    ImpostaFiltroDettagli()

    For indice As Integer = 0 To mDetails.Count - 1

Se l'operazione è lecita, si imposta il filtro per i dati di dettaglio e si scorre l'insieme delle tabella di dettaglio.

      If Not mDetails(indice).CancellaRiga(False) Then
        Return False
      End If
    Next indice

Si sfrutta il metodo CancellaRiga di ciascun oggetto Tabella dell'insieme mDetails (passando l'argomento False per indicare che l'operazione non riguarda la tabella master). Se l'operazione fallisce, si restituisce il fallimento.

    If Not mMaster.CancellaRiga(True) Then
      Return False
    End If

    Return True
  End Function

Qundi si procede in modo analogo per la tabella master (passando ovviamente l'argomento True). Infine, se tutto è andato bene, si restituisce il successo.

In uno scenario di tabelle master-detail, le cancellazioni interessano prima le tabelle di dettaglio e poi la master, al contrario degli inserimenti, che interessano prima la tabella master e poi quelle di dettaglio, per non incorrere in errori di correlazione.

Per aggiornare il database, è opportuno considerare che si può voler aggiornare prima solo la tabella master, quindi, se l'operazione ha successo, solo le tabelle di dettaglio, oppure tutto, a esempio in caso di modifica. Per questo scopo cade a fagiolo un tipo enumerato:

  Public Enum TipoSalvataggio
    SoloMaster = 1
    SoloDettagli = 2
    Tutto = 3
  End Enum

Ed ecco il metodo che lo usa:

  Public Sub AggiornaDataBase(ByVal tipoSalvataggio As TipoSalvataggio)
    Try
      If tipoSalvataggio = DatiMasterDetails.TipoSalvataggio.SoloMaster Or _
         tipoSalvataggio = DatiMasterDetails.TipoSalvataggio.Tutto Then
        mMaster.AggiornaDataBase()
      End If

      If tipoSalvataggio = DatiMasterDetails.TipoSalvataggio.SoloDettagli Or _
         tipoSalvataggio = DatiMasterDetails.TipoSalvataggio.Tutto Then
        For indice As Integer = 0 To mDetails.Count - 1
          mDetails(indice).AggiornaDataBase()
        Next indice
      End If

    Catch ex As Exception
      'Errori non gestiti
      Messaggi.Errore(mClassName, System.Reflection.MethodBase.GetCurrentMethod(), ex)
    End Try
  End Sub

Come si può vedere, esso richiama, a seconda dei casi, l'omologo metodo degli oggetti Tabella interessati.

I metodi di navigazione e di filtro
La navigazione da un record all'altro della tabella master comporta la gestione della sincronizzazione tra esso e i record correlati nelle tabelle di dettaglio. Per questo scopo bisogna impostare il filtro, costruendo, per ogni tabella di dettaglio e per ogni campo di relazione, l'espressione

<nome campo chiave di relazione> = <valore campo chiave di relazione>

da aggiungere alla clausola WHERE che verrà applicata alla query di selezione della tabella di dettaglio.

Nel codice originale, Oscar faceva uso dei due vettori di chiavi di relazione, per correlare tabella master e tabelle di dettaglio, così:

  Private Sub ImpostaFiltroDettagli()

    For indiceDettagli As Integer = 0 To mDetails.Count - 1
      Dim filtro As String = ""

Si imposta il ciclo per scorrere l'insieme delle tabelle di dettaglio e, per ciascuna tabella di dettaglio, si predispone la variabile filtro azzerandola.

      For indiceCampi As Integer = 0 To mColonneMaster(indiceDettagli).Count - 1
        Dim rigaCorrente As DataRow = Nothing
        If mMaster.RigaCorrente(rigaCorrente) = False Then
          Exit Sub
        End If

Quindi si scorre l'insieme dei campi chiave di relazione (della tabella master, ma sarebbe la stessa cosa per qualsiasi tabella di dettaglio: il numero dei campi dovrebbe essere lo stesso, se il programmatore non ha dimenticato qualcosa), si dichiara una DataRow destinata a contenere la riga corrente della tabella master per passarla all'apposito metodo della classe Tabella (vedi articolo precedente). Se esso non ha successo, si esce dalla procedura.

        Dim valore As String = (rigaCorrente(mColonneMaster(indiceDettagli)(indiceCampi))).ToString
        Dim tipoCampo As String = mMaster.DataSet.Tables(0).Columns( _
                                          mColonneMaster(indiceDettagli)(indiceCampi)).DataType.Name

Si ricava il valore e il tipo del campo. Può essere opportuno ricordare che l'insieme mColonneMaster contiene i nomi dei campi chiave utilizzati nelle relazioni, che DataRow(nomecampo) restituisce il valore contenuto nel campo, mentre l'oggetto Column esposto nel DataSet fornisce, tra le sue proprietà, il tipo del campo.

        If tipoCampo = "String" Then
          valore = "'" & Stringhe.ValQ(valore) & "'"
        ElseIf tipoCampo = "DateTime" Then
          valore = "'" & DateTime.Parse(valore).ToShortDateString & "'"
        Else
          'Caso numeri
        End If

In base al tipo di campo si formatta adeguatamente il valore. Si ricorda che ValQ si occupa di raddoppiare gli apici eventualmente presenti nelle stringhe.

        If filtro <> "" Then
          filtro = String.Concat(filtro, " AND ")
        End If
        filtro = String.Concat(filtro, _
                               (mColonneDettaglio(indiceDettagli)(indiceCampi)).ToString, _
                               "=", valore)

      Next indiceCampi

Quindi si procede nella costruzione del filtro (si noti il metodo Concat).

Nel codice di Diego, dopo l'introduzione dell'insieme relazioni al posto dei due insiemi usati da Oscar, quanto sopra diventa:

  Private Sub ImpostaFiltroDettagli()

    For indiceDettagli As Integer = 0 To mDetails.Count - 1
      Dim filtro As String = ""

      Dim relazioniDettaglio As List(Of Relazione) = mRelazioni(indiceDettagli)
      For indiceRelazioni As Integer = 0 To relazioniDettaglio.Count - 1

        Dim rigaCorrente As DataRow = Nothing
        If mMaster.RigaCorrente(rigaCorrente) = False Then
          Exit Sub
        End If

        Dim valore As String = _
                  (rigaCorrente(relazioniDettaglio(indiceRelazioni).NomeCampoMaster)).ToString
        Dim tipoCampo As String = _
                      mMaster.DataSet.Tables(0).Columns( _
                      relazioniDettaglio(indiceRelazioni).NomeCampoMaster).DataType.Name

        If tipoCampo = "String" Then
          valore = "'" & Stringhe.ValQ(valore) & "'"
        ElseIf tipoCampo = "DateTime" Then
          valore = "'" & DateTime.Parse(valore).ToShortDateString & "'"
        Else
          'Caso numeri
        End If

        If filtro <> "" Then
          filtro = String.Concat(filtro, " AND ")
        End If
        filtro = String.Concat(filtro, _
                               relazioniDettaglio(indiceRelazioni).NomeCampoDettaglio, _
                               "=", valore)
      Next

Non dovrebbe essere difficile individuare le differenze.
La procedura prosegue con:

      If filtro <> "" Then filtro = filtro.Insert(0, " WHERE ")

      Dim posizioneWhere As Integer = mDetails(indiceDettagli).QuerySelezione.IndexOf("WHERE")
      If posizioneWhere > 0 Then
        mDetails(indiceDettagli).QuerySelezione = _
                                mDetails(indiceDettagli).QuerySelezione.Substring(0, posizioneWhere - 1)
      End If
      mDetails(indiceDettagli).QuerySelezione += filtro
    Next indiceDettagli
  End Sub

Si completa la stringa di filtro inserendo (si noti il metodo Insert) la clausola WHERE all'inizio, si controlla se nella query di selezione della tabella di dettaglio esiste o meno la clausola e si reimposta la relativa proprietà.

  Private Sub SincronizzaDettagli()
    For indice As Integer = 0 To mDetails.Count - 1
      mDetails(indice).CaricaDati()
    Next indice
  End Sub

Questo metodo si occupa della sincronizzazione effettiva dei dettagli con il record corrente della tabella master, in seguito all'impostazone del filtro. Viene infatti invocato ai metodi di navigazione (MoveNext. MoveFirst, MovePrev, MoveLast e TrovaRecord) di cui, poiché usano gli omologhi metodi dell'oggetto Tabella mMaster - per i quali si rimanda all'articolo precedente - e hanno la struttura del tutto simile, si mostra solo l'ultimo:

  Public Function TrovaRecord() As Boolean

    If Not mMaster.TrovaRecord() Then
      Return False
    End If

    ImpostaFiltroDettagli()
    SincronizzaDettagli()

    Return True
  End Function

A differenza dei quattro metodi di solo spostamento, in questo metodo si controlla l'esito del metodo TrovaRecord della Tabella mMaster, per ovvie ragioni.

  Public Sub LeggiRecordMaster()
    mMaster.LeggiRecord()
  End Sub

  Public Sub LeggiRecordMaster(ByVal indice As Integer)
    mMaster.LeggiRecord(indice)
  End Sub

Anche i metodi di lettura dei valori di un record non fanno altro che invocare gli omologhi metodi della Tabella mMaster, già illustrati nell'articolo precedente.

Conclusione
In questo articolo, illustrando la classe DatiMasterDetails, uno dei costituenti fondamentali per il Data Layer della nostra appllicazione, si è spiegato anche il modo di concepire una classe, attraverso una critica del codice originale di Oscar e la proposizione motivata delle alternative.

Nei prossimi articoli, verranno illustrate applicazioni concrete di queste classi.

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.