Applicazione filtri su DataGrid WPF usando il paradigma MVVM

Impostazione di un filtro su una DataGrid WPF usando il paradigma MVVM.

il
Sviluppatore software, Collaboratore di IProgrammatori

In questo articolo vedremo come impostare un filtro sui dati di una DataGrid WPF usando il paradigma MVVM (Model-View-ViewModel).

Per farlo creeremo un'applicazione WPF che visualizzerà un elenco di persone (DataGrid) e un combo box tramite il quale poter filtrare il contenuto dell'elenco in base al sesso.

Videata principale

Paradigma MVVM

Il paradigma Model-View-ViewModel prevede che il progetto segua questa architettura:

  • Model: sono i dati da gestire
  • View: videate in cui i dati vengono visualizzati
  • ViewModel: è il componente che fa da tramite fra View e Model ed esegue eventuali logiche applicative

Lo scompo del paradigma è quello di avere un prodotto facilmente manutenibile e, soprattutto, riutilizzabile. Pensiamo ad esempio se volessimo visualizzare lo stesso elenco su dispositivi diversi, dando a ciascuno la propria interfaccia grafica. Usando il paradigma MVVM risulta tutto molto facile: si riusano Model e ViewModel, e si creano semplicemente più view.

Sviluppo dell'applicazione

Creiamo un'applicazione WPF usando il tool standard di Visual Studio: chiamiamola WpfFilter e laciamo tutto il resto come default.

Solo per questioni di organizzazione, creiamo al suo interno 3 nuove cartelle:

  • Models: conterrà tutte le classi del modello dati. Nel nostro caso la classe Persona e l'enum Sesso
  • Views: conterrà tutta la parte grafica. Nel nostro caso il componente che visualizza l'elenco delle persone e relativo filtro.
  • ViewModels: conterrà i legami fra Models e Views. Nel nostro caso ci sarà l'estrazione dei dati e l'applicazione del filtro

Al termine di questa fase, la struttura dovrà essere simile alla seguente:

Struttura progetto

Model

Per prima cosa creiamo il modello dati del nostro progetto (cartella models), ovvero la classe relativa alle persone e l'enum sul sesso:

class Persona
{

   public int Id { get; set; }

   public string Nome { get; set; }

   public string Cognome { get; set; }

   public Sesso Sesso { get; set; }
}

public enum Sesso
{
   NonSpecificato,
   Uomo,
   Donna
}

Per comodità abbiamo creato la voce "NonSpecificato" nell'enum sesso quando in realtà avremmo potuto gestire il tutto mediante una proprietà di tipo nullable, dove il valore null stava appunto a specificare il non impostato. Usando la voce specifica nell'enum, possiamo associare il combo box di filtro direttamente all'enum, senza dover gestire il null.

E' solo uno dei modi di gestire la cosa, nelle applicazioni reali potete fare come preferite, anche usare una enum per i dati (uomo/donna) e una per l'interfaccia grafica (non specificato/uomo/donna) e quindi gestire il tutto all'interno del viewmodel.

ViewModel

Creiamo ora il ViewModel che rappresenta la logica del nostro programma. Le cose da fare sono abbastanza semplici:

  • Il view model deve implementare INotifyPropertyChanged per poter gestire il cambio di proprietà. Tramite questa interfaccia è possibile comunicare alla view quando cambia un valore (e quindi la view cambia il dato a video) ma anche quando tramite view viene cambiato un campo, così da poter gestire l'evento nel view model (nel nostro caso quando cambia il filtro da applicare)
  • Si devono caricare le persone da una sorgente dati. Nel nostro caso avremo un array in memoria poichè lo scopo di questo articolo è solo quello di applicarvi un filtro
  • Al cambio del combo box di filtro, attivare le logiche applicative per filtrare i dati

Creiamo quindi la classe PersonaViewModel all'interno della cartella ViewModels che avrà il seguente codice:

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Data;
using WpfFilter.Models;

namespace WpfFilter.ViewModels
{
class PersonaViewModel : INotifyPropertyChanged
{

public PersonaViewModel()
{
// Caricamento delle persone
Persone = CaricaPersone();

// Sottoscrizione al cambio di proprietà per intercettare il cambio filtro
PropertyChanged += OnPropertyChanged;

}


/// <summary>
/// Elenco delle persone visualizzate in griglia
/// </summary>
public ObservableCollection<Persona> Persone { get; }


private Sesso _filtroSulSesso = Sesso.NonSpecificato;
/// <summary>
/// Filtro sul sesso
/// </summary>
public Sesso FiltroSulSesso
{
get
{
return _filtroSulSesso;
}
set
{
if (_filtroSulSesso != value)
{
_filtroSulSesso = value;
NotifyPropertyChanged();
}
}
}


/// <summary>
/// Cambio proprietà
/// </summary>
private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(FiltroSulSesso))
{
ApplicaFiltroSulSesso();
}
}


/// <summary>
/// Applico filtro sul sesso
/// </summary>
private void ApplicaFiltroSulSesso()
{
var view = CollectionViewSource.GetDefaultView(Persone);
if (view == null) return;

switch (FiltroSulSesso)
{
case Sesso.NonSpecificato:
view.Filter = null;
break;
default:
view.Filter = item =>
{
var persona = item as Persona;
return persona?.Sesso == FiltroSulSesso;
};
break;
}
}

#region INotifyPropertyChanged

public event PropertyChangedEventHandler PropertyChanged;


/// <summary>
/// Segnala il cambio di una proprietà
/// </summary>
/// <param name="propertyName">Nome della proprietà modificata, se non specificata è la proprietà chiamante</param>
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}


#endregion


#region Funzione in cui leggiamo i dati, qui è mockata, nella realtà pesca dal DB

private ObservableCollection<Persona> CaricaPersone()
{
var result = new ObservableCollection<Persona>();
result.Add(new Persona
{
Id = 1,
Nome = "Piero",
Cognome = "Rossi",
Sesso = Sesso.Uomo
});
result.Add(new Persona
{
Id = 2,
Nome = "Luca",
Cognome = "Verdi",
Sesso = Sesso.Uomo
});
result.Add(new Persona
{
Id = 3,
Nome = "Lucia",
Cognome = "Rossi",
Sesso = Sesso.Donna
});
result.Add(new Persona
{
Id = 4,
Nome = "Gianluca",
Cognome = "Bianchetti",
Sesso = Sesso.Uomo
});
result.Add(new Persona
{
Id = 5,
Nome = "Silvia",
Cognome = "Azzurrini",
Sesso = Sesso.Donna
});
result.Add(new Persona
{
Id = 6,
Nome = "Francesca",
Cognome = "Bianchi",
Sesso = Sesso.Donna
});
return result;
}


#endregion

}
}

Si evidenziano le seguenti implementazioni:

  • Interfaccia INotifyPropertyChanged: fra i parametri viene usato l'attributo CallerMemberName. In questo modo il runtime passa il nome del metodo chiamante che nel nostro caso corrisponde al nome della proprietà. In questo modo evitiamo di dover passare sempre il nome della proprietà e relativa possibilità di errore.
  • Oltre ad implementare l'interfaccia INotifyPropertyChanged, il view model si mette anche in ascolto per poter intercettare il cambio del filtro eseguito dalla View (lo vedremo dopo)
  • L'elenco delle persone viene esposto come ObservableCollection che è una classe di comodo che implementa INotifyPropertyChanged per tutte le modifice che avvengono all'elenco. In questo modo, appena si aggiunge/rimuove un elemento, l'interfaccia grafica verrà avvisata del cambiamento per aggiungerlo/rimuoverlo a video

Una menzione a parte per l'applicazione del filtro (che poi è il topic di questo post).

Per applicare il filtro (metodo ApplicaFiltroSulSesso) abbiamo usato questa logica:

  • Reperiamo il controllo che sta visualizzando l'elenco dei dati mediante il metodo CollectionViewSource.GetDefaultView
  • Verifichiamo che il controllo esista (potrebbero esserci delle view che per qualche motivo non lo visualizzino)
  • Applichiamo il filtro sfruttando la proprietà Filter della view (è generica e va bene sia per il DataGrid che per altri componenti grafici che visualizzano degli elenchi)

View

Creiamo ora un componente per visualizzare l'elenco delle persone e il relativo combo box di filtro. Per farlo aggiungiamo alla cartella Views uno UserControl (WPF) chiamato PersoneUserControl così definito:

<UserControl x:Class="WpfFilter.Views.PersoneUserControl"
             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:viewModels="clr-namespace:WpfFilter.ViewModels"
             xmlns:system="clr-namespace:System;assembly=mscorlib"
             xmlns:models="clr-namespace:WpfFilter.Models"
             xmlns:local="clr-namespace:WpfFilter.Views"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <!-- Associo il DataContext -->
    <UserControl.DataContext>
        <viewModels:PersonaViewModel />
    </UserControl.DataContext>
    <!-- Necessari per il binding dell'enum sul sesso persona -->
    <UserControl.Resources>
        <ObjectDataProvider x:Key="SessoPersonaDataProvider"
                            MethodName="GetValues"
                            ObjectType="{x:Type system:Enum}">
            <ObjectDataProvider.MethodParameters>
                <x:Type TypeName="models:Sesso" />
            </ObjectDataProvider.MethodParameters>
        </ObjectDataProvider>
    </UserControl.Resources>
    <Grid>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="50" />
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="100" />
                <ColumnDefinition Width="200" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            <!-- Filtro-->
            <Label Grid.Row="0" Grid.Column="0" VerticalAlignment="Center">Sesso:</Label>
            <ComboBox Grid.Row="0" Grid.Column="1" Margin="5" VerticalAlignment="Center"
                  ItemsSource="{Binding Source={StaticResource SessoPersonaDataProvider}}"
                  SelectedItem="{Binding Path=FiltroSulSesso}"
                  >
            </ComboBox>
            <!-- elenco -->
            <DataGrid Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3"
                  ItemsSource="{Binding Path=Persone}"
                  AutoGenerateColumns="true"
                  >

</DataGrid>
</Grid>
</Grid>
</UserControl>

Si evidenziano le seguenti caratteristiche:

  • Nella parte iniziale (xmlns) abbiamo dovuto aggiungere i namespace necessari per poter accedere ai models, view models e system. I primi è facile capirne il motivo, l'ultimo per poter gestire un combo box basato sul valore dell'enum sesso
  • Abbiamo impostato come DataContext del controllo il nostro ViewModel. In questo modo comunichiamo quale sia il ViewModel che deve servire i dati per questa view. Lo si può fare anche da codice, però se è fisso secondo me è più comodo farlo direttamente nella View
  • Fra le risorse (UserControl.Resources) abbiamo definito una risorsa particolare che ci consente di avere come provider per la combo box del filtro i dati presenti nell'enum Sesso. Aggiungendo/rimuovendo dei valori all'enum, la view risulta già corretta
  • Abbiamo aggiunto un combo box la cui sorgente dati è quella specificata nelle risorse (ItemsSource = SessoPersonaDataProvider) e il cui valore selezionato è in binding con la proprietà FiltroSulSesso (del ViewModel)
  • Abbiamo aggiunto un DataGrid la cui sorgente dati è quella specificata dalla proprietà Persone del ViewModel

La logica del filtro funziona in questo modo:

  • Il combo box di filtro carica tutti i valori dell'enum. Essendo la proprietà SelectedItem in binding con la proprietà del ViewModel FiltroSulSesso, la prima voce selezionata sarà quella. Dato che non ci sono logiche particolari nel ViewModel, la proprietà prende in automatico il primo valore dell'enum che è NonImpostato
  • Quando l'utente cambia la selezione del combo box, la proprietà FiltroSulSesso cambia (la View aggiorna il valore nel ViewModel). Essendo in ascolto dell'evento OnPropertyChanged, il ViewModel recepisce il cambiamento della proprietà ed applica il filtro relativo come visto in precedenza. In questo caso impostando/rimuovendo il filtro del controllo che sta visualizzando le persone (nel nostro caso il DataGrid)
  • Cambiano la proprietà Filter del DataGrid, si scatena l'evento di cambiamento (gestito dalla classe DataGrid) che si occupa quindi di aggiornare l'elenco a video coi dati filtrati

MainWindow

Per provare il tutto non ci resta che cambiare la finestra principale dell'applicazione per visualizzare lo UserControl appena creato. Per farlo ci basta modificare la MainWindows in questo modo:

<Window x:Class="WpfFilter.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:views="clr-namespace:WpfFilter.Views"
        xmlns:local="clr-namespace:WpfFilter"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <views:PersoneUserControl />
    </Grid>
</Window>

Anche in questo caso abbiamo aggiunto nella parte iniziale (xmlns) il riferimento alla nostra cartella Views, quindi aggiunto il componente all'interno del Grid principale.

Avviando il debug (F5) possiamo vedere l'applicazione in funzione.