Press "Enter" to skip to content

9 – MIniSqlAgent – Uno User control per generare un Job

In questo post, proseguiamo lo sviluppo della console per il servizio MiniSqlAgent che stiamo sviluppando per dimostrare come costruire un servizio windows funzionante e non banale ed una applicazione WPF che ne gestisca l’amministrazione in tutti i suoi aspetti. Nel post precedente abbiamo generato una classe per definire un Job per il nostro servizio che contiene funzionalità minimali, adesso realizziamo uno User Control WPF per poter generare un Job e salvarlo su un file su disco, e testeremo lo User Control nella applicazione della nostra console.

Job_uc_01

Questo è il risultato finale che otterremo, uno user control in grado di generare un MiniSqlAgentJob. Vediamo che cosa abbiamo modificato a livello di classi nel progetto MiniSqlAgentConsole che ospiterà questo User Control.

Job_uc_02

Abbiamo effettuato le seguenti modifiche al progetto MiniSqlAgentConsole

  • Abbiamo aggiunto la cartella Commands, che ospiterà i Routed commands specifici di questa applicazione.
  • Abbiamo creato una classe statica, MiniSqlAgentCommands, in cui abbiamo inserito il primo dei commands specifici della console.
  • Abbiamo aggiunto la cartella Controls, che ospiterà tutti gli user control specifici della console.
  • Abbiamo creato uno User Control MIniSqlAgentJobControl, che fornirà la UI per generare un MiniSqlAgentJob e salvarlo.
  • Abbiamo creato il View Model per questo controllo che abbiamo chiamato MiniSqlAgentJobModel.
  • Abbiamo modificato la MainWindow per testare temporaneamente lo user control.

Vediamo in dettaglio cosa abbiamo creato partendo dallo User Control.

La classe MiniSqlAgentJobControl

Partiamo con lo XAML di cui esaminiamo le porzioni più interessanti in ordine di apparizione all’interno del file .XAML della classe.

 xmlns:gconv="clr-namespace:Dnw.Base.Wpf.Converters;assembly=Dnw.Base.Wpf.v4.0"
xmlns:lconv="clr-namespace:Dnw.MiniSqlAgent.Console.Converters"
xmlns:gcmds="clr-namespace:Dnw.Base.Wpf.Commands;assembly=Dnw.Base.Wpf.v4.0"

I namespace che abbiamo incluso, che nell’ordine forniscono, i converters globali, definiti in Dnw.Base.Wpf.Converters, i converters specifici di questa applicazione, definiti in Dnw.MiniSqlAgent.Console.Converters, i commands globali definiti in Dnw.Base.Wpf.Commands.

<gconv:BoolToIsValidImageConverter x:Key="boolToIsValidImageConverter" />
<lconv:BoolToIsInEditModeImageConverter x:Key="boolToIsInEditModeImageConverter" />
<Style x:Key="PanelEditMode" TargetType="{x:Type Grid}">
    <Setter Property="Background" Value="#ffefefef"  />
    <Style.Triggers>
        <DataTrigger Binding="{Binding IsInEditMode}" Value="True">
            <Setter Property="Background" Value="#ffcccccc" />
        </DataTrigger>
    </Style.Triggers>
</Style>

La dichiarazione dei converters, uno importato dalla libreria di base, l’altro dalla libreria locale. E lo stile per il pannello contenitore dei controlli, che modifica il background per evidenziare quando stiamo modificando i dati.

<UserControl.CommandBindings>
    <CommandBinding 
        Command="ApplicationCommands.Save"
        CanExecute="Save_CanExecute"
        Executed="Save_Executed"/>
    <CommandBinding 
        Command="ApplicationCommands.Close"
        Executed="Close_Executed"/>
    <CommandBinding 
        Command="{x:Static gcmds:EditingCommands.Edit}"
        CanExecute="Edit_CanExecute"
        Executed="Edit_Executed"/>
</UserControl.CommandBindings>

Il binding degli event handler per i commands, abbiamo creato il Command Edit nei commands globali della libreria Dnw.Base.Wpf visto che questo comando non esiste fra quelli standard delle librerie WPF.

<TextBlock Grid.Row="0" Grid.Column="0" 
    Style="{StaticResource MainInstruction}" 
    Text="{Binding Job.JobFileName}"  
    HorizontalAlignment="Left" 
    VerticalAlignment="Center" Margin="5,2,5,2"/>
