
WPF ListBox и DataTable: привязка данных и DataTemplate
В WPF нет привычных DataSource и DataBind. Вместо них работают DataContext, ItemsSource и шаблон элемента, и, разобравшись с ними один раз, вы сможете вывести таблицу из базы в ListBox в виде аккуратных карточек.
Чем привязка в WPF отличается от Windows Forms и ASP.NET
В ASP.NET разработчик пишет GridView1.DataSource = table и обязательно вызывает DataBind. В Windows Forms достаточно присвоить DataSource, а выводимое поле указать через DisplayMember. В WPF оба приёма не работают, и первое знакомство с привязкой данных часто заканчивается пустым списком без единой ошибки.
Причина в том, что WPF строит привязку декларативно. Код только кладёт объект с данными в свойство DataContext, а XAML описывает, какие поля этого объекта и как показывать. Вызывать DataBind не нужно: как только DataContext получает значение, все привязки внутри элемента обновляются сами.
Важно и то, что DataContext наследуется по дереву элементов. Если назначить таблицу окну, её увидят все вложенные элементы, у которых собственный контекст не задан. Поэтому DataContext удобно назначать один раз на уровне окна или панели.
Три участника: DataContext, ItemsSource и DataTemplate
Чтобы ListBox показал строки таблицы, нужно связать три вещи. Каждая отвечает за свою часть, и если одной нет, список останется пустым или покажет непонятный текст.
- DataContext: объект-источник, в нашем случае DataTable. Назначается из кода;
- ItemsSource=«{Binding}»: говорит списку взять коллекцию прямо из DataContext. Пустая привязка без пути означает «сам контекст целиком»;
- ItemTemplate: шаблон DataTemplate, который описывает, как выглядит одна строка. Без него ListBox вызовет ToString у каждой строки и выведет «System.Data.DataRowView».
Шаблон элемента в ресурсах окна
DataTemplate можно описать прямо внутри ListBox, но удобнее вынести его в Window.Resources с ключом x:Key. Разметка самого списка остаётся короткой, а шаблон легко переиспользовать в другом списке или окне.
У DataTemplate может быть только один корневой элемент, поэтому несколько TextBlock группируем панелью. Grid с колонками даёт ровное выравнивание: название компании слева, должность контакта справа, город мелким шрифтом ниже.
<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="550">
<Window.Resources>
<!-- Как выглядит одна строка таблицы Customers -->
<DataTemplate x:Key="CustomerTemplate">
<Border BorderBrush="Silver" BorderThickness="1" CornerRadius="5"
Padding="6" Margin="2">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition /><RowDefinition />
</Grid.RowDefinitions>
<TextBlock Text="{Binding CompanyName}" FontSize="15"
FontWeight="Bold" Foreground="#515151" />
<TextBlock Grid.Column="1" Text="{Binding ContactTitle}"
Foreground="SteelBlue" />
<TextBlock Grid.Row="1" Text="{Binding City}" Foreground="Gray" />
</Grid>
</Border>
</DataTemplate>
</Window.Resources>
<DockPanel>
<Button x:Name="LoadButton" DockPanel.Dock="Bottom" Content="Загрузить"
HorizontalAlignment="Left" Margin="4" Padding="10,2" />
<ListBox x:Name="CustomersList" ItemsSource="{Binding}"
ItemTemplate="{StaticResource CustomerTemplate}"
HorizontalContentAlignment="Stretch" />
</DockPanel>
</Window>Загрузка DataTable из SQL Server
Код загрузки обычный для ADO.NET. SqlDataAdapter сам открывает и закрывает соединение в методе Fill, а блок Using гарантирует освобождение ресурсов даже при исключении. Строку подключения лучше держать в app.config, а не в коде: так её можно поменять без перекомпиляции.
Обратите внимание на последнюю строку обработчика. Мы не трогаем ListBox напрямую и не вызываем никаких методов привязки, а лишь назначаем DataContext.
Imports System.Data
Imports System.Data.SqlClient
Imports System.Configuration
Class Window1
Private Sub LoadButton_Click(sender As Object, e As RoutedEventArgs) _
Handles LoadButton.Click
Try
' Вся привязка: таблица становится контекстом списка
CustomersList.DataContext = GetCustomers()
Catch ex As SqlException
MessageBox.Show("Не удалось загрузить данные: " & ex.Message)
End Try
End Sub
Private Function GetCustomers() As DataTable
Dim cs = ConfigurationManager.ConnectionStrings("Northwind").ConnectionString
Dim sql = "SELECT CustomerID, CompanyName, ContactTitle, City " &
"FROM Customers ORDER BY CompanyName"
Dim table As New DataTable("Customers")
Using cn As New SqlConnection(cs)
Using da As New SqlDataAdapter(sql, cn)
da.Fill(table) ' Fill сам откроет и закроет соединение
End Using
End Using
Return table
End Function
End ClassВыбранная строка и реакция на выбор
Когда пользователь щёлкает по карточке, свойство SelectedItem возвращает тот самый DataRowView. Из него можно взять любую колонку по имени, даже ту, что не показана в шаблоне, например ключ CustomerID для открытия карточки клиента.
Есть и чисто декларативный вариант: свойство IsSynchronizedWithCurrentItem=True синхронизирует выбор с текущей позицией представления коллекции. Тогда TextBlock рядом со списком с привязкой {Binding CompanyName} сам покажет данные выбранной строки, без единой строчки кода.
Private Sub CustomersList_SelectionChanged(sender As Object,
e As SelectionChangedEventArgs) Handles CustomersList.SelectionChanged
Dim row = TryCast(CustomersList.SelectedItem, DataRowView)
If row Is Nothing Then Return ' выбор снят
Dim id = CStr(row("CustomerID"))
Title = "Клиент: " & CStr(row("CompanyName")) & " (" & id & ")"
End SubТипичные ошибки и как их найти
Привязки WPF не бросают исключений при опечатке в имени поля. Вместо этого в окне Output отладчика появляется строка с текстом «System.Windows.Data Error: 40 BindingExpression path error». Если список пуст или поле не выводится, первым делом загляните туда.
- в списке видно «System.Data.DataRowView»: не задан ItemTemplate или ключ шаблона указан с ошибкой;
- список пуст, хотя таблица заполнена: DataContext назначен не тому элементу или ItemsSource не указан;
- попытка присвоить ItemsSource = table из кода не компилируется: DataTable не реализует IEnumerable, используйте table.DefaultView;
- на тысячах строк список тормозит: виртуализация отключается, если ListBox положить в ScrollViewer или StackPanel без ограничения высоты.
Как это делают сегодня
Вопросы и ответы
Нужно ли вызывать DataBind после назначения DataContext?
Нет. В WPF такого метода нет: привязки обновляются автоматически, как только DataContext получает новое значение.
Можно ли привязать ListBox сразу к DataSet?
Можно, но тогда в ItemsSource нужно указать путь к таблице, например {Binding Customers}. Проще назначить DataContext конкретной DataTable.
Как отобразить в списке вычисляемое значение?
Добавьте в DataTable колонку с Expression, например объединение города и страны, или используйте конвертер IValueConverter в привязке.