
WPF ListView в режиме GridView: колонки, шаблоны ячеек и сортировка
ListView с представлением GridView это лёгкая таблица только для чтения: заголовки колонок, изменение ширины мышью и привязка к DataTable без отдельного пакета. Разберём разметку, шаблоны ячеек и сортировку.
Когда выбирать ListView, а не DataGrid
До выхода .NET 4 встроенного DataGrid в WPF не было, и для табличного вывода чаще всего использовали ListView. Он и сегодня остаётся разумным выбором, когда данные нужно только показать: список файлов, журнал событий, результаты поиска. ListView легче DataGrid, в нём нет режима редактирования ячеек, а внешний вид строк полностью задаётся шаблонами.
Сам по себе ListView это обычный список с выбором элементов. Табличным его делает свойство View, в которое помещается объект GridView. Название путает разработчиков ASP.NET: этот GridView не имеет ничего общего с серверным элементом Web Forms. Здесь это всего лишь описание набора колонок с заголовками.
Разметка: ListView.View и GridViewColumn
Каждая колонка описывается элементом GridViewColumn. Свойство Header задаёт заголовок, Width начальную ширину, а DisplayMemberBinding указывает, какое поле строки выводить. Пользователь может менять ширину колонок мышью и переставлять их перетаскиванием заголовков, это работает без единой строки кода.
<Window x:Class="Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Клиенты" Height="400" Width="560">
<DockPanel>
<Button x:Name="LoadButton" DockPanel.Dock="Bottom" Content="Загрузить"
HorizontalAlignment="Left" Margin="4" Padding="10,2" />
<ListView x:Name="CustomersView" ItemsSource="{Binding}"
GridViewColumnHeader.Click="Header_Click">
<ListView.View>
<GridView>
<GridViewColumn Header="Компания" Width="200"
DisplayMemberBinding="{Binding CompanyName}" />
<GridViewColumn Header="Контакт" Width="150"
DisplayMemberBinding="{Binding ContactName}" />
<GridViewColumn Header="Город" Width="120"
DisplayMemberBinding="{Binding City}" />
</GridView>
</ListView.View>
</ListView>
</DockPanel>
</Window>Ошибка «Items collection must be empty before using ItemsSource»
Самая частая ошибка при первом знакомстве с ListView возникает, если забыть обёртку ListView.View и написать GridView или колонки прямо внутри ListView. XAML трактует любой дочерний элемент списка как элемент коллекции Items. Список получает один элемент вручную, а затем ещё и ItemsSource из привязки. Совмещать эти два способа заполнения нельзя, и WPF бросает InvalidOperationException с этим текстом.
То же исключение появится, если в коде вызвать CustomersView.Items.Add после назначения ItemsSource или если в разметке случайно остался пустой ListViewItem от дизайнера. Правило простое: либо Items заполняется вручную, либо данные приходят через ItemsSource, но не оба сразу.
Загрузка данных в код-бихайнде
Как и с ListBox, вызывать DataBind не нужно. Достаточно положить DataTable в DataContext: разметка уже содержит ItemsSource=«{Binding}», и WPF сам возьмёт представление таблицы. Строки придут в список как DataRowView, к колонкам которых обращаются привязки по имени.
Imports System.Data
Imports System.Data.SqlClient
Imports System.ComponentModel
Class Window1
Private Sub LoadButton_Click(sender As Object, e As RoutedEventArgs) _
Handles LoadButton.Click
CustomersView.DataContext = GetCustomers()
End Sub
Private Function GetCustomers() As DataTable
Dim sql = "SELECT CompanyName, ContactName, City, Phone FROM Customers"
Dim table As New DataTable()
Using cn As New SqlConnection(My.Settings.NorthwindConnection)
Using da As New SqlDataAdapter(sql, cn)
da.Fill(table)
End Using
End Using
Return table
End Function
End ClassCellTemplate: больше, чем текст
DisplayMemberBinding выводит значение простым текстом. Если в ячейке нужна ссылка, значок, флажок или несколько полей сразу, вместо него используется CellTemplate с DataTemplate внутри. Важная тонкость: если у колонки задан DisplayMemberBinding, шаблон игнорируется, поэтому указывайте что-то одно.
В примере ниже телефон выводится серым моноширинным шрифтом, а город и компания объединены в одной ячейке.
<GridViewColumn Header="Компания" Width="230">
<GridViewColumn.CellTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding CompanyName}" FontWeight="SemiBold" />
<TextBlock Text="{Binding City}" Foreground="Gray" FontSize="11" />
</StackPanel>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Телефон" Width="140">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Phone}" FontFamily="Consolas"
Foreground="DimGray" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>Сортировка по щелчку на заголовке
В отличие от DataGrid, ListView не сортирует сам. Зато у любой привязанной коллекции есть представление ICollectionView, которое получают через CollectionViewSource.GetDefaultView. Для DataView это BindingListCollectionView, и он понимает коллекцию SortDescriptions. Обработчику остаётся определить, по какому полю щёлкнули, и переключить направление.
Private _sortField As String
Private _sortDir As ListSortDirection = ListSortDirection.Ascending
Private Sub Header_Click(sender As Object, e As RoutedEventArgs)
Dim header = TryCast(e.OriginalSource, GridViewColumnHeader)
If header Is Nothing OrElse header.Column Is Nothing Then Return
Dim b = TryCast(header.Column.DisplayMemberBinding, Binding)
If b Is Nothing Then Return ' у колонок с шаблоном поле задаём иначе
Dim field = b.Path.Path
' Повторный щелчок по той же колонке меняет направление
If field = _sortField AndAlso _sortDir = ListSortDirection.Ascending Then
_sortDir = ListSortDirection.Descending
Else
_sortDir = ListSortDirection.Ascending
End If
_sortField = field
Dim view = CollectionViewSource.GetDefaultView(CustomersView.ItemsSource)
view.SortDescriptions.Clear()
view.SortDescriptions.Add(New SortDescription(field, _sortDir))
End SubКак это делают сегодня
Вопросы и ответы
Почему CellTemplate не применяется?
У колонки одновременно задан DisplayMemberBinding, а он имеет приоритет над шаблоном. Удалите DisplayMemberBinding, и шаблон заработает.
Можно ли редактировать ячейки ListView?
Только вручную: поместить TextBox в CellTemplate и привязать его двусторонне. Для полноценного редактирования удобнее DataGrid.
Как сохранить ширину колонок между запусками?
Читайте свойство Width у каждой GridViewColumn при закрытии окна, сохраняйте в настройках приложения и восстанавливайте при открытии.