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

Premessa
In seguito a quanto esposto da Diego nella puntata precedente, ho apportato alcune modifiche per allinearmi alle sue indicazioni.
La prima parte di questo articolo illustra queste modifiche, la seconda parte tratterà il disegno della form principale del nostro progetto, la FrmMain.

Classe Program
La classe "di accesso" all'applicativo si arricchisce di un metodo e di altro codice con l'obiettivo di controllare subito se il programma può avviarsi o meno.

Si deve controllare l'esistenza del file crittato dei parametri di connessione, quindi la validità di questi ultimi. Se tali controlli hanno esito positivo, viene avviata la FrmMain, altrimenti viene proposta la FrmParametriConnessione, che deve restituire un valore di tipo Dialogresult in base al quale il programma procede con la FrmMain oppure si conclude. Il tutto con le dovute informazioni all'utente.

Poiché si ha bisogno di metodi e costanti posti in progetti satelliti, bisogna inserire direttive Imports:

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

Nell'ordine, permettono di istanziare la connessione, di testarla, di decrittare i parametri, di esporre messaggi all'utente.

Nel metodo Main, l'ultima riga Application.Run(New FrmMain) viene sostituita da:

    If Not TestParametriConnessione() Then
      Dim dlg As New FrmParametriConnessione("Program")
      If dlg.ShowDialog = DialogResult.OK Then
        Application.Run(New FrmMain)
      End If
    Else
      Application.Run(New FrmMain)
    End If

Si chiama il metodo TestParametriConnessione, che controlla esistenza e validità dei parametri di connessione. Se il controllo ha esito negativo, si istanzia la FrmParametriConnessione passando il nome del chiamante.
Questa novità è resa necessaria dal fatto che la FrmParametriConnessione deve restituire un DialogResult diverso a seconda del chiamante (infatti può essere chiamata anche da un menu della FrmMain).
Quando il controllo torna a Program, l'applicazione procede solo in caso di DialogResultOk, altrimenti si conclude.

Nel metodo TestParametriConnessione vengono definite alcune costanti per la stringa di connessione e per i messaggi:

  Private Shared Function TestParametriConnessione() As Boolean

    Try

      Const FMT_ConnectionString As String = "Data Source={0}\SQLEXPRESS;Initial Catalog=" & _
                                             FrmParametriConnessione.CATALOG & _
                                             ";Persist Security Info=True;User ID={1};Password={2}"
      Const WAR_FileParametriMancante As String = _
                           "Il file dei parametri di connessione al database non esiste ancora. " & _
                           "Verrà ora aperta la form di salvataggio degli stessi."
      Const WAR_ParametriNonCorretti As String = _
                           I parametri di connessione al database sono mancanti o non corretti. " & _
                           "Verrà ora aperta la form di salvataggio degli stessi."

Quindi si verifica l'esistenza del file crittato:

      Dim serverName As String = DeserializeConnString("Server")
      If serverName = "File non creato" Then
        Messaggi.Avviso(WAR_FileParametriMancante, "Test Connessione")
        Return False
      End If

Se non esiste, si avvisa l'utente e viene restituito il fallimento del controllo. Altrimenti si procede con la lettura degli altri parametri e con il test della connessione:

      Dim userID As String = DeserializeConnString("UserID")
      Dim password As String = DeserializeConnString("Password")

      If TestConnection(String.Format(FMT_ConnectionString, serverName, userID, password)) Then

Se il test ha esito positivo, viene istanziata la connessione e viene restituito il successo del controllo, altrimenti si avvisa l'utente e viene restituito il fallimento.

        ConnessioneDatabase = New SqlConnection(String.Format(FMT_ConnectionString, serverName, _
                                                              userID, password))
        Return True

      Else

        Messaggi.Avviso(WAR_ParametriNonCorretti, "Test Connessione")
        Return False

      End If

