使 WPF ListView/GridView 在列标题单击时排序的最佳方法?

互联网上有 很多的解决方案,试图填补 WPF 看似非常基本的遗漏。我真的很困惑,什么才是“最好”的方式。例如... ... 我希望在列标题中有一些向上/向下的箭头来指示排序方向。显然有3种不同的方法可以做到这一点,一些使用代码,一些使用标记,一些使用标记加代码,所有这些看起来都相当像一个黑客。

以前有人遇到过这个问题,并且找到了一个他们完全满意的解决方案吗?WPF 缺少这样一个基本的 WinForms 功能,需要被黑客入侵,这似乎很奇怪。

123702 次浏览

It all depends really, if you're using the DataGrid from the WPF Toolkit then there is a built in sort, even a multi-column sort which is very useful. Check more out here:

Vincent Sibals Blog

Alternatively, if you're using a different control that doesn't support sorting, i'd recommend the following methods:

Li Gao's Custom Sorting

Followed by:

Li Gao's Faster Sorting

I wrote a set of attached properties to automatically sort a GridView, you can check it out here. It doesn't handle the up/down arrow, but it could easily be added.

<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>

MSDN has an easy way to perform sorting on columns with up/down glyphs. The example isn't complete, though - they don't explain how to use the data templates for the glyphs. Below is what I got to work with my ListView. This works on .Net 4.

In your ListView, you have to specify an event handler to fire for a click on the GridViewColumnHeader. My ListView looks like this:

<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>

In your code behind, set up the code to handle the sorting:

// 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();
}

And then in your XAML, you need to add two DataTemplates that you specified in the sorting method:

<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>

Using the DockPanel with LastChildFill set to true will keep the glyph on the right of the header and let the label fill the rest of the space. I bound the DockPanel width to the ActualWidth of the GridViewColumnHeader because my columns have no width, which lets them autofit to the content. I did set MinWidths on the columns, though, so that the glyph doesn't cover up the column title. The TextBlock Text is set to an empty binding which displays the column name specified in the header.

I made an adaptation of the Microsoft way, where I override the ListView control to make a 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();
}
}

The line ((Binding)clickedHeader.Column.DisplayMemberBinding).Path.Path bit handles the cases where your column names are not the same as their binding paths, which the Microsoft method does not do.

I wanted to intercept the GridViewColumnHeader.Click event so that I wouldn't have to think about it anymore, but I couldn't find a way to to do. As a result I add the following in XAML for every SortableListView:

GridViewColumnHeader.Click="SortableListViewColumnHeaderClicked"

And then on any Window that contains any number of SortableListViews, just add the following code:

private void SortableListViewColumnHeaderClicked(object sender, RoutedEventArgs e)
{
((Controls.SortableListView)sender).GridViewColumnHeaderClicked(e.OriginalSource as GridViewColumnHeader);
}

Where Controls is just the XAML ID for the namespace in which you made the SortableListView control.

So, this does prevent code duplication on the sorting side, you just need to remember to handle the event as above.

I use MVVM, so I created some attached properties of my own, using Thomas's as a reference. It does sorting on one column at a time when you click on the header, toggling between Ascending and Descending. It sorts from the very beginning using the first column. And it shows Win7/8 style glyphs.

Normally, all you have to do is set the main property to true (but you have to explicitly declare the 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>

If you want to sort on a different property than the display, than you have to declare that:

<GridViewColumn DisplayMemberBinding="{Binding Property3}"
local:App.GridViewSortPropertyName="Property4">
<GridViewColumnHeader Content="Prop 3" />
</GridViewColumn>

Here's the code for the attached properties, I like to be lazy and put them in the provided App.xaml.cs:

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;
}
}
}

Try this:

using System.ComponentModel;
youtItemsControl.Items.SortDescriptions.Add(new SortDescription("yourFavoritePropertyFromItem",ListSortDirection.Ascending);

Solution that summarizes all working parts of existing answers and comments including column header templates:

View:

<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>

Code 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;
}
}

If you have a listview and turn it into a gridview you can easily make your gridview columns headers clickable by doing this.

        <Style TargetType="GridViewColumnHeader">
<Setter Property="Command" Value="{Binding CommandOrderBy}"/>
<Setter Property="CommandParameter" Value="{Binding RelativeSource={RelativeSource Self},Path=Content}"/>
</Style>

Then just set a delegate command in your code.

    public DelegateCommand CommandOrderBy { get { return new DelegateCommand(Delegated_CommandOrderBy); } }


private void Delegated_CommandOrderBy(object obj)
{
throw new NotImplementedException();
}

Im going to assume you all know how to make the ICommand DelegateCommand here. this allowed me to keep all my View clicking in the ViewModel.

I only added this so that there is multiple ways to accomplish the same thing. I did not write code for adding arrow buttons in the header, but that would be done in XAML style, you would need to redesign the entire header which JanDotNet has in their code.

Just wanted to add another simple way someone can sort the the WPF ListView

void SortListView(ListView listView)
{
IEnumerable listView_items = listView.Items.SourceCollection;
List<MY_ITEM_CLASS> listView_items_to_list = listView_items.Cast<MY_ITEM_CLASS>().ToList();


Comparer<MY_ITEM_CLASS> scoreComparer = Comparer<MY_ITEM_CLASS>.Create((first, second) => first.COLUMN_NAME.CompareTo(second.COLUMN_NAME));


listView_items_to_list.Sort(scoreComparer);
listView.ItemsSource = null;
listView.Items.Clear();
listView.ItemsSource = listView_items_to_list;
}

After search alot, finaly i found simple here https://www.wpf-tutorial.com/listview-control/listview-how-to-column-sorting/

private GridViewColumnHeader listViewSortCol = null;
private SortAdorner listViewSortAdorner = null;
private void GridViewColumnHeader_Click(object sender, RoutedEventArgs e)
{
GridViewColumnHeader column = (sender as GridViewColumnHeader);
string sortBy = column.Tag.ToString();
if (listViewSortCol != null)
{
AdornerLayer.GetAdornerLayer(listViewSortCol).Remove(listViewSortAdorner);
yourListView.Items.SortDescriptions.Clear();
}


ListSortDirection newDir = ListSortDirection.Ascending;
if (listViewSortCol == column && listViewSortAdorner.Direction == newDir)
newDir = ListSortDirection.Descending;


listViewSortCol = column;
listViewSortAdorner = new SortAdorner(listViewSortCol, newDir);
AdornerLayer.GetAdornerLayer(listViewSortCol).Add(listViewSortAdorner);
yourListView.Items.SortDescriptions.Add(new SortDescription(sortBy, newDir));
}

Class:

public class SortAdorner : Adorner
{
private static Geometry ascGeometry =
Geometry.Parse("M 0 4 L 3.5 0 L 7 4 Z");


private static Geometry descGeometry =
Geometry.Parse("M 0 0 L 3.5 4 L 7 0 Z");


public ListSortDirection Direction { get; private set; }


public SortAdorner(UIElement element, ListSortDirection dir)
: base(element)
{
this.Direction = dir;
}


protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);


if(AdornedElement.RenderSize.Width < 20)
return;


TranslateTransform transform = new TranslateTransform
(
AdornedElement.RenderSize.Width - 15,
(AdornedElement.RenderSize.Height - 5) / 2
);
drawingContext.PushTransform(transform);


Geometry geometry = ascGeometry;
if(this.Direction == ListSortDirection.Descending)
geometry = descGeometry;
drawingContext.DrawGeometry(Brushes.Black, null, geometry);


drawingContext.Pop();
}
}

Xaml

<GridViewColumn Width="250">
<GridViewColumn.Header>
<GridViewColumnHeader Tag="Name" Click="GridViewColumnHeader_Click">Name</GridViewColumnHeader>
</GridViewColumn.Header>
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" ToolTip="{Binding Name}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>