Press "Enter" to skip to content

WPF Utilizzare una ListView in modo Classico o in Binding con MVVM

Un post che deriva da un thread su Stack Overflow in cui un principiante non riusciva a capire come recuperare il selected item di una ListView intercettando gli eventi. Dal codice esempio che ha mostrato, stava utilizzando l’interfaccia WPF nella stessa modalità in cui avrebbe fatto in una applicazione Windows Forms, cosa che è fattibile e funziona perfettamente, ma è un po come utilizzare un trattore da 500 cavalli per arare le aiuole del nostro orto condominiale.

Pertanto, ho deciso di creare questo esempio che mostra come utilizzare WPF come eravamo abituati a fare con le windows forms, e quindi creare una applicazione event driven, e come utilizzare invece WPF nel modo corretto e quindi creare una applicazione Data Driven, dove tutto quello che è la forma e il design della parte grafica è indipendente dalla gestione dei dati.

mainwindow_01

L’Applicazione

L’applicazione che abbiamo creato per dimostrare come utilizzare WPF in modalità classica o in modalità MVVM è così strutturata:

  • Una finestra contenente un Tab Control con due diversi Tabs
  • Uno User Control, chiamato ClassicList che implementa la gestione della lista dei clienti in modalità Event-driven
  • Uno User Control, chiamato BoundList che implementa la gestione della lista dei clienti in modalità Data-driven
  • Una classe, CustomersManager, che si occupa di fornire i dati ad entrambi i controlli
  • Una classe, Customer, che rappresenta un record delle nostre liste

La struttura della soluzione è la seguente

solution_01

Potete notare che abbiamo creato una “struttura” dell’applicazione diversa dalla forma base creata da visual studio per una nuova applicazione WPF, abbiamo infatti creato una cartella Windows, dove abbiamo posto la MainWindow, una cartella Controls dove abbiamo posto i due User Control che gestiscono le nostre liste, una cartella Entities, che ospita la classe Customer.

Vediamo ora le varie classi e come funziona la nostra applicazione.

App.xaml e App.xaml.cs

Avendo spostato la main window su una sottocartella, abbiamo modificato lo standard App.xaml e di conseguenza il suo code behind, per pilotare in modo a noi congeniale il comportamento dell’applicazione.

<Application x:Class="ListClassicMvvm.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ListClassicMvvm"
>
<Application.Resources>

</Application.Resources>
</Application>

Come potete notare, abbiamo tolto l’attributo StartupUri, che nel progetto standard contiene “MainWindow.xaml” ed abbiamo modificato App.xaml.cs nel seguente modo:

public partial class App : Application
{


protected override void OnStartup(StartupEventArgs e)
{
this.ShutdownMode = System.Windows.ShutdownMode.OnMainWindowClose;
base.OnStartup(e);

this.MainWindow = new MainWindow();
MainWindow.Show();
}

}

Per poter configurare il comportamento dell’applicazione ed eventualmente gestire tutte le operazioni di “startup” come ad esempio controlli sulla rete, le risorse e il computer, possiamo utilizzare l’evento OnStartup della classe Application. Nel nostro caso, abbiamo semplicemente istanziato la classe MainWindow e l’abbiamo assegnata alla property omonima, mostrandola, abbiamo inoltre indicato che l’applicazione si chiude alla chiusura della MainWindow.

MainWindow.xaml e MainWindow.xaml.cs

<Window x:Class="ListClassicMvvm.Windows.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:lctl ="clr-namespace:ListClassicMvvm.Controls"
xmlns:local="clr-namespace:ListClassicMvvm"
mc:Ignorable="d"
Title="MainWindow" Height="640" Width="960">
<Grid>
<TabControl>
<TabItem Header="Classic List">
<Grid>

<lctl:ClassicList/>
</Grid>
</TabItem>
<TabItem Header="Bound List">
<Grid>
<lctl:BoundList />
</Grid>

</TabItem>
</TabControl>
</Grid>
</Window>

