Press "Enter" to skip to content

WPF Beginners – le basi del DataBinding

Introduzione

Questo articolo, segue una risposta data sui forum WPF di Microsoft ove un principiante, abituato alla programmazione per eventi si chiedeva come poter mettere un valore in una label da codice in WPF. Anche se il WPF permette di dare un nome ai controlli e quindi vederli dal codice per aggiornare le loro proprietà, non è il corretto modo di procedere in WPF in WPF le parole chiave da ricordare sono:

DataContext, DataBinding o Binding e separazione fra XAML (Parte visuale dell’interfaccia) e CodeBehind, il codice che la pilota.

 

In questo articolo, creeremo una Window con al suo interno 4 controlli (in realtà sono 6 per darvi alcuni concetti in più) discuteremo nel dettaglio come ho messo i controlli nella window e perchè l’ho fatto in questo modo. Ovviamente il codice a corredo sarà a vostra disposizione con un link in fondo a questo articolo.

 

Ambiente di sviluppo e progetto

Sto utilizzando l’ultima versione di Visual Studio 2017 aggiornato all’ultimo update disponibile, se siete dei beginner vi consiglio di scaricare la community edition gratuita di Visual Studio 2017.

Per il progetto ho usato il template base standard di Visual Studio per Windows Classic applications, WPF.

Ho inoltre utilizzato il framework 4.5.2 ma potete usare qualsiasi versione superiore alla 3.0 che credo sia la prima con il WPF.

 

Alcune nozioni di base su WPF

Innanzi tutto vi consiglio di cercare un libro o un e-book se volete qualcosa di stringato e gratuito potete provare WPF Succintly uno dei molti della serie Succintly disponibile sul sito Syncfusion.com uno dei migliori produttori di componenti per .Net. Ci sono vari e comprensivi libri su WPF, io consiglio Pro WPF di Matthew McDonald sfortunatamente è solo in inglese, ma Mc Donald è fra i miei autori preferiti per la chiarezza di esposizione e l’approfondimento degli argomenti.

Raccontiamo ora un paio di cose fondamentali che sicuramente sono scritte nei libri, ma magari non avete avuto tempo di distillare.

Data Driven

WPF è Data Driven, al contrario delle Windows Forms, che sono Event Driven. Questo vuol dire che l’interfaccia WPF viene pilotata dai Dati invece che dagli Eventi. Ciò non significa che in WPF non ci siano eventi o non siano usati, tutt’altro, però in wpf per aggiornare il valore di un dato non utilizzeremo il TextChanged e per controllarne il contenuto non utilizzeremo il Validate, invece utilizzeremo massivamente le classi, le loro property e soprattutto l’evento PropertyChanged, che vi consiglio di tener caro perché sarà il vostro migliore alleato nello sviluppo di programmi e interfacce funzionali e funzionanti.

Grafica e Codice separati

La parte grafica e il codice che gestisce la parte funzionale delle applicazioni sono ben separati, ed anche il codice che comunque è a supporto della parte grafica è sempre ben distinto da tutto il codice funzionale.

In WPF è importante imparare a suddividere la vostra applicazione in “strati” anche senza creare applicazioni enormi e con molte e diverse dll. Gli strati base per una applicazione che lavora con dati e database, siano essi Sqlite, Mysql (MariaDb, AuroraDb), SqlServer o altri database sono i seguenti:

  • Classi Dati, le classi che interagiscono con il Database.
  • Classi Entity, le classi che mappano il database su oggetti utilizzabili dall’interfaccia utente.
  • Classi Grafiche, le window e gli user control, che usano i dati forniti dagli altri strati per creare l’interazione con l’utente.

Possiamo declinare questi tre strati in ulteriori sotto strati, ma in questo caso essendo solo un articolo per iniziare, memorizzatelo e archiviatelo in un angolo della mente per uso futuro. Questo articolo lavorerà su concetti molto molto più semplici.

Grafica vettoriale

WPF funziona in grafica vettoriale, utilizzando al meglio la scheda grafica di ogni computer, pertanto il disegno e il posizionamento dei controlli su una Window o uno User control sono diversi da come si faceva in Windows Forms, WPF possiede una serie di controlli nutrita e combinabile per ottenere interfacce professionali e molto efficaci. Ci sono due classi primarie di controlli, i controlli di Layout ed i controlli visuali.

