
Привязка WPF DataGrid к DataSet: от WPF Toolkit до .NET 4
WPF DataGrid ближе всего к привычному GridView: колонки создаются автоматически, ячейки редактируются, строки сортируются щелчком по заголовку. Разберём, как привязать его к DataSet и вернуть правки в базу.
Откуда взялся 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, чтобы ячейки можно было править.
<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 сам возьмёт представление.
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Настройка автоматических колонок
Автоколонки удобны, но заголовки получаются техническими, а даты выводятся с временем. Не обязательно сразу отказываться от AutoGenerateColumns: событие AutoGeneratingColumn вызывается для каждой колонки до её добавления. В нём можно поменять заголовок, задать формат или вовсе отменить колонку через e.Cancel.
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 для редактирования даты.
<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, иначе последняя изменённая ячейка может не попасть в таблицу.
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Как это делают сегодня
Вопросы и ответы
Почему 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 у сетки или у отдельных колонок.