Guarda! Senza mani! Sesta parte
a cura di Sabrina Cosolo e Diego Cattaruzza (requisiti: conoscenza intermedia di Sql Server e di .Net)Fra poco...
Finalmente iniziamo a costruire una form che farà parte dell'applicazione vera e propria: questa form deve permettere di visualizzare e modificare i setting utente o i setting applicativi da noi predisposti all'interno della classe UsingSqlServerConfig, di salvarli su disco e di testare la connessione a database.ma prima...
Come sempre, verificando quel che ci serve per la gestione della form, abbiamo bisogno di qualche funzionalità ausiliaria, pertanto dovremo modificare o aggiungere classi nelle librerie. Le funzioni di cui abbiamo bisogno sono le seguenti:
- Una funzione di conferma, la classica messagebox con pulsanti "Si/No"
- Una funzione di test della connessione
Per la prima funzione, ce la caviamo con poco: infatti andremo ad aggiungere un metodo all'interno della classe Warnings che abbiamo predisposto in TAndT.UI
public static DialogResult SiNo(string pMessage) { return MessageBox.Show(pMessage, "Conferma", MessageBoxButtons.YesNo, MessageBoxIcon.Question); }Public Shared Function SiNo(ByVal message As String) As DialogResult Return MessageBox.Show(message, "Conferma", MessageBoxButtons.YesNo, MessageBoxIcon.Question) End FunctionCome possiamo notare, anche in questo caso abbiamo semplicemente mappato la messagebox, lasciandoci solo la necessità di scrivere la domanda, sempre perché siamo pigri.
La classe SqlHelper
Per la seconda funzione, creeremo un'altra classe helper, la prima classe che utilizza SQL Server, andando ad inaugurare la libreria TAndT.Data, finora vuota. Selezioniamola nel Solution Explorer, click destro Add>Class> e generiamo la classe SqlHelper, una nuova classe statica.using System; using System.Collections.Generic; using System.Text; using System.Data.SqlClient;Imports System Imports System.Collections.Generic Imports System.Text Imports System.Data.SqlClientNelle direttive, oltre a quelle standard, abbiamo aggiunto System.Data.SqlClient.
public class SqlHelper { private static readonly string mClassName = System.Reflection.MethodBase.GetCurrentMethod().ReflectedType.Name; }Public Class SqlHelper Private Shared ReadOnly mClassName As String = _ System.Reflection.MethodBase.GetCurrentMethod.ReflectedType.Name End ClassLa struttura base della classe, ricordo a chi scrive in VB a cui il namespace non compare, di verificare che il Root Namespace sia corretto (nella zona My.Project, oppure nella visualizzazione classi)
public static bool TestConnection(string pCnString) { bool retResult = false; try { SqlConnection cn = new SqlConnection(); cn.ConnectionString = pCnString; cn.Open(); cn.Close(); retResult = true; } catch (Exception) { retResult = false; } return (retResult); }Public Shared Function TestConnection(ByVal cnString As String) As Boolean Dim retResult As Boolean = False Try Dim cn As New SqlConnection cn.ConnectionString = cnString cn.Open() cn.Close() retResult = True Catch ex As Exception retResult = False End Try Return retResult End FunctionIl nostro primo metodo che lavora con i database, un metodo molto semplice, che testa la connessione a database semplicemente aprendola e chiudendola, e restituisce la riuscita o il fallimento.
Io non ho ancora trovato un metodo più elegante, anche se sono certa che c'è il modo per verificare che il SQL Server esista e sia raggiungibile, di certo, per quanto sia un po' lenta, questa funzione è quella più facile da applicare. Dico che è lenta perché chiaramente se la connessione è sbagliata deve aspettare il timeout per dare errore. Chi sa un modo migliore, faccia un fischio e ci dia il link al suo articolo, al suo blog, o ci mandi il codice che lo inseriamo e lo citiamo, oltre a ringraziarlo sentitamente.La classe FrmConfig
Aggiungiamo una form al progetto UsingSqlServer (tasto destro Add>Windows Forms>), chiamiamola FrmConfig e, dopo averla generata, apriamola nel designer, assegnamo "Configura Setting" alla sua Text.
Dentro, ci disegnamo:
- Un ToolStrip che si chiama tbrTools (tbr=toolbar)
- Nella tbrTools inseriamo tre bottoni che chiameremo tooSalva, tooAnnulla, tooTestConnessione, e scriveremo gli stessi nomi senza il prefisso nelle loro rispettive proprietà Text.
- Assegnamo ai toolstripbutton tre icone significative, quelle delle immagini le trovate nel progetto scaricabile in area download, nella cartella UsingSqlServer\Resources, e sono concesse in uso da For You R.& D.
Potete aggiungere come immagini al file Resources.resx e le assegnate ai bottoni tramite la proprietà Image.- Un TabControl che si chiama tbc (no, non è contagioso), con Dock impostata a Fill
- Inseriamo due TabPage o modifichiamo quelle esistenti e le chiamiamo tbpApp e tbpUser, con Text rispettivamente "Setting Applicazione" e "Setting Utente".
- Nella tbpApp inseriamo una DataGridView che chiamiamo dgApp
- Nella tbpUser inseriamo una DataGridView che chiamiamo dgUser
- Impostiamo per entrambe le DataGridView la proprietà Dock a Fill e assegnamo a ciascuna un colore di sfondo (BackgroundColor) diverso da quello grigio di default, così le riconosciamo
Disegnata la form, vediamo che cosa metteremo nella partial class .cs o nella class .vb (per VB è partial class solo la parte designer, per C# sono entrambe partial class, come è più giusto, ma non stiamo a disquisire):
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using TAndT.Base.Collections; using TAndT.UI; using TAndT.Data;Imports System Imports System.Collections Imports System.Data Imports System.Drawing Imports System.Text Imports System.Windows.Forms Imports TAndT.Base.Collections Imports TAndT.UI Imports TAndT.DataLe direttive using/Imports: oltre ad alcuni namespace del Framework, inseriamo quelli delle nostre librerie, ove si trovano membri che utilizzeremo nella form. Naturalmente, abbiamo anche aggiunto TAndT.Data alle References di UsingSqlServer.
namespace TAndT.UsingSqlServer { public partial class FrmConfig : Form { private static readonly string mClassName = System.Reflection.MethodBase.GetCurrentMethod().ReflectedType.Name; } }Public Class FrmConfig Private Shared ReadOnly mClassName As String = _ System.Reflection.MethodBase.GetCurrentMethod.ReflectedType.Name End ClassLa struttura base della classe, come sempre non cambia rispetto alle classi che non sono parte della User Interface.
private const string FMP_ConnectionKo = "Impossibile connettersi al database {0} su server {1}"; private const string FMP_ConnectionOk = "Connessione a database {0} su server {1} OK"; private const string TBL_ApplicationSettings = "ApplicationSettings"; private const string TBL_UserSettings = "UserSettings"; private const string TXT_Nome = "Nome"; private const string TXT_Valore = "Valore"; private const string WAR_DatiModificati = "I dati sono stati modificati, si desidera salvare " & "la configurazione prima di testare la connessione?"; private const string WAR_SaveOnExit = "Ci sono state modifiche ai dati, si desidera salvarle prima di uscire"; private const string WAR_Salva = "Salvo i settings?"; private const string WAR_Salvato = "Setting salvati con successo.";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 TBL_ApplicationSettings As String = "ApplicationSettings" Private Const TBL_UserSettings As String = "UserSettings" Private Const TXT_Nome As String = "Nome" Private Const TXT_Valore As String = "Valore" 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 settings?" Private Const WAR_Salvato As String = "Setting salvati con successo."Le costanti sopraelencate rappresentano varie stringhe che useremo nella form. I loro nomi sono contrassegnati da particolari prefissi:
- FMP sono messaggi con parametri, pertanto la sigla sta per "ForMat Pattern".
- TBL sono i nomi delle dataTaBLe che forniranno i dati alle datagrid
- TXT sono le intestazioni di colonna
- WAR sono gli avvisi (WARning) e le richieste di conferma senza parametri.
Usare costanti (come fatto già nella puntata precedente) è uno dei due metodi per fare in modo che le stringhe fisse di applicazione si trovino tutte in uno stesso luogo, onde facilitarne la gestione. E' anche un buon modo per dare più lavoro al compilatore e meno lavoro all'applicazione. Se prevedete di tradurre i vostri programmi in più lingue, usate invece i file di risorse come resources.resx, dove abbiamo messo anche le immagini delle icone. Nel corso dello sviluppo del codice relativo a questa serie di articoli, useremo un po' le costanti nel codice, un po' le risorse, a seconda di come ci sembra meglio fare in base al contesto.
bool mDataModified = false; DataTable mDtApp; DataTable mDtUser;Private mDataModified As Boolean = False Private mDtApp As DataTable Private mDtUser As DataTableAbbiamo tre campi in questa classe, un flag per segnarci se ci sono state modifiche nei dati e due DataTable, che alimenteranno le datagrid.
public FrmConfig() { InitializeComponent(); mDtApp = null; mDtUser = null; }Public Sub New() InitializeComponent() mDtApp = Nothing mDtUser = Nothing End SubIl costruttore, ove non facciamo nulla di speciale, salvo indicare che all'inizio le DataTable sono nulle.
In fase di caricamento della form, vogliamo caricare le nostre DataTable nelle nostre DataGrid, delle quali dobbiamo curare la formattazione. Inoltre, vogliamo gestire le eventuali modifiche ai dati con un solo gestore di evento.
Quindi prepariamo due metodi:private void ConfiguraDataGrid() { try { dgApp.AllowUserToAddRows = false; dgApp.AllowUserToDeleteRows = false; dgApp.AllowUserToResizeColumns = false; dgApp.AllowUserToResizeRows = false; dgUser.AllowUserToAddRows = false; dgUser.AllowUserToDeleteRows = false; dgUser.AllowUserToResizeColumns = false; dgUser.AllowUserToResizeRows = false; DataGridViewColumn col = this.dgApp.Columns[TAndTSettings.FLD_NAME]; col.ReadOnly = true; col.HeaderText = TXT_Nome; col.Width = 100; col = this.dgApp.Columns[TAndTSettings.FLD_VALUE]; col.HeaderText = TXT_Valore; col.Width = 250; col = this.dgUser.Columns[TAndTSettings.FLD_NAME]; col.HeaderText = TXT_Nome; col.Width = 100; col = this.dgUser.Columns[TAndTSettings.FLD_VALUE]; col.HeaderText = TXT_Valore; col.Width = 250; } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name, ex); } }Private Sub ConfiguraDataGrid() Try dgApp.AllowUserToAddRows = False dgApp.AllowUserToDeleteRows = False dgApp.AllowUserToResizeColumns = False dgApp.AllowUserToResizeRows = False dgUser.AllowUserToAddRows = False dgUser.AllowUserToDeleteRows = False dgUser.AllowUserToResizeColumns = False dgUser.AllowUserToResizeRows = False Dim col As DataGridViewColumn = dgApp.Columns(TAndTSettings.FLD_NAME) col.ReadOnly = True col.HeaderText = TXT_Nome col.Width = 100 col = dgApp.Columns(TAndTSettings.FLD_VALUE) col.HeaderText = TXT_Valore col.Width = 250 col = dgUser.Columns(TAndTSettings.FLD_NAME) col.HeaderText = TXT_Nome col.Width = 100 col = dgUser.Columns(TAndTSettings.FLD_VALUE) col.HeaderText = TXT_Valore col.Width = 250 Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End SubIl metodo ConfiguraDatagrid, una volta assegnate le DataSource (le nostre DataTable) alle due DataGrid, si occupa di formattare le colonne e di verificare che l'utente possa modificare solo i dati che decidiamo noi. Ci siamo limitati a qualcosa di semplice, ma con un po' di fantasia e curiosando negli oggetti della DataGrid sicuramente si possono fare molte cose simpatiche.
void DataChangesHandler(object sender, DataColumnChangeEventArgs e) { try { this.mDataModified = true; } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name, ex); } }Private Sub DataChangeHandler(ByVal sender As Object, ByVal e As DataColumnChangeEventArgs) Try mDataModified = True Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End SubL'event handler della modifica dati su un campo delle DataTable fa una cosa molto semplice, pone a true il flag mDataModified. Gli eventi della DataTable sono un buon posto per gestire varie cose, oltre al segnalino che eviterà la chiusura senza salvataggio: anche una validazione dei dati, ad esempio. Infatti, se curiosate dentro ai DataColumnChangeEventArgs vi troverete vari oggetti interessanti. Ovviamente vi sono vari eventi che hanno lo scopo di aiutarci in questo anche sulla DataGridView e probabilmente li vedremo nei futuri articoli.
private void FrmConfig_Load(object sender, EventArgs e) { try { mDtApp = UsingSqlServerConfig.AppSettings.GetDataTable(TBL_ApplicationSettings); mDtApp.AcceptChanges(); mDtApp.ColumnChanged += new DataColumnChangeEventHandler(DataChangesHandler); mDtUser = UsingSqlServerConfig.UserSettings.GetDataTable(TBL_UserSettings); mDtApp.AcceptChanges(); mDtUser.ColumnChanged += new DataColumnChangeEventHandler(DataChangesHandler); this.dgApp.DataSource = mDtApp; this.dgUser.DataSource = mDtUser; ConfiguraDataGrid(); } catch (Exception ex) { Warnings.Errore(mClassName, System.Reflection.MethodBase.GetCurrentMethod(), ex); } }Private Sub FrmConfig_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load Try mDtApp = UsingSqlServerConfig.AppSettings.GetDataTable(TBL_ApplicationSettings) mDtApp.AcceptChanges() AddHandler mDtApp.ColumnChanged, AddressOf DataChangeHandler mDtUser = UsingSqlServerConfig.UserSettings.GetDataTable(TBL_UserSettings) mDtApp.AcceptChanges() AddHandler mDtUser.ColumnChanged, AddressOf DataChangeHandler dgApp.DataSource = mDtApp dgUser.DataSource = mDtUser ConfiguraDataGrid() Catch ex As Exception Warnings.Errore(mClassName, System.Reflection.MethodBase.GetCurrentMethod(), ex) End Try End SubNella gestione dell'evento Load della FrmConfig carichiamo le nostre DataTable fissando i dati con un AcceptChanges e assegnamo all'evento ColumnChanged il gestore precedentemente implementato. Inoltre assegnamo alle DataGrid le tabelle come DataSource e lanciamo il metodo ConfiguraDataGrid.
Il salvataggio dei dati è opportuno sia fatto non solo manualmente, tramite il metodo che gestisce il click sul pulsante tooSalva, ma anche in modo automatico, dal metodo che controlla la connessione al database, perché la connection string viene composta usando i setting eventualmente appena modificati. Quindi implementiamo un metodo a parte.
private void Salva() { try { UsingSqlServerConfig.AppSettings.SetFromDataTable(mDtApp, false); UsingSqlServerConfig.UserSettings.SetFromDataTable(mDtUser, false); UsingSqlServerConfig.SaveSettings(); UsingSqlServerConfig.LoadSettings(); this.mDataModified = false; } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name, ex); } }Private Sub Salva() Try UsingSqlServerConfig.AppSettings.SetFromDataTable(mDtApp, False) UsingSqlServerConfig.UserSettings.SetFromDataTable(mDtUser, False) UsingSqlServerConfig.SaveSettings() UsingSqlServerConfig.LoadSettings() mDataModified = False Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End SubIl metodo di salvataggio dati aggiorna le collection sulla classe dei setting, salva i dati e per sicurezza ricarica i dati salvati, impostando a false il flag mDataModified.
private void tooSalva_Click(object sender, EventArgs e) { try { this.dgApp.EndEdit(); this.dgUser.EndEdit(); if (Warnings.SiNo(WAR_Salva) == DialogResult.Yes) { Salva(); Warnings.Info(WAR_Salvato); } } catch (Exception ex) { Warnings.Errore(mClassName, System.Reflection.MethodBase.GetCurrentMethod(), ex); } }Private Sub tooSalva_Click(ByVal sender As Object, ByVal e As System.EventArgs) _ Handles tooSalva.Click Try dgApp.EndEdit() dgUser.EndEdit() If Warnings.SiNo(WAR_Salva) = DialogResult.Yes Then Salva() Warnings.Info(WAR_Salvato) End If Catch ex As Exception Warnings.Errore(mClassName, System.Reflection.MethodBase.GetCurrentMethod(), ex) End Try End SubIl bottone tooSalva, molto semplicemente, salva gli eventuali dati non ancora aggiornati sulle due DataGrid, chiede conferma e chiama la funzione di salvataggio dati. Se tutto va bene, viene mostrato il messaggio di salvataggio effettuato.
private void tooAnnulla_Click(object sender, EventArgs e) { this.dgApp.EndEdit(); this.dgUser.EndEdit(); this.mDtApp.RejectChanges(); this.mDtUser.RejectChanges(); this.mDataModified = false; }Private Sub tooAnnulla_Click(ByVal sender As Object, ByVal e As System.EventArgs) _ Handles tooAnnulla.Click dgApp.EndEdit() dgUser.EndEdit() mDtApp.RejectChanges() mDtUser.RejectChanges() mDataModified = False End SubIl bottone tooAnnulla termina le modifiche effettuate sulle DataGrid, per prassi, poi annulla le modifiche alle due DataTable, ripristinando quanto caricato al caricamento della form o dopo l'ultimo uso del tasto tooSalva, infine ripristina il flag mDataModified.
private void tooTestConnessione_Click(object sender, EventArgs e) { try { this.dgApp.EndEdit(); this.dgUser.EndEdit(); if (this.mDataModified) { if (Warnings.SiNo(WAR_DatiModificati) == DialogResult.Yes) { Salva(); } } if (SqlHelper.TestConnection(UsingSqlServerConfig.CnString)) { Warnings.Info( string.Format(FMP_ConnectionOk, UsingSqlServerConfig.SqlDatabase, UsingSqlServerConfig.SqlServer)); } else { Warnings.Avviso( string.Format(FMP_ConnectionKo, UsingSqlServerConfig.SqlDatabase, UsingSqlServerConfig.SqlServer)); } } catch (Exception ex) { Warnings.Errore(mClassName, System.Reflection.MethodBase.GetCurrentMethod(), ex); } }Private Sub tooTestConnessione_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles tooTestConnessione.Click Try dgApp.EndEdit() dgUser.EndEdit() If mDataModified Then If Warnings.SiNo(WAR_DatiModificati) = DialogResult.Yes Then Salva() End If End If If SqlHelper.TestConnection(UsingSqlServerConfig.CnString) Then Warnings.Info(String.Format(FMP_ConnectionOk, UsingSqlServerConfig.SqlDatabase, _ UsingSqlServerConfig.SqlServer)) Else Warnings.Avviso(String.Format(FMP_ConnectionKo, UsingSqlServerConfig.SqlDatabase, _ UsingSqlServerConfig.SqlServer)) End If Catch ex As Exception Warnings.Errore(mClassName, System.Reflection.MethodBase.GetCurrentMethod(), ex) End Try End SubIl tasto di controllo della connessione verifica ed eventualmente salva automaticamente le modifiche, poi esegue un test della connessione utilizzando i setting 'attuali' e il metodo helper precedentemente implementato.
private void FrmConfig_FormClosing(object sender, FormClosingEventArgs e) { if (e.CloseReason != CloseReason.ApplicationExitCall && e.CloseReason != CloseReason.WindowsShutDown && e.CloseReason != CloseReason.TaskManagerClosing) { if (this.mDataModified) { if (Warnings.SiNo(WAR_SaveOnExit) == DialogResult.Yes) { e.Cancel = true; } } } }Private Sub FrmConfig_FormClosing(ByVal sender As Object, _ ByVal e As System.Windows.Forms.FormClosingEventArgs) _ Handles Me.FormClosing If Not (e.CloseReason = CloseReason.ApplicationExitCall _ OrElse e.CloseReason = CloseReason.WindowsShutDown _ OrElse e.CloseReason = CloseReason.TaskManagerClosing) Then If mDataModified Then If Warnings.SiNo(WAR_SaveOnExit) = DialogResult.Yes Then e.Cancel = True End If End If End If End SubIl controllo di chiusura della form verifica che la chiusura non sia richiesta tramite una di tre possibilità, che i dati siano da salvare, e che si vogliano salvare, nel qual caso annulla la chiusura.
La FrmMain
Vediamo ora le modifiche alla form MDI per gestire la nuova form. Aggiungiamo sul MenuStrip un nuovo menu, che chiamiamo "Strumenti" (ci viene generato un menuitem strumentiToolStripMenuItem, nella cui Text aggiungiamo un & prima della S), entriamo nel menu e aggiungiamo un'opzione "Configura" (il che ci genera un configuraToolStripMenuItem, nella cui Text aggiungiamo un & prima della C). Facciamoci doppio click e generiamo il metodo di gestione del click dell'opzione Configura.
In questo metodo vogliamo verificare se la form è già aperta: se non lo è, la apriamo; altrimenti la portiamo in primo piano. Quindi prepariamo un metodo a questo scopo:private Form VediSeIlFormEAperto(string pName) { try { Form retForm = null; foreach (Form child in this.MdiChildren) { if (child.Name == pName) { retForm = child; break; } } return (retForm); } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name, ex); } }Private Function VediSeIlFormEAperto(ByVal name As String) As Form Try Dim retForm As Form = Nothing For Each child As Form In Me.MdiChildren If child.Name = name Then retForm = child Exit For End If Next Return retForm Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End FunctionIl metodo per il controllo dei form aperti è molto semplice: scorre i nomi delle form figlie dell'MDI.
Ovviamente non è il solo modo né il più efficiente; magari in un prossimo futuro adotteremo una soluzione più professionale, ma, per ora e per i nostri limitati scopi, possiamo accontentarcene.private void configuraToolStripMenuItem_Click(object sender, EventArgs e) { FrmConfig frm = (FrmConfig)VediSeIlFormEAperto("FrmConfig"); if (frm == null) { frm = new FrmConfig(); frm.Icon = this.Icon; frm.MdiParent = this; frm.Show(); } else { frm.WindowState = FormWindowState.Normal; frm.BringToFront(); } }Private Sub ConfiguraToolStripMenuItem_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles ConfiguraToolStripMenuItem.Click Dim frm As FrmConfig = DirectCast(VediSeIlFormEAperto("FrmConfig"), FrmConfig) If frm Is Nothing Then frm = New FrmConfig frm.Icon = Me.Icon frm.MdiParent = Me frm.Show() Else frm.WindowState = FormWindowState.Normal frm.BringToFront() End If End SubCome già detto, facciamo in modo di avere una sola FrmConfig aperta: o una nuova, o quella già aperta.
Arrivederci alla prossima puntata
Bene, se non abbiamo dimenticato qualcosa, ora abbiamo gli strumenti (sia pure rozzi) per iniziare a vedere le cose più interessanti riguardo SQL Server 2005 e ADO.NET.
Nella prossima puntata, inizieremo ad introdurre le strutture che ci servono per generare un database.
Come al solito, il codice fin qui prodotto è scaricabile dall'Area Download.
(nota: il codice è 'sempre' aggiornato, con eventuali correzioni al codice precedente, non citate nell'articolo).
Potete scrivere Feedback (commenti, critiche, suggerimenti, correzioni) sul blog di Sabrina.