WPF: implementare l'interfaccia IValueConverter
a cura di Alessandro Del Sole (requisiti: conoscenze intermedie di Windows Presentation Foundation)Introduzione
Spesso nell'interfaccia grafica delle nostre applicazioni dobbiamo presentare dei dati. Si pensi, per esempio, all'esposizione degli ordini ricevuti dalla nostra azienda, rappresentati da tanti record quanti sono gli ordini, per i quali c'è la data dell'ordine, la data e l'indirizzo di consegna, il costo del trasporto da addebitare e molte altre informazioni utili.Nella generalità delle applicazioni .NET, queste informazioni vengono estratte da una sorgente dati, come per esempio la tabella di un database, e collegate per mezzo del data-binding all'interfaccia. Quest'ultima diventa anche il mezzo per l'invio di dati alla sorgente.
Sempre in linea generale, i dati vengono presentati e inseriti sotto forma di stringa o per il tramite di specifici controlli utente.
Una problematica che si può verificare in queste circostanze è quella di essere certi della corrispondenza tra tipo di dato proveniente dalla sorgente dati e quello specificato tramite interfaccia, tipicamente tramite validazione dei dati immessi. Esempio molto semplice: l'operatore deve inserire la data di un ordine e lo fa tramite una TextBox. È molto probabile che in un'applicazione moderna tale operazione venga eseguita tramite un controllo utente dedicato (come il DateTimePicker), ma questo esempio ci serve per capire la logica. Cosa succede se la data immessa non corrisponde a uno dei formati accettati dall'oggetto DateTime di .NET Framework? Ci possono essere dei problemi.Ecco quindi l'opportunità di implementare all'interno dell'applicazione degli oggetti che permettano di intervenire sulla logica del data-binding. Nelle applicazioni Windows Presentation Foundation, questo può essere messo in pratica sfruttando un'apposita interfaccia chiamata IValueConverter, di cui vedremo un paio di esempi nel corso dell'articolo, al termine del quale avrete gli elementi e la curiosità necessarie per implementare la vostra logica personalizzata anche nei confronti di altri tipi di dato.
Il codice sorgente a corredo dell'articolo è disponibile in Area Download di Visual Basic Tips & Tricks.
Predisposizione di un esempio
Il nostro esempio sarà molto semplice: ci occuperemo di visualizzare, all'interno di una griglia, il contenuto delle colonne OrderDate e Freight della tabella Orders del database dimostrativo Northwind (incluso nel progetto a corredo). Sfrutteremo un modello a oggetti basato su ADO.NET Entity Framework, ma faremo un lavoro di mapping e data-binding molto essenziale, per poterci concentrare sull'interfaccia che dà il titolo all'articolo. Se non avete mai avuto a che fare con Entity Framework e LINQ to Entities, potete dare un'occhiata a questo mio precedente articolo. La scelta di ADO.NET Entity Framework è dovuta, più che ad altro, alla sua attualità e modernità, ma nulla vieta, comunque, di riscrivere l'esempio sfruttando altre tipologie di sorgenti dati. Ciò premesso, creiamo un nuovo progetto WPF con Visual Basic 2008 (va bene anche l'edizione Express):![]()
Fatto questo, aggiungiamo al progetto (comando Project|Add new item) un nuovo Entity Data Model per mappare la tabella Orders del database Northwind:
![]()
A questo punto Visual Studio avvierà la procedura guidata per la generazione dell'Entity Data Model. Nella prima schermata della procedura guidata, dobbiamo selezionare l'opzione Generate from database, affinché il modello a oggetti sia generato a partire da un database esistente. Nella seconda schermata, dobbiamo specificare il database Northwind.mdf utilizzando l'apposito menu a tendina o, se non disponibile nell'elenco, il pulsante New Connection. Trattandosi di un esempio, possiamo lasciare invariata l'opzione relativa alla memorizzazione della stringa di connessione nel file di configurazione:
![]()
Nella schermata successiva, relativa alla selezione degli elementi del database da far confluire nel modello a oggetti, è sufficiente selezionare la tabella Orders:
![]()
Possiamo anche lasciare inalterato l'identificatore proposto di default da Visual Studio per il namespace che esporrà le definizioni delle classi costituenti l'Entity Data Model. Se avete una minima familiarità col designer di Entity Framework, potrete notare come la colonna OrderDate della tabella Orders sia stata mappata in una proprietà OrderDate di tipo DateTime, mentre la Freight in una proprietà di tipo Decimal. Ora passiamo al codice XAML relativo alla finestra principale dell'applicazione. In linea generale non è buona norma iniziare dalla definizione dall'interfaccia, ma andando avanti nella lettura dell'articolo capirete il perché di questo approccio 'al contrario', che servirà proprio per illustrare le tecniche relative all'interfaccia IValueConverter.
Definiamo una griglia nell'interfaccia, in modo 'solo testo'
Come accennato all'inizio dell'articolo, vogliamo visualizzare in una griglia il contenuto delle entità OrderDate e Freight esposte dall'insieme Orders (la terminologia ora cambia, non siamo più in ambito database ma in ambito Entity Framework, quindi modello a oggetti). Possiamo quindi utilizzare il controllo ListView e definire il suo layout mediante i DataTemplate, in modo da stabilire quali controlli debbano essere utilizzati per visualizzare/immettere i dati. Lo facciamo scrivendo il seguente codice all'interno del contenitore Grid proposto di default da Visual Studio:<ListView Name="OrdersListView" IsSynchronizedWithCurrentItem="True" ItemsSource="{Binding}"> <!--Impostare nel modo seguente l'ItemContainerStyle ci consente di allineare i vari campi alla larghezza della colonna--> <ListView.ItemContainerStyle> <Style TargetType="ListViewItem"> <Setter Property="HorizontalContentAlignment" Value="Stretch" /> </Style> </ListView.ItemContainerStyle> <ListView.View> <GridView> <GridViewColumn Header="Order date"> <GridViewColumn.CellTemplate> <DataTemplate> <TextBox Text="{Binding Path=OrderDate}"/> </DataTemplate> </GridViewColumn.CellTemplate> </GridViewColumn> <GridViewColumn Header="Freight"> <GridViewColumn.CellTemplate> <DataTemplate> <TextBox Text="{Binding Path=Freight}"/> </DataTemplate> </GridViewColumn.CellTemplate> </GridViewColumn> </GridView> </ListView.View> </ListView>Come potete osservare, utilizziamo nel template delle celle i controlli TextBox. Questo ci permetterà di utilizzare il medesimo controllo sia per mostrare che per immettere i dati (le TextBox, in WPF, consentono il cosiddetto data-binding Two Ways). La proprietà Text di ciascuna TextBox è determinata tramite binding, puntando alla proprietà (attributo Path) desiderata della classe che poi associeremo da codice Visual Basic. A tal proposito, passiamo al file di code-behind (Window1.xaml.vb) e, all'interno della definizione della classe, scriviamo il seguente codice:
'Otteniamo l'istanza del contesto di Entity Framework Private northwindContext As New NORTHWNDEntities Public Sub New() ' This call is required by the Windows Form Designer. InitializeComponent() 'Poniamo in essere il data-binding, associando l'insieme 'Orders al contesto dati della finestra Me.DataContext = northwindContext.Orders End SubRicordo che la proprietà DataContext può essere assegnata con qualunque oggetto di tipo IEnumerable (quindi anche IQueryable) o collezione .NET, quindi non solo insiemi di entità di ADO.NET Entity Framework. Non è inoltre superfluo ricordare che assegnare la proprietà DataContext della Window farà sì che i controlli da questa dipendenti si alimentino dalla DataContext stessa. Se ora proviamo ad avviare l'applicazione, dovremmo ottenere un risultato simile alla seguente figura:
![]()
WPF ha correttamente estratto due tipologie di oggetti .NET, DateTime (corrispondente alla OrderDate) e Decimal (corrispondente all'entità Freight), collegandoli all'interfaccia. Ci sono però due considerazioni da fare: Freight rappresenta il costo del trasporto, quindi sarebbe opportuno visualizzarlo in forma di valuta; la data dell'ordine, invece, è estratta dalla sorgente dati come DateTime ma sarà immessa nell'interfaccia come una stringa che sarà poi inviata alla sorgente dati come DateTime, quindi è opportuno prevedere una logica che analizzi la stringa immessa verificando che sia compatibile col tipo DateTime. Ecco dove interviene l'interfaccia IValueConverter.
Implementare l'interfaccia IValueConverter
Definiamo ora una nuova classe, che si occuperà di eseguire tutte le verifiche del caso sui dati immessi, prima di inviarli alla sorgente dati. Non solo: la nuova classe sarà in grado di inviare all'interfaccia i dati formattati secondo le proprie necessità. Aggiungiamo quindi al progetto una nuova classe chiamata CustomConverter.vb. La nuova classe dovrà implementare l'interfaccia IValueConverter, di conseguenza questo sarà il codice di partenza:Public Class CustomConverter Implements IValueConverter Public Function Convert(ByVal value As Object, ByVal targetType As System.Type, _ ByVal parameter As Object, _ ByVal culture As System.Globalization.CultureInfo) _ As Object Implements System.Windows.Data.IValueConverter.Convert End Function Public Function ConvertBack(ByVal value As Object, ByVal targetType As System.Type, _ ByVal parameter As Object, _ ByVal culture As System.Globalization.CultureInfo) _ As Object Implements System.Windows.Data.IValueConverter.ConvertBack End Function End ClassL'adozione dell'interfaccia richiede che la classe implementi due metodi, Convert e ConvertBack. Il primo si occupa di gestire i dati nella fase che va dalla sorgente dati all'interfaccia mentre il secondo si occupa di convertire i dati che dall'interfaccia devono essere inviati alla sorgente dati, eseguendo le verifiche richieste. Nel nostro esempio (ma in scenari reali la cosa può essere diversa), i dati richiesti saranno evidenziati nell'interfaccia sotto forma di testo. Di conseguenza ci potrebbe bastare la formattazione dei dati sfruttando la localizzazione del sistema in uso, pertanto il metodo Convert può essere implementato come segue:
'Dalla sorgente all'interfaccia Public Function Convert(ByVal value As Object, ByVal targetType As System.Type, _ ByVal parameter As Object, _ ByVal culture As System.Globalization.CultureInfo) _ As Object Implements System.Windows.Data.IValueConverter.Convert If parameter IsNot Nothing Then Return String.Format(culture, parameter.ToString, value) End If Return value End FunctionParameter è un oggetto che viene specificato nel codice XAML, che vedremo tra non molto. Esso consente di specificare la modalità di visualizzazione (es. data in formato esteso, data in formato breve, valuta). Value, invece, rappresenta l'oggetto da formattare. Nel caso Parameter non sia nullo, viene restituita una stringa formattata in base alle impostazioni di localizzazione del sistema. Nel caso invece non sia specificato alcun parametro, viene restituito il valore così com'è.
Ora dobbiamo implementare il metodo ConvertBack, che dovrà analizzare i dati immessi nell'interfaccia prima di inviarli alla sorgente dati. In questo metodo possiamo analizzare più tipi di dato, quindi, nel nostro caso, sia il DateTime corrispondente a OrderDate che il Decimal corrispondente a Freight. Segue, quindi, l'implementazione del metodo ConvertBack, all'interno del quale ho aggiunto dei commenti per rendere più fluida la lettura:
'Dall'interfaccia alla sorgente Public Function ConvertBack(ByVal value As Object, ByVal targetType As System.Type, _ ByVal parameter As Object, _ ByVal culture As System.Globalization.CultureInfo) _ As Object Implements System.Windows.Data.IValueConverter.ConvertBack 'Se il tipo da inviare alla sorgente dati è DateTime o DateTime? If targetType Is GetType(DateTime) OrElse targetType Is GetType(Nullable(Of DateTime)) Then Dim resultDate As DateTime = Nothing 'verifica che il dato immesso sia valido If DateTime.TryParse(value, resultDate) = True Then 'se sì, lo restituisce Return CDate(value) 'se vuoto, restituisce Nothing ElseIf value.ToString() = String.Empty Then Return Nothing Else 'se non è valido, restituisce una data generica Return DateTime.Now End If 'vedi commenti sopra ElseIf targetType Is GetType(Decimal) OrElse targetType Is GetType(Nullable(Of Decimal)) Then Dim resultMoney As Decimal = Nothing If Decimal.TryParse(value, resultMoney) = True Then Return CDec(value) ElseIf value.ToString = String.Empty Then Return Nothing Else Return 0D End If End If Return value End FunctionIn aggiunta ai commenti all'interno del codice, è opportuno specificare che l'analisi viene condotta anche sui tipi Nullable(Of T) perché gli Entity Data Model supportano anche questo tipo di oggetto. Come vedete, quindi, a seconda del dato immesso nell'interfaccia, il codice consente di prendere una determinata direzione. Tendenzialmente l'implementazione di ConvertBack non è richiesta qualora l'applicazione si limiti a mostrare dei dati, senza consentirne l'immissione e quindi l'invio alla sorgente dati, limitandosi a far sollevare al metodo una NotImplementedException.
In ogni caso la classe, così com'è, è inutilizzabile. È infatti necessario assegnarla al codice XAML della finestra, che la utilizzerà per le conversioni.Assegnazione del converter al data-binding
Torniamo al file Window1.xaml, quello che definisce l'interfaccia della finestra. Per prima cosa, dobbiamo aggiungere un riferimento al nostro stesso assembly, che espone la classe CustomConverter. Questo può essere fatto aggiungendo la seguente riga di codice (dopo aver compilato il tutto) all'interno del tag: xmlns:local="clr-namespace:adsWpfValueConverters"Chiaramente, Local è un identificatore che potete sostituire con uno di vostra preferenza.
È ora necessario includere, nelle risorse dell'oggetto Window, un identificatore che consenta di istanziare l'oggetto CustomConverter in fase di data-binding. Questo si ottiene scrivendo il seguente codice XAML all'interno della definizione della Window:<Window.Resources> <local:CustomConverter x:Key="customConverter"/> </Window.Resources>Il tag x:Key ci permette di specificare un identificatore che corrisponde all'istanza dell'oggetto CustomConverter.
Ora dobbiamo indicare a WPF, nel codice che esegue il binding alle TextBox, che deve usare la classe sopra indicata per convertire i dati nel formato corretto. Riscriviamo i due oggetti GridViewColumn nel modo seguente (le modifiche riguardano solo le TextBox):<GridViewColumn Header="Order date"> <GridViewColumn.CellTemplate> <DataTemplate> <TextBox Text="{Binding Path=OrderDate, Converter={StaticResource customConverter}, ConverterParameter='\{0:D\}'}"/> </DataTemplate> </GridViewColumn.CellTemplate> </GridViewColumn> <GridViewColumn Header="Freight"> <GridViewColumn.CellTemplate> <DataTemplate> <TextBox Text="{Binding Path=Freight, Converter={StaticResource customConverter}, ConverterParameter='\{0:c\}'}"/> </DataTemplate> </GridViewColumn.CellTemplate> </GridViewColumn>Abbiamo quindi modificato la XAML markup extension relativa al data-binding specificando un attributo Converter che va a richiamare la risorsa statica customConverter sopra definita, quindi abbiamo specificato un ConverterParameter, ossia un parametro che specifica la modalità di conversione del dato. Nel nostro esempio, la D maiuscola indica il formato di data esteso, mentre la c minuscola indica la rappresentazione in valuta. Esiste una nutrita serie di simboli che permette di specificare la modalità di rappresentazione/modifica dei dati, della quale potete trovare un riassunto a questo indirizzo, inoltre i parametri di conversione non sono rigidi, ma si può specificare un formato personalizzato per l'esposizione dei dati. Ad esempio, per formattare una data si può utilizzare un parametro come il seguente: {0:MM/dd/yyyy} che mostrerà nell'ordine il mese, il giorno e l'anno. Oppure, utilizzare la simbologia: {0:F2} farà sì che il numero ricevuto venga trattato come numero a virgola mobile, con due decimali. Su Internet troverete comunque molti esempi di formati validi. Ora siamo pronti per testare la nostra applicazione.
Test dell'applicazione
Premiamo F5 per vedere l'effetto delle nostre modifiche, che viene riportato nella figura seguente:![]()
Se utilizzate Windows in lingua italiana dovreste visualizzare il simbolo dell'Euro per la valuta e i giorni in lingua italiana per le date. Ad ogni buon conto, il modo in cui i dati vengono presentati è decisamente migliore. Ma questo è solo ciò che riguarda l'esposizione.
Proviamo ora a vedere cosa succede nella modifica, per esempio scrivendo del testo in uno dei campi relativi alla colonna Freight:![]()
Scrivendo un valore non valido (ma non nullo), WPF mostra un importo di $ 0.00, come stabilito nella validazione imposta nell'implementazione del metodo ConvertBack. Se faccio la stessa cosa in un campo relativo alla colonna Order date, ottengo il risultato illustrato in figura:
![]()
WPF ha inserito per noi la data odierna, ossia il valore di default che avevamo specificato nel metodo ConvertBack nel caso in cui venga inserito un valore non valido ma non nullo. Non avendo previsto codice per il salvataggio dei dati, le modifiche fatte non verranno persistite nel database.
L'attributo ValueConversion
Esiste un attributo chiamato ValueConversion che permette di decorare le classi che implementano l'interfaccia IValueConverter e il suo utilizzo è una best practice suggerita dalla MSDN Library. Tale attributo, infatti, consente di indicare in anticipo al run-time quali saranno i tipi di dato coinvolti nella conversione. Nell'esempio relativo alla conversione di stringhe in date, l'attributo può essere applicato come segue:<ValueConversion(GetType(String), GetType(DateTime))> _ Public Class CustomConverter Implements IValueConverterNovità di WPF in .NET Framework 3.5 Service Pack 1
.NET Framework 3.5 Service Pack 1 ha introdotto la possibilità di utilizzare un attributo StringFormat direttamente nello XAML per formattare i dati secondo le proprie necessità. Ne ho parlato in questo mio post sul blog. Questa tecnica semplifica notevolmente la formattazione dei dati, ma è bene ricorrere alla IValueConverter quando si vuole prevedere una logica di validazione personalizzata.Conclusioni
In questo articolo avete imparato un'altra tecnica importante nel data-binding in WPF e avete visto come formattare e validare i dati per essere sicuri della coerenza con la sorgente dati. Come accennato all'inizio dell'articolo, IValueConverter permette di eseguire operazioni di questo tipo su molti oggetti .NET, anche complessi. Se avete un po' di curiosità, vi basterà fare semplici ricerche su un motore di ricerca in Internet per trovare decine di utilizzi di questa interfaccia. Per ulteriori informazioni potete contattarmi al mio indirizzo visitare il mio blog.