Press "Enter" to skip to content

4 – MiniSqlAgent – Una Console per la gestione di un servizio in WPF – Parte 2 La Console il manager e la User Interface

In questa seconda parte dell’articolo dedicato allo sviluppo della user interface per la gestione dei parametri di configurazione del nostro servizio vedremo come creare il Manager per i parametri derivato dalla classe abstract creata nel post precedente e creeremo uno User Control in grado di gestire le due collezioni dei parametri e la Window che specificamente gestirà i parametri per il nostro servizio.

Il SettingsManagerControl

Questo User control rappresenta il necessario per gestire le due collezioni di parametri di configurazione che normalmente sono necessarie ad una applicazione, fa uso di tutto quello che abbiamo costruito fino ad ora e aggiunge qualcosa, come sempre a quello che conosciamo di XAML e di C#.

<UserControl x:Class="Dnw.UI.Settings.Controls.SettingsManagerControl"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
   xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
   xmlns:p="clr-namespace:Dnw.UI.Settings.Properties"
   xmlns:ctl="clr-namespace:Dnw.UI.Settings.Controls"
   xmlns:lconv="clr-namespace:Dnw.Base.Wpf.Converters;assembly=Dnw.Base.Wpf.v4.0"
   mc:Ignorable="d"
   d:DesignHeight="640" 
   d:DesignWidth="960">
<UserControl.Resources>
<lconv:BoolToVisibilityConverter x:Key="boolToVisibilityConverter" />
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
		
<TabControl Grid.Row="0">
<TabItem Header="{x:Static p:Resources.txtSMCApplicationTabHeader}"  Visibility="{Binding AppTabIsVisible,  Converter={StaticResource boolToVisibilityConverter}}">
<ctl:SettingsPage x:Name="PageApplicationSettings"/>
</TabItem>
<TabItem Header="{x:Static p:Resources.txtSMCUserTabHeader}"  Visibility="{Binding UsrTabIsVisible,  Converter={StaticResource boolToVisibilityConverter}}">
<ctl:SettingsPage x:Name="PageUserSettings"/>
</TabItem>
 
</TabControl>
<StackPanel Grid.Row="1" FlowDirection="RightToLeft" Orientation="Horizontal" Margin="5,2,5,2">
<Button Name="SaveButton" Margin="5"  Click="SaveButton_Click" Width="100" >
<TextBlock Padding ="10,5,10,5" Text="{x:Static p:Resources.txtSMCSave}"/>
</Button>
<Button Name="CancelButton" Margin="5"  Click="CancelButton_Click" Width="100" >
<TextBlock Padding ="10,5,10,5" Text="{x:Static p:Resources.txtSMCCancel}"/>
</Button>
</StackPanel>
 
</Grid>
</UserControl>

Lo XAML non &egrave; complesso e contiene molte cose gi&agrave; discusse quindi listeremo solo le sue peculiarit&agrave;:

  • Abbiamo incluso il Namespace relativo alle Properties del progetto perch&eacute; per le stringhe utilizzeremo le risorse localizzabili
  • Abbiamo incluso il Namespace dove il controllo &egrave; stato generato perch&eacute; utilizzeremo il controllo SettingsPage gi&agrave; sviluppato per gestire i parametri.
  • Abbiamo incluso il Namespace di Base relativo ai Converters, perch&eacute; ne abbiamo sviluppato uno per poter modificare la visibilit&agrave; dei TabItems in base al fatto che abbiano un contenuto.
  • Abbiamo inserito il converter BoolToVisibilityConverter nelle risorse del controllo per poterlo poi usare sulla property Visibility dei due TabItem.
  • La struttura del controllo contiene una Grid con 2 righe, la prima contiene un TabControl con 2 TabItem, ciascuno dei quali contiene un Controllo SettingsPage. La seconda riga contiene due buttons che ci permetteranno di fornire gli eventi Save e Cancel a chi utilizzer&agrave; il controllo.

Le modifiche al progetto DnwUISettings

Solution_04[4]

Solution_05[4]

La classe BoolToVisibilityConverter

Questo converter ci permetter&agrave; di nascondere un TabItem in base ad un valore booleano inserito sul ViewModel collegato allo User Control ma essendo un oggetto non specifico nel suo uso, &egrave; stato inserito nella libreria dei controlli base di WPF da noi creata in modo da renderlo disponibile all’uso futuro in qualsiasi altra interfaccia utente.

