Привязка WPF DataGrid к DataSet: от WPF Toolkit до .NET 4

РазделWPFУровеньСредний Чтение8 минОпубликовано ТегиWPFDataGridVB.NETADO.NET

WPF DataGrid ближе всего к привычному GridView: колонки создаются автоматически, ячейки редактируются, строки сортируются щелчком по заголовку. Разберём, как привязать его к DataSet и вернуть правки в базу.

Главное. WPF DataGrid привязывается к DataTable через DefaultView, а не через саму таблицу. Автоколонки настраиваются событием AutoGeneratingColumn, правки сохраняются в базу обычным SqlDataAdapter.Update.

Откуда взялся DataGrid в WPF

В первых версиях WPF табличного элемента с редактированием не было вовсе. Разработчик, искавший в панели инструментов аналог GridView или DataGridView, находил только ListBox и ListView. Для простого вывода этого хватало, но для редактируемой таблицы приходилось писать много кода.

В 2008 году Microsoft выпустила на CodePlex отдельный пакет WPF Toolkit, в котором появился DataGrid. Пакет ставился из MSI, после чего элемент нужно было добавить в панель инструментов вручную: правый щелчок, «Choose Items», вкладка WPF Components. В XAML он подключался через собственное пространство имён http://schemas.microsoft.com/wpf/2008/toolkit.

С выходом .NET Framework 4 DataGrid вошёл в состав WPF и живёт в обычном пространстве имён System.Windows.Controls. Если вы сопровождаете проект на .NET 3.5 с префиксом my:DataGrid или toolkit:DataGrid, при переходе на .NET 4 достаточно убрать ссылку на WPFToolkit.dll и префикс: API почти не изменился.

Разметка окна

Для первого примера включим AutoGenerateColumns. DataGrid сам создаст колонку для каждой колонки DataTable и подберёт тип: для Boolean будет флажок, для остальных типов текстовое поле. Свойство IsReadOnly пока оставим False, чтобы ячейки можно было править.

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="420" Width="640">
  <DockPanel>
    <StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Margin="6">
      <Button x:Name="LoadButton" Content="Загрузить" Padding="10,2" />
      <Button x:Name="SaveButton" Content="Сохранить" Padding="10,2"
              Margin="6,0,0,0" />
    </StackPanel>
    <!-- В .NET 4+ DataGrid встроен; в .NET 3.5 нужен WPF Toolkit -->
    <DataGrid x:Name="OrdersGrid" AutoGenerateColumns="True"
              CanUserAddRows="False" AlternatingRowBackground="#FFF3F6FA" />
  </DockPanel>
</Window>

Привязка из кода: ItemsSource и DefaultView

Главная ловушка этого раздела в одной строке. Свойство ItemsSource имеет тип IEnumerable, а DataTable этот интерфейс не реализует: она отдаёт данные через IListSource. Поэтому присваивание OrdersGrid.ItemsSource = table при Option Strict On просто не компилируется, а при Option Strict Off падает во время выполнения.

Правильно передавать представление таблицы DefaultView. Это DataView, он реализует нужные интерфейсы, а кроме того поддерживает сортировку и фильтр, которыми DataGrid пользуется при щелчке по заголовку. Второй вариант: назначить таблицу в DataContext и указать в разметке ItemsSource=«{Binding}», WPF сам возьмёт представление.

Window1.xaml.vbVB.NET
Imports System.Data
Imports System.Data.SqlClient

Class Window1
    Private ReadOnly _cs As String = My.Settings.NorthwindConnection
    Private _ds As New DataSet()
    Private _adapter As SqlDataAdapter

    Private Sub LoadButton_Click(sender As Object, e As RoutedEventArgs) _
            Handles LoadButton.Click
        _ds = New DataSet()
        _adapter = New SqlDataAdapter(
            "SELECT OrderID, CustomerID, OrderDate, Freight, ShipCity " &
            "FROM Orders", _cs)
        ' Построитель сам создаст UPDATE/INSERT/DELETE по ключу таблицы
        Dim builder As New SqlCommandBuilder(_adapter)
        _adapter.Fill(_ds, "Orders")
        ' Не таблица, а её представление DefaultView
        OrdersGrid.ItemsSource = _ds.Tables("Orders").DefaultView
    End Sub
End Class
SqlCommandBuilder генерирует команды изменения только для запросов к одной таблице с первичным ключом. Для соединений нескольких таблиц команды придётся написать самому.

Настройка автоматических колонок

Автоколонки удобны, но заголовки получаются техническими, а даты выводятся с временем. Не обязательно сразу отказываться от AutoGenerateColumns: событие AutoGeneratingColumn вызывается для каждой колонки до её добавления. В нём можно поменять заголовок, задать формат или вовсе отменить колонку через e.Cancel.

