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

Premessa
Con questa puntata, dopo aver preparato tante classi accessorie - tra cui le due fondamentali per il Data Layer della nostra applicazione, Tabella e DatiMasterDetails - cominceremo a creare le form di gestione dei dati: inserimento, modifica, cancellazione e ricerca.

Partiremo con la form relativa agli Stati, per la quale abbiamo già creato la voce di menu nella form Main. Contenendo un unico campo, rappresenta una buona scelta per rompere il ghiaccio. Si basa su un'unica tabella e quindi sfrutterà solo la classe Tabella.

Così, prenderemo dimestichezza con la relazione Form-Tabella, prima di cimentarsi con quella Form-DatiMasterDetails.

Form Stati
In Esplora soluzioni clicchiamo con il tasto destro sul progetto PrimiPassi, selezioniamo la voce Aggiungi e quindi Nuovo elemento. Nella finestra che appare, selezioniamo Krypton Form nella sezione Modelli personali, assegniamo come nome FrmStati e clicchiamo sul pulsante Aggiungi.
L'aspetto della form a fine progettazione è il seguente:

Elenco dei controlli e relative proprietà:

Il menu della form Main
Ora che abbiamo creato la form andiamo a scrivere il codice necessario a richiamarla, nella voce di menu Stati della form FrmMain. Entriamo in progettazione della FrmMain, visualizziamo la suddetta voce di menù e facciamoci sopra un doppio click. Ci apparirà la Sub StatiToolStripMenuItem_Click il cui codice è il seguente:

  Private Sub StatiToolStripMenuItem_Click(ByVal sender As System.Object, 
                                           ByVal e As System.EventArgs) _
                                           Handles StatiToolStripMenuItem.Click
    Try
      Dim frm As FrmStati = DirectCast(FormHelper.VediSeIlFormEAperto( _
                                       "FrmStati", Me, True, False), FrmStati)

      If frm Is Nothing Then
        frm = New FrmStati
        frm.MdiParent = Me
        frm.StartPosition = FormStartPosition.Manual
        frm.Location = New Point(0, 0)
        ' <omissis>
        frm.Show()
      Else
        frm.WindowState = FormWindowState.Normal
        frm.BringToFront()
      End If

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

Dapprima si dichiara una FrmStati cercando di assegnarle il riferimento a una FrmStati già esistente, tramite il metodo VediSeIlFormEAperto, al quale passiamo come parametri: il nome della nostra form, la form chiamante (FrmMain, ovvero la nostra form MDI, quindi Me), se è MDI e se è posseduta da un'altra form (owned, non form figlia). Il metodo restituisce una form generica che va quindi tipizzata come FrmStati - DirectCast è una istruzione fondamentale, assieme a CType e a TryCast, delle quali vi consigliamo di studiare nella guida le differenze.
Attenzione: non sempre serve usare il metodo VediSeIlFormEAperto: possono esserci situazioni dove aprire più istanze della stessa form può essere necessario e sensato. Non in questo caso.

Come si può intuire dalle righe successive, se la form non è aperta il metodo restituisce Nothing e allora ne creiamo una istanza, le assegniamo il riferimento al Parent, la posizioniamo e la apriamo, viceversa la portiamo alle sue dimensioni normali e in primo piano rispetto alle altre.
La riga commentata <omissis> è una riga che andrà aggiunta dopo qualche primo intervento sulla FrmStati.

Prime implementazioni per la FrmStati: l'evento ActiveControlChanged
Innanzitutto imponiamo le direttive di importazione, quindi dichiariamo l'evento il cui scatenamento visualizza nella FrmMain e più precisamente nella sua barra di stato un breve messaggio esplicativo del controllo che ha il focus e delle operazioni che si possono compiere:

Imports APP.UI
Imports APP.Data
Imports APP.Data.SqlHelper
Imports System.Data.SqlClient

Public Class FrmStati

  Public Event ActiveControlChanged(ByVal sender As Object, ByVal e As ActiveControlChangedEventArgs)

