Well, it has been quite a while, but I was reminded by a reader that
when I say I'm going to write a part 2 to a tutorial, I probably should
actually write a part 2 (and not wait 8 months to do it). Back in
February, I wrote a tutorial on using the ListView inWPF, and after
publishing it I got bored/distracted and so never ended up continuing
the series. Well, guess what - it's back!
Today we are going to take a look at how to sort the content of a
ListView. The actual sorting is actually really easy to do in code (it
only takes a couple lines to tell the ListView to sort on a particular
field). Where the fun comes in (and most of the work) is triggering the
sort from the user interface and displaying the current sort direction
in the column header. We are going to start from the exact same code as
the end of the previous tutorial, so (since it has been a while) I
recommend that you go take a look atit before you
continue.
You've looked at the old code now? Good. Then we can move on to a
screenshot of the app we will be creating today:
As you might have guessed, the data in the grid is sorted in Ascending
by Creator - shown by that black triangle pointing downward in that
column header. We will be writing the code to make that triangle appear
in the correct header (and pointing in the right direction) - and that
code is probably the most interesting in the tutorial. Without further
ado, the XAML:
<Window x:Class="ListViewTest3.ListViewTest" Name="This"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Some Game Data" Height="216" Width="435">
<StackPanel>
<ListView x:Name="gameListView" ItemsSource=
"{Binding ElementName=This, Path=GameCollection}">
<ListView.View>
<GridView>
<GridViewColumn Width="140"
DisplayMemberBinding="{Binding GameName}">
<GridViewColumnHeader Click="SortClick"
Tag="GameName"
Content="Game Name" />
</GridViewColumn>
<GridViewColumn Width="140"
DisplayMemberBinding="{Binding Creator}">
<GridViewColumnHeader Click="SortClick"
Tag="Creator"
Content="Creator" />
</GridViewColumn>
<GridViewColumn Width="140"
DisplayMemberBinding="{Binding Publisher}">
<GridViewColumnHeader Click="SortClick"
Tag="Publisher"
Content="Publisher" />
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
<Button HorizontalAlignment="Right" Margin="5"
Content="Add Row" Click="AddRowClick" />
</StackPanel>
</Window>
First off, you might notice that I'm no longer setting the DataContext
on the
Window
like I was in the previous ListView tutorial. This isn't
because of anything specific to this tutorial, it is just that I've
grown older and wiser. Setting the DataContext
on a control can lead
to some very frustrating binding debugging, especially when nesting user
controls. Granted, every single time, it comes down to a "Duh" moment,
but I've learned it is just safer to avoid setting the DataContext
.
There are still situations where it is warranted, but trying to save a
few keystrokes when typing a binding expression is not one of those
situation.
Now that I've got that off my chest, lets dive into this code. There are
actually only two significant things that changed between this XAML and
the XAML of the previous tutorial - the removal of the
Header
attribute on the GridViewColumn
tags, and the addition of
GridViewColumnHeader
tags inside of the GridViewColumn
tags. This is
because we want to do more complicated stuff with the column headers
this time.
You can put anything you want inside of a GridViewColumnHeader (images,
fancy other controls, etc..), but by this point you probably guessed
that since everything else in WPF acts like that too. In this case we
aren't doing anything special, we are just setting the
Content
attribute to the text we want to appear. The other two attributes,
however, are more interesting. The Click event is hooked up to the
function SortClick
- the code for which we will be exploring in a
moment. The Tag
attribute holds some data that we will use in the
SortClick
function - in this case, the name of the backing field that
we want to sort on (if we were going to sort on that column).
Just for reference purposes, here is the small class for the data
displayed in the list view:
public class GameData
{
public string GameName { get; set; }
public string Creator { get; set; }
public string Publisher { get; set; }
}
I said it was small, didn't I? While it is possible to get these field
name out of the
DisplayMemberBinding
for the particular grid column,
it is just a lot easier to store the info a second time in the easy to
access Tag
attribute. But by now you are probably wondering what we do
with that info - so it is time for the SortClick
function code:public partial class ListViewTest : Window
{
private GridViewColumnHeader _CurSortCol = null;
private SortAdorner _CurAdorner = null;
/* Other class code here .... */
private void SortClick(object sender, RoutedEventArgs e)
{
GridViewColumnHeader column = sender as GridViewColumnHeader;
String field = column.Tag as String;
if (_CurSortCol != null)
{
AdornerLayer.GetAdornerLayer(_CurSortCol).Remove(_CurAdorner);
gameListView.Items.SortDescriptions.Clear();
}
ListSortDirection newDir = ListSortDirection.Ascending;
if (_CurSortCol == column && _CurAdorner.Direction == newDir)
newDir = ListSortDirection.Descending;
_CurSortCol = column;
_CurAdorner = new SortAdorner(_CurSortCol, newDir);
AdornerLayer.GetAdornerLayer(_CurSortCol).Add(_CurAdorner);
gameListView.Items.SortDescriptions.Add(new SortDescription(field, newDir));
}
}
So when this function is called, the very first thing we do is figure
out is what column header the user clicked on - pretty easy to do, we
just cast the sender as a
GridViewColumnHeader
. Then we pull that
field name out of the Tag
property. The next block of code might look
a little unfamiliar, but what we are doing is if we were
sorting on a
previous column, we remove that black triangle and tell the list to stop
sorting.
"Huh? What is all this stuff about Adorners?" is what you are probably
saying right now. Well,
Adorners
are
a handy thing in WPF for decoration type things. We don't really want to
have to build a special GridViewColumnHeader
to support the little
triangle that we want to display when sorting. It would probably be
pretty annoying and really clutter up the XAML (an extra canvas layer,
etc..). Temporary UI decorations like this work really well as Adorners,
which sit in their own layer on top of everything else. That is what the
call to AdornerLayer.GetAdornerLayer
is doing - we are getting the
adorner layer for the GridViewColumnHeader
. Adorner layers are often
shared by many elements, but you can always add your own by adding an
AdornerDecorator
element.
Ok, that is enough about adorners for the moment. The next couple lines
in the function determine the sort direction for the data. We default to
ascending, but if the user has clicked on the same column that we were
already sorting on (and we were previously in the ascending direction)
we switch to descending.
Next we make the new sort adorner and add it to the adorner layer for
the new sort column. We will take a look in a moment at the code behind
SortAdorner
. And finally, we create a new
SortDescription
and add it to the
SortDescriptions Collection
on the ItemCollection
behind the ListView
(which we just cleared out
a few lines above). That's right, all we need the the string
representing the property name, and a direction, and WPF sorts the
collection for us!
OK now for the code behind that
SortAdorner
:public class SortAdorner : Adorner
{
private readonly static Geometry _AscGeometry =
Geometry.Parse("M 0,0 L 10,0 L 5,5 Z");
private readonly static Geometry _DescGeometry =
Geometry.Parse("M 0,5 L 10,5 L 5,0 Z");
public ListSortDirection Direction { get; private set; }
public SortAdorner(UIElement element, ListSortDirection dir)
: base(element)
{ Direction = dir; }
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
if (AdornedElement.RenderSize.Width < 20)
return;
drawingContext.PushTransform(
new TranslateTransform(
AdornedElement.RenderSize.Width - 15,
(AdornedElement.RenderSize.Height - 5) / 2));
drawingContext.DrawGeometry(Brushes.Black, null,
Direction == ListSortDirection.Ascending ?
_AscGeometry : _DescGeometry);
drawingContext.Pop();
}
}
So all adorners derive from from
Adorner
,
and require a UIElement
to be passed in to the constructor. The
UIElement will be the element that the adorner will adorn (all positions
and rendering in the adorner will be relative to that element). In our
case we also want a sort direction, so that we know which way to draw
the arrow. At the top, we have two static Geometries, one for the up
arrow and one for the down arrow. If you want some help interpreting
that geometry string, you should take a look at our tutorial on Drawing Custom Shapes With XAML.
The actual drawing is done in the
OnRender
method. First, if the
column header is less than 20 pixels wide, we don't bother drawing the
arrow (it looks kind of silly). Then we push a
TranslateTransform
onto the drawing context transform stack. This makes sure that the arrow
will be drawn at the right edge of the column and vertically in the
center. In the next line, we draw the geometry (looking at the
Direction
to figure out which one), and then finally we pop the
translate transform off of the transform stack (just to keep the
transform stack clean).
And that is about it. Below is the entire C# code behind in one block:
using System;
using System.Windows;
using System.Windows.Controls;
using System.Collections.ObjectModel;
using System.Windows.Shapes;
using System.ComponentModel;
using System.Windows.Media;
using System.Windows.Documents;
namespace ListViewTest3
{
public partial class ListViewTest : Window
{
private ObservableCollection<GameData> _GameCollection =
new ObservableCollection<GameData>();
private GridViewColumnHeader _CurSortCol = null;
private SortAdorner _CurAdorner = null;
public ListViewTest()
{
_GameCollection.Add(new GameData {
GameName = "World Of Warcraft",
Creator = "Blizzard",
Publisher = "Blizzard" });
_GameCollection.Add(new GameData {
GameName = "Halo",
Creator = "Bungie",
Publisher = "Microsoft" });
_GameCollection.Add(new GameData {
GameName = "Gears Of War",
Creator = "Epic",
Publisher = "Microsoft" });
InitializeComponent();
}
public ObservableCollection<GameData> GameCollection
{ get { return _GameCollection; } }
private void AddRowClick(object sender, RoutedEventArgs e)
{
_GameCollection.Add(new GameData {
GameName = "A New Game",
Creator = "A New Creator",
Publisher = "A New Publisher" });
}
private void SortClick(object sender, RoutedEventArgs e)
{
GridViewColumnHeader column = sender as GridViewColumnHeader;
String field = column.Tag as String;
if (_CurSortCol != null)
{
AdornerLayer.GetAdornerLayer(_CurSortCol).Remove(_CurAdorner);
gameListView.Items.SortDescriptions.Clear();
}
ListSortDirection newDir = ListSortDirection.Ascending;
if (_CurSortCol == column && _CurAdorner.Direction == newDir)
newDir = ListSortDirection.Descending;
_CurSortCol = column;
_CurAdorner = new SortAdorner(_CurSortCol, newDir);
AdornerLayer.GetAdornerLayer(_CurSortCol).Add(_CurAdorner);
gameListView.Items.SortDescriptions.Add(
new SortDescription(field, newDir));
}
}
public class GameData
{
public string GameName { get; set; }
public string Creator { get; set; }
public string Publisher { get; set; }
}
public class SortAdorner : Adorner
{
private readonly static Geometry _AscGeometry =
Geometry.Parse("M 0,0 L 10,0 L 5,5 Z");
private readonly static Geometry _DescGeometry =
Geometry.Parse("M 0,5 L 10,5 L 5,0 Z");
public ListSortDirection Direction { get; private set; }
public SortAdorner(UIElement element, ListSortDirection dir)
: base(element)
{ Direction = dir; }
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
if (AdornedElement.RenderSize.Width < 20)
return;
drawingContext.PushTransform(
new TranslateTransform(
AdornedElement.RenderSize.Width - 15,
(AdornedElement.RenderSize.Height - 5) / 2));
drawingContext.DrawGeometry(Brushes.Black, null,
Direction == ListSortDirection.Ascending ?
_AscGeometry : _DescGeometry);
drawingContext.Pop();
}
}
}
So that pretty much covers sorting a
ListView
, and a whole bunch of
other stuff along the way. You can download the Visual Studio solution
here, which
includes all the code for all the ListView examples (both this and the
other tutorial). So I have yet to decide what to cover in the next
ListView tutorial - filtering or editing rows, but rest assured, the
next one won't take nearly as long before it gets written. As always,
please leave any questions or comments below (and if you have an opinion
on what the next ListView tutorial should be about let me know).
Source Files:
Comments
Post a Comment