Press "Enter" to skip to content

19 Lavorare con i dati La classe di gestione Setting

19 Lavorare con i dati La classe di gestione Setting

In questo articolo Implementiamo la classe Setting che abbiamo definito nell’articolo precedente adattata per le esigenze dellee una semplice Window per modificarne il contenuto.

La nostra applicazione UsersDb, ha 2 parametri principali che possono variare in base alle scelte dell’utente, il tipo di database a cui connettersi e la stringa di connessione.

Essendo un progetto didattico, proviamo a creare sia i setting a livello applicativo che a livello utente, pertanto utilizzeremo 2 DnwSettingsCollection, una che darà origine ad un file memorizzato su Program Data, l’altra che darà origine ad un file memorizzato su AppData.

La classe AppSettingsManager.cs

Questa classe fornisce il necessario a gestire tutti i parametri applicativi, verrà istanziata dalla classe AppContext fornendo all’applicazione il necessario a collegarsi al database e a quale tipo di database collegarsi. Vediamo nel dettaglio come è fatta.

2017_04_23_01_AppSettingsManager[3]

Generiamo la classe nella cartella Context del progetto UsersDb.

using Dnw.Users.UICollections;
using Dnw.Users.UIEntities;
using Dnw.UsersServices;
using System;
using System.IO;
 
namespace Dnw.Users.Context
{
    public class AppSettingsManager
    {
...
    }
}

La classe non deriva da alcuna classe specifica e non implementa interfacce, in teoria non interagisce con la User Interface, anche se per amore della didattica e della semplicità in questo esempio ne editeremo il contenuto su una Window, ma nella realtà è opportuno costruire qualcosa di specifico per modificare i parametri applicativi e utente. Vi rimando agli articoli di questo blog dedicati allo sviluppo dell’interfaccia dinamica per la modifica dei parametri applicativi che trovate nella serie dedicata al MiniSqlAgent sulle categorie del Blog.

Per evitare di essere esageratamente dispersiva, non riporterò nell’articolo la definiziaone di tutte le stringhe costanti,  e delle variabili a livello di classe, se avete seguito qualcuno degli articoli di questo blog, probabilmente sapete già che per me definire le stringhe direttamente nel codice non è buona pratica, perché basta usarle 2 volte e sbagliare una lettera per creare possibili errori molto difficili da diagnosticare.

public AppSettingsManager()
{
    mUserSettingsFullName = Path.Combine(Environment.GetEnvironmentVariable(TXT_APPDATA), TXT_USRDB_PATH);
    CheckPath(mUserSettingsFullName);
    mUserSettingsFullName = Path.Combine(mUserSettingsFullName, TXT_USRSETTINGSFILE);
    mAppSettingsFullName = Path.Combine(Environment.GetEnvironmentVariable(TXT_PROGRAMDATA), TXT_USRDB_PATH);
    CheckPath(mAppSettingsFullName);
    mAppSettingsFullName = Path.Combine(mAppSettingsFullName, TXT_APPSETTINGSFILE);
    AppSettings = new DnwSettingsCollection();
    UsrSettings = new DnwSettingsCollection();
    LoadAppSettings();
    LoadUsrSettings();
}

Il metodo qui sopra è il costruttore della classe, in cui andiamo a comporre il nome completo di tutto il Path dei 2 file xml in cui memorizzeremo i parametri applicativi. Andremo anche a generare le sottocartelle se queste non esistessero.

Fate attenzione, nel mondo reale, non tutti gli utenti hanno il permesso di scrivere su ProgramData, ma solo gli Amministratori, pertanto è opportuno che la generazione dei settaggi applicativi sia fatta solo se l’utente ha privilegi amministrativi. Ci sono svariati metodi per verificarli e per testare la cosa. In questo caso, noi assumiamo che la prima configurazione sia fatta con uno startup dell’applicazione effettuato dall’amministratore di sistema dopo l’installazione.

Dopo la generazione delle cartelle e dei nomi dei 2 file di configurazione, vengono generate le 2 collection di tipo DnwSettingsCollection e vengono chiamati i 2 metodi che ne caricano il contenuto.

public DnwSettingsCollection AppSettings
{
    get
    {
        return mAppSettings;
    }
    private set
    {
        mAppSettings = value;
    }
}
 