Nello xaml della MainWindow abbiamo inserito il riferimento al namespace ListClassicMvvm.Controls, che ospita gli User Controls delle due liste, abbiamo modificato la dimensione della finestra ed abbiamo creato un TabControl, con due TabItem, ponendo in ciascuno di essi uno dei nostri user control. L’Header del TabItem contiene il titolo della linguetta.

public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
WindowStartupLocation = WindowStartupLocation.CenterScreen;
}


}

Nel codice di MainWindow, non facciamo assolutamente nulla, salvo indicare al sistema di aprire la finestra sullo schermo correntemente selezionato.

Customer.cs

La classe Customer, è la classe che ospiterà i dati relativi ai clienti che andremo a leggere, come nei post precedenti dal database di esempio Northwind.

using System;
using System.ComponentModel;
using System.Linq;
using System.Text;

namespace ListClassicMvvm.Entities
{

public class Customer : INotifyPropertyChanged
{
.... 
    }
}

Commenteremo la classe per pezzi, visto che pure essendo semplice implementa varie cose interessanti ed importanti per chi si sta avvicinando all’uso di WPF. Questa classe dati è stata implementata in modo da poter supportare correttamente Mvvm e quindi il necessario a funzionare correttamente quando collegata ai controlli della User Interface.

La prima cosa che noterete è il fatto che implementa l’interfaccia INotifyPropertyChanged, questa interfaccia, permette di fare in modo che una classe notifichi alla User Interface che le sue proprietà sono cambiate così che i componenti possano reagire e modificare il loro stato.

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

L’interfaccia INotifyPropertyChanged si implementa creando l’evento che vedete qui sopra, questo evento, dovrà essere scatenato alla modifica di ciascuna delle property della classe che vogliamo interagisca con la user interface.

public const string FLD_Address = "Address";

public const string FLD_City = "City";

public const string FLD_CompanyName = "CompanyName";

public const string FLD_CustomerData = "CustomerData";

La prima cosa che troverete all’interno della classe è la dichiarazione delle costanti con i nomi delle property, non è qualcosa di obbligatorio, è un pattern che a me piace seguire semplicemente perché spesso capita di dover usare i nomi delle property dei campi di un oggetto e, usando delle costanti invece di scrivere ogni volta la stringa a mano abbiamo 2 vantaggi:

  1. Non c’è pericolo che sbagliamo di scrivere il nome e poi non capiamo perché qualcosa non funziona.
  2. Se cambiamo il nome di una proprietà, e abbiamo usato la costante in giro per il programma, cambiando il nome della costante viene automaticamente aggiornata ovunque, cambiando solo la stringa è comunque già aggiornata ovunque.
private string mAddress;


private string mCity;


private string mCompanyName;

Le variabili a livello di classe che implementano le property in questo caso sono solo 3 e vedremo perché.

public string Address
{
get
{
return mAddress;
}
set
{
mAddress = value;
OnPropertyChanged(FLD_Address);
OnPropertyChanged(FLD_CustomerData);
}
}


public string City
{
get
{
return mCity;
}
set
{
mCity = value;
OnPropertyChanged(FLD_City);
OnPropertyChanged(FLD_CustomerData);
}
}


public string CompanyName
{
get
{
return mCompanyName;
}
set
{
mCompanyName = value;
OnPropertyChanged(FLD_CompanyName);
OnPropertyChanged(FLD_CustomerData);
}
}


public string CustomerData
{
get
{
return string.Format("{0} {1} {2}", CompanyName, Address, City);
}
}

Le property che implementano i 3 campi della classe +1, come spero abbiate notato, quando viene modificato uno dei 3 campi CompanyName, Address, City, viene sollevato l’evento di modifica anche per il campo CustomerData, questo serve per informare la User Interface che anche il valore di quel campo a sola lettura deve essere aggiornato.

Il campo CustomerData, non è altro che una composizione del contenuto dei 3 campi della classe.

CustomersManager.cs