Form FrmParametriConnessione
Le modifiche apportate a questa form hanno l'intento di eliminare l'errore rilevato da Diego (la chiusura dell'applicazione da una form secondaria).
La form deve restituire un appropriato DialogResult al chiamante, in modo che questo possa adottare i comportamenti del caso.
Benché il metodo ShowDialog abbia un overload con un parametro owner, questo è di tipo IWin32Window, quindi non si può passare Program.
E' stato quindi aggiunto un costruttore che richiede come argomento il nome della classe chiamante, con il quale viene valorizzato un apposito campo:

  Public Sub New(ByVal caller As String)
    InitializeComponent()
    mCaller = caller
  End Sub

La varietà delle possibili situazioni che possono scaturire durante e alla fine dell'esecuzione della form hanno imposto l'aggiunta di nuove costanti e nuovi campi.
Le costanti sono state denominate a seconda del chiamante e del luogo in cui vengono usate (la lettura dei loro valori permette di rendersi conto delle situazioni in cui vengono usate):

  Public Const CATALOG As String = "APP-P"
  Private Const FMT_ConnectionString As String = "Data Source={0}\SQLEXPRESS;Initial Catalog=" & _
                                               CATALOG & _
                                               ";Persist Security Info=True;User ID={1};Password={2}"
  Private Const FMP_ConnectionKo As String = "Impossibile connettersi al database {0} su server {1}"
  Private Const FMP_ConnectionKo_Salva As String = _
                                         "Test di connessione al database {0} su server {1} fallita."
  Private Const FMP_ConnectionOk As String = "Connessione a database {0} su server {1}  OK"
  Private Const WAR_DatiModificati As String = _
                                          "I dati sono stati modificati, si desidera salvare la " & _
                                          "configurazione prima di testare la connessione ?"
  Private Const WAR_Chiusura_App_Program As String = _
                                              "I parametri di accesso al database non sono stati" & _
                                              " indicati pertanto l'applicazione verrà terminata."
  Private Const WAR_TestKo_Program As String = "I parametri indicati non hanno superato " & _
                                               "il test di connessione al database. " & _
                                               "Si desidera modificare i parametri? " & _
                                               "Rispondendo di no l'applicazione verrà terminata."
  Private Const WAR_TestKo_Main As String = "I parametri indicati non hanno superato " & _
                                         "il test di connessione al database. " & _
                                         "Si desidera modificare i parametri? " & _
                                         "Rispondendo di no verranno mantenuti i parametri iniziali."
  Private Const WAR_SaveOnExit_Program As String = "Ci sono state modifiche ai dati, " & _
                                                  "si desidera salvarle prima di uscire ? " & _
                                                  "Rispondendo di no l'applicazione verrà terminata."
  Private Const WAR_SaveOnExit_Main As String = "Ci sono state modifiche ai dati, " & _
                                         "si desidera salvarle prima di uscire ? " & _
                                         "Rispondendo di no verranno mantenuti i parametri iniziali."
  Private Const WAR_Salva As String = "Salvo i parametri ?"
  Private Const WAR_Salvato As String = "Parametri salvati con successo."

Sono stati aggiunti cinque nuovi campi. Il campo relativo al chiamante, tre per mantenere i valori iniziali dei parametri, l'ultimo per discriminare tra i possibili esiti del test di connessione, per i quali viene creato un enumerato:

  Private mCaller As String = String.Empty
  Private mServerIniz As String = String.Empty
  Private mUserIdIniz As String = String.Empty
  Private mPasswordIniz As String = String.Empty
  Private mTestOK As esitoTest = esitoTest.NoTest

  Private Enum esitoTest
    NoTest        ' il test di connessione non è andato a buon fine
    TestOkNoSaved ' è andato a buon fine ma non sono stati salvati i parametri
    TestOkSaved   ' è andato a buon fine e sono stati salvati i parametri
  End Enum

Depositare i parametri iniziali serve se il chiamante è FrmMain. Infatti, se il chiamante è Program e i parametri non sono impostati e salvati correttamente dall'utente all'uscita dalla form. il programma viene terminato. Invece, se il chamante è FrmMain e i parametri non sono correttamente impostati e salvati, bisogna ripristinare i parametri iniziali, letti all'apertura della form. Si parte dal presupposto che i valori dei parametri siano corretti, se la FrmMain è stata aperta.

Il campo mTestOK può assumere tre valori:

In fase di chiusura, il DialogResult da restituire dal chiamante viene deciso in base al chiamante e al valore di mTestOK, come si vedrà nel seguito.

Nel metodo FrmParametriConnessione_Load, dopo la valorizzazione dei campi 'prec', vengono valorizzati i campi 'iniz':

    mServerIniz = mServerPrec
    mUserIdIniz = mUserIdPrec
    mPasswordIniz = mPasswordPrec

Il codice del metodo FrmParametriConnessione_FormClosing viene completamente sostituito con il seguente, nel quale, in base al fatto che i parametri siano stati modificati, al chiamante e al valore di mTestOK, vengono assunti i comportamenti del caso, visualizzando i messaggi adeguati alle situazioni e impostando il DialogResult adatto:

  Private Sub FrmParametriConnessione_FormClosing(ByVal sender As System.Object, _
                                              ByVal e As System.Windows.Forms.FormClosingEventArgs) _
                                              Handles MyBase.FormClosing
    If Not (e.CloseReason = CloseReason.ApplicationExitCall _
            OrElse e.CloseReason = CloseReason.WindowsShutDown _
            OrElse e.CloseReason = CloseReason.TaskManagerClosing) Then

      If mDatiModificati Then

        If mCaller = "Program" Then

          Select Case mTestOK

            Case esitoTest.NoTest

              ' I parametri indicati dall'utente non sono corretti o non è stato effettuato il test
              ' di connessione. Viene chiesto all'utente se desidera modificare i parametri. 
              ' Se risponde di no viene avvisato che l'applicazione verrà terminata
              If Messaggi.SiNo(WAR_TestKo_Program, "Salvataggio parametri") = DialogResult.Yes Then
                e.Cancel = True
              Else
                Me.DialogResult = Windows.Forms.DialogResult.Cancel
              End If

            Case esitoTest.TestOkNoSaved

              ' Il test di connessione è andato a buon fine ma l'utente non ha ancora salvato i 
              ' parametri. Viene quindi chiesto se prima di uscire dalla form si desidera salvarli. 
              ' Se risponde di no viene avvisato che l'applicazione verrà terminata
              If Messaggi.SiNo(WAR_SaveOnExit_Program, _
                               "Salvataggio parametri") = DialogResult.Yes Then
                Salva()
                Me.DialogResult = Windows.Forms.DialogResult.OK
              Else
                Me.DialogResult = Windows.Forms.DialogResult.Cancel
              End If

            Case esitoTest.TestOkSaved

              ' Il test di connessione è andato a buon fine e i parametri sono stati salvati. 
              ' In questo caso la form verrà chiusa e verrà avviata la form Main
              Me.DialogResult = Windows.Forms.DialogResult.OK

          End Select

        Else ' mCaller = "FrmMain"

          Select Case mTestOK

            Case esitoTest.NoTest

              ' I parametri indicati dall'utente non sono corretti o non è stato effettuato il test 
              ' di connessione. viene chiesto all'utente se desidera modificare i parametri. 
              ' Se risponde di no vengono ripristinati i parametri iniziali
              If Messaggi.SiNo(WAR_TestKo_Main, "Salvataggio parametri") = DialogResult.Yes Then
                e.Cancel = True
              Else
                txtServer.Text = mServerIniz
                txtUserID.Text = mUserIdIniz
                txtPassword.Text = mPasswordIniz
                Salva()
              End If

            Case esitoTest.TestOkNoSaved

              ' Il test di connessione è andato a buon fine ma l'utente non ha ancora salvato i 
              ' parametri. Viene quindi chiesto se prima di uscire dalla form si desidera salvarli. 
              ' Se risponde di no vengono ripristinati i parametri iniziali
              If Messaggi.SiNo(WAR_TestKo_Main, "Salvataggio parametri") = DialogResult.Yes Then
                e.Cancel = True
              Else
                txtServer.Text = mServerIniz
                txtUserID.Text = mUserIdIniz
                txtPassword.Text = mPasswordIniz
                Salva()
              End If

            Case esitoTest.TestOkSaved

              ' Il test di connessione è andato a buon fine e i parametri sono stati salvati. 
              ' In questo caso la form verrà chiusa
              Me.DialogResult = Windows.Forms.DialogResult.OK
          End Select

        End If ' mCaller = "Program"

      Else ' not mDatiModificati

        If mCaller = "Program" Then
          Messaggi.Avviso(WAR_Chiusura_App_Program, "Parametri connessione database")
          Me.DialogResult = Windows.Forms.DialogResult.Cancel
        Else
          Me.DialogResult = Windows.Forms.DialogResult.OK
        End If

      End If ' mDatiModificati

    End If ' e.CloseReason

  End Sub

Nel metodo btnAnnulla_Click, dopo avere impostato mDatiModificati a False, viene aggiunta l'impostazione della variabile mTestOK a esitoTest.NoTest:

      mTestOK = esitoTest.NoTest

Analogamente,vengono aggiunte impostazioni di mTestOK nel metodo btnTestaCon_Click, a seconda del caso:

      If mDatiModificati Then
        If Messaggi.SiNo(WAR_DatiModificati, "Test connessione") = DialogResult.Yes Then
          Salva()
        End If
      End If

      'Testo la connessione del DB principale
      If SqlHelper.TestConnection(txtStringaConN.Text) Then
        Messaggi.Info(String.Format(FMP_ConnectionOk, CATALOG, txtServer.Text), "Risultato test")
        mTestOK = esitoTest.TestOkNoSaved
      Else
        Messaggi.Avviso(String.Format(FMP_ConnectionKo, CATALOG, txtServer.Text), "Risultato test")
        mTestOK = esitoTest.NoTest
      End If

Apportando queste modifiche ho corretto il secondo parametro fornito per la costruzione della stringa del messaggio: passavo, invece del nome del database (APP-P), la stringa "Test connessione". Ricordo che la costante CATALOG contiene il nome del database SQL.

Nel metodo Salva, prima di salvare i parametri crittati, è stato aggiunto un test della conessione con relativa impostazione di mTestOK:

      If SqlHelper.TestConnection(txtStringaConN.Text) Then
        mTestOK = esitoTest.TestOkNoSaved
      Else
        Messaggi.Avviso(String.Format(FMP_ConnectionKo_Salva, CATALOG, txtServer.Text), _
                        "Risultato test")
        mTestOK = esitoTest.NoTest
        Exit Sub
      End If

Infine, l'impostazione a False di mDatiModificati è stata sostituita dall'impostazione di mTestOK:

      mTestOK = esitoTest.TestOkSaved

Correzione
Prima di procedere al collaudo, c'è da correggere una riga, nel codice del metodo CartellaWinDatiApp della classe accessoria GestoreFiles, che deve restituire:

  Return Path.Combine(My.Computer.FileSystem.SpecialDirectories.AllUsersApplicationData, nomeFile)

Collaudo
Compiliamo e lanciamo la nostra applicazione. Se tutto va bene, compare il messaggio che ci informa della mancanza del file dei parametri di connessione al database e dell'apertura della form di salvataggio degli stessi.
Quindi viene aperta la form:

Nella casella per il nome del server è sufficiente digitare il nome della macchina, la scritta "\SQLEXPRESS" è aggiunta automaticamente dal programma (non ve ne eravate accorti, vero? :o)). Naturalmente, questi parametri sono quelli di Diego, la cui macchina si chiama CDSOFTDIEGO, sulla quale all'istanza SQLEXPRESS è stato aggiunto un account programmi con una password pwdprogrammi. Voi metterete il nome della vostra macchina e i dati dell'account che avete destinato all'uso del database APP-P, quello creato nella quinta puntata.

