Press "Enter" to skip to content

6 – Uno user control per gestire i dati di connessione a SQL Server

In questo sesto post ci occupiamo di tutto il Code Behind dello User Control e quindi della logica di gestione dei dati e dei servizi che il controllo mette a disposizione delle applicazioni che lo useranno.

Premetto che non stiamo cercando di creare un esempio di MVVM o di altri pattern di lavoro, ma stiamo creando un controllo che con buon senso sia costruito in modo da essere efficiente, usabile ed estensibile in futuro e soprattutto sia calato in un contesto reale.

Dopo la premessa andiamo a vedere cosa inseriamo nel code behind del nostro controllo:

public string FileName
{
	get
	{
		return mControlModel.FileName;
	}
	set
	{
		mControlModel.FileName = value;
	}
}

public bool IsChanged
{
	get
	{
		return mControlModel.IsChanged;
	}
}

Per prima cosa mettiamo a disposizone chi utilizzerá il nostro User Control due property gestite dal nostro Model, perché come vedremo nell’esempio di test l’uso primario di questo controllo è quello di permettere di leggere e scrivere i dati delle connessioni in un file. Ed il nome del file deve essere fornito da chi utilizza il controllo non fa parte di quello che gestisce autonomamente il controllo perché sarebbe una regola funzionale troppo stretta, infatti il nome del file dati potrebbe essere fornito da programma in molti modi diversi. La seconda property è l’esposizione del flag IsChanged gestito dal model quando i dati vengono modificati, lo facciamo perché la UI che utilizza il controllo dovrá essere in grado di reagire alle modifiche esattamente come quella del controllo, è quindi opportuno fornire un servizio che notifica la modifica dei dati.

public void Clear()
{
	mControlModel.Clear();
}

public void Load(bool isEncrypted)
{
	mControlModel.Load(isEncrypted);
}

public void Save(bool isEncrypted)
{
	mControlModel.Save(isEncrypted);
}

Aggiungiamo tre Servizi, forniti da altrettanti metodi, ovvero la creazione di una collezione vuota  Clear,  il caricamento dei dati da file Load, il salvataggio dei dati su file Save.

public ImportExportDelegate GetExportData;

public ImportExportDelegate GetImportData;

Aggiungiamo altri due servizi, ma forniti in modo diverso, infatti, L’ import e l’export dei dati potrebbero essere implementati in modi completamente diversi in base al contesto d’uso del controllo quindi ci danno modo di proporre uno dei possibili metodi per dare libertá di azione a chi implementerá l’uso del controllo.

namespace Dnw.UI.SqlServer.Models
{
	public class ImportExportData
	{
 
		public ImportExportData(string fileName, bool encrypt)
		{
			FileName = fileName;
			Encrypt = encrypt;
		}
 
 		public string FileName
		{
			get;
			private set;
		}
 
		public bool Encrypt
		{
			get;
			private set;
		}
 
	}
 
	public delegate ImportExportData ImportExportDelegate();  }

Ecco l’implementazione del delegate e della classe dati che viene utilizzata per restituire il risultato dell’operazione di import ed export. Vedremo poi come saranno utilizzati i delegate nel progetto di test. Vediamo invece come sono implementati all’interno del nostro controllo:

private void Export()
{
	if (GetExportData != null)
	{
		ImportExportData data = GetExportData();
		mControlModel.Export(data);
	}
	else
	{
		MessageBox.Show(Properties.Resources.warSCICNoExportDelegate,  Properties.Resources.warTitle, 
		MessageBoxButton.OK, MessageBoxImage.Warning);
	}
}

private void Import()
{
	if (GetImportData != null)
	{
		ImportExportData data = GetImportData();
		mControlModel.Import(data);
	}
	else
	{
		MessageBox.Show(Properties.Resources.warSCICNoImportDelegate,  Properties.Resources.warTitle, 
		MessageBoxButton.OK, MessageBoxImage.Warning);
	}
}

Come possiamo notare, i nostri metodi Import ed Export Richiedono i dati necessari all’import o export tramite il delegate fornito e poi utilizzano il metodo inserito nel Model per effettuare l’operazione. Quando vedremo il code behind del progetto di test vedremo perché lasciar decidere in quella sede i dati relativi all’import o all’export.