Quindi, giusto per non dimenticarcene, scriviamo subito il codice in cui scateniamo questo evento. Per la TextBox gestiremo l’evento Enter, per i pulsanti gestiremo l’evento MouseEnter. Si potrebbe scrivere il codice per ciascun pulsante, a esempio (non copiatelo):

  Private Sub btnAggiorna_EnabledChanged(ByVal sender As System.Object, _
                                         ByVal e As System.EventArgs) _
                                         Handles btnEsci.EnabledChanged
    RaiseEvent ActiveControlChanged(Me, New ActiveControlChangedEventArgs("Info : " & _
                      "Permette di allineare i propri dati con quelli più aggiornati del database."))
  End Sub

Per fare ciò, cerchiamo nella combo degli oggetti il nome del BtnEsci, nella combo degli eventi scegliamo l’evento MouseEnter, quindi scriviamo il codice. Per ciascuno dei pulsanti. In fondo l’unica cosa che cambia è il messaggio di informazione, no? Allora possiamo scrivere una sola procedura per l’evento MouseEnter di tutti i pulsanti, discriminando il messaggio in base al nome del pulsante. Basta aggiungere eventi alla clausola Handles, e Intellisense ci aiuta a scrivere la maggior parte del codice (questo copiatelo, ovviamente senza gli allineamenti un po' disordinati che Diego usa per ragioni tipografiche):

  Private Sub Pulsante_MouseEnter(ByVal sender As Object, ByVal e As System.EventArgs) _
                                  Handles BtnAggiorna.MouseEnter, BtnAnnulla.MouseEnter, _
                                  BtnCancella.MouseEnter, BtnCerca.MouseEnter, _
                               BtnConferma.MouseEnter, BtnEsci.MouseEnter, BtnModifica.MouseEnter, _
                                  BtnNuovo.MouseEnter, BtnPrec.MouseEnter, BtnPrimo.MouseEnter, _
                                  BtnSblocca.MouseEnter, BtnSucc.MouseEnter, BtnUltimo.MouseEnter
    Dim m As String = "Info: "
    Select Case DirectCast(sender, ToolStripButton).Name
      Case "BtnAggiorna" : m &= _
                        "Permette di allineare i propri dati con quelli più aggiornati del database."
      Case "BtnAnnulla" : m &= "Permette di annullare l'operazione in corso."
      Case "BtnCancella" : m &= "Permette di cancellare l'anagrafica dello stato visualizzato."
      Case "BtnCerca" : m &= "Permette di cercare l'anagrafica di uno stato."
      Case "BtnConferma" : m &= "Permette di confermare l'operazione in corso."
      Case "BtnEsci" : m &= _
                        "Permette di uscire dalla videata di gestione delle anagrafiche degli stati."
      Case "BtnModifica" : m &= "Permette di modificare i dati dello stato visualizzato."
      Case "BtnNuovo" : m &= "Permette di inserire un nuovo stato."
      Case "BtnPrec" : m &= "Permette di spostarsi sul record precedente."
      Case "BtnPrimo" : m &= "Permette di spostarsi sul primo record. " & _
                            "Il più recente se i dati sono ordinati cronologicamente."
      Case "BtnSblocca" : m &= "Permette di sbloccare un record in stato di modifica. " & _
                              "DA USARE SOLO NEL CASO IN CUI LO STATO DI BLOCCO SIA RIMASTO " & _
                              "IMPOSTATO PER UN ERRORE IMPREVISTO"
      Case "BtnSucc" : m &= "Permette di spostarsi sul record successivo."
      Case "BtnUltimo" : m &= "Permette di spostarsi sull'ultimo record. " & _
                             "Il meno recente se i dati sono ordinati cronologicamente."
    End Select
    RaiseEvent ActiveControlChanged(Me, New ActiveControlChangedEventArgs(m))
  End Sub

La pigrizia ci viene in aiuto: se scriviamo 'Case Bt', Intellisense ci propone i nomi dei pulsanti: scegliamo quello che ci serve, poi scriviamo le virgolette prima e dopo. Così il codice si scrive quasi da solo e velocemente.
Naturalmente, se gestiamo l'evento MouseEnter, è opportuno gestire l'evento MouseLeave:

  Private Sub Pulsante_MouseLeave(ByVal sender As Object, ByVal e As System.EventArgs) _
                                  Handles BtnAggiorna.MouseLeave, BtnAnnulla.MouseLeave, _
                              BtnCancella.MouseLeave, BtnCerca.MouseLeave, BtnConferma.MouseLeave, _
                                  BtnEsci.MouseLeave, BtnModifica.MouseLeave, BtnNuovo.MouseLeave, _
                                  BtnPrec.MouseLeave, BtnPrimo.MouseLeave, BtnSblocca.MouseLeave, _
                                  BtnSucc.MouseLeave, BtnUltimo.MouseLeave

    RaiseEvent ActiveControlChanged(Me, New ActiveControlChangedEventArgs(" "))
  End Sub

Aggiungiamo anche il codice per la TextBox:

  Private Sub TxtStato_Enter(ByVal sender As Object, ByVal e As System.EventArgs) _
                             Handles TxtStato.Enter
    RaiseEvent ActiveControlChanged(Me, New ActiveControlChangedEventArgs("Info : " & _
                                    "Nome dello stato. Digitazione obbligatoria. Campo chiave."))
  End Sub

Adesso possiamo aggiungere, nel codice della FrmMain:StatiToolStripMenuItem_Click, l’istruzione che assegna il gestore di evento, al posto della <omissis>:

        AddHandler frm.ActiveControlChanged, AddressOf FormFiglia_ActiveControlChanged

Se non vi ricordate questo metodo FormFiglia_ToolStripInfoChanged della FrmMain, che scrive nella StatusStrip della FrmMain un messaggio di aiuto all'utente nel momento in cui un controllo della form figlia ottiene il focus, potreste fare un ripasso.
E' con questo sistema che Oscar gestisce l'help dei suoi programmi. Se voi lo gestite diversamente tutto il codice relativo all'evento ActiveControlChanged (scatenamento e gestione), non sono più necessari.

I campi
La prima variabile privata, nel codice originale di Oscar, è così:

  'Può assumere i valori 0 nessuna operazione in corso, 1 nuovo inserimento, 2 modifica
  Private Operazione As Byte

Operazione, come vedete dal commento, indica l'operazione in corso da parte dell'utente e può assumere i valori: 0 nessuna operazione in corso, 1 nuovo inserimento o 2 modifica. A seconda del valore assunto vengono svolte delle porzioni di codice piuttosto che altre.

[Diego] Invece è meglio non usare solo il commento e la propria memoria: apriamo il file ConvalidaDati (scelto perché ha uno scopo vagamente affine), aggiungiamo, in fondo, un enumerato apposito:

Public Enum TipoOperazione
  Nessuna
  Inserimento
  Modifica
End Enum

Adesso possiamo cambiare la dichiarazione di Oscar in quella di Diego (e vedremo gli altri campi):

  Private mOperazione As TipoOperazione
  Private Dati As Tabella
  Private mPulisciControlli As SvuotaControlli

In tal modo, avremo codice più leggibile senza doverci ricordare il significato delle costanti numeriche utilizzate (0, 1 e 2).
Dati è un'istanza della classe Tabella analizzata in precedenza. Data la speciale importanza, questa variabile ha il nome che inizia con una maiuscola, senza la m iniziale, come i nomi degli altri campi. Ora vedremo come viene sfruttata. Ricordo che Tabella serve per la gestione delle tabelle singole, non per i rapporti master/details.
L'ultima variabile, mPulisciControlli, è un'istanza della classe SvuotaControlli contenuta nel progetto UI. Ricordo che la classe permette di svuotare tutti i controlli della form.

I metodi

  Private Sub FrmStati_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) _
                            Handles MyBase.Load

    mOperazione = TipoOperazione.Nessuna
    mPulisciControlli = New SvuotaControlli
    Dati = New Tabella(ConnessioneDatabase, "Stati")

    Dati.NomeCampoModifica = "Modifica"
    Dati.Ordinamento = "Stato"

    Dati.CaricaDati()

    VisualizzaDati()
  End Sub

