This article is currently in the process of being translated into Russian (~99% done).
How-to: ListView with column sorting
В последней главе мы увидели, как легко можно задать сортировку ListView из кода. Несмотря на то, что данного подхода будет достаточно для некоторых сценариев, это не дает возможности конечному пользователю самостоятельно управлять сортировкой. Кроме того, визуально непонятно, по какому именно полю выполнена сортировка. В Windows и многих других приложениях, направление сортировки обозначается треугольником рядом с именем столбца, по которому сейчас установлена сортировка.
Это статья с практическими рекомендациями. В ней вы получите решения, которые помогут выполнить все вышеперечисленные задачи. Но имейте ввиду, что часть кода приведенного в этой статье выходит за рамки изученного вами ранее. Поэтому она помечена как «Практическое руководство».
Эта статья основана на предыдущей, однако в ней так же будет объясняться каждый шаг по мере продвижения. Наша цель – ListView с сортировкой и визуальным индикатором столбца и направления сортировки. Пользователь будет устанавливать сортировку простым нажатием на заголовок колонки, а при повторном нажатии сортировка будет менять направление. Вот как это выглядит:
XAML код
Первое, что нам понадобится, это немного кода XAML для определения интерфейса пользователя. Сейчас он выглядит примерно так:
<Window x:Class="WpfTutorialSamples.ListView_control.ListViewColumnSortingSample"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="ListViewColumnSortingSample" Height="200" Width="350">
<Grid Margin="10">
<ListView Name="lvUsers">
<ListView.View>
<GridView>
<GridViewColumn Width="120" DisplayMemberBinding="{Binding Name}">
<GridViewColumn.Header>
<GridViewColumnHeader Tag="Name" Click="lvUsersColumnHeader_Click">Name</GridViewColumnHeader>
</GridViewColumn.Header>
</GridViewColumn>
<GridViewColumn Width="80" DisplayMemberBinding="{Binding Age}">
<GridViewColumn.Header>
<GridViewColumnHeader Tag="Age" Click="lvUsersColumnHeader_Click">Age</GridViewColumnHeader>
</GridViewColumn.Header>
</GridViewColumn>
<GridViewColumn Width="80" DisplayMemberBinding="{Binding Sex}">
<GridViewColumn.Header>
<GridViewColumnHeader Tag="Sex" Click="lvUsersColumnHeader_Click">Sex</GridViewColumnHeader>
</GridViewColumn.Header>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
</Grid>
</Window>
Обратите внимание, как были определены заголовки для каждого из столбцов. Вместо обычной строки используется объект GridViewColumnHeader. Это сделано для того, что бы я мог задать дополнительные свойства, в нашем случае это свойство Tag и событие Click.
Свойство Tag используется для хранения имени поля, по которому будет производиться сортировка, если этот конкретный столбец будет нажат. Сама сортировка будет осуществляться в событии lvUsersColumnHeader_Click, на которое подписан каждый из столбцов.
Это ключевая концепция XAML. Так, мы привяжем наши свойства Name, Age и Sex, которые обсудим далее.
Код модуля
В коде происходит довольно много вещей. Я использую всего 3 класса, которые вы обычно храните в отдельных файлах, но для удобства я сохранил их в одном, что позволило мне уложиться примерно в 100 строк. Сначала посмотрим на код, потом я объясню, как это работает:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Media;
namespace WpfTutorialSamples.ListView_control
{
public partial class ListViewColumnSortingSample : Window
{
private GridViewColumnHeader listViewSortCol = null;
private SortAdorner listViewSortAdorner = null;
public ListViewColumnSortingSample()
{
InitializeComponent();
List<User> items = new List<User>();
items.Add(new User() { Name = "John Doe", Age = 42, Sex = SexType.Male });
items.Add(new User() { Name = "Jane Doe", Age = 39, Sex = SexType.Female });
items.Add(new User() { Name = "Sammy Doe", Age = 13, Sex = SexType.Male });
items.Add(new User() { Name = "Donna Doe", Age = 13, Sex = SexType.Female });
lvUsers.ItemsSource = items;
}
private void lvUsersColumnHeader_Click(object sender, RoutedEventArgs e)
{
GridViewColumnHeader column = (sender as GridViewColumnHeader);
string sortBy = column.Tag.ToString();
if(listViewSortCol != null)
{
AdornerLayer.GetAdornerLayer(listViewSortCol).Remove(listViewSortAdorner);
lvUsers.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);
lvUsers.Items.SortDescriptions.Add(new SortDescription(sortBy, newDir));
}
}
public enum SexType { Male, Female };
public class User
{
public string Name { get; set; }
public int Age { get; set; }
public string Mail { get; set; }
public SexType Sex { get; set; }
}
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();
}
}
}
Позвольте мне начать описание снизу и далее продвигаться вверх объясняя, что происходит. Последний класс в файле это Adorner класс, названный SortAdorner. Он выполняет одну цель - рисует треугольник указывающий вверх или вниз, в зависимости от направления сортировки. WPF использует концепцию графических элементов (adorners) для предоставления возможности нарисовать что-то поверх других элементов управления (controls), и это как раз то, что нам нужно: возможность нарисовать треугольник сортировки поверх наших заголовков в ListView.
SortAdorner определяет два объекта Geometry, которые как правило используются для описания плоских фигур: один треугольник остриём вверх, другой - вниз. Метод Geometry.Parse() в свою очередь использует список точек для прорисовки этих треугольников, о чём более подробно - в следующей статье.
По направлению прорисовки треугольника SortAdorner знает направление сортировки, но пока не знает поле, по которому нужно выполнить сортировку, что будет выяснено на уровне пользовательского интерфейса.
Класс User является простым базовым классом, который используется для хранения информации о пользователе. Часть этой информации используется в слое UI, где мы осуществили связь со свойствами Name, Age и Sex.
В классе Window у нас два метода - конструктор, в котором мы создаём список пользователей и назначаем его свойству ItemSource нашего ListView, и ещё более интересный обработчик события нажатия мыши, который будет вызван кликом пользователя по столбцу. В начале класса Window мы определили две приватных переменных: listViewSortCol и listViewSortAdorner. Это поможет нам запомнить, какой из столбцов сейчас отсортирован и размещённый нами для пометки этого столбца декоратор.
В обработчике события lvUsersColumnHeader мы начинаем с получения ссылки на столбец, по которому кликнул пользователь. Таким образом, мы можем решить, по какому из свойств класса User должна быть выполнена сортировка, просто посмотрев на значение свойства Tag, которое мы определили в XAML. Затем мы проверяем, был ли столбец уже отсортирован - в этом случае мы удаляем декоратор и очищаем описание текущей сортировки.
После этого, мы готовы определить направление сортировки. По умолчанию установлено по возрастанию, но мы проверяем, отсортирован ли уже этот столбец пользователем, который по нему кликнул - если так, мы меняем направление сортировки на противоположное (по убыванию).
В конце мы создаём новый объект SortAdorner, передавая столбец, на котором он должен быть отрисован, и направление сортировки. Затем мы добавляем его к свойству AdornerLayer заголовка этого столбца, и в самом конце добавляем SortDescription к ListView, чтобы дать ему знать, по какому из свойств и в каком направлении выполнить сортировку.
Итоги
Поздравляю, у вас сейчас есть полностью сортируемый ListView с визуальной индикацией столбца, к которому применена сортировка, и её направление. В случае, если вы хотите узнать больше о некоторых используемых в данной статье концепций, таких как привязка данных, геометрия или ListView в общем, тогда посмотрите другие статьи, где каждая из тем раскрыта подробней.