Anche se tutti i controlli o quasi hanno qualcosa di Visuale, alcuni sono dedicati a fornire servizi di layout, fra questi la Grid, lo StackPanel, il WrapPanel, il Canvas, il ContentContainer. Altri sono dedicati a gestire i dati visualmente, come ad esempio TextBlock, Image, TextBox, CheckBox, RadioButton. Altri forniscono servizi grafici a supporto, come il Border, lo ScrollViewer ed altri.

Tenendo presente queste tre cose fondamentali, iniziamo a parlare di come sono fatte le cose.

Il tag Window

Per questo progetto base, utilizzeremo MainWindow.xaml e MainWindow.xaml.cs che sono generati automaticamente dal template base WPF di Visual Studio. Ogni componente visuale di WPF è formato da due files, la parte XAML che gestisce la parte visuale, e la parte di CodeBehind ovvero il file .xaml.cs che contiene il codice per gestire il componente e quello che esso fa nel programma.

Assumiamo che abbiate almeno una vaga idea di cosa sia XML e cosa sia XAML e descriveremo brevemente i vari componenti, come li abbiamo configurati e perché.

Il primo di tutti è proprio Window, ovvero la classe base WPF da cui deriva MainWindow.

<Window x:Class="DataBinding.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:DataBinding"
        mc:Ignorable="d"
        Title="Databinding basics" Height="350" Width="525">
</Window>

Ho detto che MainWindow è una classe derivata perché XAML è solo un altro modo per scrivere codice C#, e quindi Ereditarietà, Polimorfismo, Incapsulamento sono termini che si applicano anche alla porzione XAML delle applicazioni WPF.

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }
}


Nel CodeBehind (ovvero la classe MainWindow.xaml.cs) che unita al codice XAML compone la nostra MainWindow, vediamo che è una classe che eredita da Window.

Ma torniamo alla descrizione dettagliata di quello che viene inserito negli attributi del tag Window di MainWindow.

<Window x:Class="DataBinding.MainWindow"

 

L’attributo x:Class, serve per indicare come si chiama la classe derivata da window, con il suo Namespace, nel caso del nostro progetto, il root namespace è DataBinding, da qui la dicitura. DataBinding.MainWindow. E’ importante controllare che questo dato sia coerente se spostate la classe da una cartella ad un altra ovvero da un namespace a uno diverso se non sono coerenti riceverete degli orrendi errori di compilazione che vi dicono che la classe non possiede un costruttore o simili errori solitamente incomprensibili.

        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:DataBinding"

Gli attributi xmlns, sono i corrispondenti delle clausole using all’interno di una classe C#, in questo caso, il template contiene i namespace base di WPF ed inoltre contiene il namespace base dove si trova la classe MainWindow indicato con il prefisso local.

A cosa serve, nella nostra applicazione, non serve a nulla perché non utilizziamo altre classi oltre a MainWindow, in una applicazione reale, potrebbe essere utilizzato per mappare risorse e classi utilizzate dentro alla MainWindow e scritte nello stesso namespace.

Nello XAML non è possibile inserire più namespaces senza un alias, per questo, dalla seconda dichiarazione in poi ognuno degli xmlns ha un codice che è il prefisso utilizzato dai controlli che si trovano in quel namespace.

        mc:Ignorable="d"
        d:DesignHeight="400"
        d:DesignWidth="640"

La dicitura mc:Ignorable, serve ad introdurre un prefisso che permette di definire degli attributi che servono al designer di Visual Studio ma sono ignorati durante la compilazione, in questo caso per fare un esempio ho definito l’altezza e la larghezza della finestra nel designer, mentre le dimensioni reali della finestra Height e Width sono quelle che assumerà a Runtime.

        Title="Databinding basics" Height="350" Width="525">

Title contiene il titolo della nostra finestra, in questo caso una stringa, ma potrebbe essere anche un testo dinamico che viene inserito a Runtime.

Height e Width rappresentano la dimensione della finestra allo startup.

Tutte le caratteristiche e gli attributi che possono essere inseriti in questo tag, possono essere esplorati nella finestra Properties di Visual Studio posizionando il cursore all’interno del Tag Window.

Ho inserito uno screenshot delle property per comodità, come vedete vi sono molte property (che divengono attributi del tag) per la Window, che se non modificati rimangono inizializzati al loro valore di default, come ad esempio il ResizeMode che permette alla Window di essere ridimensionata e WindowState che indica che la finestra si apre in modalità normale. potete provare a cambiare alcune di queste proprietà per verificare come cambia la finestra a runtime.