Se tutto va senza intoppi, dovreste arrivare all'apertura della FrmMain provvisoria, che fra poco elimineremo, per sostituirla con quella definitiva.

Creazione della FrmMain
In Esplora soluzioni (Solution Explorer) facciamo 'clic-destro' sul file FrmMain.vb e cancelliamolo.
Facciamo 'clic-destro' sul nome del progetto PrimiPassi e aggiungiamo un nuovo elemento, scegliendo Kripton Form nella sezione Modelli personali e assegnando il nome FrmMain.

Facendo riferimento alla figura precedente, disegnamo la form:

Il sistema di comunicazione delle informazioni tra le form figlie e la FrmMain
L'ultimo controllo disegnato ha la funzione di informare l'utente sull'operazione in corso quando un controllo di una form figlia riceve il focus.

Le informazioni da visualizzare vengono passate dalla form figlia alla FrmMain tramite lo scatenamento di un evento, cui risponde un apposito gestore nel codice della FrmMain, che imposta una proprietà, la quale valorizza il contenuto della label di stato.

[Diego] Per implementare questo sistema serve una classe per l'argomento dell'evento (le informazioni da visualizzare). Perché il sistema funzioni non è indispensabile questa classe (infatti Oscar non ci ha pensato), ma una programmazione corretta lo impone: la firma normale di qualsiasi evento in .Net richiede due parametri: l'oggetto origine dell'evento e gli argomenti dell'evento, che ci siano oppure no:

  Public Event nomeEvento(ByVal sender As Object, ByVal e As EventArgs)