All'apertura della form svolgo le seguenti operazioni di inizializzazione:
- valorizzo la variabile mOperazione, creo un'istanza della classe di svuotamento dei controlli SvuotaControlli, creo una nuova istanza di Tabella passandole come parametri la connessione con il database e il nome della tabella del database da gestire (ricordo che ConnessioneDatabase è proprietà della classe SqlHelper)
- valorizzo la proprietà relativa al nome del campo indicante lo stato di modifica di un record per la tabella in questione (in questo caso Stati)
- valorizzo la proprietà relativa alla stringa di ordinamento dei dati. L'ordine con il quale i dati ci saranno proposti a video
- richiamo il metodo ci caricamento dei dati. Carica tutti i record della tabella
- infine richiamo il metodo VisualizzaDati che analizzeremo fra poco. Verranno visualizzati i dati del primo record della tabella (rispettando il tipo di ordinamento che abbiamo appena impostato) oppure nessun dato nel caso la tabella non contenga record.

  Private Sub VisualizzaDati()

    SvuotaCampi()

    Dim riga As DataRow = Nothing
    If Not Dati.RigaCorrente(riga) Then
      'Nessun record presente nella tabella
      PreparaVuoto()
    Else
      'Inserisco i valori nei controlli
      TxtStato.Text = CStr(riga("Stato"))
    End If
  End Sub

Questo metodo ha il compito di visualizzare i dati del record corrente. Per prima cosa svuota il contenuto dei controlli della form, tramite il metodo SvuotaCampi (nel seguito). Successivamente, grazie al metodo RigaCorrente di Dati (oggetto Tabella) al quale passiamo come argomento una DataRow vuota, ci facciamo restituire la stessa DataRow compilata con i dati del record corrente. Se la tabella è vuota richiameremo il metodo PreparaVuoto (nel seguito), altrimenti valorizzeremo ogni singolo controllo con il relativo valore contenuto nella DataRow (in FrmStati abbiamo un unico controllo da trattare).

  Private Sub SvuotaCampi()
    mPulisciControlli.PulisciTextBox(Me)
  End Sub

Richiamando il metodo PulisciTextBox della classe SvuotaControlli di cui PulisciControlli è un'istanza, svuoto l'unica TextBox presente sul form.

  Private Sub PreparaVuoto()
    BtnEsci.Enabled = True
    BtnNuovo.Enabled = True
    BtnModifica.Enabled = False
    BtnCancella.Enabled = False
    BtnCerca.Enabled = False
    BtnPrimo.Enabled = False
    BtnPrec.Enabled = False
    BtnSucc.Enabled = False
    BtnUltimo.Enabled = False
    BtnAggiorna.Enabled = False
    BtnConferma.Enabled = False
    BtnAnnulla.Enabled = False
    SvuotaCampi()
    TbrStrumenti.Select()
  End Sub

Se la tabella in oggetto, in questo caso Stati, non contiene record, viene richiamato questo metodo che abilita solo i pulsanti di uscita e di inserimento nuovo record, disabilitando tutto il resto. Se non vi sono record nella tabella, infatti, all'utente deve essere permesso solo inserire un nuovo record oppure uscire dalla form.

Vi sono altre situazioni in cui è necessario abilitare o disabilitare pulsanti della barra a seconda della loro funzione. Per questo sono stati implementati due metodi, uno antitetico all’altro:

  Private Sub BloccaBarra()
    AbilitaPulsanti(False)
  End Sub

  Private Sub SbloccaBarra()
    AbilitaPulsanti(True)
  End Sub

  Private Sub AbilitaPulsanti(ByVal flag As Boolean)
    btnEsci.Enabled = flag
    btnNuovo.Enabled = flag
    btnModifica.Enabled = flag
    btnCancella.Enabled = flag
    btnCerca.Enabled = flag
    btnPrimo.Enabled = flag
    btnPrec.Enabled = flag
    btnSucc.Enabled = flag
    btnUltimo.Enabled = flag
    btnAggiorna.Enabled = flag
    btnConferma.Enabled = Not flag
    btnAnnulla.Enabled = Not flag
    PnlDati.Tag = Not Convert.ToBoolean(PnlDati.Tag)
  End Sub