[ValueConversion(typeof(bool), typeof(Visibility))]
public class BoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
	{
Visibility result = Visibility.Visible;
bool input = (bool)value;
if (input)
		{
			result = Visibility.Visible;
		}
else
		{
			result = Visibility.Collapsed;
		}
return result;
	}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
	{
return value;
	}
}

La classe Converter, molto semplice, implementa una conversione One Way, per ora da Boolean a Visibility perch&eacute; non ci serve quella contraria, nel caso quando necessario implementeremo il ConvertBack.

Il code Behind di SettingsManagerControl

namespace Dnw.UI.Settings.Controls
{
public partial class SettingsManagerControl : UserControl, INotifyPropertyChanged
	{

Anche in questo caso, la dichiarazione ci mostra come il controllo sar&agrave; il ViewModel di se stesso.

public static readonly RoutedEvent CancelledEvent = EventManager.RegisterRoutedEvent(
"Cancelled", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsManagerControl));
public static readonly RoutedEvent SavedEvent = EventManager.RegisterRoutedEvent(
"Saved", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsManagerControl));

public event RoutedEventHandler Saved
{
add
	{
		AddHandler(SavedEvent, value);
	}
remove
	{
		RemoveHandler(SavedEvent, value);
	}
}
public event RoutedEventHandler Cancelled
{
add
	{
		AddHandler(CancelledEvent, value);
	}
remove
	{
		RemoveHandler(CancelledEvent, value);
	}
}

Registriamo due RoutedEvent specifici per il controllo per permettere di intercettare il click dei due button alla window che implementer&agrave; l’uso di questo controllo. Ed i relativi gestori per l’add e remove handler.

private void CancelButton_Click(object sender, System.Windows.RoutedEventArgs e)
{
	RaiseCancelledEvent();
}
private void RaiseCancelledEvent()
{
RoutedEventArgs args = new RoutedEventArgs(SettingsManagerControl.CancelledEvent);
	RaiseEvent(args);
}
private void RaiseSavedEvent()
{
RoutedEventArgs args = new RoutedEventArgs(SettingsManagerControl.SavedEvent);
	RaiseEvent(args);
}
private void SaveButton_Click(object sender, System.Windows.RoutedEventArgs e)
{
	RaiseSavedEvent();
}

L’implementazione della logica dei due Button e i metodi per sollevare i due eventi che in WPF per convenzione hanno il prefisso Raise invece del prefisso On come era per le Windows Forms.

public SettingsManagerControl()
{
	InitializeComponent();
	DataContext = this;
}

Il costruttore, che inizializza la classe come ViewModel di se stessa.

public void Init(DnwSettingsCollection appSettings = null, DnwSettingsCollection usrSettings = null)
{
	AppTabIsVisible = appSettings != null;
	UsrTabIsVisible = usrSettings != null;
if (AppTabIsVisible)
	{
		PageApplicationSettings.Init(appSettings);
	}
if (UsrTabIsVisible)
	{
		PageUserSettings.Init(usrSettings);
	}
if (!AppTabIsVisible && !UsrTabIsVisible)
	{
MessageBox.Show("You don't have any settings file to modify Check your code!");
		RaiseCancelledEvent();
	}
}

La funzione di inizializzazione del controllo, che verifica e aggiorna le property che riguardano la visibilit&agrave; dei TabItem ed eventualmente avvisa se non sono stati indicati alcun tipo di parametri, cosa che riguarda pi&ugrave; il programmatore che l’utente da qui il messaggio diretto.

public bool AppTabIsVisible
{
get
	{
return mAppTabIsVisible;
	}
set
	{
		mAppTabIsVisible = value;
		OnPropertyChanged(FLD_AppTabIsVisible);
	}
}
public bool UsrTabIsVisible
{
get
	{
return mUsrTabIsVisible;
	}
set
	{
		mUsrTabIsVisible = value;
		OnPropertyChanged(FLD_UsrTabIsVisible);
	}
}

Aggiungiamo le property per gestire la visibilit&agrave; dei TabItem.

Possiamo notare che avendo utilizzato vari componenti la logica dietro al nostro controllo &egrave; abbastanza semplice. Spero si possa notare sia in questo post che nei precedenti che ci piacciono molto i Lego e che costruire Lego informatici aiuta a semplificare la vita dei programmatori C# soprattutto lavorando con WPF.

Avendo terminato di sviluppare i componenti, passiamo alla Console del servizio, visto che sar&agrave; legata a filo doppio al servizio avr&agrave; delle cose in comune con il servizio, a partire dalla Libreria del Service Layer, e ovviamente le classi relative al contesto applicativo, pertanto iniziamo mostrando le modifiche che abbiamo apportato al ServiceContext creando il ServiceSettingsManager per gestire i parametri di configurazione che nel precedente post avevamo invece inserito nella classe del contesto.

