Sorting a ListView in WPF – Part II
Some time ago I wrote a post about how to sort a ListView by clicking on the header of the column. The problem with that solution was that you needed to implement it each time and you have to define an explicit header for each column.
As a more general solution I use attached properties to extend the ListView and GridViewColumn. The first attached property is tied to the ListView itself, and it indicates that the control supports sorting. This property attach or detach to the Click event of the GridViewColumnHeader depending on its value. In order to enable sorting, this property should be true:
<ListView ItemsSource="{Binding Notes}" l:SortBehavior.CanUserSortColumns="true">
...
</ListView>
If you simply need to allow sorting for every column in your ListView and you are not applying data templates to the columns it is all that you need.
The rest of the attached properties are applicable to GridViewColum and are intended to be used in some special cases. For example, if you do not want to allow sorting in a specific column, you can disable this behavior using the CanUseSort property:
<GridViewColumn
Width="50"
Header="Id"
DisplayMemberBinding="{Binding Id}"
l:SortBehavior.CanUseSort="false" />
The value of this property indicates that you can not sort the column containing the Id.
The second property (SortExpression) is useful in cases where you are using a DataTemplate. The sorting property is extracted from the binding that you are using automatically, in the case of the DataTemplate this automatic behavior is simply not possible. In that case you will need to indicate the sorting property explicitly:
<GridViewColumn Width="210" Header="Title" l:SortBehavior.SortExpression="Title">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock>
<Run Text="{Binding Title}" FontWeight="Bold" />
<LineBreak />
<Run Text="{Binding Text}" />
</TextBlock>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
The last property (SortDirection) is used internally to track the current sort direction of each column, its values are based on ListSortDirection enum, also it can be null in case this column is not sorted. You can set this property in any column in order to define the initial sorting of the items:
<GridViewColumn
Width="130"
DisplayMemberBinding="{Binding Written}"
Header="Date"
l:SortBehavior.SortDirection="Ascending" />
Finally, you can define a DataTemplate that applies to every column to display some visual indicator that shows the user that this column is sorted.
<DataTemplate x:Key="SortHeader">
<Grid Margin="3,0" Grid.IsSharedSizeScope="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="15" SharedSizeGroup="ArrowHolder" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="15" SharedSizeGroup="ArrowHolder" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding}" Grid.Column="1" HorizontalAlignment="Center" />
<Path x:Name="arrow" StrokeThickness="1" Fill="DarkGray" Visibility="Hidden" Grid.Column="2" />
</Grid>
<DataTemplate.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Path=Column.(l:SortBehavior.CanUseSort),
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type GridViewColumnHeader}}}" Value="true" />
<Condition Binding="{Binding Path=Column.(l:SortBehavior.SortDirection),
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type GridViewColumnHeader}}}" Value="Ascending" />
</MultiDataTrigger.Conditions>
<MultiDataTrigger.Setters>
<Setter TargetName="arrow" Property="Data" Value="M 5,10 L 15,10 L 10,5" />
<Setter TargetName="arrow" Property="Visibility" Value="Visible" />
</MultiDataTrigger.Setters>
</MultiDataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Path=Column.(l:SortBehavior.CanUseSort),
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type GridViewColumnHeader}}}" Value="true" />
<Condition Binding="{Binding Path=Column.(l:SortBehavior.SortDirection),
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type GridViewColumnHeader}}}" Value="Descending" />
</MultiDataTrigger.Conditions>
<MultiDataTrigger.Setters>
<Setter TargetName="arrow" Property="Data" Value="M 5,5 L 10,10 L 15,5" />
<Setter TargetName="arrow" Property="Visibility" Value="Visible" />
</MultiDataTrigger.Setters>
</MultiDataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
You can download the sample here.