Quando la barra è bloccata, sono abilitati solo i pulsanti conferma e annulla, e i controlli sono compilabili. Il contrario accade quando la barra è sbloccata.
Il blocco della compilazione dei controlli viene regolata gestendo l'evento Enter del Panel:

  Private Sub pnlDati_Enter(ByVal sender As System.Object, ByVal e As System.EventArgs) _
                            Handles PnlDati.Enter
  If Convert.ToBoolean(PnlDati.Tag) = False Then
      TbrStrumenti.Select()
    End If
  End Sub

Questo codice è frutto di un suggerimento che mi diede Diego a suo tempo: in VB6 ero abituato a mettere tutti i controlli compilabili dall'utente, esclusi barra dei pulsanti e griglie dati di dettaglio, in un contenitore nascosto; questo mi serviva per evitare di dover abilitare o disabilitare i controlli singolarmente a seconda che l'utente intraprendesse un operazione a video (inserimento o modifica) oppure la concludesse.
Sfruttando la proprietà Tag di un controllo Panel dentro al quale disegno tutti i controlli, anche qui esclusi barra dei pulsanti e griglie dati di dettaglio, ottengo lo stesso risultato: se l'utente ha in corso un'operazione di inserimento o modifica (Tag a vero) permetto di compilare i controlli, viceversa (Tag a falso) quando l'utente entra nel controllo Panel rimando il focus alla barra dei pulsanti.
Tanto semplice quanto efficace.

Anche la chiusura della form è regolata:

  Private Sub FrmStati_FormClosing(ByVal sender As System.Object, _
                                   ByVal e As System.Windows.Forms.FormClosingEventArgs) _
                                   Handles MyBase.FormClosing
    If mOperazione <> TipoOperazione.Nessuna Then
      If mOperazione = 1 Then
        Messaggi.Avviso("Prima di chiudere salvare o annullare il nuovo inserimento in corso !", _
                        "Attenzione")
      Else
        Messaggi.Avviso("Prima di chiudere salvare o annullare la modifica in corso !", "Attenzione")
      End If
      e.Cancel = True
    End If
  End Sub

Se l'utente dovesse tentare di chiudere la form mentre c'è un operazione in corso, lo impediremmo avvisandolo con opportuni messaggi e impostando a True la proprietà Cancel dell'argomento di evento (vedi guida).

I pulsanti

  Private Sub BtnEsci_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
                            Handles BtnEsci.Click
    Me.Close()
  End Sub

  Private Sub BtnNuovo_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
                             Handles BtnNuovo.Click
    'Mi salvo la chiave del record visualizzato per poter rivisualizzarlo
    'se viene cliccato il pulsante annulla
    Dati.LeggiRecord()

    BloccaBarra()
    SvuotaCampi()
    TxtStato.Select()
    mOperazione = TipoOperazione.Inserimento
  End Sub

Tralasciando la spiegazione del codice di BtnEsci_Click, ecco cosa fa BtnNuovo_Click: come indicato nel commento, sfrutto il metodo LeggiRecord della classe Tabella per salvare i valori dei campi chiave del record corrente (grazie al metodo RiempiValoriColonneChiave, sempre di Tabella, da esso invocato) in modo che, se l'utente decidesse di annullare l'operazione avremmo gli estremi per ripristinare la visualizzazione del record corrente; blocco la barra, svuoto tutti i campi compilabili dall'utente, do il focus alla casella di testo, imposto l'operazione 'inserimento nuovo record.

  Private Sub BtnModifica_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
                                Handles BtnModifica.Click
    'Controllo se record è già stato variato
    If Not Dati.Modificabile Then
      'Errore di concorrenza
      Exit Sub
    End If

    'Lo imposto allo stato di modifica
    If Not Dati.ImpostaModificaRecord Then
      'Errore di concorrenza
      Exit Sub
    End If

    VisualizzaDati()
    BloccaBarra()
    TbrStrumenti.Select()
    TxtStato.ReadOnly = True
    mOperazione = TipoOperazione.Modifica
  End Sub

