In WPF, per creare effetti collegati ai valori visualizzati sui controlli della User Interface non è necessario implementare event handler sui controlli stessi, grazie a MVVM e alle classi converter, funziona tutto in modo automatico. In risposta ad una richiesta di un principiante sul forum Microsoft Italiano, ecco come fare a cambiare l’aspetto dei controlli, oppure visualizzare un immagine come segnalino in base al valore di un dato.
Per il nostro esempio ho creato una applicazione WPF, in questa applicazione, per prima cosa creo 4 classi converter.
Cosa sono i converter, sono delle classi speciali di WPF, che implementano l’interfaccia IValueConverter, questa interfaccia è composta da 2 metodi, Convert e ConvertBack, lo scopo dell’interfaccia è fornire a chi implementa la UI la possibilità di trasformare un dato di un certo tipo in uno completamente diverso e se necessario, viceversa.
I converter che ho implementato sono i seguenti:
- Da Intero a 32 bit a SolidColorBrush, per poter cambiare lo sfondo di una textbox in base al suo contenuto.
- Da Intero a 32 bit a Stringa e viceversa, per poter effettuare il Binding di un intero alla proprietà Text di una Textbox e riconvertirlo quando modificato.
- Da Intero a 32 bit a FontWeight, per poter far divenire Bold la font di una textbox quando il valore raggiunge un certo dato.
- Da Intero a 32 bit a BitmapImage, per poter visualizzare un immagine diversa in base al valore del numero intero.
Vediamo le 4 classi una per volta:
La classe IntToSolidColorBrushConverter.cs
using System; using System.Windows.Data; using System.Globalization; using System.Windows.Media; namespace ConverterOnValue.Converters { [ValueConversion(typeof(int), typeof(SolidColorBrush))] public class IntToSolidColorBrushConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { SolidColorBrush result = new SolidColorBrush(Colors.White); int input = (int)value; if (input < 0) { result = new SolidColorBrush(Colors.Red); } else if (input >= 0 && input < 20) { result = new SolidColorBrush(Colors.Orange); } else if (input >= 20 && input < 50) { result = new SolidColorBrush(Colors.Yellow); } else if (input >= 50) { result = new SolidColorBrush(Colors.LightGreen); } return result; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { //If you don't need a convert back this does nothing return value; } } }
Il primo converter converte un Intero in un Solid color brush che è l’oggetto da fornire alla property Background perché modifichi il colore standard della Textbox.
Il Binding di una property del ViewModel di una Window, o uno UserControl WPF, permette di indicare un Converter da utilizzare per convertire il valore messo in Binding in qualcosa di diverso.
[ValueConversion(typeof(int), typeof(SolidColorBrush))]
public class IntToSolidColorBrushConverter : IValueConverter
La prima cosa da fare è indicare tramite l’attributo ValueConversion, il tipo di dato sorgente e quello di destinazione in questo caso int e SolidColorBrush.
Inoltre, la classe deve implementare IValueConverter, altrimenti anche se avesse i 2 metodi richiesti, darebbe un exception.
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { SolidColorBrush result = new SolidColorBrush(Colors.White); int input = (int)value; if (input < 0) { result = new SolidColorBrush(Colors.Red); } else if (input >= 0 && input < 20) { result = new SolidColorBrush(Colors.Orange); } else if (input >= 20 && input < 50) { result = new SolidColorBrush(Colors.Yellow); } else if (input >= 50) { result = new SolidColorBrush(Colors.LightGreen); } return result; }
Il metodo convert, riceve dal controllo della UI che istanzia il converter, il valore della variabile espresso come object e può ricevere inoltre, il tipo di dato che si aspetta, un parametro arbitrario, e una informazione relativa alla cultura del thread WPF. Essendo questo un articolo base, useremo solo Value, il TargetType dovrebbe farvi capire che è possibile generare dei converter multi tipo o multipli, e che il parametro può essere utilizzato per dare ulteriori informazioni se necessario al metodo di conversione.
Per prima cosa, generiamo un SolidColorBrush con un valore di default, convertiamo il valore passato nel suo tipo di origine, quindi in questo caso int. Dopo di ciò si tratta solo di cambiare il colore del pennello in base al valore indicato, nel nostro caso abbiamo fatto 4 diversi valori, Rosso se minore di zero, Arancio tra 0 e 19, Giallo da 20 a 49, rosso da 50 in su.
ritorniamo il SolidColorBrush che colorerà il Background o il Foreground di quello che vogliamo.
Tengo a precisare, che il cambiamento di colore basato sul valore di un dato del ViewModel, può cambiare colore a qualsiasi cosa sul controllo non necessariamente solo sul controllo che è in binding al nostro valore, vedremo come.
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { //If you don't need a convert back this does nothing return value; }
La convert Back in questo caso non è implementata, perché il converter è destinato a cambiare una property che può essere inizializzata in un solo “verso” se possiamo definirlo tale, ma non potrà in questo caso essere modificata dall’utente e restituita per una riconversione.
La classe IntToFontWeightConverter.cs
[ValueConversion(typeof(int), typeof(FontWeight))] public class IntToFontWeightConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { FontWeight result = FontWeights.Normal; int input = (int)value; if (input < 50) { result = FontWeights.Bold; } return result; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { //If you don't need a convert back this does nothing return value; } }
Il secondo converter è simile, varia la classe di destinazione e il fatto che abbiamo solo 2 valori, minore di 50 oppure maggiore o uguale a 50.
La classe IntToStringConverter.cs
[ValueConversion(typeof(int), typeof(string))] public class IntToStringConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { return value.ToString(); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { int result = 0; string input = (string)value; int.TryParse(input, out result); return result; } }
Il terzo converter, al contrario dei precedenti, pure essendo semplice è diverso perché implementa anche il ConvertBack infatti lo useremo per collegare in Binding un valore intero con il Text di una TextBox pertanto l’utente potrà modificarlo.
Come potete desumere dal codice, se nel convert value è un intero, nel ConvertBack value è del tipo in cui abbiamo convertito il value di origine quindi in questo caso string. Questo ci permette di controllare che il valore sia coerente.
La classe IntToImageConverter.cs
Per terminare anche se a mio avviso è il più bello dei converter, il converter che converte il numero in una immagine.
[ValueConversion(typeof(int), typeof(BitmapImage))] public class IntToImageConverter : IValueConverter { private static BitmapImage[] mImages; static IntToImageConverter() { mImages = new BitmapImage[] { new BitmapImage(new Uri("pack://application:,,,/Images/btn_032_761.png", UriKind.RelativeOrAbsolute)) ,new BitmapImage(new Uri("pack://application:,,,/Images/btn_032_360.png", UriKind.RelativeOrAbsolute)) ,new BitmapImage(new Uri("pack://application:,,,/Images/btn_032_762.png", UriKind.RelativeOrAbsolute)) ,new BitmapImage(new Uri("pack://application:,,,/Images/btn_032_763.png", UriKind.RelativeOrAbsolute)) }; } public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { BitmapImage result = mImages[0]; int input = (int)value; if (input < 0) { result = mImages[0]; } else if (input >= 0 && input < 20) { result = mImages[1]; } else if (input >= 20 && input < 50) { result = mImages[2]; } else if (input >= 50) { result = mImages[3]; } //Whatever you need to implement to convert sourceClass to destinationclass here return result; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { //If you don't need a convert back this does nothing return value; } }
Per convertire un Intero in immagine, facciamo qualcosa di simile a tutti gli altri casi, fatto salvo che abbiamo inserito 4 immagini nelle risorse applicative, quindi abbiamo creato una cartella Images sul progetto e dentro a detta cartella abbiamo aggiunto 4 immagini, sono delle sfere dei 4 colori usati per i pennelli nel converter per il colore di sfondo. Queste immagini, sono state incluse come Resource, pertanto WPF le include nel nostro eseguibile ed è in grado di recuperarle utilizzando l’URI (Unique, resource identifier) che è la stringa simile ad un path internet (anche i path internet infatti sono URI) che ha come prefisso un codice standard che indica che la risorsa si trova all’interno dell’assembly corrente ovvero l’applicazione. poi c’è la parte che rappresenta il Path all’interno della cartella progetto ed infine il nome immagine.
Detto questo, credo abbiate notato questo pezzetto di codice:
private static BitmapImage[] mImages; static IntToImageConverter() { mImages = new BitmapImage[] { new BitmapImage(new Uri("pack://application:,,,/Images/btn_032_761.png", UriKind.RelativeOrAbsolute)) ,new BitmapImage(new Uri("pack://application:,,,/Images/btn_032_360.png", UriKind.RelativeOrAbsolute)) ,new BitmapImage(new Uri("pack://application:,,,/Images/btn_032_762.png", UriKind.RelativeOrAbsolute)) ,new BitmapImage(new Uri("pack://application:,,,/Images/btn_032_763.png", UriKind.RelativeOrAbsolute)) }; }
Che cosa fa questo codice, semplicemente la seguente cosa: Istanzia le immagini una sola volta la prima volta che il converter viene usato, in modo da non generare gli oggetti bitmap più e più volte ad ogni uso del converter, serve per evitare di usare troppe risorse che potrebbero essere pesanti per la memoria e rendere più rapida l’applicazione nel fornire l’immagine che è già pronta alla chiamata.
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { BitmapImage result = mImages[0]; int input = (int)value; if (input < 0) { result = mImages[0]; } else if (input >= 0 && input < 20) { result = mImages[1]; } else if (input >= 20 && input < 50) { result = mImages[2]; } else if (input >= 50) { result = mImages[3]; } //Whatever you need to implement to convert sourceClass to destinationclass here return result; }
Il metodo Convert è simile ai precedenti, semplicemente restituisce una delle immagini.
Una volta che abbiamo generato i Converter, Vediamo come utilizzarli:
La classe MainWindow.cs
Vediamo per prima cosa la parte di ViewModel che in questo test coincide con la Window stessa.
public partial class MainWindow : Window, INotifyPropertyChanged { public MainWindow() { InitializeComponent(); DataContext = this; NumberOne = -20; NumberTwo = 10; NumberThree = 34; NumberFour = 55; } public const string FLD_NumberOne = "NumberOne"; private int mNumberOne; public int NumberOne { get { return mNumberOne; } set { mNumberOne = value; OnPropertyChanged(FLD_NumberOne); } } public const string FLD_NumberTwo = "NumberTwo"; private int mNumberTwo; public int NumberTwo { get { return mNumberTwo; } set { mNumberTwo = value; OnPropertyChanged(FLD_NumberTwo); } } public const string FLD_NumberThree = "NumberThree"; private int mNumberThree; public int NumberThree { get { return mNumberThree; } set { mNumberThree = value; OnPropertyChanged(FLD_NumberThree); } } public const string FLD_NumberFour = "NumberFour"; private int mNumberFour; public int NumberFour { get { return mNumberFour; } set { mNumberFour = value; OnPropertyChanged(FLD_NumberFour); } } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } }
La sola cosa che implementiamo nel codice della finestra sono 4 property di tipo int che saranno collegate ad altrettante Textbox e ai controlli per utilizzare i Converter, non abbiamo bisogno di event handler o di qualsiasi altro codice per poter gestire la modifica dei colori o dell’aspetto dei controlli in base al valore assunto dai dati.
MainWindow.xaml
<Window x:Class="ConverterOnValue.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:wr="clr-namespace:ConverterOnValue" xmlns:lconv="clr-namespace:ConverterOnValue.Converters" mc:Ignorable="d" Title="Converters Demo" Height="350" Width="525"> <Window.Resources> <ResourceDictionary> <lconv:IntToFontWeightConverter x:Key="intToFontWeightConverter"/> <lconv:IntToSolidColorBrushConverter x:Key="intToSolidColorBrushConverter"/> <lconv:IntToStringConverter x:Key="intToStringConverter"/> <lconv:IntToImageConverter x:Key="intToImageConverter"/> </ResourceDictionary> </Window.Resources>
La prima cosa che facciamo è includere il Namespace in cui abbiamo messo le classi converter, dopo di che nelle Resources della window definiamo i quattro converter in modo che si possa utilizzarli.
<Grid Margin="5"> <Grid.RowDefinitions> <RowDefinition Height="33*"/> <RowDefinition Height="33*"/> <RowDefinition Height="33*"/> <RowDefinition Height="33*"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions>
Creiamo una grid per inserire i controlli della nostra interfaccia, con 4 righe e 3 colonne.
<TextBlock Grid.Row="0" Grid.Column="0" Text="First Number" Background="{Binding NumberOne, Converter={StaticResource intToSolidColorBrushConverter}}" Margin="4,2,4,2" HorizontalAlignment="Right" VerticalAlignment="Center"/>
Nella prima colonna della grid inseriamo 4 TextBlock, contengono le descrizioni dei 4 campi che abbiamo creato nel ViewModel e per mostrare come possiamo effettuare il Binding di qualsiasi property di qualsiasi controllo con le property del ViewModel, ho collegato in Binding la property Background di queste TextBlock alla Property del ViewModel in cui inseriremo il valore della TextBox, ho agganciato il Converter al converter da intero a Solid Color Brush in modo che il colore di sfondo del testo sia quello corrispondente al valore del numero inserito.
Le altre 3 Textblock della prima colonna sono identiche fatto salvo per il testo, e la Property a cui sono in Binding che ovviamente è una delle altre tre disponibili, inoltre varia ovviamente la riga della grid in cui si trova la textblock.
<TextBox Grid.Row="1" Grid.Column="1" Margin="4,2,4,2" HorizontalAlignment="Left" VerticalAlignment="Center" MinWidth="200" Text="{Binding NumberTwo, Converter={StaticResource intToStringConverter}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Background="{Binding NumberTwo, Converter={StaticResource intToSolidColorBrushConverter}, UpdateSourceTrigger=PropertyChanged}" FontWeight="{Binding NumberTwo, Converter={StaticResource intToFontWeightConverter}, UpdateSourceTrigger=PropertyChanged}"/>
La seconda colonna ospita le TextBox che sono in Binding con le Property del ViewModel, come potete notare, ho messo in Binding con la stessa Property (NumberTwo) il Text, Il Background e il FontWeight, semplicemente ho assegnato a ciascuna property il Converter corretto.
Le altre 3 Textbox sono uguali, fatto salvo che cambia la property a cui sono in Binding e la riga ove si trovano.
<Image Grid.Row="2" Grid.Column="2" Width="32" Height="32" Margin="0" Source ="{Binding NumberThree, Converter={StaticResource intToImageConverter}, UpdateSourceTrigger=PropertyChanged}"/>
Nella Terza colonna ho voluto inserire un immagine, per mostrare che il converter può servire anche per trasformare un valore in un oggetto visuale. Certamente un icona è uno dei modi migliori per far notare ad un utente se qualcosa va male oppure bene, esattamente come ad esempio sono le luci di un semaforo. Anche cambiare il colore del controllo è una cosa che si può fare, ma a mio avviso è più complicato e spesso peggiora la visibilità del dato vero e proprio. Ad ogni modo potete notare come WPF vi permetta di fare quello che volete.
Questo è tutto, se provate a eseguire il progetto, questo è quello che accadrà:
Allo startup i valori che ho impostato da codice sono visualizzati e vediamo i colori dei controlli e delle immagini.
Modificando i valori delle textbox come potete vedere i colori e le immagini variano di conseguenza.
Riepilogo
Cosa abbiamo spiegato in questo esempio:
- Come creare un Converter per trasformare un tipo di dato in uno completamente diverso.
- Come collegare qualsiasi tipo di Property dei controlli di una Window ad una property del ViewModel usando i converter.
Potete scaricare il progetto esempio al link seguente:
Companion code, Introduzione ai Converter
Per qualsiasi domanda, approfondimento, curiosità, osservazione, o se trovate un errore, usate pure il link al modulo di contatto in cima alla pagina.