Il tag Grid

<Grid>
</Grid>

L’oggetto Grid di WPF non ha nulla a che vedere con una Datagrid o una DataGridView anche se sospetto che siauno degli oggetti di base su cui quei componenti sono costruiti, è un oggetto normalmente non visibile, che fornisce semplicemente un supporto al layout dei componenti nella Window. E’ un componente che può contenere una collection di controlli al suo interno, al contrario di altri componenti di Layout come  ContentContainer ed altri che possono contenere un singolo oggetto. Inoltre può essere suddiviso in Righe e Colonne ed i controlli inseriti nella grid possono essere posizionati nelle righe e colonne tramite le Attached Properties Row e Column, inoltre i controlli possono occupare spazi non regolari nella grid utilizzando le Attached Properties Rowspan e Columnspan.

Se mi chiedete ma cos’è una Attached Property, si tratta di una Property definita nell’oggetto Grid ma che viene inizializzata negli oggetti in essa contenuti. in questo esempio utilizzeremo solo la attached property Row, ma ci darà una idea in merito.

Per definire righe e colonne, la Grid contiene al suo interno due collection RowDefinitions e ColumnDefinitions in cui possiamo inserire degli oggetti di tipo RowDefinition e ColumnDefinition per la definizione della forma delle righe e delle colonne della grid.

Gli oggetti RowDefinition contengono una property Height che può assumere un valore fisso in unità standard WPF (attenti che le misure sono di vari tipi  Qui trovate la definizione completa) i due tipi più usati sono n+* (star) e Auto dove n è un numero intero che permette di dare delle proporzioni all’altezza delle righe.

Gli oggetti ColumnDefinition contengono una property Width che funziona allo stesso modo di Height per poter definire la dimensione delle colonne in modo statico o dinamico.

<Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="50*"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="50*"/>
</Grid.RowDefinitions>

Nel nostro esempio, abbiamo definito 4 righe nella nostra grid, due con dimensione Auto ovvero che assumono l’altezza di quello che è il loro contenuto e due di altezza 50+* ovvero in questo caso il 50% dello spazio libero tolto quello occupato dalle 2 colonne Auto. Il numero accanto all’asterisco è arbitrario, avremmo potuto ottenere lo stesso risultato scrivendo 1* in entrambi o 5* o 500* il numero serve solo a noi umani per costruire delle suddivisioni proporzionali più o meno complesse (pensate a una grid con 50 righe e con controlli che devono occupare spazi più o meno grandi in base al loro uso) mantenendo la proporzionalità in modo da adattarsi alla dimensione della finestra.

Il tag Label

<LabelGrid.Row="0">
</Label>

Se guardiamo il solo tag Label ci accorgiamo che in realtà ha un solo attributo, questo è perché la Label è un altro controllo per il layout, quindi un controllo contenitore che non ha alcuna parte visibile. Quello che fa il controllo Label è fornire un rettangolo che può essere riempito da altri controlli ed alcune altre funzionalità. Infatti, nel nostro caso, vi abbiamo inserito un ulteriore controllo contenitore, uno StackPanel in cui abbiamo inserito un Immagine e una TextBlock.
L’attributo Grid.Row=”0″ della Label, è l’Attached Property della grid in cui la Label è stata inserita che indica alla Grid che la Label (ed il suo contenuto) devono essere posti nella prima riga della grid stessa.

<Label Grid.Row="0">
    <StackPanel 
         
         Orientation="Horizontal"
         Margin="4,2,4,2">
        <Image
         Width="24"
         Height="24"
         Source="pack://application:,,,/Images/Shield.png"/>
        <TextBlock
         HorizontalAlignment="Left"
         VerticalAlignment="Center"
         Text="{Binding LabelText, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"/>
    </StackPanel>
</Label>

Potevamo evitare di usare la Label e usare solo lo StackPanel ? SI. perché l’ho aggiunta? Perché la risposta sul forum riguardava il controllo Label e come mettervi del testo dentro, quindi volevo mostrare a chi ha posto la domanda che in realtà la label può ospitare molto più che un semplice testo. Ma vediamo in dettaglio i controlli dentro alla Label.

Il tag StackPanel

<StackPanel 
    Orientation="Horizontal"
    Margin="4,2,4,2">
</StackPanel>

Lo StackPanel è un contenitore che può contenere una Pila di controlli, l’attributo Orientation ci dice se gli oggetti saranno allineati in Orizzontale (come nel nostro caso) o Verticale. Inoltre l’attributo Margin dello StackPanel crea uno spazio attorno al controllo, i numeri sono i margini Sinistro, Superiore, Destro, Inferiore, quindi 2 unità grafiche sopra e sotto, 4 unità grafiche a sinistra e a destra.

I controlli inseriti nello StackPanel sono impilati nell’ordine in cui compaiono al suo interno, quindi in questo caso prima l’immagine e poi il testo.

Il tag Image

<Image
    Width="24"
    Height="24"
    Source="pack://application:,,,/Images/Shield.png" />

L’Image è un controllo creato per contenere immagini, in questo caso, usiamo un immagine inserita nel progetto come Resource che sarà inserita dal compilatore nell’Exe della nostra applicazione. lo strano URL inserito nella property Source è in realtà un URI = Unique Resource Identifier ovvero un Identificatore Univoco di Risorsa. pack: è il protocollo che indica al programma che la risorsa è embedded nell’EXE.
application:, indica che si trova nell’exe dell’applicazione, se si trovasse in una dll, qui ci sarebbe il nome della stessa. le 3 virgole servono da placeholder per 3 diverse ulteriori aggiunte all’URI personalmente non mi è mai successo di scrivere qualcosa fra una virgola e l’altra. /Images/nomeimmagine.ext è il path della risorsa immagine relativo al progetto, nel mio caso la cartella Images dentro al progetto:

Come potete vedere dall’immagine qui sopra.

Se l’mmagine che volessi visualizzare fosse dinamica (ad esempio un immagine scelta dall’utente sul suo disco) per gestirla potrei utilizzare il Binding ad una property e inserire in quella property l’immagine quando l’utente la seleziona. Magari potrebbe essere oggetto di un ulteriore articolo.

Il tag TextBlock

<TextBlock
    HorizontalAlignment="Left"
    VerticalAlignment="Center"
    Text="{Binding LabelText, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" />

A completare il contenuto della Label, c’è finalmente un controllo che può contenere un testo, ovvero il TextBlock, è un controllo visuale, il suo contenuto può essere allineato, posizionato, può avere ulteriori Margini ed eventualmente il Padding (ovvero la possibilità di distanziare il testo contenuto dai bordi del controllo quindi margine interno).

La strana serie di cose che abbiamo inserito nella property Text invece di inserire un semplice testo ci servono per mostrare come si fa in WPF a inserire dinamicamente del testo in una etichetta.

{Binding LabelText, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}

Binding = Collegamento, pertanto in italiano la riga qui sopra si traduce in: Collega il tuo Testo alla Property che si chiama LabelText, la modalità è a senso unico quindi solo il programma può modificare il contenuto (Textblock non può essere editato), Aggiorna il tuo contenuto ogni volta che la property LabelText viene modificata (PropertyChanged).

Il tag TextBox

<TextBox
    Grid.Row="1"
    Margin="4,2,4,2"
    HorizontalAlignment="Stretch"
    VerticalAlignment="Stretch"
    HorizontalContentAlignment="Left"
    AcceptsReturn="True"
    AcceptsTab="True"
    Text="{Binding LabelText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
    VerticalScrollBarVisibility="Auto" />

Per vedere come si modifica il testo in base alla modifica della property, ho deciso di inserire una casella di testo che permetta di scrivere un testo nella nostra Property. Come potete vedere, la TextBox ha degli attributi simili agli altri controlli, ed essi definiscono che: la TextBox è contenuta nella Row=”1″ della Grid (ovvero la seconda riga) che il suo Margine è 2 sopra e sotto e 4 a sinistra e destra, Il suo allineamento Orizzontale e Verticale occupa tutto lo spazio della riga della Grid, il testo contenuto è allineato a Sinistra, permette di inserire più righe e permette di inserire anche i tabulatori.

La sua property Text ovvero il contenuto è stato collegato alla Property LabelText come quello della TextBlock della Label, cambia solo il Mode=TwoWay, questo specifico attributo dice al sistema che la Textbox può ricevere un testo programmaticamente ma può anche modificare il suo contenuto.

Abbiamo aggiunto, visto che la TextBox è multilinea anche l’automatismo per mostrare una scrollbar verticale se ci fosse troppo testo al suo interno.

