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

Premessa
Dopo aver aggiustato (vedi articolo precedente), alcune cose relativamente a Sql Server (installazione e spostamento dei file di database, creazione di un nuovo utente da usare con le nostre applicazioni), possiamo finalmente dedicarci allo sviluppo del primo componente della nostra applicazione.

[Diego] In questo articolo intervengo a rifare parti del lavoro svolto da Oscar. Però invito a seguire comunque lo sviluppo, anche se contempla rifacimenti, senza limitarsi a scaricare il codice a corredo.

Creazione della form per i parametri di connessione al database
Dopo diversi tentativi, decine di domande poste nei forum e lunghe riflessioni, ho deciso di salvare e prelevare i parametri di connessione al database SQL Server da un mio file XML crittografato e con una mia estensione proprietaria.

Sono arrivato a queste conclusioni dopo ore, anzi giorni interi di esperimenti. La vita dell'autodidatta è dura e spesso frustrante. Non sempre la lettura di qualche buon testo risolve tutti i problemi, il più delle volte li risolve solo in parte.
Inizialmente usavo i wizard per la connessione ai database e la creazione di DataSet tipizzati.
Riflettendo su quanto facevo in Visual Basic 6 o (meglio) sulle esigenze che avevo quando sviluppavo per i clienti, mi sono ben presto reso conto che uscire dai binari dorati di quanto prodotto dai wizard comportava una conoscenza di .NET che io non possedevo (e credo di non possedere tuttora). Parlo di situazioni banali, ma frequenti, come: cosa comporta nel DataSet tipizzato un cambiamento della struttura di una tabella nel database, dato che ogni cliente ha una rete diversa dagli altri, come personalizzo l'App.Config, ecc.
Sentivo di essere legato a una struttura troppo rigida. Comoda, certamente sì: di primo acchito si producono delle soluzioni con una velocità impensabile per il Visual Basic 6. Ma... a scapito di cosa? Ogni volta che c'era da mettere mano a qualcosa, causa la mia ignoranza lo ammetto, perdevo molto più tempo di quello guadagnato usando il wizard.

Ho perso giorni e scambiato decine di mail per capire come gestire al meglio la connessione al database SQL Server attraverso la modifica dell'App.Config e del codice scritto dal wizard. Con risultati discreti, ma non del tutto soddisfacenti e, poiché, quando porto un applicativo a un cliente, voglio essere sicuro di portare qualcosa di funzionante, ma anche rapidamente modificabile, secondo le nuove esigenze che dovessero eventualmente sorgere, quel 'discreto ma non soddisfacente' mi metteva in ansia.
Da qui, nonostante il gran tempo investito fino a quel momento, ho deciso di dare un taglio netto e ripartire da capo. Un ritorno alla filosofia del passato, quella del Visual Basic 6, un fai da te pressoché totale, quindi DataSet non tipizzati, gestione manuale dell'accesso ai dati, dell'interrogazione e salvataggio e di conseguenza della connessione al database.

Sono comunque convinto che una gestione manuale di questo tipo non debba necessariamente portare a uno sviluppo del codice più lento e macchinoso. Ho convinto un amico (Stefano) a farmi un mini corso di VB.NET e con lui abbiamo sviluppato delle classi di gestione dei dati che, a mio avviso, velocizzano la stesura del codice. Quello che cerco di dire è che quanto fa il wizard ce lo possiamo ricreare da noi, con il vantaggio di saper dove mettere le mani quando dobbiamo apportare modifiche, anche significative.

L'applicazione che stiamo sviluppando segue questa logica di avvio:

Quest'ultima form è il componente che stiamo per sviluppare, utile nel caso di primo avvio assoluto dell'applicazione oppure nel caso in cui sia stato cambiato uno o più parametri di accesso.
Una funzionalità molto importante di questa form è la sua criticità: deve assolutamente essere modale per ovvi motivi: durante questa operazione non si deve lavorare sul programma e viceversa se questa form appare è perchè non si riesce ad accedere al database e quindi sono io che non voglio che si lavori su un applicativo che in quel momento potrebbe solo lanciare eccezioni a raffica. Di conseguenza, alla sua chiusura, se non si è stabilita una connessione valida, si deve uscire anche dall'applicazione.