private void mnuClick(object sender, RoutedEventArgs e)
{
	try
	{

		MenuItem itm = sender as MenuItem;
		if (itm != null)
		{
			switch (itm.Name)
			{
				case "mnuClone":
					mControlModel.Clone();
					this.txtConnectionID.Focus();
					break;
				case "mnuDelete":
					mControlModel.Delete();
					break;
				case "mnuEdit":
					mControlModel.Edit();
					this.txtConnectionID.Focus();
					break;
				case "mnuExport":
					Export();
					break;
				case "mnuImport":
					Import();
					break;
				case "mnuNew":
					mControlModel.AddNew();
					this.txtConnectionID.Focus();
					break;
				case "mnuUndo":
					mControlModel.Undo();
					break;
			}
		}

	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		MessageBox.Show(ex.Message);
	}

}

Veniamo alla porzione interna della gestione del controllo, ovvero la funzione di gestione delle opzioni di menu. Come potete notare, ho gestito tutti i MenuItem con un solo event handler, questa è una modalitá di gestione che piace fare a me, non è obbligatoria, a me piace farlo cosí perché cosí ho un unico punto dove controllo tutto quello che sono le azioni del controllo o della window, se preferite implementare event handler separati, fatelo senza alcun timore di infrangere qualche regola non scritta o qualche pattern indispensabile.

Come potete notare dal contenuto del menu, tutte le opzioni chiamano direttamente un metodo del model oppure del codice che abbiamo giá esaminato, im piú, i tre metodi che modificano o aggiungono un elemento aggiornano il focus sui controlli del dettaglio. Quest’ultima operazione è necessariamente da farsi in questa porzione di codice, visto che il Model deve essere Agnostico rispetto alla UI, infatti, potrebbe anche essere che lo utilizziamo per un altro controllo volendo. Ovviamente non è questo il caso, peró tanto per dare delle indicazioni teoriche, potremmo creare un ViewModel che è utilizzabile su due diversi control o window, ad esempio un Editor in forma di lista (Datagrid) oppure un editor come questo (lista e dettaglio) oppure Un controllo Lista con una window dettaglio. Tutti potrebbero avere il modello in comune ma rappresentare gli stessi dati in forma diversa.

Prima di vedere i metodi, osserviamo una modifica che abbiamo fatto per gestire correttamente l’Undo delle operazioni di modifica:

private SqlConnectionInfosCollection mUndoSqlConnections;

public bool IsInEditMode
{
	get
	{
		return mIsInEditMode;
	}
	set
	{
		if (!mIsInEditMode && value)
		{
			mUndoSqlConnections = mSqlConnections.Clone();
		}
		mIsInEditMode = value;

		OnPropertyChanged(FLD_IsInEditMode);
		OnPropertyChanged(FLD_IsNotInEditMode);
		OnPropertyChanged(FLD_SqlConnectionIsInEditMode);
		OnPropertyChanged(FLD_EditImage);
	}
}

Abbiamo aggiunto una seconda variabile privata alla classe di tipo SqlConnectionInfosCollection. Ed abbiamo modificato il Setter di IsInEditMode in modo tale che quando passa dallo stato Visualizzazione allo stato Modifica, salvi una copia della collection cosí come si trovava al primo caricamento o dopo l’ultimo salvataggio.

Passiamo ora ad esaminare i metodi di Servizio del nostro Model, in ordine sparso.

public void AddNew()
{
	SqlConnectionInfo newItem = new SqlConnectionInfo();
	SqlConnections.Add(newItem);
	CurrentConnection = newItem;
	IsInEditMode = true;
	IsChanged = true;

}

Il metodo di aggiunta di una nuova SqlConnectionInfo, è molto semplice, infatti modifica solo i dati e i flag del model, chi come me ha lavorato co WIndows Forms ricorda probabilmente che dopo aver inserito qualcosa da codice erano necessarie una serie di operazioni per riposizionare l’interfaccia sulla riga aggiunta o aggiornare i dati di dettaglio, in WPF, la “carpenteria” che abbiamo costruito in XAML fa in modo che tutto funzioni automaticamente, infatti, la modifica della CurrentConnection riposizionerá il SelectedItem della listbox sulla riga aggiunta, la modifica dei due flag attiverá la modifica sul controllo (se non fosse giá attiva) e avviserá l’UI che ci sono modifiche effettuate in modo che possa reagire di conseguenza. Credo che solo questo metodo possa iniziare a farci apprezzare l’uso di WPF.