Al click sul pulsante di modifica da parte dell'utente, sfruttando il metodo Modificabile della classe Tabella, controllo che il record sia modificabile. Se così non fosse, attraverso il metodo stesso, avviso l'utente e termino l'operazione.
Viceversa, con il metodo ImpostaModifcaRecord (sempre della classe Tabella), imposto a vero lo stato del campo di modifica della tabella Stati e di fatto impedisco ad altri utenti di apportare modifiche.
Come avrete iniziato a capire, il lavoro fatto sulle due classi Data Layer è fondamentale. Tanto, tanto codice è centralizzato e demandato a queste due classi. Nelle form mi limito a richiamare i metodi ed eventualmente passare parametri. Quanto lavoro di sviluppo risparmiato... ;o)
[Oscar] Anche in questo caso, dopo aver visulizzato i dati, blocco la barra, le do il focus e imposto a sola lettura il campo chiave Stato, oltre a settare l'operazione in corso a modifica record. Si, lo avrete già intuito: è un'operazione inutile: entrare in modifica per poi non potere modificare alcunché è un nonsenso.
La logica che adotto è quella di non permettere la modifica di campi chiave della tabella sottesa: se di un record si devono modificare i campi che lo identificano, a mio avviso quel record non ha più senso di esistere: lo si elimina e se ne inserisce uno nuovo.
Caso vuole che la tabella Stati sia composta da un solo campo, Stato per l'appunto, che è anche campo chiave.
Mi ritrovo quindi a dare ragione a Diego quando, nella puntata che illustrava la creazione del database, diceva che questa ed altre tabelle necessitavano di un campo identificativo.
A questo punto, perchè non eliminare il pulsante di modifica? Non lo faccio per una questione di uniformità fra le form. Al limite lo si potrebbe sempre lasciare disabilitato. A voi la scelta.

[Diego] Per il momento, infatti, ci limitiamo a disabilitare il pulsante e a commentare il metodo sopra illustrato, e lasciato per ragioni didattiche (anche illustrare gli errori è istruttivo, a volte: in questo caso, Oscar è stato costretto a un accrocchio che avrebbe evitato con una maggior cura nello strutturare il database). Quindi correggiamo:

    BtnModifica.Enabled = False

in AbilitaPulsanti.

  Private Sub BtnCancella_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
                                Handles BtnCancella.Click

    Try
      Dim risposta As DialogResult = Messaggi.SiNo( _
                                     "Sei sicuro/a di volere eliminare il record visualizzato?", _
                                     "Elimina record")
      If risposta = Windows.Forms.DialogResult.No Then Exit Sub

      Dati.LeggiRecord()

      If Not Dati.CancellaRiga(True) Then Exit Sub 'Errore di concorrenza

      BtnPrimo.PerformClick()

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

Come prima cosa verifico che l'utente voglia veramente eliminare il record. Poi, con il metodo LeggiRecord della classe Tabella salvo i valori dei campi chiave del record corrente, in modo da ripristinarlo caso mai qualcosa non dovesse andare a buon fine. Invoco quindi il metodo CancellaRiga della classe Tabella passandole come parametro il valore vero che sta ad indicare al metodo stesso che si tratta dell'eliminazione di un record di una tabella non di dettaglio. Il metodo restituisce un valore booleano a seconda della riuscita (true) o meno (false) della cancellazione fisica del record dal database.
Se l'operazione è riuscita, emulo la pressione del pulsante che visualizza i dati del primo record della tabella.

La gestione del pulsante BtnCerca richiede la creazione della form di dialogo per la ricerca, che verrà illustrata e discussa nella prossima puntata (ci sono, per il momento, diverse cose che Diego vuole modificare).

Prima di illustrare il codice del pulsante di conferma dell'operazione in corso, devo spiegare il codice di alcuni metodi in esso richiamati.

  Private Function DatiValidi() As Boolean
    If ConvalidaDati.Convalida(TxtStato.Text, "Stato",ConvalidaDati.TipoConv.Testo,True) = False Then
      Messaggi.Avviso("Il campo Stato non può essere lasciato vuoto !", "Attenzione")
      Return False
    End If
    Return True
  End Function

Con questo metodo verifico la correttezza dei dati immessi dall'utente: obbligatorietà di compilazione, tipologia corretta (per esempio numeri in campi numerici), ecc. Se il controllo non va a buon fine il metodo restituisce il valore False o True.
Per la verifica viene sfruttato il metodo Convalida della classe ConvalidaDati del progetto Data, che richiede come parametri: il testo contenuto nella TextBox, il nome del campo o l'etichetta che apparirà negli eventuali messaggi di errore costruiti per l'occasione, il tipo di controllo che deve essere effettuato, l'eventuale obbligatorietà del campo.

  Private Sub CaricaRiga()
    Dim riga As DataRow = Nothing

    If mOperazione = TipoOperazione.Inserimento Then
      riga = Dati.RigaNuova
    Else
      Dati.RigaCorrente(riga)
    End If

    riga("Stato") = txtStato.Text.Trim
    riga("Modifica") = False

    If mOperazione = TipoOperazione.Inserimento Then
      If Not Dati.AggiungiRigaNuova(riga) Then
        'Errore di concorrenza
        Exit Sub
      Else
        Dati.LeggiRecord(Dati.DataSet.Tables(0).Rows.Count - 1)
        Dati.TrovaRecord()
      End If
    End If
  End Sub

Dapprima si invoca, a seconda del tipo di operazione, il metodo RigaNuova, oppure il metodo RigaCorrente (della classe Tabella). Entrambi influiscono sulla DataRow riga, della quale si valorizzano i campi. Quindi, se l'operazione è un inserimento, si invoca il metodo AggiungiRigaNuova: se non ha successo l'utente è stato avvisato e si esce dalla procedura; altrimenti si legge l'ultimo record immesso e ci si posiziona su di esso.

  Private Sub btnConferma_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
                                Handles BtnConferma.Click
    Try
      If Not DatiValidi() Then
        Exit Sub
      End If

      CaricaRiga()
      Dati.AggiornaDataBase()
      TxtStato.ReadOnly = False

      Dati.CaricaDati()
      If Not Dati.TrovaRecord() Then
        Messaggi.Avviso("Il record appena salvato non è stato trovato !", "Salvataggio record")
      Else
        VisualizzaDati()
      End If
      mOperazione = TipoOperazione.Nessuna
      SbloccaBarra()
      TbrStrumenti.Select()

    Catch ex As SqlException
      Select Case ex.ErrorCode
        Case -2146232060
          Messaggi.Errore(ex.Message, "Attenzione")
          mOperazione = TipoOperazione.Nessuna
        Case Else
          Messaggi.Errore(ex.Errors(0).Message, "Attenzione")
      End Select

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

Controllo la validità dei dati, carico la riga (nuova o corrente), allineo DataSet e database, NON reimposto la casella di testo come compilabile perchè è capo chiave, ricarico i dati nel DataSet, cerco di posizionarmi sul record che ho tentato di salvare; se ci riesco visualizzo i dati, oppure avviso l'utente; reimposto l'operazione corrente (a nessuna operazione), sblocco la barra e vi sposto il focus.

  Private Sub btnAnnulla_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
                               Handles BtnAnnulla.Click
    'Controllo se si stava inserendo il primo record in assoluto
    If Dati.NumeroRighe = 0 Then
      mOperazione = TipoOperazione.Nessuna
      PreparaVuoto()
      TxtStato.ReadOnly = False
      Exit Sub
    End If

    Try
      'Tolgo il blocco del record impostato mettendo il campo di modifica della tabella a falso
      Dati.ImpostaColonnaModifica(False)
      If Not Dati.TrovaRecord() Then
        Messaggi.Avviso("Errore di posizionamento sul record precedentemente visualizzato!", _
                        "Annulla operazione")
      Else
        VisualizzaDati()
      End If

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

    mOperazione = TipoOperazione.Nessuna
    SbloccaBarra()
    TbrStrumenti.Select()
    TxtStato.ReadOnly = False
  End Sub

Per prima cosa gestisco un caso limite, ovvero l'annullamento dell'inserimento del primo record in assoluto nella tabella degli Stati. Sfruttando il medoto NumeroRighe della classe Tabella mi faccio restituire il numero di record presenti. Se il numero è pari a zero, riporto la variabile dello stato delle operazioni in corso a nessuna operazione, richiamo il metodo PreparaVuoto già spiegato in precedenza e riporto al suo stato iniziale la TextBox (non è mai cambiato, in realtà, perché non è compilabile, essendo un campo chiave, ma è una spiegazione di quello che si fa con i controlli compilabili).
Se la tabella non era vuota riportiamo a falso il valore del campo relativo allo stato di modifica della tabella Stati attraverso il metodo ImpostaColonnaModifica della classe Tabella. Le operazioni successive sono identiche a quelle descritte per la conferma.

  Private Sub btnPrimo_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
                             Handles BtnPrimo.Click
    Dati.MoveFirst()
    VisualizzaDati()
  End Sub

Con i quattro pulsanti di spostamento fra i record non facciamo altro che richiamare i relativi metodi della classe Tabella e visualizzare i dati del nuovo record attuale. Se ne mostra solo uno come esempio.

  Private Sub btnAggiorna_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
                                Handles BtnAggiorna.Click
    'Mi salvo la chiave del record visualizzato in modo da potere tornare a visualizzare quel record
    Dati.LeggiRecord()
    Dati.CaricaDati()
    If Not Dati.TrovaRecord() Then
      Messaggi.Avviso("Errore di posizionamento sul record precedentemente visualizzato !", _
                      "Annulla operazione")
    Else
      VisualizzaDati()
    End If
  End Sub

Lo scopo del pulsante di aggiornamento è quello di riallineare il DataSet con il database attraverso il metodo CaricaDati e di riposizionarsi, visualizzandone i dati aggiornati, sul record che si aveva a video, attraverso i metodi LeggiRecord (che si salva i valori dei campi chiave), TrovaRecord (che sposta l'indice dei record su quello corrispondente ai valori dei campi chiave salvati) e VisualizzaDati.

  Private Sub btnSblocca_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
                               Handles BtnSblocca.Click
    'Tolgo il blocco del record impostato mettendo il campo di modifica della tabella a vero
    Dati.ImpostaColonnaModifica(False)
  End Sub

Questo pulsante dovrebbe essere usato dall'utente solamente in casi estremi. Sfruttando il metodo ImpostaColonnaModifica della classe Tabella, riportiamo a falso il valore del campo relativo allo stato di modifica della tabella Stati.
L'intento è quello di dare una via di uscita all'utente in casi tipo... "stavo effettuando una modifica al record, è andata via la corrente ed il record mi è rimasto sullo stato di modifica, pertanto non riesco più ad entrare a modificarlo"... oppure... "ecco, siamo alle solite, il collega dell'altro ufficio ha iniziato la modifica del record e poi è andato a prendersi un caffè, lasciandomi il record bloccato". :o)

Oscar ci tiene a segnalare che, nelle form più complesse di questa, è presente anche:

#Region "Metodi di convalida"
  'QUI SI CONTROLLA LA CONGRUITA' DEI DATI INSERITI NEI CAMPI ATTRAVERSO IL VALIDATING. 
  'NON SI FA IL CONTROLLO DELL'OBBLIGATORIETA' DEI CAMPI
#End Region

Anche se si tratta solo di un commento vale la pena soffermarsi un attimo. Se vi ricordate, in precedenza, ho descritto il metodo ControlloDati che verifica la correttezza dei dati immessi dall'utente: obbligatorietà di compilazione, tipologia corretta (per esempio numeri in campi numerici), ecc.
Al posto del commento sopra indicato, invece, andranno scritti tutti quei metodi che svolgono operazioni particolari, come per esempio, all'abbandono di una TextBox, il calcolo automatico e la relativa valorizzazione di un'altra.

Conclusione
In questa puntata è stata illustrata la FrmStati, la prima delle form che sfruttano una delle classi Data Layer argomento delle scorse puntate. Abbiamo visto anche qualche facilitazione offertaci dall'IDE di Visual Studio e spiegato qualche utile stratagemma di programmazione.

Manca un metodo, quello per la ricerca, perché implica la predisposizione di una apposita form.
Di questo parleremo nella prossima puntata.

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.