Press "Enter" to skip to content

Usare il ViewModel per il Binding di Controlli generati a runtime

Rispondo ad una domanda posta sui forum Microsoft da uno sviluppatore che sta generando una interfaccia utente WPF in cui vi sono dei controlli generati a Runtime con alcuni dati, che sono calcolati in base al valore impostato su uno dei controlli generati a runtime.

In questo caso, il suo codice cercava di utilizzare il metodo che si utilizzava sulle Windows Forms, utilizzando un event handler per calcolare un valore e il risultato non era quello che lui si aspettava. In questo caso, lavorando con WPF bisogna cambiare completamente la prospettiva, cambiare il modo di lavorare, perché utilizzare le funzionalità forniteci “gratis” da WPF e da MVVM ci permettono di risparmiare codice e soprattutto l’ufficio complicazione affari semplici.

Ecco un esempio in cui utilizzando MVVM si risparmia tempo e fatica.

Nel mio progetto, ho per prima cosa creato un oggetto che mi rappresenta una riga di una “tabella” se così possiamo definirla, che viene generata a runtime.

public class TankData : INotifyPropertyChanged
{
 
	public TankData()
	{
 
	}
 
 
	public const string FLD_ID = "ID";
 
	private string mID;
 
	public string ID
	{
		get
		{
			return mID;
		}
		set
		{
			mID = value;
			OnPropertyChanged(FLD_ID);
		}
	}
 
 
	public const string FLD_Temperature = "Temperature";
 
	private string mTemperature;
 
	public string Temperature
	{
		get
		{
			return mTemperature;
		}
		set
		{
			mTemperature = value;
			OnPropertyChanged(FLD_Mc);
			OnPropertyChanged(FLD_Temperature);
		}
	}
 
 
	public const string FLD_Ullage = "Ullage";
 
	private string mUllage;
 
	public string Ullage
	{
		get
		{
			return mUllage;
		}
		set
		{
			mUllage = value;
			OnPropertyChanged(FLD_Mc);
			OnPropertyChanged(FLD_Ullage);
		}
	}
 
 
	public const string FLD_Mc = "Mc";
 
	public string Mc
	{
		get
		{
			double temp = 0.0;
			double.TryParse(Temperature.Replace(",","."), out temp);
			double ull = 0.0;
			double.TryParse(Ullage.Replace(",","."), out ull);
			return Math.Round(temp*ull,2).ToString();
		}
	}
 

	public event PropertyChangedEventHandler PropertyChanged;
 
	protected virtual void OnPropertyChanged(string propertyName)
	{
		if (PropertyChanged != null)
			PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
	}
 
 
}

Questo è l’oggetto che voglio rappresentare a Video in una serie di N elementi, l’oggetto ha 4 properties, ID, Temperature, Ullage e Mc. Per semplicità sono tutti stringhe, visto che è un semplice esempio e usare controlli diversi dallla TextBox per gestire dei numeri avrebbe esulato dallo scopo dell’esempio stesso. Di queste 4 property, tre sono modificabili, la quarta è calcolata, per semplicità Mc vale Temperature * Ullage.

Da notare nel codice qui sopra implementato sono un paio di cose:

  • Ogni Property ha la definizione della costante per il suo nome, vedremo poi come sia comodo in fase di creazione dei controlli da codice.
  • Quando viene modificato uno degli operandi (Temperature o Ullage), oltre a lanciare l’evento PropertyChanged per se stesse, le property lanciano anche quello di Mc, perché alla variazione di una delle due ovviamente anche Mc deve essere ridisegnato a video.
  • L’evento PropertyChanged e l’interfaccia INotifyPropertyChanged permettono a WPF di ricevere notifica delle modifiche effettuate ai dati delle property e aggiornare automaticamente ogni occorrenza delle stesse sulla User Interface.

Vediamo ora il codice della MainWindow.

<Window x:Class="ProgrammaticallyGenerated.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ProgrammaticallyGenerated"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525" Loaded="MainWindow_Loaded">
    <Grid >
        <StackPanel Orientation="Vertical" Name="ContentContainer">
            
        </StackPanel>
    </Grid>
</Window>

La parte XAML della Window è alquanto semplice, infatti c’è solo la grid principale con all’interno uno StackPanel a cui abbiamo inizializzato l’attributo Name, infatti per poter inserirvi dentro i controlli a Runtime, dobbiamo poterlo referenziare.

L’unica altra modifica alla window standard creata quando viene creato il progetto è il fatto di aver inserito un event handler per l’evento Loaded della Window, in questo evento andremo a generare i nostri controlli.

Vediamo ora il Code Behind della nostra MainWindow.

List<TankData> mTanks;

Definiamo nella classe una collection di oggetti TankData, che andremo a riempire con le classi che genereremo assieme ai controlli.

public MainWindow()
{
	InitializeComponent();
	DataContext = this;
	mTanks = new List<TankData>();
}

Nel costruttore inseriamo due importantissime righe di codice oltre all’InitializeComponent standard, ovvero Indichiamo alla Window che il suo code behind fa anche da ViewModel, inizializzando il DataContext con this.

Inizializziamo anche la lista degli oggetti.

private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
	for (int item = 0; item < 10; item++)
	{
		StackPanel stackCisternaAtz = new StackPanel();
		stackCisternaAtz.Orientation = Orientation.Horizontal;
		stackCisternaAtz.Margin = new Thickness(2, 2, 2, 2);
				
 
		TankData tkd = new TankData();
		tkd.ID = item.ToString();
		tkd.Temperature = "45";
		tkd.Ullage = "";
		mTanks.Add(tkd);
 
 
		Label lblCisterna = new Label();
		Binding vBinding = new Binding(TankData.FLD_ID);
		vBinding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
		vBinding.Source = tkd;
		vBinding.Mode = BindingMode.OneWay;
		//vBinding.Converter = new StringToDateTimeConverter();
		lblCisterna.SetBinding(Label.ContentProperty, vBinding);
		lblCisterna.Background = new SolidColorBrush(Colors.LightBlue);
		lblCisterna.MinWidth = 60;
		stackCisternaAtz.Children.Add(lblCisterna);
 
		TextBox txtTemp = new TextBox();
		vBinding = new Binding(TankData.FLD_Temperature);
		vBinding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
		vBinding.Source = tkd;
		vBinding.Mode = BindingMode.TwoWay;
		//vBinding.Converter = new StringToDateTimeConverter();
		txtTemp.SetBinding(TextBox.TextProperty, vBinding);
		txtTemp.Background = new SolidColorBrush(Colors.Red);
		txtTemp.Foreground = new SolidColorBrush(Colors.White);
		txtTemp.MinWidth = 60;
		stackCisternaAtz.Children.Add(txtTemp);
 
 
		TextBox txtUllageCisterna = new TextBox();
		vBinding = new Binding(TankData.FLD_Ullage);
		vBinding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
		vBinding.Source = tkd;
		vBinding.Mode = BindingMode.TwoWay;
		//vBinding.Converter = new StringToDateTimeConverter();
		txtUllageCisterna.SetBinding(TextBox.TextProperty, vBinding);
		txtUllageCisterna.Background = new SolidColorBrush(Colors.Lime);
		txtUllageCisterna.MinWidth = 60;
		txtUllageCisterna.FontSize = 16;
		txtUllageCisterna.HorizontalContentAlignment = HorizontalAlignment.Center;
		txtUllageCisterna.VerticalContentAlignment = VerticalAlignment.Center;
		stackCisternaAtz.Children.Add(txtUllageCisterna);
 
		TextBox txtMcCisterna = new TextBox();
		vBinding = new Binding(TankData.FLD_Mc);
		vBinding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
		vBinding.Source = tkd;
		vBinding.Mode = BindingMode.OneWay;
		//vBinding.Converter = new StringToDateTimeConverter();
		txtMcCisterna.SetBinding(TextBox.TextProperty, vBinding);
		txtMcCisterna.MinWidth = 80;
		txtMcCisterna.FontSize = 16;
		txtMcCisterna.IsReadOnly = true;
		txtMcCisterna.Background = new SolidColorBrush(Colors.LightCoral);
		stackCisternaAtz.Children.Add(txtMcCisterna);
 
 
		ContentContainer.Children.Add(stackCisternaAtz);
 
	}
}

Ed ora il codice che genera i controlli all’interno della finestra. Questo metodo è complesso pertanto ne commenterò anche il codice per step in modo che possiamo spiegare ognuno degli step.

for (int item = 0; item < 10; item++)
{

Il ciclo for, in questo caso è un ciclo di 10 iterazioni, che provocherà la creazione di 10 righe ovviamente in un contesto reale probabilmente questo ciclo dipenderà da una lista di dati, magari letta da un database, oppure molto semplicemente le righe potrebbero essere generate in risposta ad un dato arrivato da una device esterna, creando quindi un numero di righe che crescerà nel tempo. Qui lascio alla vostra immaginazione decidere.

StackPanel stackCisternaAtz = new StackPanel();
stackCisternaAtz.Orientation = Orientation.Horizontal;
stackCisternaAtz.Margin = new Thickness(2, 2, 2, 2);
		
 
TankData tkd = new TankData();
tkd.ID = item.ToString();
tkd.Temperature = "45";
tkd.Ullage = "";
mTanks.Add(tkd);

Per ogni giro del nostro ciclo, per prima cosa genero uno StackPanel, che conterrà una riga di controlli. Oltre a indicare che i controlli saranno inseriti in orizzontale, assegno un margine in modo da poter vedere le righe generate.

Generato il contenitore per i controlli, genero una classe TankData contenente dei dati arbitrari, l’ID è l’indice del ciclo, e la Temperatura è impostata a 45, aggiungo l’oggetto generato alla nostra collection, che in seguito potrebbe essere utilizzata per elaborare i dati che l’utente inserirà nei controlli a video.

Label lblCisterna = new Label();
Binding vBinding = new Binding(TankData.FLD_ID);
vBinding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
vBinding.Source = tkd;
vBinding.Mode = BindingMode.OneWay;
//vBinding.Converter = new StringToDateTimeConverter();
lblCisterna.SetBinding(Label.ContentProperty, vBinding);
lblCisterna.Background = new SolidColorBrush(Colors.LightBlue);
lblCisterna.MinWidth = 60;
stackCisternaAtz.Children.Add(lblCisterna);

Genero un controllo Label, che conterrà il valore dell’ID del nostro oggetto TankData, per fare in modo che la Label la visualizzi devo effettuare il Binding della sua property Content con la property ID del mio oggetto TankData. Per farlo creo un oggetto Binding, nel costruttore indico subito a quale property sarà collegato, per farlo uso la costante definita nella classe, l’uso delle costanti non è obbligatorio, io lo faccio per un motivo molto semplice, potrei semplicemente scrivere la stringa “ID” invece di usare la costante, però se a distanza di mesi o anni andassi a modificare il mio programma cambiando i nomi di una property referenziata in questo modo non mi accorgerei mai del fatto che ho provocato un errore nel codice, in questo modo invece se modificassi il nome della costante provocherei un errore del compilatore e immediatamente potrei aggiornare anche questo codice, se mantenessi il nome della costante potrei semplicemente modificarne il contenuto con il nuovo nome della property e ancora una volta tutto funzionerebbe.

Torniamo al codice, dopo aver indicato a quale property effettuare il binding, indico che la property deve aggiornare la label quando si scatena l’evento PropertyChanged, indico che l’oggetto che fornisce il contenuto della property ID è il mio tkd; indico inoltre che la Label riceverà sempre il dato dal codice e non lo modificherà.

Il commento che ho lasciato è a beneficio di sviluppi nel mondo reale, se volessimo convertire in qualche modo il valore (ad esempio da stringa a numero e viceversa) la riga commentata indica come poter inserire un Converter per il valore.

Infine andiamo a agganciare il Binding alla Label appena generata, indicando che il Binding è attivato sulla property Content della Label. Attenzione che dobbiamo indicare in modo generico quale property della classe, non un oggetto istanziato. di qui il nome Label.ContentProperty.

Infine modifichiamo il Background della Label e la sua dimensione minima, il colore ci serve per vedere a video i vari oggetti e sapere esattamente quali sono.

Quando la Label è pronta, andremo ad aggiungerla ai Children dello StackPanel contenitore, ovvero gli elementi in esso contenuti.

TextBox txtTemp = new TextBox();
vBinding = new Binding(TankData.FLD_Temperature);
vBinding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
vBinding.Source = tkd;
vBinding.Mode = BindingMode.TwoWay;
//vBinding.Converter = new StringToDateTimeConverter();
txtTemp.SetBinding(TextBox.TextProperty, vBinding);
txtTemp.Background = new SolidColorBrush(Colors.Red);
txtTemp.Foreground = new SolidColorBrush(Colors.White);
txtTemp.MinWidth = 60;
stackCisternaAtz.Children.Add(txtTemp);

La TextBox che permette di vedere e modificare il valore di Temperature è generata in modo molto simile alla Label, cambia il Mode del Binding, che è TwoWay perché il valore viene inizializzato da codice, ma può essere modificato dall’utente dentro alla TextBox stessa, quindi il valore è bidirezionale. Anche in questo caso abbiamo colorato sfondo e testo per rendere evidente a video che cosa sia.

TextBox txtUllageCisterna = new TextBox();
vBinding = new Binding(TankData.FLD_Ullage);
vBinding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
vBinding.Source = tkd;
vBinding.Mode = BindingMode.TwoWay;
//vBinding.Converter = new StringToDateTimeConverter();
txtUllageCisterna.SetBinding(TextBox.TextProperty, vBinding);
txtUllageCisterna.Background = new SolidColorBrush(Colors.Lime);
txtUllageCisterna.MinWidth = 60;
txtUllageCisterna.FontSize = 16;
txtUllageCisterna.HorizontalContentAlignment = HorizontalAlignment.Center;
txtUllageCisterna.VerticalContentAlignment = VerticalAlignment.Center;
stackCisternaAtz.Children.Add(txtUllageCisterna);

Anche la seconda textbox in Binding alla property Ullage modificabile è generata in modo simile alla precedente, variano solo alcune delle property per il suo aspetto.

TextBox txtMcCisterna = new TextBox();
vBinding = new Binding(TankData.FLD_Mc);
vBinding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
vBinding.Source = tkd;
vBinding.Mode = BindingMode.OneWay;
//vBinding.Converter = new StringToDateTimeConverter();
txtMcCisterna.SetBinding(TextBox.TextProperty, vBinding);
txtMcCisterna.MinWidth = 80;
txtMcCisterna.FontSize = 16;
txtMcCisterna.IsReadOnly = true;
txtMcCisterna.Background = new SolidColorBrush(Colors.LightCoral);
stackCisternaAtz.Children.Add(txtMcCisterna);

Anche la terza TextBox è simile alle precedenti, solo che in questo caso, essendo la property calcolata e quindi a sola lettura, usiamo il Mode OneWay perché arriverà sempre dal codice verso la UI e impostiamo anche la Textbox come ReadOnly.

ContentContainer.Children.Add(stackCisternaAtz);

L’ultima operazione che facciamo è aggiungere lo StackPanel appena generato allo StackPanel verticale posto nella Window, in modo che i controlli siano visibili ed utilizzabili.

Se testiamo il nostro progetto, il risultato è il seguente.

mainwindow_01

Ho modificato manualmente i valori del campo Temperature e Ullage e ottenuto il ricalcolo del campo Mc dei record modificati.

A questo punto, potrei usare la mia collection ad esempio per riportare su un database i valori rilevati e calcolati.

Riepilogo

Cosa abbiamo spiegato in questo articolo:

  • Come Generare una classe dati per supportare il ViewModel
  • Come creare una property calcolata e gestire il suo ricalcolo e comunicarne la variazione alla User Interface.
  • Come generare una semplice window con un contenitore di controlli.
  • Come generare una serie di controlli a runtime e collegare alcuni di essi ad un istanza della classe di supporto dati generando i Binding fra le property della classe e i controlli generati.

Il progetto esempio può essere scaricato al link  seguente:

Per qualsiasi domanda, chiarimento o per segnalare un errore, utilizzate il modulo di contatto che trovate in cima alla pagina.