¿La mejor forma de hacer que WPF ListView/GridView ordene en el encabezado de la columna haciendo clic?
sorting (8)
Hay muchas soluciones en Internet que intentan llenar esta omisión aparentemente muy básica de WPF. Estoy realmente confundido en cuanto a cuál sería la "mejor" manera. Por ejemplo ... Quiero que haya pequeñas flechas arriba / abajo en el encabezado de la columna para indicar la dirección de clasificación. Aparentemente hay 3 formas diferentes de hacer esto, algunos usan código, otros usan marcas, otros usan código-marcado y todos parecen un truco.
¿Alguien ha tenido este problema antes y ha encontrado una solución con la que están completamente satisfechos? Parece extraño que una pieza de funcionalidad tan básica de WinForms no se encuentre en WPF y deba ser pirateada.
Solución que resume todas las partes activas de las respuestas y comentarios existentes, incluidas las plantillas de encabezado de columna:
Ver:
<ListView x:Class="MyNamspace.MyListView"
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
ItemsSource="{Binding Items}"
GridViewColumnHeader.Click="ListViewColumnHeaderClick">
<ListView.Resources>
<Style TargetType="Grid" x:Key="HeaderGridStyle">
<Setter Property="Height" Value="20" />
</Style>
<Style TargetType="TextBlock" x:Key="HeaderTextBlockStyle">
<Setter Property="Margin" Value="5,0,0,0" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style TargetType="Path" x:Key="HeaderPathStyle">
<Setter Property="StrokeThickness" Value="1" />
<Setter Property="Fill" Value="Gray" />
<Setter Property="Width" Value="20" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="Margin" Value="5,0,5,0" />
<Setter Property="SnapsToDevicePixels" Value="True" />
</Style>
<DataTemplate x:Key="HeaderTemplateDefault">
<Grid Style="{StaticResource HeaderGridStyle}">
<TextBlock Text="{Binding }" Style="{StaticResource HeaderTextBlockStyle}" />
</Grid>
</DataTemplate>
<DataTemplate x:Key="HeaderTemplateArrowUp">
<Grid Style="{StaticResource HeaderGridStyle}">
<Path Data="M 7,3 L 13,3 L 10,0 L 7,3" Style="{StaticResource HeaderPathStyle}" />
<TextBlock Text="{Binding }" Style="{StaticResource HeaderTextBlockStyle}" />
</Grid>
</DataTemplate>
<DataTemplate x:Key="HeaderTemplateArrowDown">
<Grid Style="{StaticResource HeaderGridStyle}">
<Path Data="M 7,0 L 10,3 L 13,0 L 7,0" Style="{StaticResource HeaderPathStyle}" />
<TextBlock Text="{Binding }" Style="{StaticResource HeaderTextBlockStyle}" />
</Grid>
</DataTemplate>
</ListView.Resources>
<ListView.View>
<GridView ColumnHeaderTemplate="{StaticResource HeaderTemplateDefault}">
<GridViewColumn Header="Name" DisplayMemberBinding="{Binding NameProperty}" />
<GridViewColumn Header="Type" Width="45" DisplayMemberBinding="{Binding TypeProperty}"/>
<!-- ... -->
</GridView>
</ListView.View>
</ListView>
Código Behinde:
public partial class MyListView : ListView
{
GridViewColumnHeader _lastHeaderClicked = null;
public MyListView()
{
InitializeComponent();
}
private void ListViewColumnHeaderClick(object sender, RoutedEventArgs e)
{
GridViewColumnHeader headerClicked = e.OriginalSource as GridViewColumnHeader;
if (headerClicked == null)
return;
if (headerClicked.Role == GridViewColumnHeaderRole.Padding)
return;
var sortingColumn = (headerClicked.Column.DisplayMemberBinding as Binding)?.Path?.Path;
if (sortingColumn == null)
return;
var direction = ApplySort(Items, sortingColumn);
if (direction == ListSortDirection.Ascending)
{
headerClicked.Column.HeaderTemplate =
Resources["HeaderTemplateArrowUp"] as DataTemplate;
}
else
{
headerClicked.Column.HeaderTemplate =
Resources["HeaderTemplateArrowDown"] as DataTemplate;
}
// Remove arrow from previously sorted header
if (_lastHeaderClicked != null && _lastHeaderClicked != headerClicked)
{
_lastHeaderClicked.Column.HeaderTemplate =
Resources["HeaderTemplateDefault"] as DataTemplate;
}
_lastHeaderClicked = headerClicked;
}
public static ListSortDirection ApplySort(ICollectionView view, string propertyName)
{
ListSortDirection direction = ListSortDirection.Ascending;
if (view.SortDescriptions.Count > 0)
{
SortDescription currentSort = view.SortDescriptions[0];
if (currentSort.PropertyName == propertyName)
{
if (currentSort.Direction == ListSortDirection.Ascending)
direction = ListSortDirection.Descending;
else
direction = ListSortDirection.Ascending;
}
view.SortDescriptions.Clear();
}
if (!string.IsNullOrEmpty(propertyName))
{
view.SortDescriptions.Add(new SortDescription(propertyName, direction));
}
return direction;
}
}
En realidad, todo depende de que, si utilizas DataGrid desde WPF Toolkit, hay un tipo incorporado, incluso un tipo de varias columnas, que es muy útil. Ver más aquí:
Alternativamente, si está usando un control diferente que no admite clasificación, recomendaría los siguientes métodos:
Clasificación personalizada de Li Gao
Seguido por:
Hice una adaptación de la manera de Microsoft, donde SortableListView
el control ListView
para hacer una SortableListView
:
public partial class SortableListView : ListView
{
private GridViewColumnHeader lastHeaderClicked = null;
private ListSortDirection lastDirection = ListSortDirection.Ascending;
public void GridViewColumnHeaderClicked(GridViewColumnHeader clickedHeader)
{
ListSortDirection direction;
if (clickedHeader != null)
{
if (clickedHeader.Role != GridViewColumnHeaderRole.Padding)
{
if (clickedHeader != lastHeaderClicked)
{
direction = ListSortDirection.Ascending;
}
else
{
if (lastDirection == ListSortDirection.Ascending)
{
direction = ListSortDirection.Descending;
}
else
{
direction = ListSortDirection.Ascending;
}
}
string sortString = ((Binding)clickedHeader.Column.DisplayMemberBinding).Path.Path;
Sort(sortString, direction);
lastHeaderClicked = clickedHeader;
lastDirection = direction;
}
}
}
private void Sort(string sortBy, ListSortDirection direction)
{
ICollectionView dataView = CollectionViewSource.GetDefaultView(this.ItemsSource != null ? this.ItemsSource : this.Items);
dataView.SortDescriptions.Clear();
SortDescription sD = new SortDescription(sortBy, direction);
dataView.SortDescriptions.Add(sD);
dataView.Refresh();
}
}
La línea ((Binding)clickedHeader.Column.DisplayMemberBinding).Path.Path
bit ((Binding)clickedHeader.Column.DisplayMemberBinding).Path.Path
maneja los casos donde los nombres de sus columnas no son los mismos que sus rutas de enlace, lo cual no hace el método de Microsoft.
Quería interceptar el evento GridViewColumnHeader.Click
para no tener que pensarlo más, pero no pude encontrar una manera de hacerlo. Como resultado, agrego lo siguiente en XAML para cada SortableListView
:
GridViewColumnHeader.Click="SortableListViewColumnHeaderClicked"
Y luego, en cualquier Window
que contenga cualquier cantidad de SortableListView
, simplemente agregue el siguiente código:
private void SortableListViewColumnHeaderClicked(object sender, RoutedEventArgs e)
{
((Controls.SortableListView)sender).GridViewColumnHeaderClicked(e.OriginalSource as GridViewColumnHeader);
}
Donde Controls
es solo el ID de XAML para el espacio de nombres en el que hizo el control SortableListView
.
Por lo tanto, esto evita la duplicación de código en el lado de clasificación, solo necesita recordar manejar el evento como se indicó anteriormente.
Prueba esto:
using System.ComponentModel;
youtItemsControl.Items.SortDescriptions.Add(new SortDescription("yourFavoritePropertyFromItem",ListSortDirection.Ascending);
Si tiene una vista de lista y la convierte en una vista de cuadrícula, puede hacer que sus encabezados de columnas de vista de cuadrícula puedan hacer clic fácilmente haciendo esto.
<Style TargetType="GridViewColumnHeader">
<Setter Property="Command" Value="{Binding CommandOrderBy}"/>
<Setter Property="CommandParameter" Value="{Binding RelativeSource={RelativeSource Self},Path=Content}"/>
</Style>
Luego, simplemente configure un comando de delegado en su código.
public DelegateCommand CommandOrderBy { get { return new DelegateCommand(Delegated_CommandOrderBy); } }
private void Delegated_CommandOrderBy(object obj)
{
throw new NotImplementedException();
}
Voy a suponer que todos ustedes saben cómo hacer que ICommand DelegateCommand aquí. esto me permitió mantener toda mi Vista haciendo clic en ViewModel.
Solo agregué esto para que haya múltiples formas de lograr lo mismo. No escribí código para agregar botones de flecha en el encabezado, pero eso se haría en estilo XAML, necesitaría rediseñar todo el encabezado que JanDotNet tiene en su código.
Utilizo MVVM, así que creé algunas propiedades adjuntas propias, usando Thomas''s como referencia. Lo hace ordenando en una columna a la vez cuando haces clic en el encabezado, alternar entre Ascendente y Descendente. Se ordena desde el principio con la primera columna. Y muestra glifos estilo Win7 / 8.
Normalmente, todo lo que tiene que hacer es establecer la propiedad principal en verdadero (pero debe declarar explícitamente GridViewColumnHeaders):
<Window xmlns:local="clr-namespace:MyProjectNamespace">
<Grid>
<ListView local:App.EnableGridViewSort="True" ItemsSource="{Binding LVItems}">
<ListView.View>
<GridView>
<GridViewColumn DisplayMemberBinding="{Binding Property1}">
<GridViewColumnHeader Content="Prop 1" />
</GridViewColumn>
<GridViewColumn DisplayMemberBinding="{Binding Property2}">
<GridViewColumnHeader Content="Prop 2" />
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
</Grid>
<Window>
Si desea ordenar en una propiedad diferente de la pantalla, entonces debe declarar que:
<GridViewColumn DisplayMemberBinding="{Binding Property3}"
local:App.GridViewSortPropertyName="Property4">
<GridViewColumnHeader Content="Prop 3" />
</GridViewColumn>
Aquí está el código para las propiedades adjuntas, me gusta ser flojo y ponerlos en la App.xaml.cs proporcionada:
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data.
using System.Windows.Media;
using System.Windows.Media.Media3D;
namespace MyProjectNamespace
{
public partial class App : Application
{
#region GridViewSort
public static DependencyProperty GridViewSortPropertyNameProperty =
DependencyProperty.RegisterAttached(
"GridViewSortPropertyName",
typeof(string),
typeof(App),
new UIPropertyMetadata(null)
);
public static string GetGridViewSortPropertyName(GridViewColumn gvc)
{
return (string)gvc.GetValue(GridViewSortPropertyNameProperty);
}
public static void SetGridViewSortPropertyName(GridViewColumn gvc, string n)
{
gvc.SetValue(GridViewSortPropertyNameProperty, n);
}
public static DependencyProperty CurrentSortColumnProperty =
DependencyProperty.RegisterAttached(
"CurrentSortColumn",
typeof(GridViewColumn),
typeof(App),
new UIPropertyMetadata(
null,
new PropertyChangedCallback(CurrentSortColumnChanged)
)
);
public static GridViewColumn GetCurrentSortColumn(GridView gv)
{
return (GridViewColumn)gv.GetValue(CurrentSortColumnProperty);
}
public static void SetCurrentSortColumn(GridView gv, GridViewColumn value)
{
gv.SetValue(CurrentSortColumnProperty, value);
}
public static void CurrentSortColumnChanged(
object sender, DependencyPropertyChangedEventArgs e)
{
GridViewColumn gvcOld = e.OldValue as GridViewColumn;
if (gvcOld != null)
{
CurrentSortColumnSetGlyph(gvcOld, null);
}
}
public static void CurrentSortColumnSetGlyph(GridViewColumn gvc, ListView lv)
{
ListSortDirection lsd;
Brush brush;
if (lv == null)
{
lsd = ListSortDirection.Ascending;
brush = Brushes.Transparent;
}
else
{
SortDescriptionCollection sdc = lv.Items.SortDescriptions;
if (sdc == null || sdc.Count < 1) return;
lsd = sdc[0].Direction;
brush = Brushes.Gray;
}
FrameworkElementFactory fefGlyph =
new FrameworkElementFactory(typeof(Path));
fefGlyph.Name = "arrow";
fefGlyph.SetValue(Path.StrokeThicknessProperty, 1.0);
fefGlyph.SetValue(Path.FillProperty, brush);
fefGlyph.SetValue(StackPanel.HorizontalAlignmentProperty,
HorizontalAlignment.Center);
int s = 4;
if (lsd == ListSortDirection.Ascending)
{
PathFigure pf = new PathFigure();
pf.IsClosed = true;
pf.StartPoint = new Point(0, s);
pf.Segments.Add(new LineSegment(new Point(s * 2, s), false));
pf.Segments.Add(new LineSegment(new Point(s, 0), false));
PathGeometry pg = new PathGeometry();
pg.Figures.Add(pf);
fefGlyph.SetValue(Path.DataProperty, pg);
}
else
{
PathFigure pf = new PathFigure();
pf.IsClosed = true;
pf.StartPoint = new Point(0, 0);
pf.Segments.Add(new LineSegment(new Point(s, s), false));
pf.Segments.Add(new LineSegment(new Point(s * 2, 0), false));
PathGeometry pg = new PathGeometry();
pg.Figures.Add(pf);
fefGlyph.SetValue(Path.DataProperty, pg);
}
FrameworkElementFactory fefTextBlock =
new FrameworkElementFactory(typeof(TextBlock));
fefTextBlock.SetValue(TextBlock.HorizontalAlignmentProperty,
HorizontalAlignment.Center);
fefTextBlock.SetValue(TextBlock.TextProperty, new Binding());
FrameworkElementFactory fefDockPanel =
new FrameworkElementFactory(typeof(StackPanel));
fefDockPanel.SetValue(StackPanel.OrientationProperty,
Orientation.Vertical);
fefDockPanel.AppendChild(fefGlyph);
fefDockPanel.AppendChild(fefTextBlock);
DataTemplate dt = new DataTemplate(typeof(GridViewColumn));
dt.VisualTree = fefDockPanel;
gvc.HeaderTemplate = dt;
}
public static DependencyProperty EnableGridViewSortProperty =
DependencyProperty.RegisterAttached(
"EnableGridViewSort",
typeof(bool),
typeof(App),
new UIPropertyMetadata(
false,
new PropertyChangedCallback(EnableGridViewSortChanged)
)
);
public static bool GetEnableGridViewSort(ListView lv)
{
return (bool)lv.GetValue(EnableGridViewSortProperty);
}
public static void SetEnableGridViewSort(ListView lv, bool value)
{
lv.SetValue(EnableGridViewSortProperty, value);
}
public static void EnableGridViewSortChanged(
object sender, DependencyPropertyChangedEventArgs e)
{
ListView lv = sender as ListView;
if (lv == null) return;
if (!(e.NewValue is bool)) return;
bool enableGridViewSort = (bool)e.NewValue;
if (enableGridViewSort)
{
lv.AddHandler(
GridViewColumnHeader.ClickEvent,
new RoutedEventHandler(EnableGridViewSortGVHClicked)
);
if (lv.View == null)
{
lv.Loaded += new RoutedEventHandler(EnableGridViewSortLVLoaded);
}
else
{
EnableGridViewSortLVInitialize(lv);
}
}
else
{
lv.RemoveHandler(
GridViewColumnHeader.ClickEvent,
new RoutedEventHandler(EnableGridViewSortGVHClicked)
);
}
}
public static void EnableGridViewSortLVLoaded(object sender, RoutedEventArgs e)
{
ListView lv = e.Source as ListView;
EnableGridViewSortLVInitialize(lv);
lv.Loaded -= new RoutedEventHandler(EnableGridViewSortLVLoaded);
}
public static void EnableGridViewSortLVInitialize(ListView lv)
{
GridView gv = lv.View as GridView;
if (gv == null) return;
bool first = true;
foreach (GridViewColumn gvc in gv.Columns)
{
if (first)
{
EnableGridViewSortApplySort(lv, gv, gvc);
first = false;
}
else
{
CurrentSortColumnSetGlyph(gvc, null);
}
}
}
public static void EnableGridViewSortGVHClicked(
object sender, RoutedEventArgs e)
{
GridViewColumnHeader gvch = e.OriginalSource as GridViewColumnHeader;
if (gvch == null) return;
GridViewColumn gvc = gvch.Column;
if(gvc == null) return;
ListView lv = VisualUpwardSearch<ListView>(gvch);
if (lv == null) return;
GridView gv = lv.View as GridView;
if (gv == null) return;
EnableGridViewSortApplySort(lv, gv, gvc);
}
public static void EnableGridViewSortApplySort(
ListView lv, GridView gv, GridViewColumn gvc)
{
bool isEnabled = GetEnableGridViewSort(lv);
if (!isEnabled) return;
string propertyName = GetGridViewSortPropertyName(gvc);
if (string.IsNullOrEmpty(propertyName))
{
Binding b = gvc.DisplayMemberBinding as Binding;
if (b != null && b.Path != null)
{
propertyName = b.Path.Path;
}
if (string.IsNullOrEmpty(propertyName)) return;
}
ApplySort(lv.Items, propertyName);
SetCurrentSortColumn(gv, gvc);
CurrentSortColumnSetGlyph(gvc, lv);
}
public static void ApplySort(ICollectionView view, string propertyName)
{
if (string.IsNullOrEmpty(propertyName)) return;
ListSortDirection lsd = ListSortDirection.Ascending;
if (view.SortDescriptions.Count > 0)
{
SortDescription sd = view.SortDescriptions[0];
if (sd.PropertyName.Equals(propertyName))
{
if (sd.Direction == ListSortDirection.Ascending)
{
lsd = ListSortDirection.Descending;
}
else
{
lsd = ListSortDirection.Ascending;
}
}
view.SortDescriptions.Clear();
}
view.SortDescriptions.Add(new SortDescription(propertyName, lsd));
}
#endregion
public static T VisualUpwardSearch<T>(DependencyObject source)
where T : DependencyObject
{
return VisualUpwardSearch(source, x => x is T) as T;
}
public static DependencyObject VisualUpwardSearch(
DependencyObject source, Predicate<DependencyObject> match)
{
DependencyObject returnVal = source;
while (returnVal != null && !match(returnVal))
{
DependencyObject tempReturnVal = null;
if (returnVal is Visual || returnVal is Visual3D)
{
tempReturnVal = VisualTreeHelper.GetParent(returnVal);
}
if (tempReturnVal == null)
{
returnVal = LogicalTreeHelper.GetParent(returnVal);
}
else
{
returnVal = tempReturnVal;
}
}
return returnVal;
}
}
}
MSDN tiene una forma fácil de realizar la clasificación en columnas con glifos arriba / abajo. Sin embargo, el ejemplo no está completo: no explican cómo usar las plantillas de datos para los glifos. A continuación es lo que tengo que trabajar con mi ListView. Esto funciona en .Net 4.
En su ListView, debe especificar un controlador de eventos para activarlo haciendo clic en el GridViewColumnHeader. Mi ListView se ve así:
<ListView Name="results" GridViewColumnHeader.Click="results_Click">
<ListView.View>
<GridView>
<GridViewColumn DisplayMemberBinding="{Binding Path=ContactName}">
<GridViewColumn.Header>
<GridViewColumnHeader Content="Contact Name" Padding="5,0,0,0" HorizontalContentAlignment="Left" MinWidth="150" Name="ContactName" />
</GridViewColumn.Header>
</GridViewColumn>
<GridViewColumn DisplayMemberBinding="{Binding Path=PrimaryPhone}">
<GridViewColumn.Header>
<GridViewColumnHeader Content="Contact Number" Padding="5,0,0,0" HorizontalContentAlignment="Left" MinWidth="150" Name="PrimaryPhone"/>
</GridViewColumn.Header>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
En su código detrás, configure el código para manejar la clasificación:
// Global objects
BindingListCollectionView blcv;
GridViewColumnHeader _lastHeaderClicked = null;
ListSortDirection _lastDirection = ListSortDirection.Ascending;
// Header click event
void results_Click(object sender, RoutedEventArgs e)
{
GridViewColumnHeader headerClicked =
e.OriginalSource as GridViewColumnHeader;
ListSortDirection direction;
if (headerClicked != null)
{
if (headerClicked.Role != GridViewColumnHeaderRole.Padding)
{
if (headerClicked != _lastHeaderClicked)
{
direction = ListSortDirection.Ascending;
}
else
{
if (_lastDirection == ListSortDirection.Ascending)
{
direction = ListSortDirection.Descending;
}
else
{
direction = ListSortDirection.Ascending;
}
}
string header = headerClicked.Column.Header as string;
Sort(header, direction);
if (direction == ListSortDirection.Ascending)
{
headerClicked.Column.HeaderTemplate =
Resources["HeaderTemplateArrowUp"] as DataTemplate;
}
else
{
headerClicked.Column.HeaderTemplate =
Resources["HeaderTemplateArrowDown"] as DataTemplate;
}
// Remove arrow from previously sorted header
if (_lastHeaderClicked != null && _lastHeaderClicked != headerClicked)
{
_lastHeaderClicked.Column.HeaderTemplate = null;
}
_lastHeaderClicked = headerClicked;
_lastDirection = direction;
}
}
// Sort code
private void Sort(string sortBy, ListSortDirection direction)
{
blcv.SortDescriptions.Clear();
SortDescription sd = new SortDescription(sortBy, direction);
blcv.SortDescriptions.Add(sd);
blcv.Refresh();
}
Y luego en su XAML, necesita agregar dos DataTemplates que haya especificado en el método de clasificación:
<DataTemplate x:Key="HeaderTemplateArrowUp">
<DockPanel LastChildFill="True" Width="{Binding ActualWidth, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type GridViewColumnHeader}}}">
<Path x:Name="arrowUp" StrokeThickness="1" Fill="Gray" Data="M 5,10 L 15,10 L 10,5 L 5,10" DockPanel.Dock="Right" Width="20" HorizontalAlignment="Right" Margin="5,0,5,0" SnapsToDevicePixels="True"/>
<TextBlock Text="{Binding }" />
</DockPanel>
</DataTemplate>
<DataTemplate x:Key="HeaderTemplateArrowDown">
<DockPanel LastChildFill="True" Width="{Binding ActualWidth, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type GridViewColumnHeader}}}">
<Path x:Name="arrowDown" StrokeThickness="1" Fill="Gray" Data="M 5,5 L 10,10 L 15,5 L 5,5" DockPanel.Dock="Right" Width="20" HorizontalAlignment="Right" Margin="5,0,5,0" SnapsToDevicePixels="True"/>
<TextBlock Text="{Binding }" />
</DockPanel>
</DataTemplate>
El uso de DockPanel
con LastChildFill
establecido en verdadero mantendrá el glifo a la derecha del encabezado y permitirá que la etiqueta llene el resto del espacio. DockPanel
ancho del DockPanel
Ancho ActualWidth
del GridViewColumnHeader
porque mis columnas no tienen ancho, lo que les permite realizar el ajuste automático del contenido. Sin embargo, establecí MinWidth
s en las columnas, de modo que el glifo no cubra el título de la columna. El TextBlock Text
se establece en un enlace vacío que muestra el nombre de columna especificado en el encabezado.
Escribí un conjunto de propiedades adjuntas para ordenar automáticamente un GridView
, puedes consultarlo here . No maneja la flecha arriba / abajo, pero se puede agregar fácilmente.
<ListView ItemsSource="{Binding Persons}"
IsSynchronizedWithCurrentItem="True"
util:GridViewSort.AutoSort="True">
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn Header="Name"
DisplayMemberBinding="{Binding Name}"
util:GridViewSort.PropertyName="Name"/>
<GridViewColumn Header="First name"
DisplayMemberBinding="{Binding FirstName}"
util:GridViewSort.PropertyName="FirstName"/>
<GridViewColumn Header="Date of birth"
DisplayMemberBinding="{Binding DateOfBirth}"
util:GridViewSort.PropertyName="DateOfBirth"/>
</GridView.Columns>
</GridView>
</ListView.View>
</ListView>