<Image  Grid.Row="0" Grid.Column="1" Width="32" 
    Height="32" Margin="5,2,5,2" 
    Source="{Binding Job.IsValid, 
    Converter={StaticResource boolToIsValidImageConverter}}" 
    ToolTip="{Binding StatusInstruction}" VerticalAlignment="Center"/>
<Image  Grid.Row="0" Grid.Column="2" 
    Width="32" Height="32" Margin="5,2,5,2" 
    Source="{Binding IsInEditMode, 
    Converter={StaticResource boolToIsInEditModeImageConverter}}" 
    VerticalAlignment="Center"/>

I tre controlli dell’intestazione dello user control, un testo in binding con il nome del file che ospita il Job. E due immagini, la prima è un immagine di status, che mostra un icona diversa se il Job è valido oppure no, inoltre mostra un tooltip che da indicazione relativa al motivo per cui il Job può oppure non può essere salvato. La seconda immagine cambia quando lo User Control permette di modificare il suo contenuto.

<TextBlock Grid.Row="0" Grid.Column="0" 
    Text="Job Description" Margin="10" />
<TextBox Grid.Row="0" Grid.Column="1" 
    HorizontalAlignment="Stretch" 
    VerticalAlignment="Center" 
    Text="{Binding Job.Description}" AcceptsReturn="True" 
    VerticalScrollBarVisibility="Auto"  
    MinHeight="32" Height="52" 
    Margin="10,5,10,5"
    IsEnabled="{Binding IsInEditMode}"/>
<TextBlock Grid.Row="1" Grid.Column="0" 
    Text="SQL Script to execute" Margin="10" />
<TextBox Grid.Row="1" Grid.Column="1" 
    HorizontalAlignment="Stretch" 
    VerticalAlignment="Center" 
    Height="160" MinHeight="48" 
    AcceptsReturn="True"  
    Text="{Binding Job.SqlScript}" 
    TextWrapping="Wrap" 
    VerticalScrollBarVisibility="Auto" 
    Margin="10,5,10,5"
    IsEnabled="{Binding IsInEditMode}"/>
<TextBlock Grid.Row="2" Grid.Column="0" 
    Text="Next Execution time (DD/MM/YYYY HH:MM)" Margin="10" />
<TextBox Grid.Row="2" Grid.Column="1" 
    HorizontalAlignment="Left" Width="130" 
    VerticalAlignment="Center" 
    Text="{Binding Job.NextExecutionTime}"  
    Margin="10,5,10,5"
    IsEnabled="{Binding IsInEditMode}"/>
<TextBlock Grid.Row="3" Grid.Column="0" 
    Text="Execution interval (in Minutes)" Margin="10" />
<TextBox Grid.Row="3" Grid.Column="1" 
    HorizontalAlignment="Left" Width="80" 
    VerticalAlignment="Center" 
    Text="{Binding Job.ExecutionInterval}"  
    Margin="10,5,10,5"
    IsEnabled="{Binding IsInEditMode}"/>
<TextBlock Grid.Row="4" Grid.Column="0" 
    Text="Sql Server connection name" Margin="10" />
<TextBox Grid.Row="4" Grid.Column="1" 
    HorizontalAlignment="Stretch" 
    VerticalAlignment="Center" 
    Text="{Binding Job.ConnectionID}"  
    Margin="10,5,10,5"
    IsEnabled="{Binding IsInEditMode}"/>

I controlli per la modifica del Job, per il momento non abbiamo messo i testi delle descrizioni sulle Risorse in modo da permettere la gestione multilingua dell’interfaccia, ma lo faremo nel prossimo post. Abbiamo agganciato le textbox alle property del Job sul nostro ViewModel, non abbiamo usato controlli specializzati o combobox, saranno oggetto dei prossimi post, per ora ci interessa avere una interfaccia di base funzionante che ci permetta di creare un Job e salvarlo, così da testare il funzionamento dell’entity.

<Button  Margin="10,2,10,2" Padding="25,0,25,0" 
         VerticalAlignment="Center" 
         Command="ApplicationCommands.Save" >
         Save
</Button>
<Button  Margin="10,2,10,2" Padding="25,0,25,0" 
         Command="ApplicationCommands.Close"  
         VerticalAlignment="Center" >
         Close
</Button>
<Button  Margin="10,2,10,2" Padding="25,0,25,0" 
         Command="{x:Static gcmds:EditingCommands.Edit}"  
         VerticalAlignment="Center" >
         Edit
</Button>

Infine i tre button, in binding con i command per le azioni possibili sull’interfaccia. Vediamo ora il code behind della classe.

public partial class MiniSqlAgentJobControl : UserControl
{
	public static readonly RoutedEvent ClosedEvent = EventManager.RegisterRoutedEvent(
		"Closed", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(MiniSqlAgentJobControl));

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

	private MiniSqlAgentJobModel mJobModel;

La dichiarazione della classe e la definizione delle variabili a livello di classe, abbiamo predisposto due routed event per notificare all’interfaccia che utilizza il nostro user control l’avvenuto salvataggio e la richiesta di chiusura, in modo che la window che istanzierà il controllo possa agire di conseguenza. Abbiamo inoltre dichiarato la variabile per il ViewModel dello User Control.

public void Init(string fileName)
{
	mJobModel = new MiniSqlAgentJobModel(fileName);
	this.DataContext = mJobModel;

}

public MiniSqlAgentJobControl()
{
	InitializeComponent();
}

Il costruttore e la funzione Init, che dovrà essere utilizzata da chi istanzia il controllo per inizializzarlo e attivarlo caricandovi i dati del Job ad esso collegato.

private void Edit_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
	e.CanExecute = mJobModel != null && !mJobModel.IsInEditMode;
}

private void Save_CanExecute(object sender, System.Windows.Input.CanExecuteRoutedEventArgs e)
{
	e.CanExecute = mJobModel != null && mJobModel.Modified && mJobModel.Job.IsValid;
}

Gli event handler che attivano o disattivano i Bottoni Save e Edit del controllo verificando che sia stato inizializzato ed i parametri funzionali relativi alle due funzioni.

private void Close_Executed(object sender, System.Windows.Input.ExecutedRoutedEventArgs e)
{
	bool closeMe = true;
	if (mJobModel.Modified)
	{
		MessageBoxResult res = MessageBox.Show(Properties.Resources.warMSAJCUnsavedEditsExitAnyway,
			Properties.Resources.warGENWarning, MessageBoxButton.YesNo, MessageBoxImage.Question);
		if (res != MessageBoxResult.Yes)
		{
			closeMe = false;
		}
	}
	if (closeMe)
	{
		RaiseClosedEvent();
	}
}

private void Edit_Executed(object sender, ExecutedRoutedEventArgs e)
{
	mJobModel.IsInEditMode = true;
}

private void Save_Executed(object sender, System.Windows.Input.ExecutedRoutedEventArgs e)
{
	mJobModel.Save();
	MessageBox.Show(Properties.Resources.txtMSAJCUSaved);
	RaiseSavedEvent();
}

Gli event handlers dei tre buttons, con la verifica in caso di modifiche prima di sollevare l’evento di chiusura. Facciamo notare che per attivare la modifica facciamo solo una modifica ad un flag, che tramite il binding attiverà il necessario.

private void RaiseClosedEvent()
{
	RoutedEventArgs args = new RoutedEventArgs(MiniSqlAgentJobControl.ClosedEvent);
	RaiseEvent(args);
}

private void RaiseSavedEvent()
{
	RoutedEventArgs args = new RoutedEventArgs(MiniSqlAgentJobControl.SavedEvent);
	RaiseEvent(args);
}

I metodi che sollevano i due eventi che abbiamo definito nella classe. (Per chi arriva da Windows forms, sono i corrispondenti dei metodi OnEventName in quell’ambiente.) Il prefisso Raise davanti ai nomi degli eventi è un pattern consigliato ma non obbligatorio.

Il view model dello User control, la classe MiniSqlAgentJobModel

public class MiniSqlAgentJobModel : INotifyPropertyChanged
{
 
	private bool mIsInEditMode;

	private MiniSqlAgentJob mJob;
 