[Diego] Quest'ultimo punto costituisce un errore strategico. Non va bene far terminare l'applicazione dall'interno di una form di dialogo. Bisogna, piuttosto, scatenare un evento o fornire una informazione che permetta, al chiamante della form, di chiudere correttamente l'applicazione.
Nella prospettiva di condizionare l'applicazione alla validità della connessione, non ha senso caricare all'inizio la FrmMain: bisogna dapprima testare la connessione e uscire dall'applicazione in modo pulito.
Inoltre, poiché si vuole crittare i dati per la connessione, i metodi per crittare e decrittare questi dati vanno posti in una classe a parte, dato che servono sia all'inizio del programma, per leggere le informazioni, che a questa form, sia per leggerle che per scriverle.
Per il momento continuiamo nello sviluppo di questa form, ma teniamo presente queste considerazioni, che preludono a sostanziali modifiche, che verranno esplicate in seguito.

Per rendere le idee più chiare durante la creazione della form, anticipo un'immagine dell'aspetto finale che essa dovrà avere:

In Esplora soluzioni clicchiamo con il tasto destro sul progetto PrimiPassi, selezioniamo la voce Aggiungi e quindi Nuovo elemento. Nella finestra che appare, dalla sezione Modelli personali selezioniamo Krypton Form, assegniamo il nome FrmParametriConnessione e clicchiamo sul pulsante Aggiungi.

[Diego] Per le versioni Express, l'installazione del Krypton Toolkit non si completa con l'aggiunta dei modelli di form e di progetto e dei controlli alla Toolbox. Bisogna intervenire manualmente:

  1. Aprire la cartella con i modelli Krypton. Sul mio pc si trova in C:\Programmi\Component Factory\Krypton Toolkit 3.5.2\Templates
  2. Aprire la cartella per i modelli personali. Sul mio pc si trova in C:\Documents and Settings\cattaruzza\Documenti\Visual Studio 2008\Templates
  3. Copiare (non 'spostare') i file zip ciascuno nella cartella giusta, a seconda del nome del file: i file contenenti la parola Project sono ProjectTemplates, gli altri sono ItemTemplates; i file contenenti la sigla CS sono relativi al linguaggio C#, gli altri a Visual Basic
  4. Nella Toolbox, fare 'clic destro' nella scheda General, aggiungere una scheda e denominarla Krypton Controls
  5. Fare 'clic destro' nella nuova scheda, scegliere 'scegli elementi' (Choose Items), selezionare tutti i controlli il cui nome comincia con Krypton

Disegno dell'interfaccia:

[Diego] In qualche caso le larghezze che eccedono gli 800 punti sono sconsigliabili (nel progetto allegato alla serie ci sono altre dimensioni).

Infine assegniamo l'ordine di tabulazione. Clicchiamo sulla voce di menù Visualizza e quindi su Ordine di tabulazione. Clicchiamo sulla ToolStrip e poi su ciascuna TextBox (nell'ordine), infine premiamo il tasto Esc sulla tastiera per confermare.

Il modulo ConDB

[Diego] Questa parte verrà modificata radicalmente. Per ragioni didattiche, comunque, si riporta quanto scritto da Oscar. Seguiranno le mie considerazioni critiche e le spiegazioni del cambiamento.

Prima di scrivere il codice per la FrmParametriConnessione, conviene creare un componente per la connessione al database, che sia accessibile da ogni oggetto dell'applicazione senza che ne debba essere istanziato un oggetto. Significa che aggiungeremo un nuovo modulo.

Per capire meglio le differenze fra una classe e un modulo trascrivo quanto riporta la guida:

"Un modulo (denominato a volte modulo standard) è simile a una classe ma con alcune distinzioni importanti. Ogni modulo ha esattamente un'istanza e non deve essere creata o assegnata a una variabile. I moduli non sono in grado di supportare interfacce di ereditarietà o implementazione. Si noti che un modulo non è un tipo nello stesso senso di una classe o struttura. Non è possibile dichiarare che un elemento di programmazione abbia il tipo di dati di un modulo.

La parola chiave Module può essere utilizzata solo a livello di spazio dei nomi. In altri termini, il contesto della dichiarazione per un modulo deve essere costituito di un file di origine o uno spazio dei nomi e non può essere una classe, una struttura, un modulo, un'interfaccia, una routine o un blocco. Non è possibile nidificare un modulo all'interno di un altro modulo o all'interno di un tipo.

La durata di un modulo corrisponde a quella del programma. Poiché i suoi membri sono tutti Shared la loro durata corrisponde anch'essa a quella del programma.

L'impostazione predefinita dei moduli è l'accesso Friend. È possibile modificarne i livelli di accesso mediante gli appositi modificatori.

Tutti i membri di un modulo sono Shared in modo implicito".

Aggiungiamo quindi un nuovo modulo al progetto PrimiPassi, denominandolo ConDB. Impementiamo il seguente codice:

#Region "Dichiarazione degli Imports"
Imports System.Data.SqlClient
#End Region

'Modulo per la connessione ai database
Module ConDB

#Region "Proprietà pubbliche"

  Private Con As SqlConnection

  'Stringa di connessione al database principale
  Public Property StringaCon() As SqlConnection
    Get
      Return Con
    End Get
    Set(ByVal value As SqlConnection)
      Con = value
    End Set
  End Property

#End Region

End Module

Come si può vedere il modulo contiene un'unica proprietà (StringaCon) del tipo SqlConnection che, come si può intuire, conterrà la nostra connessione. Essendo questa proprietà in un modulo risulterà sempre disponibile e avrà una vita pari a quella del programma.

Ho letto che i moduli sono un retaggio del passato e non andrebbero usati, ma avevo bisogno di queste caratteristiche. Se ci sono vie alternative ... battete un colpo.;o)

[Diego] Le mie considerazioni critiche sul modulo ConDb sono diverse, ma tutte solamente filosofiche. Riguardano la disciplina di programmazione, più che l'effetto pratico. Ciò non toglie che un notevole vantaggio pratico nel programmare con disciplina ci sia, prima di tutto nella successiva manutenzione del progetto.

Se si ha necessità che una informazione sia accessibile a tutta l'applicazione per la durata dell'esecuzione, si fa uso di una o più classi statiche, cioè classi in cui tutti i membri sono Shared. Nel corso di questa serie, ne sono già state implementate diverse, nei progetti satellite di PrimiPassi. Questo significa che Oscar, come molti programmatori ex-VB6, non ha collegato la caratteristica 'sempre disponibile' (tipica del modulo alla VB6) alla caratteristica 'istanza non necessaria' (tipica delle classi statiche VB.Net). E' stato 'condizionato' dalla propria esperienza VB6 - nonché dal fatto che stava migrando in vb.net un progetto VB6 - e ha riproposto una 'variabile globale' travestendola da proprietà, ma ponendola comunque in un modulo, alla maniera VB6, appunto.

Il Modulo è 'un retaggio del passato' quando viene usato in questo modo per questo solo scopo, ma non è stato progettato solo per permettere ai programmatori ex-vb6, quelli troppo pigri, di non pensare. Il Modulo è da intendere come una parte di codice che sta al di sopra delle classi, che riguarda più lo spazio dei nomi che non l'applicazione. A esempio, i metodi di estensione vanno posti in moduli (vedi sul mio blog "Mid$ da VB6 a VB.Net e metodi Extension").

In base a quanto sopra, non si scriverà quindi un modulo ConDb, ma eventualmente una classe statica.
In quale namespace? Non certo in quello di PrimiPassi: visto che riguarda la connessione al database, il suo posto è sicuramente il progetto APP.Data. Quest'ultimo contiene già la classe statica SqlHelper, la quale per giunta espone proprio un metodo TestConnection.

Altra cosa che non mi piace è la scelta dei nomi: ConDb sembra più la marca di un condimento in tubetto (:o))) che il nome di un componente Net. Anche se, nel caso specifico, può sembrare evidente anche agli sciocchi che ConDB significa ConnessioneDatabase, è preferibile, almeno per me, il nome più verboso a quello sintetico. Così, anche il nome del campo Con pecca sia nella eccessiva brevità che nella forma (comincia con la maiuscola). Per giunta, la proprietà esposta è un oggetto SqlConnection, ma si chiama StringaCon (non è una stringa, né sta 'con' alcunché).

Di conseguenza, apriamo il file SqlHelper.vb e aggiungiamo il codice seguente:

#Region "Proprietà"

  Private Shared mConnessioneDataBase As SqlConnection
  Public Shared Property ConnessioneDatabase() As SqlConnection
    Get
      Return mConnessioneDataBase
    End Get
    Set(ByVal value As SqlConnection)
      mConnessioneDataBase = value
    End Set
  End Property

#End Region

Il codice della FrmParametriConnessione
Si comincia, come sempre, dalle direttive Imports:

Imports System
Imports System.IO
Imports System.Data.SqlClient
Imports System.Security
Imports System.Security.Cryptography
Imports System.Runtime.InteropServices
Imports System.Text
Imports System.Xml
Imports APP.Base
Imports APP.UI
Imports APP.Data

Tutte le direttive, tranne quella per SqlClient e le tre relative ai progetti APP, sono necessarie ai procedimenti di serializzazione/crittazione e deserializzazione/decrittazione.
Seguono le costanti predisposte per comunicazioni con l'utente:

  Public Const APP_NAME As String = "PrimiPassi"
  Private Const FMP_ConnectionKo As String = "Impossibile connettersi al database {0} su server {1}"
  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_SaveOnExit As String = _
"Ci sono state modifiche ai dati, si desidera salvarle prima di uscire ?" Private Const WAR_Salva As String = "Salvo i parametri ?" Private Const WAR_Salvato As String = "Parametri salvati con successo."

[Diego] C'è anche una costante pubblica APP_NAME, la quale, nella mia visione delle cose, richiede che la form sia istanziata, per esistere. Il che significa che probabilmente è sbagliata. Siamo di nuovo in presenza di un difetto indotto dall'esperienza VB6: poiché in VB6 una form 'esiste sempre' (almeno nella sua istanza predefinita), FrmParametriConnessione.APP_NAME restituirà sempre un valore. In VB.Net non è più così: bisogna curarsi del fatto che FrmParametriConnessione è solo la definizione di un oggetto che deve essere istanziato per poter 'vedere' quanto espone.
Purtroppo, hanno rimesso questa fesseria, in Visual Basic (in C# non c'è mai stata), infatti possiamo scrivere, in FrmMain_Load, una riga come questa:

    Debug.Print(FrmParametriConnessione.APP_NAME)

e viene accettata ed è funzionante; ma noi, se vogliamo programmare con un minimo di coerenza e rigore, dobbiamo dimenticarcene.

La costante APP_NAME viene chiamata in causa una sola volta: come argomento del metodo che crea il nome del file XML con estensione proprietaria, con il quale nome viene valorizzato un campo di questa form:

  Private Shared ReadOnly mAppFileName As String = GestoreFiles.CreaNomeFileConfig(APP_NAME)

L'idea sarebbe quella di riutilizzare questa form per tutte le altre future applicazioni che richiedono parametri di connessione a SqlServer da crittare e serializzare in un file, il cui nome deve richiamare quello dell'applicazione.
Perché 'doversi ricordare' che bisogna cambiare il valore della costante APP_NAME?. Abbiamo a disposizione il magico namespace My:

  Private Shared ReadOnly mAppFileName As String = _
                                    GestoreFiles.CreaNomeFileConfig(My.Application.Info.AssemblyName)

Quindi, nel codice allegato, non troverete la costante APP_NAME. Al suo posto ce ne saranno altre due, collegate tra loro, che nel codice originale mancavano:

  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}"

La prima costante serve a mantenere in un unico luogo il nome del database (ma è un luogo provvisorio, in base allo stesso discorso fatto per APP_NAME). La seconda costante è la stringa di formato della tipica stringa di connessione, che ci servirà in vari punti del codice di questa form.

Un altro campo di cui parlare è questo (nel codice originale):

  Const SecretKeyString As String = "aGIa07PI"

[Oscar] Questa riga di codice onestamente non mi convince fino in fondo e spiego il perché. Questa è la chiave segreta - la cui lunghezza deve essere al massimo di 8 caratteri - utilizzata per creare e successivamente accedere al file XML crittografato. E' qui, in chiaro nel codice. Non è possibile generare una chiave a estrazione casuale in quanto, se il file è stato creato con una chiave, vi si può accedere solo con quella. Salvarla esternamente su un file mi riporta al punto di partenza. Un reverse engineering potrebbe riportare alla luce la chiave, ma a me non sono venute in mente altre idee. Se qualcuno ne ha di diverse lo prego di espormele.

