WPF ListView в режиме GridView: колонки, шаблоны ячеек и сортировка

РазделWPFУровеньНовичок Чтение7 минОпубликовано ТегиWPFListViewVB.NETПривязка данных

ListView с представлением GridView это лёгкая таблица только для чтения: заголовки колонок, изменение ширины мышью и привязка к DataTable без отдельного пакета. Разберём разметку, шаблоны ячеек и сортировку.

Главное. ListView превращается в таблицу через ListView.View с GridView внутри. Колонки описываются GridViewColumn с DisplayMemberBinding или CellTemplate, а сортировка делается через ICollectionView и SortDescriptions.

Когда выбирать ListView, а не DataGrid

До выхода .NET 4 встроенного DataGrid в WPF не было, и для табличного вывода чаще всего использовали ListView. Он и сегодня остаётся разумным выбором, когда данные нужно только показать: список файлов, журнал событий, результаты поиска. ListView легче DataGrid, в нём нет режима редактирования ячеек, а внешний вид строк полностью задаётся шаблонами.

Сам по себе ListView это обычный список с выбором элементов. Табличным его делает свойство View, в которое помещается объект GridView. Название путает разработчиков ASP.NET: этот GridView не имеет ничего общего с серверным элементом Web Forms. Здесь это всего лишь описание набора колонок с заголовками.

Разметка: ListView.View и GridViewColumn

Каждая колонка описывается элементом GridViewColumn. Свойство Header задаёт заголовок, Width начальную ширину, а DisplayMemberBinding указывает, какое поле строки выводить. Пользователь может менять ширину колонок мышью и переставлять их перетаскиванием заголовков, это работает без единой строки кода.

Window1.xamlXML
<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>
Обработчик GridViewColumnHeader.Click подключён к самому ListView: событие всплывает от заголовков вверх по дереву, поэтому один обработчик обслуживает все колонки.

Ошибка «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, к колонкам которых обращаются привязки по имени.

Window1.xaml.vbVB.NET
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 Class

CellTemplate: больше, чем текст

DisplayMemberBinding выводит значение простым текстом. Если в ячейке нужна ссылка, значок, флажок или несколько полей сразу, вместо него используется CellTemplate с DataTemplate внутри. Важная тонкость: если у колонки задан DisplayMemberBinding, шаблон игнорируется, поэтому указывайте что-то одно.

В примере ниже телефон выводится серым моноширинным шрифтом, а город и компания объединены в одной ячейке.

Колонки с шаблонамиXML
<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>
Чтобы выровнять содержимое ячеек по правому краю (например, суммы), задайте у ListView стиль ListViewItem с HorizontalContentAlignment=Stretch, а в шаблоне ячейки TextAlignment=Right.

Сортировка по щелчку на заголовке

В отличие от DataGrid, ListView не сортирует сам. Зато у любой привязанной коллекции есть представление ICollectionView, которое получают через CollectionViewSource.GetDefaultView. Для DataView это BindingListCollectionView, и он понимает коллекцию SortDescriptions. Обработчику остаётся определить, по какому полю щёлкнули, и переключить направление.

СортировкаVB.NET
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

Как это делают сегодня

В новых WPF-проектах ListView с GridView по-прежнему используют для лёгких таблиц только для чтения, но привязывают его к ObservableCollection моделей и строят по MVVM, а сортировку выносят в модель представления. Если нужны редактирование, группировка и фильтры из коробки, проще взять встроенный DataGrid. В WinUI 3 похожую задачу решает DataGrid из Windows Community Toolkit или ListView с собственным шаблоном строки.

Вопросы и ответы

Почему CellTemplate не применяется?

У колонки одновременно задан DisplayMemberBinding, а он имеет приоритет над шаблоном. Удалите DisplayMemberBinding, и шаблон заработает.

Можно ли редактировать ячейки ListView?

Только вручную: поместить TextBox в CellTemplate и привязать его двусторонне. Для полноценного редактирования удобнее DataGrid.

Как сохранить ширину колонок между запусками?

Читайте свойство Width у каждой GridViewColumn при закрытии окна, сохраняйте в настройках приложения и восстанавливайте при открытии.

Читайте также