89

There are lots of solutions on the internet attempting to fill this seemingly very-basic omission from WPF. I'm really confused as to what would be the "best" way. For example... I want there to be little up/down arrows in the column header to indicate sort direction. There are apparently like 3 different ways to do this, some using code, some using markup, some using markup-plus-code, and all seeming rather like a hack.

Has anyone run into this problem before, and found a solution they are completely happy with? It seems bizarre that such a basic WinForms piece of functionality is missing from WPF and needs to be hacked in.

Domenic
  • 110,262
  • 41
  • 219
  • 271

10 Answers10

117

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>
Thomas Levesque
  • 286,951
  • 70
  • 623
  • 758
  • 8
    Thank you Thomas, your solution to the sorting problem is elegant, easy to use and very flexible. In other words: perfect! Tip for others: 1) use the updated version linked to in Thomas' article, and 2) use the prettier glyph version of Alex in the comments. – Helge Klein Feb 11 '11 at 23:04
  • I am new to WPF and don't quite understand this "util" bit. What is that referencing? Edit: nm... there was this tiny little link called "View Source" that didn't jump out at me right away. This expands the source code for the class – oscilatingcretin Jul 22 '11 at 13:18
  • 1
    I like that very much! I'm adding/removing items dynamically and this works great without changing order. But how can I set an initial state for the sorting? `CollectionViewSource.GetDefaultView(MyList.ItemsSource).SortDescriptions.Add(new SortDescription("Number", ListSortDirection.Ascending));` doesn't work. – zee Dec 05 '11 at 21:00
  • @zee, it should work, but it won't display the sort glyph... I haven't implemented a way to set an initial order, but you can always try to modify my code ;) – Thomas Levesque Dec 05 '11 at 22:28
  • My fault - it works perfectly! (I instanced a new List instead of clearing, so the SortDescriptions was resettet) Sorry – zee Dec 05 '11 at 23:17
  • The API would be a little bit nicer if it wouldn't be required to set the `util:GridViewSort.AutoSort="True"` attached property on the parent ListView. If I have defined custom sorting for a gridviewcolumn, it should just sort. Alternatively if AutoSort is enabled and it finds a `DisplayMemberBinding` and no `util:GridViewSort.PropertyName="FirstName"` it could use that for sorting. – bitbonk Dec 07 '12 at 12:22
  • @bitbonk, ideally, yes, it would be better to have AutoSort="True" by default. Unfortunately there is no easy way to do it; I need the value of the property to change in order to enable the behavior. And I can't do it with just the PropertyName property set on the column, because there is no way to access the ListView from the column... – Thomas Levesque Dec 07 '12 at 14:26
  • Can't you get to the ListView using `VisualTreeHelper.GetParent(DependencyObject)` or better this helper method: http://www.hardcodet.net/2008/02/find-wpf-parent ? – bitbonk Dec 10 '12 at 12:43
  • @bitbonk, actually you can't, because the GridViewColumn is not a visual element. It's just the definition of the column, not the visual column itself. It appears neither in the logical tree nor the visual tree. – Thomas Levesque Dec 10 '12 at 13:28
  • @ThomasLevesque great solution! However, when I click on a column header, the glyph appears, but the sort order does not change. I can click multiple times and the glyph alternated between ascending/descending, but the items stay in the same order. – dub stylee Jun 25 '14 at 18:33
  • @dubstylee, there is probably something wrong in your XAML... Please post it on Gist – Thomas Levesque Jun 25 '14 at 19:02
  • Sure, here it is: https://gist.github.com/anonymous/54a2620b2c9954228f9b I am adding the ListViewItems dynamically from a database in code-behind. – dub stylee Jun 25 '14 at 19:05
  • @dubstylee, I can't see anything wrong... How do you set the ItemsSource ? – Thomas Levesque Jun 25 '14 at 19:13
  • I am loading the items from a database in the code-behind. The relevant code is in this gist: https://gist.github.com/anonymous/1694ef709f260ac6d6df – dub stylee Jun 25 '14 at 19:50
  • @dubstylee, you're not using ItemsSource, you're adding items directly to the Items. Try to put your items in a list, then assign that list to the ItemsSource. – Thomas Levesque Jun 25 '14 at 22:32
  • That totally did the trick! Using an `ArrayList` of `UserInfo` objects, and assigning that to the `ItemsSource` rather than adding `ListViewItems` of `UserInfo` directly to the `Items` collection. Thanks @ThomasLevesque! – dub stylee Jun 25 '14 at 22:39
  • Thomas, I'm having issues sorting by dates... it sorts them as strings. So, 12/23/2014 and 12/22/2013 will be places together before 11/05/2014... as an example. – B.K. Oct 21 '14 at 23:57
  • @B.K., I can't be sure without seeing your code, but I suspect you're binding to a string property, instead of a DateTime property... – Thomas Levesque Oct 22 '14 at 00:49
  • @ThomasLevesque That's correct. I'll change that. Thanks. – B.K. Oct 22 '14 at 01:33
  • This is cool. But recently I came across a problem when my GridViewColumn contains complex structure. It has TextBlock with MultiBinding StringFormat inside. How can I sort the string formatted column with this? – Boy Sep 16 '15 at 12:59
  • @Boy, can't you do the formatting in your ViewModel instead? this way you can sort on the same property you bind to – Thomas Levesque Sep 16 '15 at 14:03
  • @ThomasLevesque, actually it's not just a string format. Let's say I have a column where it takes values from 2 columns and add together. How can I sort this one using GridViewSort? Example of snippet: http://pastebin.com/raw.php?i=ZTc4q3eu. – Boy Sep 16 '15 at 15:54
  • @Boy, just add a Result property to your ViewModel and put the calculation in the getter – Thomas Levesque Sep 16 '15 at 16:19
  • Has anyone managed to add move up/down to this? I know it's kind of opposed to sorting in a way, but it might be possible. – Chris Aug 15 '17 at 16:53
25

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.

Jared Harley
  • 8,219
  • 4
  • 39
  • 48
  • 1
    This doesn't specify where in the XAML to place the DataTemplates Grid.Resources? –  Jul 03 '13 at 23:32
  • 4
    @Mark This is probably too late to help you, but the templates should be put in the Resources of the root element, typically `` or ``. HTHS ;) – CptRobby Dec 04 '13 at 18:50
  • 1
    @CptRobby hi.. for me, as blcv is not initialized, it gives a null reference exception... so how would this code work for anyone? – Jay Nirgudkar May 04 '15 at 07:43
  • @JayNirgudkar I'm not the author of this, Jared Harley is. But I can tell you that blcv is what he was using as the ItemsSource of his ListView. You don't have to do the same thing. Click the MSDN link that he provided for an alternative way of dealing with the ItemsSource. – CptRobby May 06 '15 at 16:38
  • Thank you so much Jared. I would have voted this one as an answer, best I found so far. Just put the templates in your Window.Resources or UserControl.Resources or ResourceDictionary as I did in my case, and it works great! Thumbs Up for posting this Jared, thumbs up!!! – pixel Aug 17 '16 at 21:12
  • @CptRobby Thanks for clarifying where to put the DataTemplates – pixel Aug 17 '16 at 22:14
  • @dbnex14 I'm just glad that it helped someone. ;) – CptRobby Aug 18 '16 at 03:09
  • 2
    The MSDN example assumes that `headerClicked.Column.Header` (which is the header text) is equivalent to `(headerClicked.Column.DisplayMemberBinding as Binding).Path.Path` (which is the binding path). Sorting on the header text does not work. Very weird. – Chris Nov 18 '16 at 17:20
23

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

quinmars
  • 11,175
  • 8
  • 32
  • 41
Kyle Rosendo
  • 25,001
  • 7
  • 80
  • 118
5

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;
      }
  }
}
Sean Hall
  • 7,629
  • 2
  • 29
  • 44
3

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.

Greg
  • 1,673
  • 4
  • 20
  • 27
  • 1
    I took inspiration from your solution and went down the same road, to access the GridViewColumnHeader.Click event you can add a handler in the constructor. this.AddHandler(GridViewColumnHeader.ClickEvent, new RoutedEventHandler(GridViewColumnHeaderClicked)); – Derrick Moeller Feb 24 '15 at 12:47
3

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;
    }
}
JanDotNet
  • 3,746
  • 21
  • 30
  • 1
    Throwing out hundreds of lines of code means nothing if you don't explain what they're doing – schizoid04 Sep 13 '18 at 03:43
  • This answer applies sorting only on DisplayMemberBinding, so It's not quite perfect. But at least It shows how you could draw up/down arrows in headers. – Lucy82 Oct 26 '22 at 10:30
3

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.

New Bee
  • 991
  • 1
  • 13
  • 24
0

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;
}
IntegratedHen
  • 399
  • 4
  • 4
0

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>
  • 1
    Thanks. For extra points, could you also add an explanation summary? This is currently link-and-code-only (which is already better than link-only...). – Yunnosch Sep 15 '20 at 07:51
-2

Try this:

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