public string AppSettingsFullName
{
    get
    {
        return mAppSettingsFullName;
    }
}
 
public string CnStringOleDb
{
    get
    {
        return (AppSettings[STT_CnStringOleDb].Value);
    }
    set
    {
        AppSettings[STT_CnStringOleDb].Value = value;
    }
}
 
public string CnStringSQLite
{
    get
    {
        return (AppSettings[STT_CnStringSQLite].Value);
    }
    set
    {
        AppSettings[STT_CnStringSQLite].Value = value;
    }
}
 
public string CnStringSQLServer
{
    get
    {
        return (AppSettings[STT_CnStringSQLServer].Value);
    }
    set
    {
        AppSettings[STT_CnStringSQLServer].Value = value;
    }
}
 
public DataSourceType SelectedDataSource
{
    get
    {
        DataSourceType val = DataSourceType.SqlServer;
        Enum.TryParse(UsrSettings[STT_SelectedDataSource].Value, out val);
        return (val);
    }
    set
    {
        UsrSettings[STT_SelectedDataSource].Value = value.ToString();
    }
}
 
public string UserSettingsFullName
{
    get
    {
        return mUserSettingsFullName;
    }
}
 
public DnwSettingsCollection UsrSettings
{
    get
    {
        return mUsrSettings;
    }
    private set
    {
        mUsrSettings = value;
    }
}

Qui sopra il codice che definisce le Property esposte all’applicazione dal nostro Settings Manager, oltre alle 2 property read only che espongono il nome del file dove si troveranno i nostri parametri applicativi, vi sono le property che rappresentano le tre stringhe di connessione ed il tipo di data source selezionato dall’utente. Potrete notare come, tutte le property espongono un Elemento specifico di una delle due collezioni, questo il motivo per cui abbiamo definito gli elementi con un Nome preciso.

Faccio notare come nel caso del SelectedDataSource, pur salvando una stringa che rappresenta uno dei valori enumerati, la property restituisce un elemento tipizzato, effettuando un controllo ed un Parse della stringa prima di restituirlo. Questo ci serve per evitare errori ed eccezioni se il file contenente i parametri venisse manipolato manualmente.

private void CheckPath(string path)
{
    if (!Directory.Exists(path))
    {
        Directory.CreateDirectory(path);
    }
}

Il metodo helper che genera le cartelle ove memorizzare i setting se non esistessero.

private void LoadAppSettings()
{
    //Generate all valid settings for the application wide settings
    DnwSetting stt = new DnwSetting();
    stt.ID = STT_CnStringOleDb;
    stt.Value = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=C:\\TestAccessDb\\UsersDb.accdb;Persist Security Info=True;";
    AppSettings.Add(stt);
    stt = new DnwSetting();
    stt.ID = STT_CnStringSQLite;
    stt.Value = "Data Source=C:\\TestSqliteDb\\UsersDb.sqlite;Version=3;";
    AppSettings.Add(stt);
    stt = new DnwSetting();
    stt.ID = STT_CnStringSQLServer;
    stt.Value = "Server=localhost;Database=UsersDb;Trusted_Connection=True;Persist Security Info=True";
    AppSettings.Add(stt);
 
    //If exists the setting file saved on disk
    //Read the file and update settings with saved values
    if (File.Exists(AppSettingsFullName))
    {
        DnwSettingsCollection savedSettings = DnwSettingsCollection.ReadXml(this.AppSettingsFullName);
        foreach (DnwSetting aps in AppSettings)
        {
            DnwSetting saved = savedSettings[aps.ID];
            if (saved != null)
            {
                aps.Value = saved.Value;
            }
        }
    }
}

Il metodo che carica i parametri di applicazione, spendo 2 parole sulla filosofia di questo metodo, i parametri applicativi sono un oggetto fortemente controllato dall’applicazione, pertanto il controllo al caricamento che io effettuo è un controllo Forte se posso definirlo così, infatti, non leggo semplicemente il contenuto del file XML, perché ci sono cento motivi diversi per cui potrebbe essere stato manomesso, rovinato eccetera. Quindi, per evitare di trovarsi parametri errati le mie classi agiscono in questo modo:

  1. per prima cosa generano tutti i parametri gestiti con un valore di default, in questo caso i 3 diversi valori per le stringhe di connessione, anche se ovviamente, in un progetto nel mondo reale, conterrebbero delle stringhe vuote.
  2. Leggono in una nuova e temporanea collection il contenuto del file xml dei setting e vanno ad aggiornare i soli valori letti sui parametri ammessi.

Perché questo strano modo di procedere?

  • Se durante lo sviluppo volessimo aggiungere o eliminare parametri, siamo certi che la struttura da noi generata è sempre quella corretta.
  • Se qualcuno avesse manipolato il file XML cambiandone il contenuto, aggiungendo valori non gestiti, rovinando dati, tutto questo non avrebbe alcun effetto sul funzionamento dell’applicazione, fatto salvo che le modifiche siano state fatte ai valori.
private void LoadUsrSettings()
{
    DnwSetting stt = new DnwSetting();
    stt.ID = STT_SelectedDataSource;
    stt.Value = DataSourceType.SqlServer.ToString();
    UsrSettings.Add(stt);
 
    //If exists the setting file saved on disk
    //Read the file and update settings with saved values
    if (File.Exists(UserSettingsFullName))
    {
        DnwSettingsCollection savedSettings = DnwSettingsCollection.ReadXml(this.UserSettingsFullName);
        foreach (DnwSetting uss in UsrSettings)
        {
            DnwSetting saved = savedSettings[uss.ID];
            if (saved != null)
            {
                uss.Value = saved.Value;
            }
        }
    }
}

Anche il metodo che carica i parametri di configurazione utente viene creato allo stesso modo del precedente, e con questo, abbiamo terminato la nostra classe.

Le modifiche ad AppContext.cs

Avendo introdotto il nuovo AppSettingsManager, andiamo a modificare di conseguenza anche l’AppContext, per utilizzare i nuovi parametri di configurazione.

Per prima cosa, eliminiamo da questo file le costanti con le stringhe di connessione, visto che ora le stringhe di connessione sono contenute nei parametri applicativi.

private static volatile AppContext mInstance;
 
private static object syncRoot = new Object();
 
private AppSettingsManager mSettingsManager;

Nelle variabili a livello di classe ora rimangono solo l’istanza singleton, il syncRoot per il controllo dei thread e aggiungiamo un istanza dell’AppSettingsManager.

public AppSettingsManager SettingsManager
{
    get
    {
        if (mSettingsManager == null)
        {
            mSettingsManager = new AppSettingsManager();
        }
        return mSettingsManager;
    }
    set
    {
        mSettingsManager = value;
    }
}

Creiamo una property che espone il nostro AppSettingsManager, anche se al momento non lo utilizzeremo in modo diretto nell’applicazione.

public string CnString
{
    get
    {
        switch (DataSource)
        {
            case DataSourceType.SqlServer:
                return SettingsManager.CnStringSQLServer;
 
            case DataSourceType.OleDb:
                return SettingsManager.CnStringOleDb;
 
            case DataSourceType.SQLite:
                return SettingsManager.CnStringSQLite;
 
            default:
                return null;
        }
    }
}

Infatti, per non modificare il codice già scritto nella nostra applicazione, modifichiamo semplicemente le proprietà che prima esponevano le costanti e la scelta dell’utente, quindi CnString, utilizzerà il tipo di datasource scelto dall’utente e restituirà una delle property esposte dal manager.

Se vi chiedete per quale motivo ho esposto delle property invece che un metodo per chiedere il valore alle collection, è semplicemente perché per me è più chiaro nel codice chiedere qualcosa che si chiama SettingsManager.CnStringSqlServer che eseguire un metodo

GetAppSetting(AppSettingsManager.STT_CnStringSqlServer); Ma nelle vostre applicazioni siete liberi di fare come preferite, il bello della programmazione (ed anche il brutto) è che ci sono 101 modi per fare la stessa cosa, uno diverso dall’altro.

public DataSourceType DataSource
{
    get
    {
        return SettingsManager.SelectedDataSource;
    }
}

Anche DataSource viene esposto dalla collection dei parametri utente, quindi togliamo il metodo Set della property, cosa che andrà a creare qualche errore nel codice della UsersWindow.

public void Reset()
{
    SettingsManager = null;
}

Implementiamo un metodo per forzare il ricaricamento dei Setting quando vengono modificati.

Prima di vedere le modifiche e correzioni a UsersWindow, però creiamo una semplice Window per modificare i nostri parametri e salvarli su Disco.

La classe SettingsWindow.xaml e SettingsWindow.xaml.cs

2017_04_23_02_SettingsWindow01[3]

Perdonate l’uso di una immagine, ma mi serve per descrivere la classe, poi mettiamo e commentiamo anche il codice Xaml. Questa Window, che sarà utilizzata in modalità Dialog dal programma (quindi una finestra modale), contiene al suo interno una struttura molto, molto semplice, infatti contiene una Grid, con 2 righe. Nella prima riga un Tab Control con 2 Tab Item, uno per i parametri Utente, uno per i parametri Applicativi. Nella seconda riga, uno StackPanel, che contiene 2 Button, uno per Salvare le modifiche e l’altro per Chiudere senza salvare.

<Grid>
 
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
...
</Grid>

La definizione della Grid, la prima riga occupa tutto lo spazio disponibile, la seconda viene dimensionata in base al suo contenuto (quindi i Button).

<TabControl Grid.Row="0">
            <TabItem Header="User settings">
...
            </TabItem>
            <TabItem Header="App settings">
...            
            </TabItem>
</TabControl>

Il Tab control, posizionato nella prima riga, e contenente i 2 TabItem che ospiteranno i controlli per modificare i parametri.

<StackPanel>
    <TextBlock
        Margin="4,2,4,2"
        HorizontalAlignment="Left"
        VerticalAlignment="Center"
        Text="Selected Data Source" />
    <ComboBox Name="DataSource" SelectedItem="{Binding Path=SettingsManager.SelectedDataSource}" />
</StackPanel>

Lo stack panel che si trova nel primo Tab Item che contiene una textblock con la descrizione di cosa viene modificato. e una ComboBox dove l’operatore può selezionare il tipo di database a cui collegarsi.
La sola cosa da notare è che abbiamo dato un Name alla combobox, per poter fornire la lista degli elementi, e abbiamo messo in Binding il suo SelectedItem con la property SelectedDataSource di una property SettingsManager, vedremo come funziona.

<StackPanel>
    <TextBlock
        Margin="4,2,4,2"
        HorizontalAlignment="Left"
        VerticalAlignment="Center"
        Text="SqlServer connection" />
    <TextBox
        Margin="4,2,4,2"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Center"
        HorizontalContentAlignment="Left"
        Text="{Binding Path=SettingsManager.CnStringSQLServer}" />
    <TextBlock
        Margin="4,2,4,2"
        HorizontalAlignment="Left"
        VerticalAlignment="Center"
        Text="OleDb (Access) connection" />
    <TextBox
        Margin="4,2,4,2"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Center"
        HorizontalContentAlignment="Left"
        Text="{Binding Path=SettingsManager.CnStringOleDb}" />
    <TextBlock
        Margin="4,2,4,2"
        HorizontalAlignment="Left"
        VerticalAlignment="Center"
        Text="SQLite connection" />
    <TextBox
        Margin="4,2,4,2"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Center"
        HorizontalContentAlignment="Left"
        Text="{Binding Path=SettingsManager.CnStringSQLite}" />
</StackPanel>

Lo stack panel che si trova nel secondo Tab Control che contiene 3 Text Block che descrivono le 3 connection string e le 3 TextBox per inserire le stringhe.
Anche in questo caso, non vi sono molte cose da notare, salvo il Binding delle 3 property Text delle TextBox alle 3 property corrispondenti alle connection string, sulla property SettingsManager.

<StackPanel
    Grid.Row="1"
    FlowDirection="RightToLeft"
    Orientation="Horizontal">
    <Button
        Margin="4,2,4,2"
        Padding="10,4,10,4"
        Click="Close_Click"
        Content="Close" />
    <Button
        Margin="4,2,4,2"
        Padding="10,4,10,4"
        Click="Save_Click"
        Content="Save" />
</StackPanel>

Lo stack panel che contiene i 2 Button che permettono di Salvare le modifiche o chiudere senza salvare. La sola cosa da notare, sono i 2 event Handler agganciati al Click dei Button.

using Dnw.Users.Context;
using Dnw.UsersServices;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
 
namespace Dnw.Users.Windows
{
    public partial class SettingsWindow : Window, INotifyPropertyChanged
    {
        public const string FLD_SettingsManager = "SettingsManager";
 
        private AppSettingsManager mSettingsManager;
 
        public event PropertyChangedEventHandler PropertyChanged;
 
        public SettingsWindow()
        {
            InitializeComponent();
            WindowStartupLocation = WindowStartupLocation.CenterOwner;
            this.DataContext = this;
            InitDataSourceCombobox();
            SettingsManager = new AppSettingsManager();
        }
 
...
    }
}

Commentiamo ora il codice che gestisce la nostra Window, prima di tutto il costruttore.

Abbiamo definito Una Property di tipo AppSettingsManager, per poter manipolare i parametri senza toccare direttamente quelli di AppContext. Non è infatti buona cosa modificare direttamente quei parametri per evitare di avere dati inconsistenti. Le modifiche vengono fatte sulla copia locale dei parametri, e dopo il salvataggio, si aggiorna i dati di AppContext, vedremo come.

Intanto, nel costruttore Inizializziamo il DataContext (per far funzionare il Binding), Inizializziamo la ComboBox, e inizializziamo la property SettingsManager.

public AppSettingsManager SettingsManager
{
    get
    {
        return mSettingsManager;
    }
    set
    {
        mSettingsManager = value;
        OnPropertyChanged(FLD_SettingsManager);
    }
}

La definizione della Property.

private void InitDataSourceCombobox()
{
    List<DataSourceType> dsTypes = new List<DataSourceType>();
    dsTypes.Add(DataSourceType.NotSet);
    dsTypes.Add(DataSourceType.SqlServer);
    dsTypes.Add(DataSourceType.OleDb);
    dsTypes.Add(DataSourceType.SQLite);
    DataSource.ItemsSource = dsTypes;
}

L’inizializzazione della combobox con i 4 valori dell’enumerazione.

private void Save_Click(object sender, RoutedEventArgs e)
{
    SettingsManager.AppSettings.WriteXml(SettingsManager.AppSettingsFullName);
    SettingsManager.UsrSettings.WriteXml(SettingsManager.UserSettingsFullName);
    AppContext.Instance.Reset();
    MessageBox.Show("Settings saved successfully");
    Close();
}

Il salvataggio delle modifiche, con la scrittura dei 2 File su disco, e il Reset dell’AppContext per fare in modo che i dati modificati siano ricaricati.

private void Close_Click(object sender, RoutedEventArgs e)
{
    Close();
}

La chiusura senza effettuare modifiche.

Le modifiche a UsersWindow.xaml e UsersWindow.xaml.cs

Modifichiamo infine la finestra principale per gestire quanto fin qui fatto.

2017_04_23_03_UsersWindow01[3]

Abbiamo eliminato i 3 RadioButton, e le property ad essi collegate, che dopo le modifiche ad AppContext davano errore, e sostituito il Button di Test con un Button ove modificare i Settings.

private void EditSettings(object sender, RoutedEventArgs e)
{
    SettingsWindow swin = new SettingsWindow();
    swin.Icon = this.Icon;
    swin.Owner = this;
    swin.Title = "Change Application and User Settings";
    swin.ShowDialog();
}

Questo è il codice inserito nell’event handler del Click del Button Edit Settings per aprire la finestra di modifica dei parametri applicativi come Dialog modale.

2017_04_23_04_SettingsWindow02[3]

L’apertura della Window.

2017_04_23_05_SettingsWindow03[3]

La lista dei tipi di connessione.

2017_04_23_06_SettingsWindow04[3]

La pagina con le 3 connection string.

Riepilogo

In questo articolo abbiamo trattato i seguenti argomenti:

  • Aggiungere ad una applicazione una classe per gestire parametri di configurazione Applicativi (a livello macchina) ed Utente (diversi per ogni utente che usa la macchina).
  • Aggiungere l’uso dei parametri al Context dell’applicazione per renderli disponibili a ogni classe dell’applicazione.
  • Generare una spartana User Interface per modificare i parametri di configurazione.
  • Modificare la User interface precedente per poter utilizzare i nuovi parametri e chiamare la finestra di modifica.

Potete scaricare il progetto esempio nella versione collegata a questo articolo al seguente link:

Per qualsiasi commento, domanda, approfondimento, potete usare il link di contatto in cima alla pagina.