Questa classe gestisce l’acquisizione dei dati dal database e la fornitura degli stessi ad entrambi gli User control, sia in modalità classica che in modalità Mvvm.

using ListClassicMvvm.Entities;
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Windows;

namespace ListClassicMvvm
{
///<summary>
/// Descrizione della classe:
///</summary>
public class CustomersManager : INotifyPropertyChanged
{
...

 

    }
}

Anche in questa classe come potete notare, abbiamo implementato l’interfaccia INotifyPropertyChanged, questo perché questa classe sarà il ViewModel della lista Mvvm.


public const string FLD_Customers = "Customers";

public const string FLD_SelectedCustomer = "SelectedCustomer";

Anche in questo caso, le costanti con i nomi delle property, sono 2 la prima è la collezione dei clienti che fornirà i dati alle liste, la seconda è un singolo cliente, che rappresenterà l’elemento selezionato della lista.

private const string SQL_AllData = "SELECT * FROM customers";


private string mConnectionString = "data source=.\\;initial catalog=NORTHWND;integrated security=SSPI;";


private ObservableCollection<Customer> mCustomers;


private Customer mSelectedCustomer;

Le variabili a livello di classe, oltre alla query per il database, la connection string a SQL Server, fate attenzione che questa connection string funziona solo se avete installato SQL Server con l’istanza di default, ovvero quella che si chiama con il nome del vostro computer, se come solitamente accade installando SqlExpress vi è stata generata un istanza nominata, (MyPCName\Sqlexpress) dovete indicare il nome completo.

Oltre a questo, abbiamo le variabili di supporto alle property della collection dei Customer e al Selected Customer.

public CustomersManager()
{
Customers = new ObservableCollection<Customer>();
}

Il costruttore della classe, ai fini di evitare stranezze, consiglio sempre che nelle classi che contengono delle collezioni, le collezioni siano create automaticamente alla creazione della classe, e soprattutto, se non per uno scopo preciso, le collezioni non possano essere generate dall’esterno. Nel nostro caso, come vedrete nel codice delle property, il costruttore della collezione Customers è infatti privato.

public ObservableCollection<Customer> Customers
{
get
{
return mCustomers;
}
private set
{
mCustomers = value;
OnPropertyChanged(FLD_Customers);
}
}

public Customer SelectedCustomer
{
get
{
return mSelectedCustomer;
}
set
{
mSelectedCustomer = value;
OnPropertyChanged(FLD_SelectedCustomer);
}
}

Le nostre due property, che sollevano l’evento property changed quando modificate, fate attenzione, che la collection solleverà questo evento solo quando viene generata e non quando inseriremo o modificheremo dati al suo interno. Però, la classe collection che abbiamo usato, l’ObservableCollection, è una delle collection create appositamente per WPF e Mvvm e implementa al suo interno il codice necessario a notificare alla User Interface che gli è stato inserito, cancellato o modificato il contenuto.

