Press "Enter" to skip to content

Generazione di Componenti da Codice in WPF

In questo post spiegheremo come generare componenti a runtime all’interno di uno User Control WPF, e come effettuarne il binding alle property degli elementi di una collection.

Introduzione

Nel post precedente abbiamo visto come creare degli user control che espongono il loro contenuto tramite una Dependency property in modo da costruire degli Editor specializzati per alcuni tipi di dati. Ora vediamo come utilizzare questi Editor assieme ai normali componenti WPF per costruire uno User Control che ci permetta di modificare il contenuto di una collezione di Settings assegnando l’editor corretto ad ogni valore della lista.

Modifiche alla classe DnwSetting e alla classe DnwSettingsCollection

Per rendere più funzionali i nostri setting, aggiungiamo due property che ci permetteranno di parametrizzare la User Interface che permette la loro modifica:

[XmlAttribute]
public int Position
{
	get
	{
		return mPosition;
	}
	set
	{
		mPosition = value;
		OnPropertyChanged(FLD_Position);
	}
}

[XmlAttribute]
public string Mask
{
	get
	{
		return mMask;
	}
	set
	{
		mMask = value;
		OnPropertyChanged(FLD_Mask);
	}
}

La property Position ci serve per organizzare in modo logico ed arbitrario la posizione dei vari settings all’interno della User Interface senza per questo dover modificare l’ ID oppure inserire dei dati posizionali nella Descrizione.

La property Mask ci permette di modificare il comportamento degli editor numerici in modo da poter rappresentare numeri in modo diverso.

Anche nella collection andiamo ad aggiungere alcune cose che sono funzionali allo sviluppo della User Interface

public IEnumerable<DnwSetting> GetOrderedList()
{
	return (this.OrderBy(x => x.Category).ThenBy(x => x.Position));
}

public int GridRowsCount()
{
	int cats = this.Select(item => item.Category).Distinct().Count();
	return (Count + cats);
}

Il primo metodo utilizza Linq, ci permette di creare una lista Ordinata secondo i campi Category e Position in modo tale che possiamo costruire una User Interface coerente.

Il secondo metodo conta le righe necessarie alla creazione della User Interface, prevedendo oltre ad una riga per ogni elemento della collection anche una riga descrittiva per ognuna delle categorie utilizzate. Anche in questo caso utilizziamo Linq per contare il numero di elementi diversi (Distinct) all’interno del campo Category degli Item della collection.

Una modifica ai 3 User Control già descritti

Dopo alcuni test abbiamo deciso di fare anche una piccola modifica ai tre User control per essere certi che possano essere correttamente bindati in lettura e scrittura, abbiamo quindi modificato la definizione delle loro dependency property nel modo seguente:

public static readonly DependencyProperty DirectoryNameProperty = DependencyProperty.Register(
	FLD_DirectoryName, typeof(string), typeof(DnwDirectoryPicker),  	
new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

La modifica è stata inserita nella definizione dei metadati della property ove non solo assegnamo un valore di default (“”) ma indichiamo anche che il Binding di default sarà bidirezionale.

Il Progetto DnwUISettings

Abbiamo aggiunto un progetto alle DnwLibraries,  questo nuovo progetto che è una libreria di controlli WPF in cui abbiamo inserito due classi, uno User control ed il suo View Model.

DnwUISettings_01[6]

Anche in questo caso abbiamo aggiornato il progetto della libreria modificando il nome ed il namespace di default della libreria:

DnwUISettings_02[6]

Ed abbiamo modificato il Post Build Event per pubblicare le Librerie in modo che siano referenziate sempre nello stesso luogo in tutti i progetti presenti e futuri.

DnwUISettings_03[6]

La classe SettingsPageModel

Questa classe è il View Model dello User Control per la gestione dei setting applicativi.

public class SettingsPageModel : INotifyPropertyChanged
{
....
}

Come in tutte le classi model, implementiamo l’interfaccia INotifyPropertyChanged per fare in modo che i controlli siano notificati delle modifiche a questo modello. Questo significa che implementeremo l’ Evento PropertyChanged e il suo metodo di generazione OnPropertyChanged.

public string SettingsFileName
{
	get
	{
		return mSettingsFileName;
	}
	set
	{
		mSettingsFileName = value;
		OnPropertyChanged(FLD_SettingsFileName);
	}
}

public DnwSettingsCollection Settings
{
	get
	{
		return mSettings;
	}
}

Aggiungiamo due property al nostro modello, la prima permette di definire il nome del file in cui leggere e dove salvare i dati dei settings. Come vedete si tratta di una User Interface molto specializzata, che assume che lavoriamo esclusivamente con un file, in futuro, se necessario potremo implementare delle modifiche che ci permettano di caricare i dati anche in modo diverso.

La seconda Property è read only ed espone la collezione dei setting che questo modello gestisce.

public void Clear()
{
	mSettings = new DnwSettingsCollection();
}

public void LoadSettings()
{
	try
	{
		if (File.Exists(SettingsFileName))
		{
			mSettings = DnwSettingsCollection.ReadXml(SettingsFileName, false);
		}
		else
		{
			mSettings = new DnwSettingsCollection();
		}

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

public void SaveSettings()
{
	Settings.WriteXml(SettingsFileName);
}

Tre metodi pubblici che useremo per interagire con il controllo, il metodo Clear, che cancella il contenuto della collezione e ne crea una vuota se volessimo creare un interfaccia per costruire setting al volo.

Il metodo LoadSettings, che carica il contenuto del file indicato al modello (e per sicurezza se il file non esiste crea una collezione vuota).

Il metodo SaveSettings, che scrive il contenuto della collezione sul file indicato al modello.

Lo user control SettingsPage

Questo User control è costruito per permetterci di modificare una collezione arbitraria di settings con gli editor che abbiamo reso disponibili nell’enumerazione costruita allo scopo.

<UserControl x:Class="Dnw.UI.Settings.Controls.SettingsPage"              
    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"               
    mc:Ignorable="d"               
    d:DesignHeight="640" 
    d:DesignWidth="960">
	<UserControl.Resources>
		<ResourceDictionary>
			<ResourceDictionary.MergedDictionaries>
				<ResourceDictionary 
Source
="pack://application:,,,/Dnw.Base.Wpf.v4.0;component/Styles.xaml"/> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </UserControl.Resources>     <Grid Margin="0,0,0,0"> <Grid.RowDefinitions> <RowDefinition Height="36"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <TextBlock Grid.Row="0" Style="{StaticResource Header}" 
HorizontalAlignment
="Left" VerticalAlignment="Center"  
Margin
="10,2,10,2" Text="{Binding SettingsFileName}"/> <Grid Grid.Row="0" Name ="grdScaffold" Margin="0,0,0,0"> <Grid.RowDefinitions> <RowDefinition Height="*"/> </Grid.RowDefinitions> </Grid> </Grid> </UserControl>

Lo XAML dello user control, possiamo osservare che è molto semplice, infatti ospita l’inclusione del resource manager degli stili comuni di WPF da noi definiti, una grid con 2 righe  che contiene un TextBlock che visualizza il contenuto del campo SettingsFileName del model ed una seconda grid con una sola riga completamente vuota che diverrà il contenitore dei controlli.

Non abbiamo definito altro perché il contenuto della seconda grid del controllo, ovvero l’editor dei settings sarà costruito via codice a Runtime. Vediamo come:

public partial class SettingsPage : UserControl
{

	private SettingsPageModel mDataModel;

	public SettingsPage()
	{

		InitializeComponent();
	}

.............

}

La classe user control contiene una sola variabile member, il modello dati.

public void Init(string fileName)
{
	mDataModel = new SettingsPageModel();
	mDataModel.SettingsFileName = fileName;
	mDataModel.LoadSettings();
	this.DataContext = mDataModel;
	GenerateControls();
}

public void Save()
{
	mDataModel.SaveSettings();
}

public void Clear()
{
	mDataModel = new SettingsPageModel();
	mDataModel.Clear();
	this.DataContext = mDataModel;
	GenerateControls();

}

Tre metodi pubblici per riempire lo User Control, per salvare i dati e per svuotarlo.

Il metodo GenerateControls

private void GenerateControls()
{

	int rows = mDataModel.Settings.GridRowsCount();
	grdScaffold.Children.Clear();
	Grid innerGrid = new Grid();
	innerGrid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(10, GridUnitType.Star) });
	innerGrid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(40, GridUnitType.Star) });
	innerGrid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(50, GridUnitType.Star) });
	for (int i = 0; i < rows; i++)
	{
		innerGrid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Auto) });
	}

	innerGrid.Margin = new Thickness(5);

	grdScaffold.Children.Add(innerGrid);
	Grid.SetRow(innerGrid, 1);

	int countRows = 0;
	string oldCat = null;
	foreach (DnwSetting item in mDataModel.Settings.GetOrderedList().ToList())
	{
		if (item.Category != oldCat)
		{
			GenerateCategoryHeader(innerGrid, item, countRows);
			SetGridBorder(innerGrid, Colors.Gray, 1, 0, countRows, true);
			countRows++;
			oldCat = item.Category;
		}

		switch (item.EditorType)
		{
			case EditorType.NumericInteger:
				GenerateNumericIntegerRow(innerGrid, item, countRows);
				break;
			case EditorType.NumericDouble:
				GenerateNumericDoubleRow(innerGrid, item, countRows);
				break;
			case EditorType.CheckBox:
				GenerateCheckboxRow(innerGrid, item, countRows);
				break;
			case EditorType.Date:
				GenerateDateRow(innerGrid, item, countRows);
				break;
			case EditorType.TimeShort:
				GenerateTimeShortRow(innerGrid, item, countRows);
				break;
			case EditorType.TimeLong:
				GenerateTimeLongRow(innerGrid, item, countRows);
				break;
			case EditorType.FileName:
				GenerateFileNameRow(innerGrid, item, countRows);
				break;
			case EditorType.DirectoryName:
				GenerateDirectoryNameRow(innerGrid, item, countRows);
				break;
			case EditorType.SqlConnectionsFile:
				GenerateSqlConnectionsFileRow(innerGrid, item, countRows);
				break;
			default:
				GenerateTextBoxRow(innerGrid, item, countRows);
				break;
		}
		SetGridBorder(innerGrid, Colors.Gray, 1, 0, countRows, false);
		countRows++;
	}
}

Il metodo che genera i controlli, vediamo che cosa fa questo metodo:

  • Chiede al modello quante righe dovrà generare, pulisce il contenuto della grid (se dovessimo riempire nuovamente il controllo)
  • Costruisce un oggetto grid che conterrà tutti i controlli, genera una serie di colonne, per la precisione 3 di diversa dimensione proporzionali al contenuto che ospiteranno i dati relativi ai setting visualizzati.
  • Assegna alla grid generata i margini e la inserisce nella riga della Grid da noi predisposta allo scopo indicando anche in quale riga inserirla.
  • Poi effettua un ciclo sulla lista dei settings organizzata per categoria e posizione (ricordate il metodo che abbiamo aggiunto alla collection).
  • Alla variazione della categoria crea un intestazione con la descrizione della categoria
  • Per ogni elemento della collection genera un editor corrispondente a quello specificato e crea un opportuno bordo per dare visualmente un aspetto a griglia all’interfaccia.

Per chi non avesse mai modificato i valori di controlli WPF da Codice  evidenziamo solo un passaggio nel codice qui sopra definito.

Grid.SetRow(innerGrid, 0);

Questo codice indica alla innerGrid in quale riga deve posizionarsi all’interno della sua Grid contenitore, quando andiamo a modificare delle Dependency property, siccome tali property vengono “agganciate” all’oggetto figlio da parte del suo contenitore (in questo caso sono agganciate alla innerGrid dalla scaffoldGrid, non vanno modificate sul padre ma sul figlio ecco perché la sintassi è:

NomeClasse.SetNomeDependencyProperty(nomeOggettoFiglio, ValoreDependencyProperty)

Esploriamo ora i metodi che generano i controlli e ne formattano il contenuto collegandolo ai dati.

Il metodo GenerateCategoryHeader

private void GenerateCategoryHeader(Grid gridToSet, DnwSetting item, int row)
{
	Grid categoryGrid = new Grid();
	categoryGrid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Auto) });
	categoryGrid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Star) });
	categoryGrid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Auto) });
	TextBlock txt = new TextBlock();
	txt.Text = item.Category;
	txt.Margin = new Thickness(5, 10, 3, 5);
	txt.Style = Resources["Header"] as Style;
	categoryGrid.Children.Add(txt);
	Grid.SetColumn(txt, 0);
	Grid.SetRow(txt, 0);
	Rectangle rect = new Rectangle();
	rect.Margin = new Thickness(5, 0, 5, 0);
	rect.HorizontalAlignment = HorizontalAlignment.Stretch;
	rect.VerticalAlignment = VerticalAlignment.Stretch;
	rect.Height = 1;
	rect.Stroke = new SolidColorBrush(Colors.DarkGray);
	rect.StrokeThickness = 1;
	categoryGrid.Children.Add(rect);
	Grid.SetColumn(rect, 1);
	Grid.SetRow(rect, 0);
	gridToSet.Children.Add(categoryGrid);
	Grid.SetRow(categoryGrid, row);
	Grid.SetColumnSpan(categoryGrid, 3);
}

Il codice che genera l’intestazione al cambio della categoria dei controlli:

DnwUISettings_04[6]

Da origine a ciò che mostra l’immagine qui sopra, vediamo in quale modo:

  • Genera una Grid con 2 colonne, la prima si dimensiona automaticamente in base al contenuto, la seconda occupa tutto lo spazio rimanente.
  • Genera un TextBlock a cui assegna il contenuto del valore della categoria dell’Item che abbiamo passato al metodo inoltre
    • Assegna allo stile del TextBlock lo stile Header, che è fornito allo User Control dal Merged Dictionary derivante da “Styles.xaml” dentro alla libreria Dnw.Base.Wpf.v4.0.
    • Assegna un margine al testo in modo che sia distanziato dal controllo che lo precede
    • Inserisce il TextBlock fra i Children della grid contenitore da noi creata.
    • Posiziona il controllo sulla prima colonna della prima (ed unica) riga della grid contenitore
  • Genera un Rectangle che creerà la linea grigia accanto al testo.
    • Gli da un margine in modo da generare un po’ di spazio fra il rettangolo ed il testo.
    • Indica al controllo che il suo allineamento orizzontale è “Stretch” ovvero occupa tutto lo spazio.
    • Indica che è alto 1 pixel
    • Lo aggiunge alla Grid contenitore
    • Indica che deve occupare la seconda colonna della prima riga
  • Aggiunge la Grid con i due controlli alla Grid principale.
  • Indica su che riga della Grid principale deve essere posizionata
  • Indica che questo controllo occupa tutte e tre le colonne della Grid che la contiene.

Il metodo GenerateDescriptions

private void GenerateDescriptions(Grid gridContainer, DnwSetting item, int row)
{
	TextBlock txt = new TextBlock();
	txt.Text = item.ID;
	txt.Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#0C670A"));
	txt.FontWeight = FontWeights.Bold;
	txt.Margin = new Thickness(5, 2, 5, 2);
	txt.Padding = new Thickness(2);
	txt.HorizontalAlignment = HorizontalAlignment.Center;
	txt.VerticalAlignment = VerticalAlignment.Center;
	gridContainer.Children.Add(txt);
	Grid.SetRow(txt, row);
	Grid.SetColumn(txt, 0);
	txt = new TextBlock();
	txt.Text = item.Description;
	txt.Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#062a7b"));
	txt.Margin = new Thickness(5, 2, 5, 2);
	txt.HorizontalAlignment = HorizontalAlignment.Right;
	txt.VerticalAlignment = VerticalAlignment.Center;
	txt.Padding = new Thickness(2);

	gridContainer.Children.Add(txt);
	Grid.SetRow(txt, row);
	Grid.SetColumn(txt, 1);
}

Il metodo Generate descriptions si occupa di generare per ogni DnwSetting le due colonne che mostrano il suo ID e la sua Descrizione che sono ovviamente uguali per tutti i controlli. vediamo come fa:

  • Genera un TextBlock cper inserire l’ ID del setting
    • Assegna il colore verde ed il grassetto alla font
    • Assegna i margini ed il padding interno al controllo
    • Assegna l’allineamento orizzontale e verticale rispetto al contenitore.
  • Aggiunge il TextBlock alla Grid contenitore
    • Indica che deve essere inserito nella prima colonna della riga corrente
  • Genera un secondo Textblock per la descrizione.
    • Assegna il colore alla font
    • Assegna il margine
    • Lo allinea orizzontalmente a destra e verticalmente al centro
    • Indica un padding interno per il testo.
  • Aggiunge il TextBlock alla Grid contenitore
    • Indica che deve essere inserito nella seconda colonna della riga corrente.

Il metodo GenerateTextBoxRow

private void GenerateTextBoxRow(Grid gridContainer, DnwSetting item, int row)
{
	GenerateDescriptions(gridContainer, item, row);

	TextBox txb = new TextBox();
	txb.HorizontalAlignment = HorizontalAlignment.Stretch;
	txb.VerticalAlignment = VerticalAlignment.Center;
	txb.Margin = new Thickness(5);
	txb.Padding = new Thickness(2);

	Binding vBinding = new Binding(DnwSetting.FLD_Value);
	vBinding.Source = item;
	txb.SetBinding(TextBox.TextProperty, vBinding);
	gridContainer.Children.Add(txb);
	Grid.SetRow(txb, row);
	Grid.SetColumn(txb, 2);
}

La generazione di un editor di tipo TextBox per un setting.

  • Il metodo genera le descrizioni per la riga corrente
  • Genera una TextBox
    • Assegna l’allineamento all’interno del contenitore
    • Assegma margini e padding
  • Genera un oggetto Binding, indicando il nome della property della classe DnwSetting a cui deve essere collegato (In questo caso la property Value).
  • Indica che l’oggetto sorgente per il Binding è l’elemento corrente della collection (item)
  • Aggancia il Binding alla dependency property Text della TextBox
  • Aggiunge la TextBox alla grid contenitore
    • Indica di posizionarla nella terza colonna della riga corrente.

Il metodo GenerateNumericIntegerRow (e Generate NumericDoubleRow)

private void GenerateNumericIntegerRow(Grid gridContainer, DnwSetting item, int row)
{
	GenerateDescriptions(gridContainer, item, row);
	IntegerUpDown iud = new IntegerUpDown();
	if (item.Mask.XDwIsNullOrTrimEmpty())
	{
		iud.FormatString = "N0";
	}
	else
	{
		iud.FormatString = item.Mask;
	}
	iud.Padding = new Thickness(2);
	iud.Margin = new Thickness(5);
	Binding vBinding = new Binding(DnwSetting.FLD_Value);
	vBinding.Source = item;
	iud.SetBinding(IntegerUpDown.ValueProperty, vBinding);
	gridContainer.Children.Add(iud);
	Grid.SetRow(iud, row);
	Grid.SetColumn(iud, 2);
}

private void GenerateNumericDoubleRow(Grid gridContainer, DnwSetting item, int row)
{
	GenerateDescriptions(gridContainer, item, row);
	DoubleUpDown dud = new DoubleUpDown();
	if (item.Mask.XDwIsNullOrTrimEmpty())
	{
		dud.FormatString = "F6";
	}
	else
	{
		dud.FormatString = item.Mask;
	}
	dud.Padding = new Thickness(2);
	dud.Margin = new Thickness(5);
	Binding vBinding = new Binding(DnwSetting.FLD_Value);
	vBinding.Source = item;
	dud.SetBinding(DoubleUpDown.ValueProperty, vBinding);
	gridContainer.Children.Add(dud);
	Grid.SetRow(dud, row);
	Grid.SetColumn(dud, 2);
}

La generazione di un editor di tipo numerico intero o un editor Double sono identici fatto salvo che utilizziamo un controllo IntegerUpDown o un controllo DoubleUpDown quindi li discuteremo come un singolo editor.

  • Il metodo genera le descrizioni per la riga corrente
  • Crea un editor IntegerUpDown (Si trova in WPFToolkit.Extended disponibile su Codeplex) o un DoubleUpDown
  • Assegna il valore della property Mask se trovato (il tipo può essere seguito da un numero che indica i decimali di default da usare)
Format Specifier Name
C Currency
F Fixed Point
G General
N Number
P Percent
  • Un esempio dei valori che possono assumere le mask
    • N0 = Numero 0 decimali
    • F6 = A virgola fissa 6 decimali quindi il valore viene sempre arrotondato in base al numero di decimali.
    • C2 = Valuta  2 decimali
    • P0 = Percentuale senza virgole
  • Viene generato un Oggetto Binding sulla property Value della classe DnwSetting
  • Viene indicato che la sorgente per il binding è l’Item corrente della collection.
  • Il Binding viene assegnato alla DependencyProperty Value del controllo IntegerUpDown (o DoubleUpDown).
  • Il controllo viene inserito nella grid contenitore assegnandogli riga e colonna.

Il metodo GenerateCheckboxRow

private void GenerateCheckboxRow(Grid gridContainer, DnwSetting item, int row)
{
	GenerateDescriptions(gridContainer, item, row);
	CheckBox chk = new CheckBox();

	chk.Padding = new Thickness(2);
	chk.Margin = new Thickness(5);
	chk.VerticalAlignment = System.Windows.VerticalAlignment.Center;
	chk.HorizontalAlignment = System.Windows.HorizontalAlignment.Left;
	Binding vBinding = new Binding(DnwSetting.FLD_Value);
	vBinding.Source = item;
	chk.SetBinding(CheckBox.IsCheckedProperty, vBinding);
	gridContainer.Children.Add(chk);
	Grid.SetRow(chk, row);
	Grid.SetColumn(chk, 2);

}

Simile ai precedenti se non per il fatto che il Value di DnwSetting viene collegato alla IsChecked property della CheckBox per l’Item corrente della collection.

I metodi GenerateDateRow, GenerateTimeShortRow, GenerateTimeLongRow

private void GenerateDateRow(Grid gridContainer, DnwSetting item, int row)
{
	GenerateDescriptions(gridContainer, item, row);
	DateTimePicker dtl = new DateTimePicker();
	dtl.Format = DateTimeFormat.ShortDate;
	dtl.Padding = new Thickness(2);
	dtl.Margin = new Thickness(5);
	Binding vBinding = new Binding(DnwSetting.FLD_Value);
	vBinding.Source = item;
	dtl.SetBinding(DateTimePicker.ValueProperty, vBinding);
	gridContainer.Children.Add(dtl);
	Grid.SetRow(dtl, row);
	Grid.SetColumn(dtl, 2);
}

private void GenerateTimeShortRow(Grid gridContainer, DnwSetting item, int row)
{
	GenerateDescriptions(gridContainer, item, row);
	DateTimeUpDown dts = new DateTimeUpDown();
	dts.Format = DateTimeFormat.ShortTime;
	dts.Padding = new Thickness(2);
	dts.Margin = new Thickness(5);

	Binding vBinding = new Binding(DnwSetting.FLD_Value);
	vBinding.Source = item;
	dts.SetBinding(DateTimeUpDown.ValueProperty, vBinding);
	gridContainer.Children.Add(dts);
	Grid.SetRow(dts, row);
	Grid.SetColumn(dts, 2);
}


private void GenerateTimeLongRow(Grid gridContainer, DnwSetting item, int row)
{
	GenerateDescriptions(gridContainer, item, row);
	DateTimeUpDown dtl = new DateTimeUpDown();
	dtl.Format = DateTimeFormat.LongTime;
	dtl.Padding = new Thickness(2);
	dtl.Margin = new Thickness(5);

	Binding vBinding = new Binding(DnwSetting.FLD_Value);
	vBinding.Source = item;
	dtl.SetBinding(DateTimeUpDown.ValueProperty, vBinding);
	gridContainer.Children.Add(dtl);
	Grid.SetRow(dtl, row);
	Grid.SetColumn(dtl, 2);
}

Anche questi tre metodi sono simili ai precedenti, utilizziamo un DateTimePicker o un DateTimeUpDown  effettuando un Binding della property Value dell’Item della collection alla DependencyProperty Value del controllo. Nei tre metodi varia solo una cosa:

  • La property Format del controllo che vale rispettivamente
    • DateTimeFormat.ShortDate  (dd/mm/yyyy)
    • DateTimeFormat.ShortTime    (hh:mm)
    • DateTimeFormat.LongTime    (hh:mm:ss)

Il metodo GernerateSqlConnectionsFileRow

private void GenerateSqlConnectionsFileRow(Grid gridContainer, DnwSetting item, int row)
{
	GenerateDescriptions(gridContainer, item, row);
	SqlConnectionFileEditorControl dp = new SqlConnectionFileEditorControl();
	dp.Padding = new Thickness(2);

	Binding vBinding = new Binding(DnwSetting.FLD_Value);
	vBinding.Source = item;
	vBinding.Mode = BindingMode.TwoWay;
	dp.SetBinding(SqlConnectionFileEditorControl.FileNameProperty, vBinding);
	gridContainer.Children.Add(dp);
	Grid.SetRow(dp, row);
	Grid.SetColumn(dp, 2);
}

Il primo dei tre User Control da noi generati, anche in questo caso il metodo è simile ai precedenti, evidenziamo solo che:

  • Ci connettiamo alla DependencyProperty FileName del controllo
  • Specifichiamo che il Binding deve essere bidirezionale.

Non dobbiamo fare nulla perché il controllo possa gestire il contenuto del file, perché l’editor per la modifica del contenuto è interno allo User Control, in questo caso quello che ci interessa è il nome del file che inseriremo nel campo Value del setting che lo ospita.

Il metodo GenerateFileNameRow

private void GenerateFileNameRow(Grid gridContainer, DnwSetting item, int row)
{
	GenerateDescriptions(gridContainer, item, row);
	DnwFilePicker fp = new DnwFilePicker();
	fp.Padding = new Thickness(2);
	Binding vBinding = new Binding(DnwSetting.FLD_Value);
	vBinding.Source = item;
	vBinding.Mode = BindingMode.TwoWay;
	fp.SetBinding(DnwFilePicker.FileNameProperty, vBinding);
	gridContainer.Children.Add(fp);
	Grid.SetRow(fp, row);
	Grid.SetColumn(fp, 2);

}

Il secondo User Control, che possiamo notare ha lo stesso tipo di generazione del precedente.

Il metodo GenerateDirectoryNameRow

private void GenerateDirectoryNameRow(Grid gridContainer, DnwSetting item, int row)
{
	GenerateDescriptions(gridContainer, item, row);
	DnwDirectoryPicker dp = new DnwDirectoryPicker();
	dp.Padding = new Thickness(2);
	Binding vBinding = new Binding(DnwSetting.FLD_Value);
	vBinding.Source = item;
	vBinding.Mode = BindingMode.TwoWay;
	dp.SetBinding(DnwDirectoryPicker.DirectoryNameProperty, vBinding);
	gridContainer.Children.Add(dp);
	Grid.SetRow(dp, row);
	Grid.SetColumn(dp, 2);

}

Il terzo User Control, identico ai precedenti se non per il nome della Dependency property che è DirectoryName.

Il Metodo SetGridBorder

private void SetGridBorder(Grid gridToSet, Color borderColor, int borderThickness, int radius, int row, bool isCategory)
{
	int rows = gridToSet.RowDefinitions.Count;
	int cols = gridToSet.ColumnDefinitions.Count;


	for (int j = 0; j < cols; j++)
	{
		Border b = new Border() { BorderBrush = new SolidColorBrush(borderColor) };
		if (isCategory)
		{
			if (row == 0)
			{
				b.BorderThickness = new Thickness(0);
			}
			else
			{
				b.BorderThickness = new Thickness(0, borderThickness, 0, 0);
			}
		}
		else
		{
			if (j == 0)
			{
				if (row == rows - 1)
				{
					b.BorderThickness = new Thickness(borderThickness);
				}
				else
				{
					b.BorderThickness = new Thickness(borderThickness, borderThickness, borderThickness, 0);
				}
			}
			else
			{
				if (row == rows - 1)
				{
					b.BorderThickness = new Thickness(0, borderThickness, borderThickness, borderThickness);
				}
				else
				{
					b.BorderThickness = new Thickness(0, borderThickness, borderThickness, 0);
				}
			}
		}
		if (radius != 0)
		{
			b.CornerRadius = new CornerRadius(radius);
		}
		gridToSet.Children.Add(b);
		Grid.SetColumn(b, j);
		Grid.SetRow(b, row);
	}

}

Il metodo SetGridBorder è, almeno per quel che riguarda la lunghezza, il più complicato della classe, non tanto perché faccia qualcosa di particolarmente difficile ma perché se non vogliamo creare dei bordi brutti a vedersi dobbiamo verificare in quale punto della lista ci troviamo. Per ora i parametri di generazione sono tutti hardcoded, in futuro potremmo esporre colore, raggio degli angoli e grossezza della linea come property dello User Control perché possano essere modificate al momento dell’uso dello stesso. Assieme alle property per i colori del testo.

Il Progetto di test dello User Control

Per testare questo User Control, costruiamo il nostro solito progetto di test con una Window.

DnwUISettings_05[6]

DnwUISettings_06[6]

Per il nostro test abbiamo creato una Window piuttosto semplice, che riutilizza anche uno degli user control  da noi creati.

<Window x:Class="Dnw.TestGenerateControls.Windows.MainWindow"
    x:Name="iome"         
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"         
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:stt="clr-namespace:Dnw.UI.Settings.Controls;assembly=Dnw.UI.Settings.v4.0"
    xmlns:ctl ="clr-namespace:Dnw.Base.Wpf.Controls;assembly=Dnw.Base.Wpf.v4.0"         
    Title="MainWindow" Height="640" Width="960" Loaded="Window_Loaded">
	<Window.Resources>
		<ResourceDictionary>
			<ResourceDictionary.MergedDictionaries>
				<ResourceDictionary  Source="pack://application:,,,/Dnw.Base.Wpf.v4.0;component/Styles.xaml"/>
			</ResourceDictionary.MergedDictionaries>
		</ResourceDictionary>
		
	</Window.Resources>
	<Grid>
		<Grid.RowDefinitions>
			<RowDefinition Height="Auto"/>
			<RowDefinition Height="Auto"/>
			<RowDefinition Height="*"/>
			<RowDefinition Height="Auto"/>
		</Grid.RowDefinitions>
		<TextBlock Grid.Row="0" Style="{StaticResource MainInstruction}"  VerticalAlignment="Center" Margin="10">Test the settings control</TextBlock>
		<Grid Grid.Row="1">
			<Grid.RowDefinitions>
				<RowDefinition Height="Auto"/>
			</Grid.RowDefinitions>
			<Grid.ColumnDefinitions>
				<ColumnDefinition Width="2*"/>
				<ColumnDefinition Width="8*"/>
			</Grid.ColumnDefinitions>
			<TextBlock  Grid.Column="0" VerticalAlignment="Center"  HorizontalAlignment="Right" Margin="5,0,5,0" Text="Nome file" />
			<ctl:DnwFilePicker Grid.Column="1"   FileName="{Binding ElementName=iome,  Path=SettingsFileName, Mode=TwoWay}"  Margin="5,0,5,0" HorizontalAlignment="Stretch"/>
 
		</Grid>
		<ScrollViewer Grid.Row="2" Margin="0,0,0,0" >
		<stt:SettingsPage Name="sttEditor" Margin="5" >
		
		</stt:SettingsPage>
		</ScrollViewer>
		<StackPanel Grid.Row="3" FlowDirection="LeftToRight" Orientation="Horizontal">
			<Button Click="btnGen_Click" Margin="5,10,5,10" Padding="5,0,5,0">
				<TextBlock Padding="5" VerticalAlignment="Center">Generate Test File</TextBlock>
			</Button>
			<Button Click="btnSave_Click" Margin="5,10,5,10" Padding="5,0,5,0">
				<TextBlock Padding="5" VerticalAlignment="Center" >Save to Test File</TextBlock>
			</Button>
			<Button Click="btnClear_Click" Margin="5,10,5,10" Padding="5,0,5,0">
				<TextBlock Padding="5" VerticalAlignment="Center">Clear Window</TextBlock>
			</Button>
			<Button Click="btnLoad_Click" Margin="5,10,5,10" Padding="5,0,5,0">
				<TextBlock Padding="5" VerticalAlignment="Center">Load from Test File</TextBlock>
			</Button>
		</StackPanel>
	</Grid>
 
 
</Window> 

Lo XAML della window, in cui abbiamo messo Una grid con quattro righe, che contiene

  • La main instruction una textblock stilizzata usando uno degli stili che sono nel Merged Dictionary importato dalla libreria di base
  • Un DnwFilePicker user control, che ci permetterà di dare il nome al nostro file XML dove leggere e scrivere i settings
  • Lo User Control con i setting dinamici
  • Uno Stack panel con i button che ci permettono di effettuare i test.

Vediamo il codice di test

public partial class MainWindow : Window, INotifyPropertyChanged
{
.....
}

La nostra classe, in questo caso fa anche da View Model a se stessa, quindi implementiamo il NotifyPropertyChanged con il suo evento.

public string SettingsFileName
{
	get
	{
		return mSettingsFileName;
	}
	set
	{
		mSettingsFileName = value;
		OnPropertyChanged(FLD_SettingsFileName);
	}
}

La property che nello XAML abbiamo messo in Binding allo User Control DnwFilePicker in modo che contenga il file selezionato.

private void Window_Loaded(object sender, RoutedEventArgs e)
{
	this.DataContext = this;
	SettingsFileName = Path.Combine(mPath, "TestSettings.xml");
}

L’evento di caricamento, ove indichiamo alla Window che lei stessa è il suo View Model e creiamo un nome di file di default per il test.

private void GenerateTestSettings()
{
	DnwSettingsCollection coll = new DnwSettingsCollection();
	coll.Add(new DnwSetting()
	{
		Category = "N.1 Category",
		ID = "Timer",
		Description = "Interval in seconds",
		EditorType = EditorType.NumericInteger,
		Mask = "N0",
		Value = "60",
		Position = 1
	});

	coll.Add(new DnwSetting()
	{
		Category = "N.1 Category",
		ID = "Money",
		Description = "Amount of money",
		EditorType = EditorType.NumericDouble,
		Mask = "F3",
		Value = "1350.55",
		Position = 3
	});
	coll.Add(new DnwSetting()
	{
		Category = "N.1 Category",
		ID = "Date",
		Description = "Start date",
		EditorType = EditorType.Date,
		Value = "04/04/1965",
		Position = 4
	});

	coll.Add(new DnwSetting()
	{
		Category = "N.1 Category",
		ID = "STime",
		Description = "Start time",
		EditorType = EditorType.TimeShort,
		Value = "09:00",
		Position = 5
	});

	coll.Add(new DnwSetting()
	{
		Category = "N.1 Category",
		ID = "BTime",
		Description = "Backup Time",
		EditorType = EditorType.TimeLong,
		Value = "06:00:10",
		Position = 6
	});

	coll.Add(new DnwSetting()
	{
		Category = "N.1 Category",
		ID = "Title",
		Description = "Title of the window",
		EditorType = EditorType.TextBox,
		Value = "Ciao",
		Position = 7
	});

	coll.Add(new DnwSetting()
	{
		Category = "N.1 Category",
		ID = "CHK",
		Description = "Attiva il backup",
		EditorType = EditorType.CheckBox,
		Value = "Ciao",
		Position = 8
	});

	coll.Add(new DnwSetting()
	{
		Category = "N.2 Category",
		ID = "DIR",
		Description = "Directory dove salvare i file",
		EditorType = EditorType.DirectoryName,
		Value = "C:\\",
		Position = 10
	});

	coll.Add(new DnwSetting()
	{
		Category = "N.2 Category",
		ID = "JOB",
		Description = "Job file name",
		EditorType = EditorType.FileName,
		Value = "",
		Position = 11
	});

	coll.Add(new DnwSetting()
	{
		Category = "N.2 Category",
		ID = "SQL",
		Description = "Stringa di connessione",
		EditorType = EditorType.SqlConnectionsFile,
		Value = "",
		Position = 12
	});

	coll.WriteXml(SettingsFileName);

}

Un metodo per generare automaticamente tutti i possibili tipi di setting con i loro editor per le nostre prove.

private void btnGen_Click(object sender, RoutedEventArgs e)
{
	GenerateTestSettings();
	sttEditor.Init(SettingsFileName);
}

private void btnSave_Click(object sender, RoutedEventArgs e)
{
	sttEditor.Save();
}

private void btnLoad_Click(object sender, RoutedEventArgs e)
{
	sttEditor.Init(SettingsFileName);
}

private void btnClear_Click(object sender, RoutedEventArgs e)
{
	sttEditor.Clear();
	this.SettingsFileName = string.Empty;
}

Gli event handler dei buttons, che utilizzano i metodi dello User Control.

Conclusioni

Vorremmo far notare, che per agganciare la gestione del nome del file dei settings al codice della nostra window non abbiamo avuto bisogno di altro che della property e dell’evento property changed, tutta la logica dietro allo user control è quella che abbiamo scritto nello scorso Post.

WPF è uno strumento complesso ma molto potente per permetterci di creare codice riutilizzabile e la sua struttura di Binding funzionale non solo alla gestione dei dati permette di costruire applicazioni complesse in modo efficace, posto che abbiamo un po’ di passione per i Lego 😀 ovvero per costruire mattoncini ricombinabili.

Il codice esempio che testa la generazione dei controlli è disponibile al seguente link:

Le librerie di uso comune dove i controlli sono stati inseriti è disponibile al link seguente:

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

Le librerie di supporto ai controlli utilizzati dalle librerie di uso comune e nei progetti successivi sono disponibili ai link seguenti: