Press "Enter" to skip to content

3 – MiniSqlAgent – Una Console per la gestione di un servizio in WPF – Parte 1 Gestione Parametri applicativi

Dopo i due post dedicati alla modifica strutturale alla funzionalità dei parametri di configurazione applicativi ed al controllo DropDownList per i nostri parametri, iniziamo lo sviluppo di una console per Gestire il servizio che abbiamo costruito nei post precedenti, questa console, sviluppata in WPF . Di che cosa parleremo in questo post?

  • Di applicazioni a singola istanza tramite l’uso di un mutex.
  • Di come implementare un oggetto che ci impedisca di aprire la stessa finestra all’interno di una applicazione più di una volta.
  • Di come implementare l’interfaccia utente per la gestione dei parametri di configurazione applicativi.

Indichiamo i post correlati a questo in modo che possiate tenerli sott’occhio se facciamo riferimento al codice che essi contengono.

E i post propedeutici:

I post propedeutici sono quelli che contengono lo sviluppo dei componenti che useremo in questo contesto e della loro evoluzione.

La struttura dell’applicazione WPF

Iniziamo lo sviluppo aggiungendo alla soluzione MiniSqlAgent che abbiamo introdotto nel primo post dedicato al servizio un nuovo progetto di tipo Applicazione WPF, vediamone la Struttura.

Solution_01

Abbiamo creato la struttura di una applicazione, molto simile a quelle dei progetti WPF che abbiamo utilizzato per i test delle nostre applicazioni, abbiamo spostato MainWindow.xaml e il suo code behind nella cartella Windows. Ed abbiamo proceduto alla consueta predisposizione dei Namespace e dei parametri di configurazione dell’applicazione. In questo caso non abbiamo inserito il Post Build Event perché non è una libreria, ma in seguito aggiungeremo il necessario a creare un setup applicativo con InnoSetup così come abbiamo fatto per il progetto del servizio.

Solution_02

Abbiamo anche firmato l’applicazione come sempre per tutte le nostre librerie ed abbiamo modificato i parametri di configurazione della Build per fare in modo che l’applicazione sia compilata a 32 bit, perché siccome sviluppiamo su una macchina a 64 bit non avremmo a disposizione l’edit and continue in debug se necessario.

Configurare l’applicazione perché parta una sola volta

In molti casi, quando un utente utilizza una applicazione, soprattutto una console di controllo per l’amministrazione di un servizio è opportuno che non possano partire più istanze dell’applicazione per l’utente corrente, pertanto vediamo come possiamo fare in modo che la console non possa essere avviata più di una volta sulla stessa macchina. Per ora ci limiteremo all’ ABC, quindi faremo solo in modo che l’applicazione se avviata due volte ci dia un messaggio chiedendoci di riportare in primo piano l’istanza già aperta, in seguito potremo sviluppare quanto necessario a riportare automaticamente in primo piano l’applicazione già partita, ma andiamo già sulle cose molto sofisticate quindi un passo per volta.

public partial class App : Application
{
private const string APP_ID = "it.dotnetwork.minisqlagentconsole";
private Mutex mMutex;

La parte iniziale della classe (abbiamo omesso tutto quello che riguarda la gestione del log applicativo, se volete approfondire come abbiamo fatto a creare il sistema di log trovate il post relativo QUI) dove abbiamo creato un identificativo univoco per l’applicazione, la notazione che chi ha frequentato Java o Objective-C conosce è un pattern usato da molto tempo, formato da Nome del dominio aziendale a rovescio seguito dal nome dell’applicazione. Abbiamo inoltre creato una variabile member di tipo Mutex. Si tratta di un oggetto del namespace System.Threading.

protected override void OnStartup(StartupEventArgs e)
{
this.ShutdownMode = System.Windows.ShutdownMode.OnExplicitShutdown;
bool isNewInstance = false;
	mMutex = new Mutex(true, APP_ID + Environment.UserName, out isNewInstance);
if (!isNewInstance)
	{
MessageBox.Show("The application is already running look at the Application bar!");
Application.Current.Shutdown();
return;
	}
this.ShutdownMode = System.Windows.ShutdownMode.OnMainWindowClose;
base.OnStartup(e);
	StartupTheLogger();
	OpenMainWindow();
}
private void OpenMainWindow()
{
this.MainWindow = new MainWindow();
	MainWindow.Show();
}

L’override del metodo OnStartup dell’applicazione è l’elemento in cui implementiamo il necessario a rendere l’applicazione a singola istanza.