	private bool mModified;

La dichiarazione della classe, con l’interfaccia per gli eventi di modifica e le variabili a livello di classe, due flag per la gestione dell’attivazione dell’interfaccia e la classe contenente il Job.

public MiniSqlAgentJobModel(string fileName)
{
	try
	{

		if (fileName.XDwIsNullOrTrimEmpty() || !File.Exists(fileName))
		{
			Job = new MiniSqlAgentJob();
		}
		else
		{
			Job = MiniSqlAgentJob.ReadXml(fileName, false);
		}
		Job.JobFileName = fileName;
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		MessageBox.Show(Properties.Resources.errMSAJMJobFileCorrupt,  Properties.Resources.errGENError,  MessageBoxButton.OK,  MessageBoxImage.Error);
		Job = new MiniSqlAgentJob();
	}
}

Il costruttore della classe che crea un nuovo Job se il file dati non esiste oppure legge il file dati e se non vi riesce crea comunque un Job vuoto.

public void Save()
{
	try
	{

		this.Job.WriteXml(this.Job.JobFileName);
		this.Modified = false;
		this.IsInEditMode = false;

	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		MessageBox.Show(ex.Message,  Properties.Resources.errGENError,  MessageBoxButton.OK,  MessageBoxImage.Error);
	}
}

Il metodo che salva il file su disco e in caso non vi riesca da un messaggio all’utente, il problema potrebbe essere generalmente dovuto al fatto che l’utente non ha diritti di scrittura sulla cartella destinazione

public MiniSqlAgentJob Job
{
	get
	{
		return mJob;
	}
	set
	{
		if (mJob != null)
		{
			mJob.PropertyChanged -= Job_PropertyChanged;
		}
		mJob = value;
		mJob.PropertyChanged += Job_PropertyChanged;
		OnPropertyChanged(FLD_Job);
	}
}

La property che rappresenta il Job e quando viene modificato aggancia al Job un Event Handler che permette di gestire i cambiamenti all’interfaccia quando le property del controllo cambiano.

void Job_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
	Modified = true;
	OnPropertyChanged(FLD_StatusInstruction);
}

L’event handler che modifica il flag di notifica della modifica ai dati e aggiorna anche il tooltip dell’immagine che verifica se il Job è sintatticamente valido.

public bool Modified
{
	get
	{
		return mModified;
	}
	set
	{
		mModified = value;
		OnPropertyChanged(FLD_Modified);
	}
}

public bool IsInEditMode
{
	get
	{
		return mIsInEditMode;
	}
	set
	{
		mIsInEditMode = value;
		OnPropertyChanged(FLD_IsInEditMode);
	}
}


public string StatusInstruction
{
	get
	{
		if (Job.IsValid)
		{
			return Properties.Resources.txtMSAJMSaveable;
		}
		else
		{
			return Properties.Resources.txtMSAJMNotSaveable;
		}
	}
}

Le properties che gestiscono i due flag dell’interfaccia, e il tooltip per l’immagine di stato del Job.

Le modifiche (temporanee) a MainWindow per testare il nostro User Control

Vediamo i punti dello XAML di MainWindow che abbiamo modificato per testare il controllo:

xmlns:lctls ="clr-namespace:Dnw.MiniSqlAgent.Console.Controls"

Il namespace per i local controls, che permette alla Window di usare lo User Control.

<TabControl Grid.Row="1" Margin="0" >
	<TabItem Header="Test Jobs">
		<lctls:MiniSqlAgentJobControl x:Name="cmdJob"  VerticalAlignment="Stretch" Margin="0"/>
	</TabItem>
</TabControl>

Il tab control che ospita lo User Control per il test. Unica cosa da notare è che siccome lo User Control è definito all’interno dell’exe e non in una diversa libreria, è necessario utilizzare l’attributo x:Name invece che Name per dichiarare il suo nome e farvi riferimento nel code behind.

Ed il code behind:

private void Window_Loaded(object sender, RoutedEventArgs e)
{
	mWindowModel.StartCheckServiceStatus();
	cmdJob.Init(Path.Combine(ServiceContext.Instance.SettingsManager.DataFolder, "testjob.xml"));
}

L’inizializzazione del controllo con un nome di file da noi deciso arbitrariamente, usando la cartella predisposta sui setting applicativi. Vediamo un mini webcast che mostra come il controllo si comporta sulla UI

Conclusioni

Con gli ultimi 3 post abbiamo costruito una calsse minimale per gestire l’esecuzione di query su SQL Server ad un determinato momento in una giornata, tutti gli articoli collegati a questa serie sono rapidamente recuperabili utilizzando la categoria MiniSqlAgent, posta nella super categoria Serie di questo Blog. Nei vari post che si snodano lungo tutto il 2013 troverete la costruzione completa dell’applicazione e tutte le librerie collegate siano esse sviluppate da noi o di terze parti.

Il codice del progetto esempio collegato a questo articolo è disponibile al link seguente:

Le librerie di uso comune collegate a questo articolo sono disponibili al link seguente:

Per qualsiasi ulteriore domanda, usate il link al modulo di contatto in cima alla pagina oppure il link al forum Microsoft su cui rispondo quasi ogni giorno.