Gli argomenti, che siano zero, o uno o più, sono esposti da un oggetto di tipo EventArgs, cioè definito da EventArgs o da una classe che eredita da EventArgs.
Il nome di questa classe riprende il nome dell'evento. Posto che il nome dell'evento - dato che esso riguarda il cambiamento del controllo attivo, poiché si vuole scatenarlo quando un controllo ottiene il focus - sia ActiveControlChanged, e che le conseguenze di questo evento riguardano essenzialmente l'interfaccia utente, facciamo clic-destro sul progetto APP.UI e aggiungiamo una classe, denominandola ActiveControlChangedEventArgs:

Public Class ActiveControlChangedEventArgs
  Inherits EventArgs

  Private mMessaggioInfo As String
  Public ReadOnly Property MessaggioInfo() As String
    Get
      Return mMessaggioInfo
    End Get
  End Property

  Public Sub New(ByVal messaggioInfo As String)
    mMessaggioInfo = messaggioInfo
  End Sub

End Class

La classe eredita da EventArgs, espone la proprietà di sola lettura MessaggioInfo attraverso il campo mMessaggioInfo che viene valorizzato nel codice del costruttore con l'apposito parametro. E' quasi più difficile decriverlo che implementarlo.

Avendo questa classe a disposizione, possiamo dichiarare un evento in qualsiasi form figlia, a esempio in FrmParametriConnessione:

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

Possiamo (vogliamo) scatenarlo laddove qualche controllo riceve il focus:

  Private Sub txtPassword_Enter(ByVal sender As Object, ByVal e As System.EventArgs) _
                                Handles txtPassword.Enter
    RaiseEvent ActiveControlChanged(Me, New ActiveControlChangedEventArgs( _
                                    "Info : " & "Password di accesso al database"))
  End Sub
  Private Sub txtServer_Enter(ByVal sender As Object, ByVal e As System.EventArgs) _
                              Handles txtServer.Enter
    RaiseEvent ActiveControlChanged(Me, New ActiveControlChangedEventArgs( _
                              "Info : " & "Nome della macchina su cui risiede l'istanza SQLEXPRESS"))
  End Sub
  Private Sub txtUserID_Enter(ByVal sender As Object, ByVal e As System.EventArgs) _
                              Handles txtUserID.Enter
    RaiseEvent ActiveControlChanged(Me, New ActiveControlChangedEventArgs( _
                                   "Info : " & "Nome dell'utente abilitato ad accedere al database"))
  End Sub

L'evento viene gestito da un gestore appositamente predisposto in FrmMain, nella quale quindi dobbiamo impostare la direttiva che fa 'vedere' la classe di argomento dell'evento, nonché la proprietà da riferire al contenuto della label di stato tssInfo:

Imports APP.UI

Public Class FrmMain

  Private mInfoMessage As String
  Public Property InfoMessage() As String
    Get
      Return mInfoMessage
    End Get
    Set(ByVal value As String)
      mInfoMessage = value
      If Not (mInfoMessage = String.Empty) Then
        tssInfo.Text = mInfoMessage
      End If
    End Set
  End Property

Si è deciso di implementare una proprietà perché potrebbe essere utile per informazioni di altro tipo.
Adesso è tutto pronto per implementare il gestore di evento:

  Private Sub FormFiglia_ActiveControlChanged(ByVal sender As Object, _
                                              ByVal e As ActiveControlChangedEventArgs)
    Me.mInfoMessage = e.MessaggioInfo
  End Sub

Questo gestore viene associato a ogni form figlia quando ne viene creata un'istanza, prima di mostrarla (in questo caso si tratta di una form di dialogo, ma la sostanza non cambia):

  Private Sub ConnessioneDatabaseToolStripMenuItem_Click(ByVal sender As System.Object, _
                                                   ByVal e As System.EventArgs) _
                                                   Handles ConnessioneDatabaseToolStripMenuItem.Click
    Dim dlg As New FrmParametriConnessione("Main")
    AddHandler dlg.ActiveControlChanged, AddressOf FormFiglia_ActiveControlChanged
    dlg.ShowDialog()
  End Sub

Del resto del codice della FrmMain, è interessante illustrare il codice del gestore dell'evento Load:

  Private Sub FrmMain_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load

    ' scandisco tutti i controlli cercando quelli di tipo MdiClient
    For Each ctl As Control In Me.Controls
      Try
        ' converto il tipo del controllo in MdiClient
        Dim ctlMDI As MdiClient = CType(ctl, MdiClient)

        ' cambio il BackColor del controllo
        ctlMDI.BackColor = Me.BackColor

      Catch exc As InvalidCastException
        ' catturo e ignoro l'errore di tipizzazione
      End Try
    Next ctl

    ' aggiungo info nell'area destra della barra del titolo
    Me.TextExtra = "Nome computer : " & My.Computer.Name & "     " & _
                   "Data : " & DateTime.Today.ToShortDateString

  End Sub

Viene impostato il colore di fondo della FrmMain con il colore impostato in fase di progettazione al posto di quello impostato di default per le form MDI, sfruttando MDIClient, (che si comporta come una sorta di panel che si sovrappone alla form MDI).
Viene inoltre sfruttata una caratteristica carina delle form Krypton: un testo aggiuntivo a destra.

Il resto del codice, al momento, è costituito solo dalla gestione dei menu Finestra ed Esci.

Conclusione
In questo articolo abbiamo corretto qualche errore, disegnato la FrmMain e impostato un comportamento di base sfruttando la comunicazione tramite eventi.
Arrivederci alla prossima puntata, in cui cominceremo a trattare con il 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.