Le ulteriori 2 righe

<TextBlock
    Grid.Row="2"
    Margin="4,2,4,2"
    Text="{Binding TextBlockText, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" />
<TextBox
    Grid.Row="3"
    Margin="4,2,4,2"
    HorizontalAlignment="Stretch"
    VerticalAlignment="Stretch"
    HorizontalContentAlignment="Left"
    AcceptsReturn="True"
    AcceptsTab="True"
    Text="{Binding TextblockText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
    VerticalScrollBarVisibility="Auto" />

La terza e quarta riga, sono simili alle precedenti, ho solo usato qualcosa di più semplice ovvero solo la TextBlock come etichetta e una ulteriore TextBox per modificarne il contenuto. Serve solo a illustrare il fatto che ci sono molti modi più o meno complessi di utilizzare i controlli nella Grid.

 

Il CodeBehind

Adesso, per far funzionare la nostra applicazione dobbiamo aggiungere il codice necessario a definire le 2 property LabelText e TextBlockText e il codice che notifica ai controlli visuali quando queste property sono modificate. Apriamo quindi il file MainWindow.xaml.cs.

using System.ComponentModel;

Prima modifica, aggiungiamo la clausola using System.ComponentModel, questo ci permetterà di implementare l’interfaccia INotifyPropertyChanged, ovvero Notifica che una property è cambiata, che è la responsabile della comunicazione alla nostra Window che il contenuto della sua property è stato modificato.

public partial class MainWindow : Window, INotifyPropertyChanged

Modifichiamo la dichiarazione della classe MainWindow per indicare che implementiamo l’interfaccia INotifyPropertyChanged, attenzione che se non lo facciamo, anche se implementiamo il codice necessario il sistema non riceverà i messaggi.

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

Implementiamo l’interfaccia nella nostra classe, questa interfaccia è composta di un evento (PropertyChanged) e del metodo che lo solleva (OnPropertyChanged), quando WPF rileva una classe con questa Interfaccia, crea automaticamente un EventHandler per l’evento PropertyChanged e provvede a inoltrare le modifiche alle property alla User Interface.

public string LabelText
{
    get
    {
        return mLabelText;
    }
    set
    {
        mLabelText = value;
        OnPropertyChanged("LabelText");
    }
}
public string TextBlockText
{
    get
    {
        return mTextBlockText;
    }
    set
    {
        mTextBlockText = value;
        OnPropertyChanged("TextBlockText");
    }
}

Implementiamo ora le 2 property e inseriamo nel loro metodo set la riga che scatena l’evento PropertyChanged indicando il nome della property che lo ha sollevato.

public MainWindow()
{
    InitializeComponent();
    DataContext = this;
}

Ultima modifica, la più importante è aggiungere la riga DataContext = this; al costruttore della classe, se provate a commentare questa riga vedrete che il programma non funzionerà. Questa riga è il comando che indica alla nostra Window che le property collegate in Binding si trovano dentro a se stessa.

Perché dobbiamo specificarlo? Perché non lo sa da sola?

Per un motivo molto semplice, nei programmi più complessi, normalmente il DataContext non è la classe window, che contiene solo la propria parte visuale ma è una classe esterna che prende il nome di:

Rullo di tamburi… ViewModel, il pattern più utilizzato per la realizzazione di applicazioni WPF, MVVM Model View ViewModel, inizia da qui, la sua implementazione più semplice è quella in cui Model, View e ViewModel stanno nella stessa classe invece di trovarsi in 3 classi separate.

In un articolo successivo potrei implementare le 3 classi e mostrarvi il modello completo anche se in modalità semplice.

Se lanciamo la nostra applicazione otteniamo questo:

La nostra window con l’immagine e le label e textbox vuote.

Se scrivo qualcosa nella prima TextBox, la sua Label viene popolata con quanto scritto.

Se scrivo nella seconda TextBox la sua Textblock viene popolata con quanto scritto.

Lo stesso succede se cancello.

Se vado a scrivere più righe vedrete che i controlli si ridimensionano automaticamente grazie al fatto che sono stati definiti in modo da avere dimensioni relative gli uni rispetto agli altri.

 

Mi fermo qui per questa sera, il codice a corredo può essere scaricato al link seguente:

Se avete domande, osservazioni, commenti, potete scrivermi utilizzando l’opzione di menu con la bustina.