public void Clear()
{
	this.SqlConnections = null;
	this.SqlConnections = new SqlConnectionInfosCollection();
	this.IsInEditMode = false;
	this.IsChanged = false;
}

Il metodo da utilizzare per generare una collection vuota, quando vogliamo creare un nuovo file di connessioni. Anche questa funzione grazie alla “carpenteria” XAML fará reagire automaticamente il controllo.

public void Clone()
{
	if (CurrentConnection != null)
	{
		SqlConnectionInfo newItem = CurrentConnection.Clone();
		newItem.ConnectionID += "(Copy)";
		SqlConnections.Add(newItem);
		CurrentConnection = newItem;
		IsInEditMode = true;
		IsChanged = true;
	}
}

Il metodo di duplicazione, simile all’addnew, fatto salvo che utilizza la funzione di Clone della classe SqlConnectionInfo che abbiamo visto nella parte 2 di questa serie di articoli. Anche in questo caso il controllo agirá automaticamente posizionandosi sul nuovo elemento aggiunto alla lista e attivando la modifica del suo dettaglio. Per questioni visuali, aggiungiamo la stringa (Copy) all’ID del nuovo elemento in modo che l’operatore si accorga che è una copia.

public void Delete()
{
	int ndx = SqlConnections.IndexOf(CurrentConnection);

	if (MessageBox.Show(Properties.Resources.warSCIMConfirmDeletion, Properties.Resources.warTitle, MessageBoxButton.YesNo, MessageBoxImage.Warning) == MessageBoxResult.Yes)
	{
		SqlConnections.Remove(CurrentConnection);
		if (SqlConnections.Count > ndx)
		{
			CurrentConnection = SqlConnections[ndx];
		}
		else
		{
			if (SqlConnections.Count > 0)
			{
				CurrentConnection = SqlConnections[SqlConnections.Count - 1];
			}
			else
			{
				CurrentConnection = null;
			}
		}
		IsChanged = true;
	}
}

Il metodo di cancellazione, in questo caso facciamo una richiesta all’utente, lo facciamo nel model, ma potremo anche inserirlo nel controllo o delegarlo cosí come abbiamo fatto per Import ed Export, se l’operazione di cancellazione in qualsiasi contesto richiede sempre conferma, la richiesta sta bene qui, se vi fossero dei casi in cui non è necessario, allora spostatela ad un livello piú alto negli strati che vanno verso ció che l’utente vede e usa.
Oltre alla richiesta di conferma, vediamo solo un operazione di logica di business, io ho arbitrariamente deciso che se esistono altre connessioni, l’interfaccia si posizionerá su quella che ha preso il posto di quella cancellata nella lista, (quella sotto per intenderci) se non ve ne fossero mi posiziono sull’ultima connessione della lista, e se avessi cancellato l’ultima connessione disattivo tutto e azzero anche il dettaglio.

