Press "Enter" to skip to content

Datagrid WPF standard un caso d’uso reale (parte 7)

Implementiamo il Column Chooser per scegliere quali colonne vedere sulla DataGrid

Nell’articolo precedente, abbiamo implementato un modo per salvare la configurazione di una DataGrid in modo da permettere agli utenti di modificare la posizione e la larghezza delle colonne della stessa. In questo settimo ed ultimo articolo dedicato alla DataGrid standard WPF Microsoft implementeremo anche il necessario per poter nascondere colonne o ripristinarne la visibilità in base alle esigenze.

Per fare tutto questo, per prima cosa ci serve una nuova Window che mostri all’utente tutte le colonne della grid esistenti permettendo di scegliere quali devono restare visibili e quali devono essere nascoste.

La classe ColumnChooserWindow.xaml e .xaml.cs

Per creare la nuova finestra ci posizioniamo sul nodo del progetto nel solution explorer e premiamo il tasto destro sul menu contestuale per aggiungere una classe. Riporto l’immagine per praticità di chi leggerà:

 

Add… e New Item… nel sottomenu. Quindi nella finestra che ci verrà presentata sceglieremo dalla tree laterale WPF e poi Window come si vede nella seguente immagine:

 

A questo punto ci sarà generata una nuova Window Vuota in cui andremo ad aggiungere le seguenti cose. Partiamo dalla parte XAML

<Window
	x:Class="WpfGridColors.ColumnChooserWindow"
	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:local="clr-namespace:WpfGridColors"
	xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
	Title="Selezione Visibilità colonne"
	Width="300"
	Height="450"
	ContentRendered="ColumnChooser_ContentRendered"
	mc:Ignorable="d">

Nel tag Window abbiamo modificato il Title e larghezza e altezza inoltre abbiamo aggiunto un Event Handler per l’evento ContentRendered su cui controlleremo se la Window può svolgere il suo lavoro e faremo la messa a punto preliminare della Window Stessa.

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

Proseguiamo con la costruzione grafica, definiamo quindi 2 righe all’interno della Grid fornita dal template della Window in modo da creare gli spazi per i controlli che utilizzeremo.

<DataGrid
	Grid.Row="0"
	AutoGenerateColumns="False"
	ItemsSource="{Binding ColumnData, UpdateSourceTrigger=PropertyChanged}"	
	SelectionUnit="Cell">
	<DataGrid.Columns>
		<DataGridTemplateColumn
			Header="Visible"
			IsReadOnly="false"
			SortMemberPath="IsVisible">
			<DataGridTemplateColumn.CellTemplate>
				<DataTemplate>
					<CheckBox IsChecked="{Binding Path=IsVisible, UpdateSourceTrigger=PropertyChanged ,Mode=TwoWay}" />
				</DataTemplate>
			</DataGridTemplateColumn.CellTemplate>
		</DataGridTemplateColumn>
 
		<DataGridTextColumn
			Binding="{Binding Header}"
			Header="Header Description"
			IsReadOnly="true" />
	</DataGrid.Columns>
</DataGrid>