public void LoadData()
{
try
{
using (SqlConnection cn = new SqlConnection(mConnectionString))
{
SqlCommand cmd = new SqlCommand();
cmd.Connection = cn;
cmd.CommandText = SQL_AllData;
cmd.CommandType = CommandType.Text;
cn.Open();
SqlDataReader reader = cmd.ExecuteReader();
Customers.Clear();
if (reader.HasRows)
{
while (reader.Read())
{
Customer c = new Customer();
c.CompanyName = reader[Customer.FLD_CompanyName].ToString();
c.Address = reader[Customer.FLD_Address].ToString();
c.City = reader[Customer.FLD_City].ToString();
Customers.Add(c);
}
}
else
{
MessageBox.Show("No records found");
}
reader.Close();
cn.Close();
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}

Il metodo qui sopra, si occupa di interrogare il database e generare la collezione dei dati da fornire alle nostre liste. Verrà chiamata da entrambi gli User Control.

ClassicList.xaml e ClassicList.xaml.cs

Lo User control che implementa la gestione della lista in modalità classica, ovvero Event Driven. Vediamo come funziona.

<UserControl x:Class="ListClassicMvvm.Controls.ClassicList"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:ListClassicMvvm.Controls"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300" Loaded="ClassicList_Loaded">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListView
Grid.Row="0"
Name="CustomersList"
SelectionMode="Single" Margin="0"
SelectionChanged="CustomersList_SelectionChanged" >
<ListView.View>
<GridView>
<GridViewColumn Header="Company Name" DisplayMemberBinding="{Binding CompanyName, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" Width="300"/>
<GridViewColumn Header="Address" DisplayMemberBinding="{Binding Address, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" Width="200"/>
<GridViewColumn Header="City" DisplayMemberBinding="{Binding City, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" Width="150"/>
</GridView>
</ListView.View>
</ListView>
<TextBlock
Margin="4,2,4,2"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Grid.Row="1"
Name="SelectedText"/>
</Grid>
</UserControl>

Nello XAML della classe, abbiamo creato una Grid con 2 righe, la prima ospita la ListView, la seconda ospita una TextBlock che useremo per mostrare come il selected item venga modificato quando l’utente si sposta sulla lista, infatti nella textblock verrà visualizzato il contenuto del campo correntemente selezionato. Cosa dobbiamo notare in questa implementazione?

  • Abbiamo implementato un event handler per l’evento Loaded dello User control, questo è l’evento in cui caricheremo i dati nello user control.
  • Abbiamo dato un nome alla ListView CustomersList
  • Abbiamo implementato un event handler per l’evento SelectionChanged della ListView
  • Abbiamo dato un nome alla TextBlock SelectedText

Abbiamo inoltre implementato il Binding dei 3 campi della classe Customer a 3 colonne della list view

public partial class ClassicList : UserControl
{
...
}

Cosa abbiamo messo nello User Control, come vedete non abbiamo implementato alcuna interfaccia.


private CustomersManager mDataManager;


public ClassicList()
{
InitializeComponent();
DataManager = new CustomersManager();
}


private CustomersManager DataManager
{
get
{
return mDataManager;
}
set
{
mDataManager = value;
}
}

Abbiamo implementato una property della nostra classe CustomersManager, che viene istanziata nel costruttore ed è privata all’interno dello User Control.

private void ClassicList_Loaded(object sender, RoutedEventArgs e)
{
DataManager.LoadData();
CustomersList.ItemsSource = DataManager.Customers;
}

Nell’ event handler dell’evento Loaded dello User Control, abbiamo chiamato il metodo che interroga il database e carica i dati, abbiamo poi assegnato la collection Customers alla property ItemsSource della ListView utilizzando il suo nome.

private void CustomersList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems != null)
{
DataManager.SelectedCustomer = (Customer)e.AddedItems[0];
SelectedText.Text = DataManager.SelectedCustomer.CustomerData;
}
}

Infine, per poter aggiornare la TextBlock e il campo SelectedCustomer del nostro DataManager, nell’event handler della SelectionChanged della ListView, abbiamo utilizzato la property AddedItems fornita dall’evento che contiene l’elemento aggiunto alla collezione degli elementi selezionati. (nel nostro caso, abbiamo scelto la selezione per singola riga, quindi ci sarà sempre un solo elemento). E abbiamo effettuato il cast dell’elemento a Customer, infatti viene passato come generico object e lo abbiamo assegnato alla property SelectedCustomer, da cui abbiamo poi estratto il valore di CustomerData per aggiornare la textblock, ancora una volta utilizzando il suo nome.

BoundList.xaml e BoundList.xaml.cs

Ed ora, lo User Control che implementa la gestione della lista in modalità Mvvm ovvero Data-Driven.

<UserControl x:Class="ListClassicMvvm.Controls.BoundList"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:ListClassicMvvm.Controls"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
Loaded="BoundList_Loaded">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListView
Grid.Row="0"
ItemsSource="{Binding Path=Customers, Mode=OneWay}"
SelectedItem="{Binding Path=SelectedCustomer, Mode=TwoWay}"
SelectionMode="Single" Margin="0"
IsSynchronizedWithCurrentItem="True" >
<ListView.View>
<GridView>
<GridViewColumn Header="Company Name" DisplayMemberBinding="{Binding CompanyName, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" Width="300"/>
<GridViewColumn Header="Address" DisplayMemberBinding="{Binding Address, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" Width="200"/>
<GridViewColumn Header="City" DisplayMemberBinding="{Binding City, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" Width="150"/>
</GridView>
</ListView.View>
</ListView>
<TextBlock
Margin="4,2,4,2"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Grid.Row="1"
Text="{Binding SelectedCustomer.CustomerData, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
</UserControl>

Come potete vedere, lo xaml è molto simile a quello del controllo classic fatto salvo per un paio di cose importanti che listeremo.

  • Anche in questo caso abbiamo implementato l’event handler per l’evento Loaded dello User control che ci serve per caricare i dati.
  • La ListView non ha alcun nome, però, ha un valore due attributi:
    • ItemsSource, il cui valore è Bound alla property Customers del viewmodel.
    • SelectedItem il cui valore e Bound alla property SelectedCustomer del viewmodel.
  • La TextBlock, non ha alcun nome ugualmente, ma anche essa ha un valore per un attributo:
    • Text, il cui valore è Bound alla property SelectedCustomer.CustomerData del view model.
public partial class BoundList : UserControl
{
...
}

Anche in questo caso, la classe non implementa alcuna interfaccia aggiuntiva.

private CustomersManager mModel;

public BoundList()
{
InitializeComponent();
Model = new CustomersManager();
this.DataContext = Model;
}


public CustomersManager Model
{
get
{
return mModel;
}
private set
{
mModel = value;
}
}

Abbiamo implementato anche in questa classe una property che è un istanza di CustomersManager, ma in questo caso, l’abbiamo chiamata Model per enfatizzare il fatto che è il ViewModel di questo UserControl.

Oltre ad istanziarlo, nel costruttore lo assegnamo alla property dello user control che si chiama DataContext, questa property, è l’oggetto che Xaml usa per collegare i controlli quando definiamo un attributo con la dicitura Binding.

private void BoundList_Loaded(object sender, RoutedEventArgs e)
{
Model.LoadData();
}

Oltre a questo, la sola cosa che facciamo in questo user control, è caricare i dati nell’event handler Loaded. E null’altro.

Qual’è il risultato?

classic_01

classic_02

Nella lista classica, la riga con la textblock reagisce mostrando il dato selezionato quando ci spostiamo sui record.

Bound_01

Bound_02

In modalità Bound succede lo stesso, ma fatto salvo l’aggancio delle property ai controlli dentro allo Xaml, non abbiamo dovuto scrivere una riga di codice per gestire questo collegamento, l’importante è implementare correttamente la classe Entità che fornisce i singoli record, e la classe ViewModel che fornisce i dati allo user control.

Riepilogo

cosa abbiamo discusso in questo Post

  • Come si può usare WPF nel modo in cui eravamo abituati a fare in WindowsForms
  • Come utilizzarlo invece nel modo corretto implementando il Binding con Mvvm
  • Quali sono i vantaggi di Mvvm
    • Che la parte grafica è sganciata dal codice se non per il collegamento dei dati
    • Che scriviamo meno codice per raggiungere lo stesso risultato

Nei prossimi post, vedremo che ovviamente ci sono molti altri vantaggi nell’uso dell’approccio Mvvm, soprattutto per manipolare i dati visualmente e separare la parte che si occupa dell’aspetto da quella che si occupa della funzionalità.

Potete scaricare il progetto esempio dal link qui indicato:

Per qualsiasi domanda, osservazione, commento, approfondimento, o per segnalare un errore, potete usare il link alla form di contatto in cima alla pagina.