public void Edit()
{
	if (SqlConnections.Count == 0)
	{

		AddNew();
	}
	else
	{
		if (this.CurrentConnection == null)
		{
			if (MessageBox.Show(Properties.Resources.txtSCIMAskToCreateNewConnection, Properties.Resources.txtSCIMNewConnection, MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
			{
				AddNew();
			}
		}
		else
		{
			this.IsInEditMode = true;
		}
	}
}

Il metodo per attivare la modifica, oltre a modificare il flag di stato che gli compete, fa anche una verifica e se non vi sono connessioni ne genera una. Se non vi sono connessioni selezionate chiede se si vuole generarne una nuova.

public void Export(ImportExportData data)
{
	try
	{
		if (!data.Encrypt)
		{
			this.SqlConnections.Serialize(data.FileName);
		}
		else
		{
			EncDec.Encrypt(this.mSqlConnections.Serialize(), data.FileName);
		}
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		throw;
	}
}

Il metodo di export, scrive i dati nel file passato come parametro e crittografa i dati se necessario.

public void Import(ImportExportData data)
{
	try
	{

		string content = null;
		if (data.Encrypt)
		{
			content = EncDec.Decrypt(data.FileName, true);
		}
		else
		{
			content = File.ReadAllText(data.FileName);
		}
		SqlConnectionInfosCollection imported = SqlConnectionInfosCollection.Deserialize(content, false);
		if (imported.Count > 0)
		{
			foreach (SqlConnectionInfo item in imported)
			{
				SqlConnections.Add(item);
			}
		}

		IsChanged = true;
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		throw;
	}
}

Il metodo di Import, legge eventualmente decrittografando i dati da un file richiesto e li accoda al contenuto attuale della collezione, è un comportamento che abbiamo deciso arbitrariamente, aggiungere non sostituire i dati.

public void Load(bool encrypted = false)
{
	if (encrypted)
	{
		LoadEncrypted(FileName);
	}
	else
	{
		Load(FileName);
	}
}

Il metodo di caricamento dei dati da file, in base alla richiesta carica dati in chiaro oppure dati crittografati.

private void LoadEncrypted(string fileName)
{
	if (!fileName.XDwIsNullOrTrimEmpty())
	{
		string jSonData = EncDec.Decrypt(fileName, true);

		this.SqlConnections = null;
		this.SqlConnections = SqlConnectionInfosCollection.Deserialize(jSonData, false);
		if (this.SqlConnections == null)
		{
			this.SqlConnections = new SqlConnectionInfosCollection();
		}
		this.IsInEditMode = false;
		this.IsChanged = false;
	}
	else
	{
		throw new NoConnectionDataFileException(Properties.Resources.excSCIMNoFileToLoad);
	}

}

private void Load(string fileName)
{
	if (!fileName.XDwIsNullOrTrimEmpty())
	{
		this.SqlConnections = null;
		this.SqlConnections = SqlConnectionInfosCollection.Deserialize(fileName);
		if (this.SqlConnections == null)
		{
			this.SqlConnections = new SqlConnectionInfosCollection();
		}
		this.IsInEditMode = false;
		this.IsChanged = false;
	}
	else
	{
		throw new NoConnectionDataFileException(Properties.Resources.excSCIMNoFileToLoad);
	}
}

I due metodi di caricamento dati da file, per cui abbiamo creato un Exception tipizzata che ci permetterá nell’implementazione di intercettare ed eventualmente gestire in modo “soft” verso gli utenti il fatto che il file indicato per l’acquisizione dei dati non esiste.

public void Save(bool encrypted = false)
{
	if (!FileName.XDwIsNullOrTrimEmpty())
	{
		if (encrypted)
		{
			SaveEncrypted(FileName);
		}
		else
		{
			Save(FileName);
		}

		mUndoSqlConnections.Clear();
		mUndoSqlConnections = null;

		this.IsInEditMode = false;
		this.IsChanged = false;
	}
	else
	{
		throw new NoConnectionDataFileException(Properties.Resources.excSCIMNoFileToSave);
	}
}
	
private void Save(string fileName)
{
	this.SqlConnections.Serialize(fileName);
}

private void SaveEncrypted(string fileName)
{
	string jSonData = this.SqlConnections.Serialize();
	EncDec.Encrypt(jSonData, fileName);

}

I corrispondenti metodi di salvataggio dati opposti ai metodi di caricamento. Il salvataggio provvede anche ad annullare la lista di Undo

public void Undo()
{
	this.SqlConnections = null;
	this.SqlConnections = mUndoSqlConnections;
}

L’ultimo metodo che permette di annullare le operazioni effettuate dal momento in cui siamo entrati in modalitá modifica, sia stato con la pressione del tasto modifica oppure con la pressione dei tasti Add New o Clone. La copia dei dati viene effettuata al passaggio da False a True del flag IsInEditMode, pertanto l’undo è sempre relativo allo stato della collezione dopo l’ultimo salvataggio.

Con questo post abbiamo completato l’esplorazione della realizzazione del nostro User Control WPF, pertanto proseguiremo con l’ultimo Post, che mostrerá un caso d’uso di test del nostro User Control

Il progetto di esempio che contiene il codice come sarà alla fine della serie è disponibile nella nuova versione delle librerie di uso comune a questo indirizzo:

Per qualsiasi domanda, curiosità approfondimento, potete usare il link al modulo di contatto in cima alla pagina.

Aggiungo i riferimenti alle librerie di supporto che saranno utilizzate anche negli articoli successivi.