  • Modifichiamo lo ShutdownMode a OnExplicitShutdown perché potremmo ricevere un errore visualizzando il messaggio di errore.
  • Creiamo l’oggetto Mutex, potete notare come il metodo di costruzione dell’oggetto abbia un parametro di output booleano che il nome ci descrive piuttosto bene. Se esiste già un Mutex nel sistema che ha il medesimo identificativo (Nome applicazione + Nome utente) il valore di ritorno sarà false e di conseguenza daremo il messaggio all’utente e chiuderemo l’applicazione.
  • Modifichiamo lo ShutdownMode a OnMainWindowClose ed eseguiamo le operazioni di default per lo startup applicativo aprendo la finestra principale dell’applicazione.

La Finestra MainWindow

Essendo una console di controllo per un servizio, la nostra applicazione dovrà implementare varie funzionalità, pertanto iniziamo creando una finestra principale con un Menu che contiene due sottomenu e due opzioni di menu.

<Menu Grid.Row="0" Grid.ColumnSpan="2"  Margin="0" VerticalAlignment="Top" BorderThickness="2"   Foreground="Black" FontSize="16" FontWeight="Normal">
<MenuItem Name="mnuFile" Header="{x:Static p:Resources.mnuMWFile}"  Margin="2,4,2,2" Height="32" IsEnabled="{Binding IsNotInEditMode}">
<MenuItem Name="mnuExit" Header="{x:Static p:Resources.mnuMWExit}"  Click="mnuClick" Margin="2,4,2,2" Height="32" IsEnabled="{Binding IsNotInEditMode}">
<MenuItem.Icon>
<Image  Source="/MiniSqlAgentConsole;component/Images/btn_032_157.png"  Height="24" Width="24"/>
</MenuItem.Icon>
</MenuItem>
</MenuItem>
<MenuItem Name="mnuTools"  Header="{x:Static p:Resources.mnuMWTools}" Click="mnuClick"  Margin="2,4,2,2" Height="32" IsEnabled="{Binding IsNotInEditMode}">
<MenuItem Name="mnuConfig" Header="{x:Static p:Resources.mnuMWConfig}"  Click="mnuClick" Margin="2,4,2,2" Height="32" IsEnabled="{Binding IsNotInEditMode}">
<MenuItem.Icon>
<Image  Source="/MiniSqlAgentConsole;component/Images/btn_032_160.png"  Height="24" Width="24"/>
</MenuItem.Icon>
</MenuItem>
</MenuItem>
</Menu>

Lo XAML qui sopra indicato darà origine a questo menu:

Menu_01

Menu_02

L’event handler per il click dei due bottoni è il seguente:

private void mnuClick(object sender, RoutedEventArgs e)
{
try
	{
MenuItem mnu = sender as MenuItem;
switch (mnu.Name)
		{
case "mnuExit":
this.Close();
break;
case "mnuConfig":
				OpenConfig();
break;
		}
	}
catch (Exception ex)
	{
EventLogger.SendMsg(ex);
MessageBox.Show(ex.Message, "ERROR", MessageBoxButton.OK, MessageBoxImage.Error);
	}
}

L’implementazione dell’ Exit Application è semplice, visto che abbiamo indicato nell’OnStartup che l’applicazione si chiude alla chiusura della MainWindow (che per inciso è la prima finestra aperta dall’applicazione) l’implementazione della gestione dei parametri di configurazione è un metodo, che discuteremo immediatamente.

private void OpenConfig()
{
	win = new ConfigWindow();
	win.Icon = this.Icon;
	win.Owner = this;
	win.Show();
}

L’implementazione più lineare dell’apertura di una finestra per i parametri di configurazione è questa, ma ha un problema, che questo metodo ci permette di aprire più volte la stessa finestra, cosa che potrebbe essere utile per vari tipi di funzionalità, ma non certo per una finestra di configurazione, visto che aprendone due istanze potremmo fare delle modifiche in una ed altre nella seconda e poi l’ultima ad essere salvata sarebbe la vincitrice.

Come possiamo ovviare alla cosa?

Il modo più semplice è aprire la finestra come Dialog

ConfigWindow win = new ConfigWindow();
win.Icon = this.Icon;
win.Owner = this;
win.ShowDialog();

Anche questo è un approccio utilizzabile e per il caso della finestra di configurazione dell’applicazione potrebbe essere la soluzione migliore.

Ma se invece non volessimo che fosse una Dialog e volessimo ugualmente fare in modo che non possa essere aperta due volte?

Possiamo impostare un meccanismo all’interno della MainWindow che ci permetta di gestire in questo modo qualsiasi finestra apriamo come “Child” della finestra principale. Vediamo come.

Per prima cosa andiamo a crearci un entità ed una collezione di entità allo scopo.

childWindows

Andiamo nella libreria di base DnwBaseWPF, creiamo una cartella Entities ed una cartella Collections ed andiamo a definire due classi:

La classe ChildWindow

public class ChildWindow
{
private string mName;
private Window mWindow;
public ChildWindow(string name, Window win)
	{
		mName = name;
		mWindow = win;
	}
	
public string Name
	{
get
		{
return mName;
		}
set
		{
			mName = value;
		}
	}
	
public Window Window
	{
get
		{
return mWindow;
		}
set
		{
			mWindow = value;
		}
	}
}

Una classe molto semplice, che implementa due proprietà, una Window ed un Name.

La classe ChildWindowsCollection

public class ChildWindowsCollection : List<ChildWindow>
{
public ChildWindow this[string windowName]
	{
get
		{
return (this.FirstOrDefault(item => item.Name == windowName));
		}
	} 
}

La collezione di ChildWindow, implementa esclusivamente una property indexer che permette di acquisire un elemento dato il suo nome.

Andiamo ora a vedere come abbiamo modificato la MainWindow:

public partial class MainWindow : Window
{
private ChildWindowsCollection mOpenedWindows;
private const string WIN_Config = "Config";
public MainWindow()
	{
		InitializeComponent();
this.Icon = BitmapFrame.Create(new Uri("pack://application:,,,/msaIco.ico", UriKind.RelativeOrAbsolute));
		mOpenedWindows = new ChildWindowsCollection();
	}

Ritorniamo alla definizione della classe MainWindow e al suo costruttore ed aggiungiamo una collezione di child windows e definiamo una costante per il nome che daremo alla finestra dei parametri di configurazione. Nel costruttore, oltre ad abbellire la finestra con un icona personalizzata, istanziamo la collection.

private void OpenConfig()
{
ChildWindow child = mOpenedWindows[WIN_Config];
ConfigWindow win = null;
if (child == null)
	{
		win = new ConfigWindow();
		win.Icon = this.Icon;
		win.Owner = this;
		win.Closed += ConfigWindow_Closed;
		mOpenedWindows.Add(new ChildWindow(WIN_Config, win));
		win.Show();
	}
else
	{
		win = (ConfigWindow)child.Window;
		win.BringIntoView();
	}
}

Il nuovo metodo per aprire la finestra di configurazione:

  • Richiediamo alla collection se c’è una finestra con il nome da noi deciso, l’Indexer usa FirstOrDefault, che se non trova un elemento che soddisfa l’espressione ritorna un null.
  • Se la finestra child non esiste creiamo la finestra e l’aggiungiamo alla collezione con il nome da noi stabilito inoltre aggiungiamo un event handler all’evento Closed della finestra.
  • Se la finestra child esiste, la portiamo in primo piano.

L’ event handler dell’evento Closed:

void ConfigWindow_Closed(object sender, EventArgs e)
{
ChildWindow child = mOpenedWindows[WIN_Config];
if (child != null)
	{
		mOpenedWindows.Remove( child);
	}
}

Alla chiusura della finestra eliminiamo l’elemento dalla collection.

Potevamo implementare la collection anche in modo più semplice, ad esempio come lista di Window ed utilizzare il Type per identificare la finestra che dovevamo togliere, ma potrebbe anche succedere di aver bisogno di Due diverse finestre dello stesso tipo e ciascuna deve essere aperta una sola volta, quindi usare un nome univoco ci permette di essere più flessibili. Questo approccio può essere usato in modo generico in qualsiasi delle nostre applicazioni dotate di User Interface e se non abbiamo una Main Window vera e propria come in questo caso potremo istanziare una static o un singleton per la collection che ci permetta di controllare comunque il contenuto della collezione di finestre aperte.

Gestire i parametri di configurazione applicativi del servizio

Nei post precedenti, abbiamo già implementato il servizio di base ed i suoi parametri di configurazione, abbiamo implementato una serie di classi per gestire i parametri di configurazone di una applicazione ed abbiamo implementato dei componenti WPF che ci permettono di gestire questi parametri di configurazione, abbiamo inoltre modificato i componenti che erano stati implementati per correggere il problema sollevato dal fatto di memorizzare non solo i dati ma anche le informazioni relative alla gestione della User Interface nel file contenente i parametri di configurazione.

Vediamo ora come implementare la User Interface per dare modo all’amministratore di sistema e agli utenti di poter modificare i parametri di configurazione applicativi e le preferenze utente senza dover imparare XML. Abbiamo parlato di preferenze perché 99 volte su 100 in una applicazione che interagisce con gli utenti oltre ai parametri di configurazione applicativi ci serviranno anche dei parametri di preferenza per l’utente, quindi ad esempio il colore o una foto per lo sfondo della main window, oppure la casella di posta elettronica dell’utente (da usare ad esempio come mittente per i messaggi di errore). Ma siamo certi che sono molte le opzioni che ci verranno in mente mentre le nostre applicazioni si evolvono.

Considerato che per qualsiasi applicazione ci potrebbero servire dei parametri e che per qualsiasi applicazione questi parametri potrebbero essere dei due tipi indicati, abbiamo deciso di creare una Classe di base da usare per generare le classi manager che useremo in tutte le applicazioni:

La classe DnwSettingsManagerBase

A questa classe daremo l’incarico di fare 2 cose principalmente: effettuare tutte le operazioni standard sui parametri indipendenti dall’implementazione. Fornirci le fondamenta per creare delle classi di parametri uguali per struttura anche se diverse per contenuto. In modo da dare consistenza al codice applicativo (E rendere più semplice la manutenzione e la eventuale correzione di problemi o implementazione di ulteriori funzionalità.)

public abstract class DnwSettingsManagerBase : INotifyPropertyChanged
{

Come buona parte dei manager generici, anche questa è un Entity, e la inseriamo nella libreria DnwBase:

Solution_03

Siamo certi vi siete accorti che la classe ha un modificatore abstract nella sua dichiarazione, questo perché è una classe che fornisce solo struttura non deve essere una classe che sarà fisicamente istanziata.

protected DnwSettingsCollection mAppSettings;
protected DnwSettingsCollection mUsrSettings;

Le due collezioni per i parametri applicativi ed i parametri utente.

public void Load()
{
	LoadAppSettings();
	LoadUsrSettings();
}
public void Save()
{
if (UserSettings != null)
	{
		SaveUsrSettings();
	}
if (AppSettings != null)
	{
		SaveAppSettings();
	}
}

Due metodi pubblici, per caricare e salvare i parametri di configurazione.

protected abstract void LoadAppSettings();
 
protected abstract void LoadUsrSettings();

Due metodi abstract, per creare e caricare i dati dai file che conterranno tali parametri, i metodi dovranno essere sviluppati all’interno delle classi derivate in quanto dipenderanno dal contesto.

private void SaveAppSettings()
{
this.AppSettings.WriteXml(AppSettingsFileName);
}
private void SaveUsrSettings()
{
this.UserSettings.WriteXml(UsrSettingsFileName);
}

I metodi di salvataggio dei dati, che invece non necessitano di essere personalizzati nelle classi derivate quindi sono privati.

public DnwSettingsCollection UserSettings
{
get
	{
return mUsrSettings;
	}
}
public DnwSettingsCollection AppSettings
{
get
	{
return mAppSettings;
	}
}
public abstract string AppSettingsFileName { get; }
public abstract string UsrSettingsFileName { get; }

Le property che espongono le due collection e due property che devono essere implementate forzatamente nelle classi derivate in quanto il luogo ove memorizzeremo i due file dei parametri di configurazione dipenderà dalle nostre applicazioni e da quello che esse fanno.

Ci fermiamo qui per questa prima parte, proseguiremo sul post successivo dove troverete anche i link al progetto MiniSqlAgent aggiornato con tutte le modifiche.

Per qualsiasi commento, domanda, approfondimento, usate pure il link al modulo di contatto in cima alla pagina.