WPF ListBox и DataTable: привязка данных и DataTemplate

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

В WPF нет привычных DataSource и DataBind. Вместо них работают DataContext, ItemsSource и шаблон элемента, и, разобравшись с ними один раз, вы сможете вывести таблицу из базы в ListBox в виде аккуратных карточек.

Главное. В WPF данные попадают в ListBox через DataContext и ItemsSource={Binding}, а внешний вид строки задаёт DataTemplate. Метод DataBind не нужен, а ошибки привязки ищите в окне Output.

Чем привязка в 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».
Строки таблицы попадают в список как объекты DataRowView, а привязка {Binding CompanyName} обращается к колонке по имени. Имена чувствительны к написанию так же, как в SQL-запросе.

Шаблон элемента в ресурсах окна

DataTemplate можно описать прямо внутри ListBox, но удобнее вынести его в Window.Resources с ключом x:Key. Разметка самого списка остаётся короткой, а шаблон легко переиспользовать в другом списке или окне.

У DataTemplate может быть только один корневой элемент, поэтому несколько TextBlock группируем панелью. Grid с колонками даёт ровное выравнивание: название компании слева, должность контакта справа, город мелким шрифтом ниже.

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="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>
HorizontalContentAlignment=Stretch растягивает шаблон на всю ширину списка. Без него карточки сжимаются по содержимому и колонки Grid не выравниваются.

Загрузка DataTable из SQL Server

Код загрузки обычный для ADO.NET. SqlDataAdapter сам открывает и закрывает соединение в методе Fill, а блок Using гарантирует освобождение ресурсов даже при исключении. Строку подключения лучше держать в app.config, а не в коде: так её можно поменять без перекомпиляции.

Обратите внимание на последнюю строку обработчика. Мы не трогаем ListBox напрямую и не вызываем никаких методов привязки, а лишь назначаем DataContext.

Window1.xaml.vbVB.NET
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} сам покажет данные выбранной строки, без единой строчки кода.

Обработчик выбораVB.NET
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 без ограничения высоты.

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

Сегодня вместо DataTable в WPF обычно привязывают ObservableCollection типизированных объектов, загруженных через Entity Framework Core или Dapper, а код окна выносят в модель представления по шаблону MVVM. Пакет CommunityToolkit.Mvvm генерирует свойства с уведомлением и команды атрибутами. Для проверки привязок в .NET 8 удобно окно XAML Binding Failures в Visual Studio.

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

Нужно ли вызывать DataBind после назначения DataContext?

Нет. В WPF такого метода нет: привязки обновляются автоматически, как только DataContext получает новое значение.

Можно ли привязать ListBox сразу к DataSet?

Можно, но тогда в ItemsSource нужно указать путь к таблице, например {Binding Customers}. Проще назначить DataContext конкретной DataTable.

Как отобразить в списке вычисляемое значение?

Добавьте в DataTable колонку с Expression, например объединение города и страны, или используйте конвертер IValueConverter в привязке.

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