Nella prima riga creata (la cui altezza occupa tutto lo spazio libero (*), andiamo ad inserire una DataGrid, questa DataGrid, visualizzerà una lista che metteremo in una property che si chiama ColumnData, tale property conterrà quanto necessario a scegliere le colonne da visualizzare o meno. Visualizzerà due colonne, la prima è una TemplateColumn con all’interno una CheckBox, la seconda è una TextColumn che contiene La descrizione della colonna (Header). Come presumo abbiate notato, la prima colonna è Modificabile (IsReadOnly=false) mentre la seconda non lo è.

Se siete stati attenti ed avete già letto gli articoli precedenti o altra documentazione sulla DataGrid, sono certa sapete che esiste un tipo di colonna che si chiama DataGridCheckBoxColumn. In questo caso non l’abbiamo usata per un motivo ben preciso. Infatti, la modifica della colonna CheckBox, funziona utilizzando il doppio click per cambiarne il valore (un click per entrare in modalità modifica, il secondo per cambiare il Check). Invece ai fini di una selezione tutti siamo abituati a premere una sola volta la CheckBox per cambiarne il valore. L’uso della CheckBox standard all’interno della Template Column ci permette di ottenere quanto atteso dall’utente.

<StackPanel
	Grid.Row="1"
	FlowDirection="RightToLeft"
	Orientation="Horizontal">
	<Button
		Margin="4"
		Padding="10,4,10,4"
		Click="OK_Click"
		Content="OK" />
	<Button
		Margin="4"
		Padding="10,4,10,4"
		Click="Cancel_Click"
		Content="Cancel" />
</StackPanel>

Nella seconda riga della Grid della nostra Window inseriamo uno StackPanel che ospiterà i due Button classici per confermare o annullare le modifiche. Abbiamo configurato il pannello in orizzontale e abbiamo chiesto che vada da destra verso sinistra così che i due Button siano allineati a destra. Gli Attributi Margin e Padding servono per dare un aspetto corretto alla forma dei due Button. Il Click fornisce l’event handler per gestire il comportamento dei due button.

Con questo abbiamo terminato la parte grafica e passiamo alla parte Codice.

using System;
using System.ComponentModel;
using System.Windows;
using WpfGridColors.Collections;
 
namespace WpfGridColors
{
	/// <summary>
	/// Interaction logic for ColumnChooserWindow.xaml
	/// </summary>
	public partial class ColumnChooserWindow : WindowINotifyPropertyChanged
	{
		public ColumnChooserWindow()
		{
			InitializeComponent();
			DataContext = this;
		}

Apriamo il codice della finestra e oltre a inserire la using relativa alla cartella Collections che contiene la collection da noi implementata nell’articolo precedente che contiene a sua volta gli oggetti che memorizzano la configurazione delle colonne, aggiungiamo il namespace System.ComponentModel per implementare l’interfaccia INotifyPropertyChanged e fornire all’interfaccia l’informazione relativa alle modifiche dei dati collegati ai componenti. Modifichiamo anche il costruttore con l’istruzione DataContext = this che informa il sistema che la Window è ViewModel di se stessa e quindi le property collegate ai controlli risiedono al suo interno.

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

Il codice qui sopra implementa l’interfaccia INotifyPropertyChanged che consta di un evento con il proprio event handler.

public void Init(ColumnDataCollection columns)
{
	ColumnData = columns;
}

Implementiamo un metodo pubblico che chiamiamo Init, che riceve in ingresso la collection delle colonne da gestire e la pone in una property. Come “Nota della Zia” io creo un metodo Init in tutte o quasi le mie Window all’interno dei programmi nel mondo reale, e passo alle finestre i dati che servono per il loro funzionamento, cercando di evitare che le finestre stesse debbano conoscere cose non legate al loro funzionamento, se una dialog a cui passo dei dati chiede all’utente qualcosa riguardo quei dati, li passo nella Init, non li faccio caricare alla Window in modo tale che essa risulti essere generica. In questo caso, se sviluppando la mia applicazione volessi usare il column chooser per diverse Grid, con questa implementazione posso farlo perché la Window serve solo a scegliere le colonne, ma non sa quali sono fino a che non glie le passo.

private const string FLD_ColumnData = "ColumnData";
 
private ColumnDataCollection mColumnData;
 
public ColumnDataCollection ColumnData
{
	get
	{
		return mColumnData;
	}
	private set
	{
		mColumnData = value;
 
		OnPropertyChanged(FLD_ColumnData);
	}
}

Il codice qui sopra implementa la collection che fa da DataSource alla DataGrid e permette all’utente di selezionare i campi da vedere o da nascondere.

private void ColumnChooser_ContentRendered(object senderEventArgs e)
{
	if (ColumnData == null || ColumnData.Count == 0)
	{
		MessageBox.Show("Attenzione, non avete eseguito Init, non ci sono dati per far funzionare la finestra Eseguire Init prima di ShowDialog",
			"Errore"MessageBoxButton.OKMessageBoxImage.Error);
		Close();
	}
}

Nell’Event Handler  dell’evento ContentRendered andiamo a controllare se ColumnData è stato inizializzato, e se non lo fosse diamo un messaggio significativo. Ovviamente questo non dovrebbe mai accadere, ma a me piace inserire il messaggio perché al primo Debug da parte nostra se non ci siamo ricordati di chiamare Init ci informa evitandoci di perdere tempo al riguardo questo è importante soprattutto per le cose riutilizzabili che, come questa Window, possiamo mettere in Libreria e riutilizzare in tutti i nostri progetti ed ovviamente non ci possiamo ricordare a memoria ogni cosa del codice che scriviamo, così, se fra sei mesi usassimo la Window ci aiutiamo a non sbagliare e a non perdere tempo a debuggare le nostre dimenticanze.

private void OK_Click(object senderRoutedEventArgs e)
{
	DialogResult = true;
	Close();
}
 
private void Cancel_Click(object senderRoutedEventArgs e)
{
	DialogResult = false;
	Close();
}

L’ultima cosa che implementiamo sono i due button, OK e Cancel che ci permetteranno di sapere cosa l’utente ha deciso di fare.

In questa Window, le modifiche non vengono applicate, perché l’applicazione delle modifiche va fatta nella Window chiamante dove la grid interessata è situata.

Pertanto ora passiamo alla modifica della MainWindow per implementare il ColumnChooser.

Modifiche a MainWindow.xaml e .xaml.cs

Per implementare il ColumnChooser, aggiungeremo un menu contestuale alla DataGrid, poi, implementeremo il click dell’opzione del menu per aprire la Window ed applicare le modifiche.

<DataGrid
	Name="dgStat"
	Grid.Row="0"
	AutoGenerateColumns="False"
	CurrentItem="{Binding CurrentStatistic, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
	ItemsSource="{Binding Statistics, UpdateSourceTrigger=PropertyChanged}"
	SelectedCellsChanged="DgStat_SelectedCellsChanged"
	SelectionUnit="Cell">
	<DataGrid.ContextMenu>
		<ContextMenu>
			<MenuItem Click="ChooseColumns_Click" Header="Choose Columns" />
		</ContextMenu>
	</DataGrid.ContextMenu>

Nella DataGrid inseriamo il ContextMenu, per rendere più comprensibile dove va aggiunto ho inserito anche il tag di apertura della DataGrid. All’interno del ContextMenu, abbiamo aggiunto un MenuItem, sul cui Click Implementiamo un evento Choose Columns ovvero (scegli le colonne).

Posizioniamo il cursore sulla parola ChooseColumns_Click e premiamo il tasto destro e sul menu contestuale dell’editor XAML premiamo Go To Definition oppure premiamo F12.

Ci verrà generato l’event handler in cui inseriremo il codice per gestire la selezione delle colonne.

try
	{
	ColumnChooserWindow win = new ColumnChooserWindow();
	win.Owner = this;
	win.WindowStartupLocation = WindowStartupLocation.CenterOwner;
	win.Init(GetColumnsData());
	bool? choosed = win.ShowDialog();
	if (choosed.HasValue && choosed.Value)
	{
		foreach (var col in dgStat.Columns)
		{
			ColumnDataItem coldata = win.ColumnData[col.Header.ToString()];
			if (coldata != null)
			{
				col.Visibility = coldata.IsVisible ? Visibility.Visible : Visibility.Collapsed;			
			}
		}
	}
}
catch (Exception ex)
{
	MessageBox.Show(ex.Message"Errore"MessageBoxButton.OKMessageBoxImage.Error);
}

Questo è il codice con cui implementiamo la selezione colonne, vediamo cosa facciamo:

  • Generiamo la Window di tipo ColumnChooserWindow e la configuriamo in modo che si apra al centro della MainWindow. Chiamiamo il metodo Init e gli passiamo il risultato della chiamata al metodo GetColumnsData che abbiamo creato nell’articolo precedente e che genera una collection con i dati di configurazione letti dalla grid corrente.
  • Apriamo la window con la ShowDialog che ci renderà un Boolean nullabile che ci indicherà cosa l’utente decide di fare.
  • Se l’utente ha premuto OK, facciamo un Ciclo su Tutte le colonne della DataGrid, e per ogni colonna, recuperiamo i dati di configurazione dalla collection che abbiamo usato nella finestra e se la checkbox IsVisible è stata posta a False nascondiamo la colonna, mentre se è a True la lasciamo visibile.

Ed abbiamo terminato l’implementazione, quindi possiamo testare il tutto e vedere cosa succede.

Se premiamo il tasto destro sulla Grid, appare il menu contestuale, cliccandolo apparirà la window per la selezione delle colonne visibili.

Come possiamo vedere, tutte le colonne hanno la checkbox a True.

Con un Click togliamo la colonna Budget e la colonna Budget percentuale.

Premendo OK le due colonne spariranno dalla Grid, vi invito a provare a chiudere e riaprire la Window per provare se le colonne tornano visibili, in realtà non è così, per farle riapparire dovete usare nuovamente il menu.

E’ opportuno che nei vostri programmi nel mondo reale, prevediate anche la possibilità di resettare le colonne allo stato iniziale predefinito. E’ una cosa un pochino Tricky da fare che io ho ottenuto facendo le seguenti cose, ma non la implementerò, se qualcuno di voi lo farà e si trova in difficoltà chieda e gli sarà spiegato.

  1. Ho creato una property boolean chiamata NotReopen, che viene sempre posta a True.
  2. Aggiungo al menu contestuale un opzione Reset to Standard Column Configuration.
  3. Nell’event handler pongo NotReopen a False e cancello il file di configurazione delle colonne.
  4. Nell’evento Closed, salvo le colonne solo se NotReopen è true.
  5. Modifico l’apertura della MainWindow in modo che se NotReopen è false, alla chiusura della stessa la torno ad aprire. E ovviamente la Window si riaprirà con la configurazione originale.

Senza il trucco del flag, anche cancellando il file verrà rigenerato immediatamente ed il reset non andrà a buon fine. Ovviamente ci sono altri metodi siete invitati a provarne di nuovi e magari fatemi sapere cosa usate e lo aggiungiamo come indicazione citandone l’autore.

Abbiamo concluso questa breve serie di articoli dedicati alla DataGrid Standard di WPF.