Привязка WPF TreeView к реляционному DataSet через HierarchicalDataTemplate

РазделWPFУровеньСредний Чтение7 минОпубликовано ТегиWPFTreeViewDataSetXAML

В WPF дереву не нужен XML-посредник: HierarchicalDataTemplate умеет ходить по DataRelation напрямую. Достаточно правильно назвать связь и сослаться на неё в привязке.

Главное. В WPF связанный DataSet привязывается к TreeView без XML: DataRowView публикует DataRelation как свойство, и HierarchicalDataTemplate берёт детей прямо из неё по имени связи.

Чем WPF отличается от ASP.NET

В ASP.NET, чтобы показать в TreeView категории и вложенные в них товары, приходится превращать DataSet в XML и подсовывать его через XmlDataSource. В WPF всё устроено иначе. TreeView здесь не знает ничего об источниках данных: он берёт коллекцию через ItemsSource, а как раскрывать каждый элемент, решает шаблон HierarchicalDataTemplate.

Шаблон говорит дереву две вещи: как нарисовать сам узел и откуда взять его детей. Для объектов дети обычно лежат в свойстве-коллекции. Для строк DataSet роль такого свойства играет DataRelation, и это главный трюк статьи.

Как DataRowView отдаёт дочерние строки

Когда DataTable привязывается к контролу, WPF работает не с DataRow, а с DataRowView. Этот класс реализует ICustomTypeDescriptor и публикует для привязки не только колонки, но и все связи, где таблица выступает родителем. Имя связи становится именем «свойства», а его значение это DataView с дочерними строками.

Значит, если в DataSet есть связь CategoryProducts от Categories к Products, выражение {Binding CategoryProducts} внутри шаблона категории вернёт товары этой категории. Никакого кода для обхода связей писать не нужно.

Имя связи используется как имя свойства в привязке, поэтому давайте связям имена без пробелов и точек: CategoryProducts, OrderLines. Иначе путь привязки не разберётся.

Готовим данные

Загрузка ничем не отличается от обычной: две выборки в один DataSet и связь между ними. Свойство Nested здесь не требуется, оно влияет только на запись XML. Подключение лучше открывать через Using, а строку соединения брать из конфигурации, а не из кода.

В качестве ItemsSource дерева отдаём DefaultView родительской таблицы. Можно заранее задать ему Sort, тогда категории будут идти по алфавиту.

MainWindow.xaml.vbVB.NET
Imports System.Configuration
Imports System.Data
Imports System.Data.SqlClient

Class MainWindow
    Private Sub Window_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
        Dim cs = ConfigurationManager.ConnectionStrings("Northwind").ConnectionString
        Dim ds As New DataSet()
        Using con As New SqlConnection(cs)
            Using da As New SqlDataAdapter("SELECT CategoryID, CategoryName FROM Categories", con)
                da.Fill(ds, "Categories")
            End Using
            Using da As New SqlDataAdapter("SELECT ProductID, CategoryID, ProductName, UnitPrice FROM Products", con)
                da.Fill(ds, "Products")
            End Using
        End Using

        ' имя связи станет путём привязки в XAML
        ds.Relations.Add("CategoryProducts",
            ds.Tables("Categories").Columns("CategoryID"),
            ds.Tables("Products").Columns("CategoryID"))

        Dim view = ds.Tables("Categories").DefaultView
        view.Sort = "CategoryName"
        trvCatalog.ItemsSource = view
    End Sub
End Class

Шаблоны в XAML

Для категорий используется HierarchicalDataTemplate: его ItemsSource указывает на связь, а содержимое описывает, как выглядит узел категории. Для товаров детей нет, поэтому хватает обычного DataTemplate, который назначается через свойство ItemTemplate родительского шаблона.

Привязки к колонкам пишутся просто по имени: {Binding CategoryName}. Для чисел и дат используйте StringFormat, чтобы не форматировать значения в коде.

MainWindow.xamlXML
<TreeView x:Name="trvCatalog">
  <TreeView.ItemTemplate>
    <!-- уровень 1: категория, дети берутся из связи -->
    <HierarchicalDataTemplate ItemsSource="{Binding CategoryProducts}">
      <TextBlock Text="{Binding CategoryName}" FontWeight="SemiBold" />

      <!-- уровень 2: товар -->
      <HierarchicalDataTemplate.ItemTemplate>
        <DataTemplate>
          <StackPanel Orientation="Horizontal">
            <TextBlock Text="{Binding ProductName}" />
            <TextBlock Text="{Binding UnitPrice, StringFormat=' ({0:C})'}"
                       Foreground="Gray" />
          </StackPanel>
        </DataTemplate>
      </HierarchicalDataTemplate.ItemTemplate>
    </HierarchicalDataTemplate>
  </TreeView.ItemTemplate>
</TreeView>

Выбранный узел и третий уровень

У TreeView свойство SelectedItem только для чтения и не поддерживает двустороннюю привязку, поэтому выбранный элемент проще ловить в событии SelectedItemChanged. В e.NewValue придёт DataRowView: по таблице его строки легко понять, что выбрано, категория или товар.

Третий уровень добавляется тем же способом: вторая связь, например ProductOrders от Products к Order Details, и вместо DataTemplate для товара HierarchicalDataTemplate с ItemsSource на неё. Глубина ограничена только объёмом данных: DataSet загружает всё сразу, поэтому для больших каталогов подумайте о ленивой подгрузке.

MainWindow.xaml.vbVB.NET
Private Sub trvCatalog_SelectedItemChanged(sender As Object,
        e As RoutedPropertyChangedEventArgs(Of Object)) _
        Handles trvCatalog.SelectedItemChanged

    Dim row = TryCast(e.NewValue, DataRowView)
    If row Is Nothing Then Return

    Select Case row.Row.Table.TableName
        Case "Categories"
            txtInfo.Text = "Категория: " & row("CategoryName").ToString()
        Case "Products"
            txtInfo.Text = String.Format("Товар #{0}: {1}", row("ProductID"), row("ProductName"))
    End Select
End Sub

Частые проблемы

Если дерево показывает только первый уровень или вовсе пустое, причина почти всегда в одном из пунктов ниже. Окно Output в Visual Studio при отладке выводит ошибки привязки с текстом BindingExpression path error: начинайте поиск с него.

  • в ItemsSource шаблона опечатка в имени связи или в имени есть пробел;
  • дереву отдали DataTable.Rows вместо DefaultView: DataRow не публикует связи для привязки;
  • связь создана в обратную сторону, от Products к Categories, и у категорий нет детей;
  • данные добавляются в таблицы после установки ItemsSource: DataView обновится сам, но связь должна существовать заранее;
  • внутренний шаблон назначен через Resources без ключа и перехватывает и категории, и товары.

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

В новых WPF-приложениях DataSet обычно заменяют моделью: классы с ObservableCollection детей, загруженные через EF Core или Dapper, и MVVM-библиотека CommunityToolkit.Mvvm для свойств и команд. HierarchicalDataTemplate остаётся тем же, только ItemsSource указывает на обычное свойство-коллекцию. Ленивую подгрузку детей делают по событию Expanded, заполняя коллекцию асинхронно.

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

Нужно ли включать Nested у DataRelation?

Нет. Nested влияет только на WriteXml и ReadXml. Для привязки в WPF достаточно самой связи с понятным именем.

Можно ли так привязать самоссылающуюся таблицу?

Да. Создайте связь внутри одной таблицы от ID к ParentID, отдайте дереву DataView с фильтром ParentID IS NULL, а в HierarchicalDataTemplate сошлитесь на эту связь. Шаблон будет применяться рекурсивно на любую глубину.

Как раскрыть все узлы при загрузке?

Задайте стиль для TreeViewItem с сеттером IsExpanded = True. Для больших деревьев так лучше не делать: раскрытие создаёт визуальные элементы для каждого узла.

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