AutoGeneratingColumnVB.NET
Private Sub OrdersGrid_AutoGeneratingColumn(sender As Object,
        e As DataGridAutoGeneratingColumnEventArgs) _
        Handles OrdersGrid.AutoGeneratingColumn
    Select Case e.PropertyName
        Case "OrderID"
            e.Column.Header = "Заказ"
            e.Column.IsReadOnly = True ' ключ не правим
        Case "CustomerID" : e.Column.Header = "Клиент"
        Case "ShipCity" : e.Column.Header = "Город"
        Case "Freight"
            e.Column.Header = "Доставка"
            Dim col = TryCast(e.Column, DataGridTextColumn)
            If col IsNot Nothing Then col.Binding.StringFormat = "N2"
        Case "OrderDate"
            e.Column.Header = "Дата"
            Dim col = TryCast(e.Column, DataGridTextColumn)
            If col IsNot Nothing Then col.Binding.StringFormat = "dd.MM.yyyy"
    End Select
End Sub

Свои колонки вместо автоматических

Когда таблица стабильна, лучше описать колонки явно, как в GridView с BoundField и TemplateField. Это даёт полный контроль над порядком, шириной и видом ячеек. В WPF доступны DataGridTextColumn, DataGridCheckBoxColumn, DataGridComboBoxColumn, DataGridHyperlinkColumn и универсальная DataGridTemplateColumn, в которой можно разместить любую разметку, например DatePicker для редактирования даты.

Явные колонкиXML
<DataGrid x:Name="OrdersGrid" AutoGenerateColumns="False"
          CanUserAddRows="False">
  <DataGrid.Columns>
    <DataGridTextColumn Header="Заказ" Binding="{Binding OrderID}"
                        IsReadOnly="True" Width="70" />
    <DataGridTextColumn Header="Город" Binding="{Binding ShipCity}"
                        Width="*" />
    <DataGridTextColumn Header="Доставка" Width="90"
        Binding="{Binding Freight, StringFormat=N2}" />
    <DataGridTemplateColumn Header="Дата" Width="120">
      <DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
          <TextBlock Text="{Binding OrderDate, StringFormat=dd.MM.yyyy}" />
        </DataTemplate>
      </DataGridTemplateColumn.CellTemplate>
      <DataGridTemplateColumn.CellEditingTemplate>
        <DataTemplate>
          <DatePicker SelectedDate="{Binding OrderDate}" />
        </DataTemplate>
      </DataGridTemplateColumn.CellEditingTemplate>
    </DataGridTemplateColumn>
  </DataGrid.Columns>
</DataGrid>

Сохранение правок обратно в базу

Поскольку DataGrid привязан к DataView, всё, что пользователь вводит в ячейки, сразу попадает в строки DataTable, а у строк меняется RowState. Остаётся вызвать Update у того же адаптера. Перед этим стоит завершить редактирование текущей строки методом CommitEdit, иначе последняя изменённая ячейка может не попасть в таблицу.

Кнопка «Сохранить»VB.NET
Private Sub SaveButton_Click(sender As Object, e As RoutedEventArgs) _
        Handles SaveButton.Click
    If _adapter Is Nothing Then Return
    ' Завершаем редактирование ячейки и строки
    OrdersGrid.CommitEdit(DataGridEditingUnit.Row, True)
    Try
        Dim n = _adapter.Update(_ds, "Orders")
        MessageBox.Show("Сохранено строк: " & n)
    Catch ex As DBConcurrencyException
        MessageBox.Show("Запись изменил другой пользователь, обновите данные.")
    End Try
End Sub
Если таблица большая, не загружайте её целиком. Ограничьте выборку условием WHERE или постраничной загрузкой: DataGrid виртуализирует строки при отображении, но память под DataTable всё равно занята.

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

В современных WPF-приложениях DataGrid обычно привязывают к ObservableCollection сущностей Entity Framework Core, а сохранение выполняют через SaveChangesAsync в модели представления. Для больших и сложных таблиц применяют коммерческие сетки (Telerik, DevExpress, Syncfusion), а в Avalonia есть свой DataGrid и TreeDataGrid с похожим API.

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

Почему ItemsSource = table не компилируется?

DataTable не реализует IEnumerable, а ItemsSource ждёт именно его. Передайте table.DefaultView или назначьте таблицу в DataContext с ItemsSource={Binding}.

Нужен ли WPF Toolkit в проекте на .NET 4 и новее?

Нет. DataGrid входит в WPF начиная с .NET 4. Ссылку на WPFToolkit.dll и пространство имён toolkit можно удалить.

Как запретить пользователю добавлять строки?

Установите CanUserAddRows=False. Для полного запрета правки используйте IsReadOnly=True у сетки или у отдельных колонок.

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