Press "Enter" to skip to content

Viewmodel, Collections, Totalizzazioni, Controlli Generati a Runtime

Nell’articolo precedente, ho mostrato, in risposta ad una domanda posta sui forum, come creare una collezione di oggetti a runtime, e farne il Binding creando i controlli da codice.  Il link all’articolo.

Oggi, la stessa persona ha chiesto, se fosse possibile, in fondo alla lista visualizzare un totalizzatore dei valori di una colonna che si aggiorni automaticamente quando i valori sono modificati.

Ovviamente, WPF ci permette di risolvere il problema con semplicità. Vediamo come ho fatto.

Ho creato una nuova applicazione, invece di modificare quella già creata ma in realtà ho copiato buona parte del codice dell’applicazione precedente. Che cosa ho fatto:

  • Ho creato un valore numerico corrispondente alla stringa che viene utilizzata presumo per semplicità nel codice mandato da chi ha implementato la soluzione facendo la domanda.
  • Ho fatto in modo che il numero sia calcolato correttamente al variare dei dati in ogni classe.
  • Ho aggiunto al viewmodel della mia MainWindow (che corrisponde a se stessa) una property per ospitare il totale.
  • Ho modificato il metodo di caricamento dei dati, in modo tale che venga generata una textbox contenente la somma ed ho effettuato il binding della property totale a questa textbox
  • Ho modificato il caricamento dei dati, in modo da creare e agganciare a tutte le classi dati, un event handler all’evento Property Changed che viene scatenato quando sono modificate le loro property.
  • In questo event handler ho verificato se il valore modificato è quello che devo totalizzare e ho effettuato un ricalcolo.

Vediamo il codice e l’effetto.

Modifiche a TankData.cs

/// <summary>
/// Mc of the tank
/// </summary>
public const string FLD_Mc = "Mc";
 
/// <summary>
/// Mc of the tank
/// </summary>
public string Mc
{
	get
	{
		return McDbl.ToString();
	}
}
 
/// <summary>
/// Mc of the tank
/// </summary>
public const string FLD_McDbl = "McDbl";
 
/// <summary>
/// Mc of the tank
/// </summary>
public double McDbl
{
	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);
	}
}

Ho creato una nuova property Double per il totalizzatore che mi serve, ed ho sostituito nella corrispondente property stringa il semplice ToString del risultato.

public string Temperature
{
	get
	{
		return mTemperature;
	}
	set
	{
		mTemperature = value;
		OnPropertyChanged(FLD_McDbl);
		OnPropertyChanged(FLD_Mc);
		OnPropertyChanged(FLD_Temperature);
	}
}

public string Ullage
{
	get
	{
		return mUllage;
	}
	set
	{
		mUllage = value;
		OnPropertyChanged(FLD_McDbl);
		OnPropertyChanged(FLD_Mc);
		OnPropertyChanged(FLD_Ullage);
	}
}

Ho modificato le 2 property della classe, in modo tale che se varia Temperature o Ullage, venga scatenato anche l’evento PropertyChanged delle due property readonly McDbl (il valore numerico) ed Mc il valore stringa in questo modo, se varia un dato WPF richiederà di ricalcolare e visualizzare il valore dei due numeri.

Modifiche a MainWindow.xaml

<Window x:Class="TotalizationInWpf.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:TotalizationInWpf"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525" Loaded="MainWindow_Loaded">
    <Grid>
        <ScrollViewer>
        <StackPanel
            Name="ContentContainer"
            Orientation="Vertical">
            
            
            
        </StackPanel>
        </ScrollViewer>
    </Grid>
</Window>

Modifiche a MainWindow.xaml.cs

Ho variato la main window, semplicemente aggiungendo uno Scroll viewer in modo che il pannello scorra con una scrollbar, se è troppo lungo per la dimensione della Window.

public string TotalTanksMc
{
	get
	{
		return (Tanks.Sum(x=>x.McDbl)).ToString("###,###.0");
	}
 
}

Ho creato la property totalizzatore che effettua la somma delle property McDbl della mia collection.

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 = "5";
		tkd.PropertyChanged += Tkd_PropertyChanged;
		Tanks.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);
 
	}
	StackPanel stackTotal = new StackPanel();
	stackTotal.Orientation = Orientation.Horizontal;
	stackTotal.Margin = new Thickness(2, 2, 2, 2);
 
	Label lblTotal = new Label();
	lblTotal.Content = "Total Mc";
	lblTotal.Background = new SolidColorBrush(Colors.LightBlue);
	lblTotal.MinWidth = 60;
	stackTotal.Children.Add(lblTotal);
 
	TextBox txtTotal = new TextBox();
	Binding tBinding = new Binding(FLD_TotalTanksMc);
	tBinding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
	tBinding.Source = this;
	tBinding.Mode = BindingMode.OneWay;
	//vBinding.Converter = new StringToDateTimeConverter();
	txtTotal.SetBinding(TextBox.TextProperty, tBinding);
	txtTotal.Background = new SolidColorBrush(Colors.Yellow);
	txtTotal.MinWidth = 60;
	txtTotal.FontSize = 16;
	txtTotal.HorizontalContentAlignment = HorizontalAlignment.Center;
	txtTotal.VerticalContentAlignment = VerticalAlignment.Center;
	stackTotal.Children.Add(txtTotal);
 
	ContentContainer.Children.Add(stackTotal);
}

Questo è l’event handler che al load della window crea tutti i controlli da codice, vediamo cosa ho modificato:

TankData tkd = new TankData();
tkd.ID = item.ToString();
tkd.Temperature = "45";
tkd.Ullage = "5";
tkd.PropertyChanged += Tkd_PropertyChanged;
Tanks.Add(tkd);

Ho modificato la generazione degli oggetti TankData, aggiungendo a ciascuno di essi l’event handler per l’evento PropertyChanged.

StackPanel stackTotal = new StackPanel();
stackTotal.Orientation = Orientation.Horizontal;
stackTotal.Margin = new Thickness(2, 2, 2, 2);
 
Label lblTotal = new Label();
lblTotal.Content = "Total Mc";
lblTotal.Background = new SolidColorBrush(Colors.LightBlue);
lblTotal.MinWidth = 60;
stackTotal.Children.Add(lblTotal);
 
TextBox txtTotal = new TextBox();
Binding tBinding = new Binding(FLD_TotalTanksMc);
tBinding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
tBinding.Source = this;
tBinding.Mode = BindingMode.OneWay;
//vBinding.Converter = new StringToDateTimeConverter();
txtTotal.SetBinding(TextBox.TextProperty, tBinding);
txtTotal.Background = new SolidColorBrush(Colors.Yellow);
txtTotal.MinWidth = 60;
txtTotal.FontSize = 16;
txtTotal.HorizontalContentAlignment = HorizontalAlignment.Center;
txtTotal.VerticalContentAlignment = VerticalAlignment.Center;
stackTotal.Children.Add(txtTotal);
 
ContentContainer.Children.Add(stackTotal);

Dopo il ciclo che genera le righe dei controlli della tabella, ho inserito la creazione di un ulteriore StackPanel che contiene una Label per indicare il totale e una Textbox che conterrà il valore del totale, notate che il Binding è stato fatto con la nuova property TotalTanksMc.

private void Tkd_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
	if (e.PropertyName == TankData.FLD_Ullage)
	{
		OnPropertyChanged(FLD_TotalTanksMc);
	}
}

Nell’event handler del property changed ho semplicemente scatenato l’evento OnPropertyChanged della property del totale in modo tale che WPF vada a rileggerla, in questo modo, sarà effettuato nuovamente il suo calcolo e quindi il totale varierà al variare delle caselle contenenti il valore di Ullage, se ci sono + property che possono variare, basta mettere una serie di if oppure togliere del tutto la IF.

Il risultato finale è quello visibile nella window qui sotto catturata:

01_totalizationinwpf_scroll[6]

Qui potete vedere la scrollbar.

02_totalizationinwpf_start[6]

Il valore iniziale dei dati, e come vedete il totale è stato calcolato correttamente durante l’inizializzazione dei dati.

03_totalizationinwpf_modified[6]

Dopo aver modificato i dati della colonna verde, come potete notare, il totale è cambiato, se provate l’applicazione, vedrete che sarà calcolato ad ogni tasto premuto su una delle caselle dati verdi.

Riepilogo

Che cosa abbiamo spiegato in questo articolo:

  • Come generare una variabile ricalcolata in una classe
  • Come creare una property del view model ricalcolata a partire dai dati di un’altra property (la collection di TankData).
  • Come creare un binding generando un controllo a runtime sulla property appena generata.
  • Come agganciare l’event handler della variazione dei dati delle classi generate a runtime per scatenare la variazione della property ricalcolata.
  • Come far scrollare uno stack panel se la finestra è piccola.

Potete scaricare il progetto esempio al link seguente.

Per qualsiasi domanda, approfondimento, curiosità, osservazione, o se trovate un errore, usate pure il link al modulo di contatto in cima alla pagina.