[Diego] Una soluzione può essere implementare un metodo che attraverso un qualsiasi algoritmo contorto a nostro piacere restituisca sempre la stessa stringa. Per fare un esperimento semplice, prepariamo qualche riga nella solita procedura FrmMain_Load che, per il momento, è un comodo posto per fare qualche prova al volo:

    Dim SecretKeyString As String = "aGIa07PI"
    Dim result As String = String.Empty
    For i As Integer = 0 To SecretKeyString.Length - 1
      result &= Char.ConvertToUtf32(SecretKeyString, i).ToString & ","
    Next
    Debug.Print(result)

Così, dopo aver fatto un lancio di prova, ci ritroviamo con la scritta "97,71,73,97,48,55,80,73" nella finestra Immediata, pronta per essere utilizzata in un altro codice di verifica:

    Dim numbers As Integer() = {97, 71, 73, 97, 48, 55, 80, 73}
    Dim result As String = String.Empty
    For i As Integer = 0 To numbers.Length - 1
      result &= Char.ConvertFromUtf32(numbers(i))(0)
    Next
    Debug.Print(result)
    Stop

Verificato che funziona, aggiungiamo un metodo alla nostra FrmParametriConnessione:

  Private Function SKS() As String

    Dim numbers As Integer() = {97, 71, 73, 97, 48, 55, 80, 73}
    Dim result As String = String.Empty
    For i As Integer = 0 To numbers.Length - 1
      result &= Char.ConvertFromUtf32(numbers(i))(0)
    Next
    Return result

  End Function

E sostituiamo la riga che non ci convince:

  Private mSecretKeyString As String = SKS()

Teniamo sempre presente che quasi sicuramente la parte 'crittografia' va posta in una classe a parte. Forse non è del tutto scontato precisare che potremmo scegliere una chiave segreta come ci aggrada, basta che abbia al massimo otto caratteri. Qui ci si limita a fornire un modo semplice per ricrearla uguale ogni volta.
Altri campi sono:

  Private mServerPrec As String = String.Empty
  Private mUserIdPrec As String = String.Empty
  Private mPasswordPrec As String = String.Empty
  Private mDatiModificati As Boolean = False

Che servono da deposito per ripristinare i valori precedenti delle modifiche, nel caso l'utente le annulli. Le avvenute modifiche sono indicate da un apposito campo, il flag mDatiModificati, che viene posto a vero nella gestione dell'evento TextChanged delle tre TextBox relative a server, utente e password:

  Private Sub TextBox_TextChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) _
                                  Handles txtServer.TextChanged, txtUserID.TextChanged, _
                                  txtPassword.TextChanged
    Try
      mDatiModificati = True

    Catch ex As Exception
      Throw New ApplicationException(" " + mClassName + "." + _
                                     System.Reflection.MethodBase.GetCurrentMethod().Name, ex)
    End Try
  End Sub

Nella gestione dell'evento LostFocus, invece si presenta la nuova stringa di connessione, qualora ci siano tutte le informazioni (notare che, come per il metodo precedente, viene gestito l'evento per tutte e tre le TextBox):

  Private Sub TextBox_LostFocus(ByVal sender As Object, ByVal e As System.EventArgs) _
                                Handles txtServer.LostFocus, txtUserID.LostFocus, _
                                txtPassword.LostFocus
    Try
      If mDatiModificati = True AndAlso txtServer.Text.Trim <> String.Empty _
        AndAlso txtUserID.Text.Trim <> String.Empty _
        AndAlso txtPassword.Text.Trim <> String.Empty Then

        'Compongo la nuova stringa di connessione
        txtStringaConN.Text = String.Format(FMT_ConnectionString, txtServer.Text.Trim, _
                                            txtUserID.Text.Trim, txtPassword.Text.Trim)
      End If

    Catch ex As Exception
      Throw New ApplicationException(" " + mClassName + "." + _
                                     System.Reflection.MethodBase.GetCurrentMethod().Name, ex)
    End Try
  End Sub

I dati, come si può facilmente intuire, vengono letti al caricamento della form e depositati negli appositi campi:

  Private Sub FrmParametriConnessione_Load(ByVal sender As System.Object, _
                                           ByVal e As System.EventArgs) _
                                           Handles MyBase.Load
    'LA FORM VA APERTA MODALE
    Try
      'Tolgo la crittografia al file dei parametri di connessione e ricavo i parametri
      txtServer.Text = DeserializeConnString("Server")
      If txtServer.Text = "File non creato" Then
        txtServer.Text = String.Empty
        Exit Sub
      End If
      txtUserID.Text = DeserializeConnString("UserID")
      txtPassword.Text = DeserializeConnString("Password")

      mServerPrec = txtServer.Text
      mUserIdPrec = txtUserID.Text
      mPasswordPrec = txtPassword.Text

      mDatiModificati = False

      'Valorizzo e visualizzo la stringa di connessione del DB principale
      txtStringaConP.Text = String.Format(FMT_ConnectionString, txtServer.Text, txtUserID.Text, _
                                          txtPassword.Text)

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

[Diego] Può sembrare una questione di lana caprina, visto che alla fin fine si giunge allo stesso preciso riultato, ma io invertirei il movimento tra caselle di testo e campi:

  Private Sub FrmParametriConnessione_Load(ByVal sender As System.Object, _
                                           ByVal e As System.EventArgs) _
                                           Handles MyBase.Load
    'LA FORM VA APERTA MODALE
    Try
      'Tolgo la crittografia al file dei parametri di connessione e ricavo i parametri
      mServerPrec = DeserializeConnString("Server")
      If mServerPrec = "File non creato" Then
        mServerPrec = String.Empty
        Exit Sub
      End If
      mUserIdPrec = DeserializeConnString("UserID")
      mPasswordPrec = DeserializeConnString("Password")

      'scrivo i parametri nelle caselle
      txtServer.Text = mServerPrec
      txtUserID.Text = mUserIdPrec
      txtPassword.Text = mPasswordPrec
      'in seguito al cambiamento viene abbassato il flag, quindi lo ripristino
      mDatiModificati = False

      'Valorizzo e visualizzo la stringa di connessione del DB principale
      txtStringaConP.Text = String.Format(FMT_ConnectionString, txtServer.Text, _
                                          txtUserID.Text, txtPassword.Text)

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

[Oscar] In fase di chiusura verifico se sono state apportate modifiche e in tal caso chiedo se debbano essere salvate:

  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 Messaggi.SiNo(WAR_SaveOnExit, "Salvataggio parametri") = DialogResult.Yes Then
          Salva()
        End If
      End If
    End If

    'Chiudo l'applicazione
    Application.Exit()
  End Sub

[Diego] Questo codice contiene l'errore di chiudere brutalmente l'applicazione dall'interno di una from di dialogo, addirittura prima della sua chiusura. E' un errore cui porremo riparo in seguito. Vediamo il resto del codice, che gestisce il clic sui pulsanti presenti nella tbrStrumenti:

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

  Private Sub btnConferma_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
                                Handles btnConferma.Click
    Try
      If Messaggi.SiNo(WAR_Salva, "Salvataggio parametri") = DialogResult.Yes Then
        Salva()
        Messaggi.Info(WAR_Salvato, "Salvataggio parametri")

        mServerPrec = txtServer.Text.Trim
        mUserIdPrec = txtUserID.Text.Trim
        mPasswordPrec = txtPassword.Text.Trim
      End If
    Catch ex As Exception
      Messaggi.Errore(mClassName, System.Reflection.MethodBase.GetCurrentMethod(), ex)
    End Try
  End Sub

Dopo avere salvato, deposito nei campi i nuovi valori appena impostati per gestire correttamente eventuali nuove modifiche apportate dall'utente prima della chiusura della form. Faccio invece il contrario, se l'utente fa clic sul pulsante Annulla:

  Private Sub btnAnnulla_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
                               Handles btnAnnulla.Click
    Try
      txtServer.Text = mServerPrec
      txtUserID.Text = mUserIdPrec
      txtPassword.Text = mPasswordPrec

      'Valorizzo e visualizzo la stringa di connessione del DB principale
      txtStringaConP.Text = String.Format(FMT_ConnectionString, mServerPrec, mUserIdPrec, _
                                          mPasswordPrec)
      txtStringaConN.Text = ""

      mDatiModificati = False

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

Nella gestione dell'evento clic sul pulsante di test della connessione, uso il metodo appositamente preparato della classe SqlHelper:

  Private Sub btnTestaCon_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
                                Handles btnTestaCon.Click
    Try
      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, "Test connessione", txtServer.Text), _
                      "Risultato test")
      Else
        Messaggi.Avviso(String.Format(FMP_ConnectionKo, "Test connessione", txtServer.Text), _
                        "Risultato test")
      End If

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

Come in fase di caricamento i dati sono stati letti attraverso un metodo di deserializzazione/decrittazione, in fase di salvataggio essi vengono scritti tramite un metodo di serializzazione/crittazione, nel metodo Salva, chiamato da vari punti del codice fin qui prodotto:

  Private Sub Salva()
    Try
      'Controllo che i campi Nome Server, User ID e Password siano tutti valorizzati
      If txtServer.Text.Trim = String.Empty OrElse txtUserID.Text.Trim = String.Empty _
         OrElse txtPassword.Text.Trim = String.Empty Then

        Messaggi.Errore("I campi Nome Server, User ID e Password devono essere tutti valorizzati!", _
                        "Attenzione")
        If txtServer.Text.Trim = String.Empty Then
          txtServer.Focus()
        ElseIf txtUserID.Text.Trim = String.Empty Then
          txtUserID.Focus()
        ElseIf txtPassword.Text.Trim = String.Empty Then
          txtPassword.Focus()
        End If
        Exit Sub
      End If

      'Imposto la crittografia al file dei parametri di connessione e lo salvo
      SerializeConnString(txtServer.Text.Trim, txtUserID.Text.Trim, txtPassword.Text.Trim)

      'Compongo le nuove stringhe di connessione e creo la connessione
      txtStringaConP.Text = String.Format(FMT_ConnectionString, txtServer.Text.Trim, _
                                          txtUserID.Text.Trim, txtPassword.Text.Trim)
      SqlHelper.ConnessioneDatabase = New SqlConnection(txtStringaConP.Text)

      mDatiModificati = False

    Catch ex As Exception
      Throw New ApplicationException(" " + mClassName + "." + _
                                     System.Reflection.MethodBase.GetCurrentMethod().Name, ex)
    End Try
  End Sub

[Diego] Il codice per i metodi di scrittura e lettura dei dati crittografati in file XML è stato trovato e adattato per i nostri scopi. E' stato inserito in questa form e anche altrove. Questa è già di per sé sola una buona ragione per fare una classe a parte, ma ce n'è un'altra più buona ancora: questo spostamento ci permetterà di riparare all'errata uscita dall'applicazione.

Con cosa hanno a che fare questi metodi? Con SqlServer? No, sono i contenuti che hanno a che fare con SqlServer. Con dei file? Sì, si tratta di scrivere un file, il cui nome ci viene fornito da un metodo della classe GestoreFiles. Però non è detto che queste funzionalità ci siano sempre necessarie quando abbiamo a che fare con dei file. Quindi abbiamo individuato in APP.Base il progetto cui aggiungere una nuova classe, di nome Crittografia. A parte la logica, questa collocazione è imposta anche dalla necessità di usare un metodo della classe GestoreFiles, perché non è visibile dalle altre librerie accessorie.

La classe Crittografia
Aggiungiamo una classe al progetto APP.Base, denominandola Crittografia. Trasferiamo nel suo codice le direttive necessarie:

Imports System.IO
Imports System.Security
Imports System.Security.Cryptography
Imports System.Text
Imports System.Xml

E anche i campi privati:

  Private Shared ReadOnly mClassName As String = _
                                    System.Reflection.MethodBase.GetCurrentMethod.ReflectedType.Name
  Private Shared mSecretKeyString As String = SKS()
  Private Shared ReadOnly mAppFileName As String = _
                                    GestoreFiles.CreaNomeFileConfig(My.Application.Info.AssemblyName)

Nonché il metodo che ri-crea la chiave segreta:

  Private Shared Function SKS() As String

    Dim numbers As Integer() = {97, 71, 73, 97, 48, 55, 80, 73}
    Dim result As String = String.Empty
    For i As Integer = 0 To numbers.Length - 1
      result &= Char.ConvertFromUtf32(numbers(i))(0)
    Next
    Return result

  End Function

Avrete già notato che tutti i membri sono Shared: anche questa è una classe statica.
Questi sono i metodi esposti (cioè Public) dalla classe, per leggere e scrivere il file dei parametri di connessione:

  Public Shared Function DeserializeConnString(ByVal nodo As String) As String
    Try
      Dim retValue As String = ""
      Dim cryptoServiceProvider As New DESCryptoServiceProvider
      cryptoServiceProvider.Key() = ASCIIEncoding.ASCII.GetBytes(mSecretKeyString)
      cryptoServiceProvider.IV = ASCIIEncoding.ASCII.GetBytes(mSecretKeyString)
      Dim fileReader As FileStream = New FileStream(mAppFileName, FileMode.Open)
      Dim cryptoReader As CryptoStream = New CryptoStream(fileReader, _
                                                          cryptoServiceProvider.CreateDecryptor, _
                                                          CryptoStreamMode.Read)
      Dim xmlReader As Xml.XmlTextReader = New Xml.XmlTextReader(cryptoReader)
      Dim xmlDoc As Xml.XmlDocument = New Xml.XmlDocument

      xmlDoc.Load(xmlReader)

      xmlReader.Close()
      cryptoReader.Close()
      fileReader.Close()

      Try
        Select Case nodo
          Case "Server"
            retValue = xmlDoc.SelectSingleNode("/ConnectionStringParameters/Server").InnerText
          Case "UserID"
            retValue = xmlDoc.SelectSingleNode("/ConnectionStringParameters/UserID").InnerText
          Case "Password"
            retValue = xmlDoc.SelectSingleNode("/ConnectionStringParameters/Password").InnerText
          Case Else
            retValue = ""
        End Select

      Catch ex As NullReferenceException
        Throw New NullReferenceException("Parametro " & nodo & _
                                         " mancante nel file parametri di connessione" & _
                                         Environment.NewLine & "Contattare l'assistenza")
      End Try

      Return retValue

    Catch ex As FileNotFoundException
      Return "File non creato"
    Catch ex As Exception
      Throw New ApplicationException(" " + mClassName + "." + _
                                     System.Reflection.MethodBase.GetCurrentMethod().Name, ex)
    End Try
  End Function

  Public Shared Sub SerializeConnString(ByVal server As String, ByVal userId As String, _
                                        ByVal password As String)
    Try
      Dim cryptoServiceProvider As New DESCryptoServiceProvider
      cryptoServiceProvider.Key() = ASCIIEncoding.ASCII.GetBytes(mSecretKeyString)
      cryptoServiceProvider.IV = ASCIIEncoding.ASCII.GetBytes(mSecretKeyString)
      Dim fileWriter As FileStream = New FileStream(mAppFileName, FileMode.Create)
      Dim cryptoWriter As CryptoStream = New CryptoStream(fileWriter, _
                                                          cryptoServiceProvider.CreateEncryptor, _
                                                          CryptoStreamMode.Write)
      Dim xmlWriter As XmlTextWriter = New XmlTextWriter(cryptoWriter, System.Text.Encoding.UTF8)

      xmlWriter.WriteStartDocument()
      xmlWriter.WriteStartElement("ConnectionStringParameters")
      xmlWriter.WriteElementString("Server", server)
      xmlWriter.WriteElementString("UserID", userId)
      xmlWriter.WriteElementString("Password", password)
      xmlWriter.WriteEndElement()
      xmlWriter.WriteEndDocument()

      xmlWriter.Close()
      cryptoWriter.Close()
      fileWriter.Close()

    Catch ex As Exception
      Throw New ApplicationException(" " + mClassName + "." + _
                                     System.Reflection.MethodBase.GetCurrentMethod().Name, ex)
    End Try
  End Sub

Nella form FrmParametriConnessione, queste sono le nuove direttive Imports:

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

Dai campi sono stati tolti mSecretKeyString e mAppFileName, e il metodo SKS.

Conclusione
Non è finita qui. Dobbiamo ancora eliminare l'errore dell'uscita dall'applicazione, ma lo risolveremo nella prossima puntata.
Naturalmente, se per caso l'esposizione del procedimento di programmazione vi ha confuso, potete rileggere l'articolo, confrontando il prima e il dopo, aiutandovi con il sorgente, scaricabile come al solito dall'area Download.
Da questa puntata in avanti, pubblicherò sul mio blog un post 'companion' per ciascuna puntata, al quale potrete aggiungere i vostri feedback (critiche, suggerimenti, richieste di chiarimento).