public void ResetSettings()
{
	mSettingsManager = null;
}
public ServiceSettingsManager SettingsManager
{
get
	{
if (mSettingsManager == null)
		{
			mSettingsManager = new ServiceSettingsManager();
			mSettingsManager.Load();
		}
return mSettingsManager;
	}
}

Abbiamo tolto tutte le property relative ai parametri applicativi ed abbiamo generato una nuova classe, ServiceSettingsManager derivata da DnwSettingsManagerBase, inoltre abbiamo creato un metodo ResetSettings che possa essere utilizzato per ricaricare i parametri quando necessario dopo averli modificati.

La classe ServiceSettingsManager

public class ServiceSettingsManager : DnwSettingsManagerBase
{
private const string APPSTT_File = "Dnw\\MiniSqlAgent\\MiniSqlAgent_APP.xml";
private const string LOGFILE = "Dnw\\Logs\\MiniSqlAgent\\MSA{0}.log";
private const string STT_CheckInterval = "CheckInterval";
private const string STT_DataFolder = "DataFolder";
private const string STT_LogVerbosity = "LogVerbosity";

La dichiarazione della classe e le costanti per i nomi dei file e i nomi dei parametri di configurazione.

protected override void LoadAppSettings()
{
try
	{
if (mAppSettings == null)
		{
			mAppSettings = new DnwSettingsCollection();
		}
		mAppSettings.Clear();
DnwDropDownItemsCollection items = new DnwDropDownItemsCollection();
		items.Add(new DnwDropDownItem()
		{
			Value = LogType.ExceptionsOnly.ToString(),
			Description = Properties.Resources.txtSMSLogExceptionsOnly
		});
		items.Add(new DnwDropDownItem()
		{
			Value = LogType.WarningsAndExceptions.ToString(),
			Description = Properties.Resources.txtSSMLogWarningsAndExceptions
		});
		items.Add(new DnwDropDownItem()
		{
			Value = LogType.AllMessages.ToString(),
			Description = Properties.Resources.txtSSMLogAllMessages
		});
		items.Add(new DnwDropDownItem()
		{
			Value = LogType.None.ToString(),
			Description = Properties.Resources.txtSSMLogNoMessages
		});
		mAppSettings.Add(new DnwSetting()
		{
			Category = Properties.Resources.txtSSMCatConfiguration,
			ID = STT_LogVerbosity,
			Description = Properties.Resources.txtSSMDesLogVerbosity,
			EditorType = EditorType.DropDown,
			DropdownValues = items,
			Position = 0,
			Value = LogType.ExceptionsOnly.ToString(),
		});
		mAppSettings.Add(new DnwSetting()
		{
			Category = Properties.Resources.txtSSMCatConfiguration,
			ID = STT_CheckInterval,
			Description = Properties.Resources.txtSSMDesCheckInterval,
			EditorType = EditorType.NumericInteger,
			Position = 1,
			Value = "60",
		});
		mAppSettings.Add(new DnwSetting()
		{
			Category = Properties.Resources.txtSSMCatConfiguration,
			ID = STT_DataFolder,
			Description = Properties.Resources.txtSSMDesDataFolder,
			EditorType = EditorType.DirectoryName,
			Position = 1,
			Value = Path.Combine(Environment.GetFolderPath(
Environment.SpecialFolder.CommonDocuments), "MiniSqlAgent"),
		});
if (File.Exists(AppSettingsFileName))
		{
try
			{
DnwSettingsCollection savedSettings = DnwSettingsCollection.ReadXml(AppSettingsFileName, false);
foreach (DnwSetting stt in savedSettings)
				{
DnwSetting item = mAppSettings[stt.ID];
if (item != null)
					{
						item.Value = stt.Value;
					}
				}
			}
catch (Exception ex)
			{
EventLogger.SendMsg(ex);
//If the file is corrupted
//we ignore the error letting the default values on the settings
			}
		}
else
		{
			mAppSettings.WriteXml(AppSettingsFileName);
		}
	}
catch (Exception ex)
	{
EventLogger.SendMsg(ex);
throw;
	}
}
protected override void LoadUsrSettings()
{
//We don't have user settings for the moment;
}

Le funzioni di Generazione e Aggiornamento dei valori dei parametri di configurazione, in questo caso solo dei parametri di applicazione, visto che non abbiamo ancora parametri utente, quello che facciamo in questo metodo &egrave; generare tutti i parametri con i valori di default e il necessario a permettere di modificarli e poi andiamo ad aggiornare i valori memorizzati sul file XML se esistono. Abbiamo mantenuto il controllo che genera il file XML se non esiste gi&agrave;.

public string LogFileName
{
get
	{
if (mLogFileName == null)
		{
			mLogFileName = string.Format(
Path.Combine(Environment.GetFolderPath(
Environment.SpecialFolder.CommonDocuments), LOGFILE),
DateTime.Now.ToString("yyyyMMdd"));
Path.GetDirectoryName(mLogFileName).XDwCheckPath();
		}
return mLogFileName;
	}
}
public override string AppSettingsFileName
{
get
	{
if (mAppSettingsFileName == null)
		{
			mAppSettingsFileName = Path.Combine(Environment.GetFolderPath(
Environment.SpecialFolder.CommonApplicationData),
				APPSTT_File);
Path.GetDirectoryName(mAppSettingsFileName).XDwCheckPath();
		}
return mAppSettingsFileName;
	}
}

public override string UsrSettingsFileName
{
get
	{
//No user settings file
return (null);
	}
}

Le property che espongono il nome del file di Log e quello del file dei parametri applicativi esattamente come erano fatte nell’esempio precedente. E la property per il file dei parametri utente che in questo caso &egrave; generato perch&eacute; previsto dalla classe manager base ma &egrave; nullo.

public LogType LogVerbosity
{
get
	{
LogType type = LogType.AllMessages;
Enum.TryParse<LogType>(mAppSettings[STT_LogVerbosity].Value, out type);
return type;
	}
}
public int CheckInterval
{
get
	{
int interval = 60;
int.TryParse(mAppSettings[STT_CheckInterval].Value, out interval);
return interval;
	}
}
public string DataFolder
{
get
	{
Path.GetDirectoryName(mAppSettings[STT_DataFolder].Value).XDwCheckPath();
return mAppSettings[STT_DataFolder].Value;
	}
}

Le property che espongono il valore dei parametri applicativi, con tutti i controlli del caso ed ovviamente tipizzando i valori altrimenti memorizzati come stringhe.

La classe ConfigWindow

Abbiamo gi&agrave; visto nel post precedente come &egrave; fatta la MainWindow della console, e i meccanismi che abbiamo introdotto per fare in modo che la finestra relativa ai parametri di configurazione sia aperta una sola volta, ora vediamo come implementarla per farle fare quel che ci serve:

<Window x:Class="Dnw.MiniSqlAgent.Console.Windows.ConfigWindow"         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
	xmlns:stc="clr-namespace:Dnw.UI.Settings.Controls;assembly=Dnw.UI.Settings.v4.0"         Title="ConfigWindow" Height="480" Width="640" Loaded="Window_Loaded">
<stc:SettingsManagerControl Name="sttCtrl" >
		
</stc:SettingsManagerControl>
 
</Window>

Lo XAML della nostra Window &egrave; molto semplice, infatti contiene solo un istanza dello User Control che abbiamo illustrato nel post precedente.

public partial class ConfigWindow : Window
{
public ConfigWindow()
	{
		InitializeComponent();
	}
private void Window_Loaded(object sender, RoutedEventArgs e)
	{
		sttCtrl.Init(appSettings: ServiceContext.Instance.SettingsManager.AppSettings );
		sttCtrl.Cancelled += sttCtrl_Cancelled;
		sttCtrl.Saved += sttCtrl_Saved;
	}
void sttCtrl_Saved(object sender, RoutedEventArgs e)
	{
ServiceContext.Instance.SettingsManager.Save();
//Forces reload of new settings at first request
ServiceContext.Instance.ResetSettings();
		
this.Close();
	}
void sttCtrl_Cancelled(object sender, RoutedEventArgs e)
	{
//Forces reload of old settings at first request
ServiceContext.Instance.ResetSettings();
this.Close();
	}
}

Anche il Code Behind della Window &egrave; piuttosto stringato, infatti abbiamo l’inizializzazione della classe UserControl al caricamento della finestra e l’intercettazione dei due eventi dei button. In entrambi i casi resettiamo il contenuto dei Parametri nel contesto, in un caso per aggiornarli, nell’altro per ripristinare il valore precedente.

Il progetto esempio relativo all’articolo &egrave; disponibile al link seguente:

Le librerie comuni che sono state usate e modificate per questo articolo sono disponibili al link seguente: