
Привязка WPF TreeView к реляционному DataSet через HierarchicalDataTemplate
В WPF дереву не нужен XML-посредник: HierarchicalDataTemplate умеет ходить по DataRelation напрямую. Достаточно правильно назвать связь и сослаться на неё в привязке.
Чем 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} внутри шаблона категории вернёт товары этой категории. Никакого кода для обхода связей писать не нужно.
Готовим данные
Загрузка ничем не отличается от обычной: две выборки в один DataSet и связь между ними. Свойство Nested здесь не требуется, оно влияет только на запись XML. Подключение лучше открывать через Using, а строку соединения брать из конфигурации, а не из кода.
В качестве ItemsSource дерева отдаём DefaultView родительской таблицы. Можно заранее задать ему Sort, тогда категории будут идти по алфавиту.
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, чтобы не форматировать значения в коде.
<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 загружает всё сразу, поэтому для больших каталогов подумайте о ленивой подгрузке.
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 без ключа и перехватывает и категории, и товары.
Как это делают сегодня
Вопросы и ответы
Нужно ли включать Nested у DataRelation?
Нет. Nested влияет только на WriteXml и ReadXml. Для привязки в WPF достаточно самой связи с понятным именем.
Можно ли так привязать самоссылающуюся таблицу?
Да. Создайте связь внутри одной таблицы от ID к ParentID, отдайте дереву DataView с фильтром ParentID IS NULL, а в HierarchicalDataTemplate сошлитесь на эту связь. Шаблон будет применяться рекурсивно на любую глубину.
Как раскрыть все узлы при загрузке?
Задайте стиль для TreeViewItem с сеттером IsExpanded = True. Для больших деревьев так лучше не делать: раскрытие создаёт визуальные элементы для каждого узла.