Pull to refresh

Отображение иерархической структуры данных в WPF с помощью привязки и шаблонов

Reading time17 min
Views24K
Введение

Представление набора данных в виде иерархической структуры (любого уровня вложенности) в WPF осуществляется очень просто. Как правило, для этого используется класс System.Windows.Controls.TreeView и выглядит результат как-то так:


Я продемонстрирую два случая построения такого дерева, отличающихся друг от друга источником данных:
  • База данных, размещённая на MS SQL Server 2008
  • XML-файл.


В приложении будет реализована возможность динамической замены одного источника данных другим.
Прежде всего необходимо создать источники данных, из которых следует получать информацию. Такими источниками будут:
  1. База данных MyTestDb.mdf
  2. Xml-файл XMLFile1.xml

Будет создано единственное решение (Solution), состоящее из двух проектов (Projects):
  1. Linq2SqlProject — библиотека, содержащая в себе набор классов, являющихся объектным проецированием реляционной структуры базы данных.
  2. WpfGuiProject — графическая часть приложения (GUI), выполненная с применением технологии WPF. В этом же проекте будет размещён код, касающийся работы с XML-данными.

Взаимодействие со всеми рассматриваемыми в теме источниками данных будет реализовано с использованием технологии LINQ.

1. Создание базы данных


В нашем примере база данных будет состоять из двух таблиц — этого достаточно для демонстрации решения озвученной задачи:



В таблицу MySchema.Categories я добавил следующие записи:



В таблицу MySchema.Books внесены такие данные:



Дабы не усложнять пример, я не стану создавать в базе данных набор хранимых процедур, с помощью которых клиентское приложение должно было бы работать с БД (хотя в реальной работе, конечно же следует вести диалог с базой данных только через хранимые процедуры, дабы избежать возможности выполнения sql-инъекций).
Скачиваем выше указанную базу данных отсюда и подключаем её к нашей СУБД.

2. Создаём объектно-ориентированную проекцию базы данных



1. Создадим новое пустое решение (Solution), присвоив ему имя «TreeStructureBrowse»:



2. Из контекстного меню, полученного щелчком мышью по имени решения в Solution Explorer, выбираем пункт Add => New Project…
3. Выбираем тип проекта «Class Library» и назначаем нашему проекту имя «Linq2Sql», после чего жмём клавишу OK.
4. Из контекстного меню, полученного щелчком мышью по имени проекта, добавленного нами в п.3, выбираем пункт Add => New Item…
5. Выбираем элемент «LINQ to SQL Classes» и назначаем файлу имя «MyTestDb»:



6. Из меню «View», выбираем пункт «Server Explorer». В появившемся окне нажимаем кнопку «Connect to Database»:



7. Указываем все необходимые параметры строки подключения (в моём случае используется аутентификация Windows):



8. Жмём кнопку «Test Connection», дабы убедиться, что подключиться нам удастся, после чего нажатием кнопки ОК закрываем окно «Add Connection».
9. Теперь в окне «Server Explorer» мы увидим такую картину:



p.s. powercomp — это имя моего компьютера

10. Удерживая нажатой клавишу Shift, выбираем обе наши таблицы (Books и Categories) и перетаскиваем их мышью на вкладку MyTestDb.dbml*, в результате чего получим следующую картину:



11. Сохраняем наш проект и выполняем команду Build => Rebuild Solution.

Т.о. мы только что создали объектно ориентированную модель нашей реляционной базы, что является первым ключевым моментом (из трёх) в решении нашей задачи.

3. Создание графической части (GUI), написание конвертеров и формирование шаблонов


1. В созданное нами в предыдущем разделе решение (Solution), добавим новый проект, задав ему имя WpfGuiProject:



2. Из контекстного меню, вызванного нажатием правой кнопки мыши на имени только что добавленного в наше решение проекта, выбираем пункт «Set as StartUp Project», сделав тем самым GUI проект запускаемым по умолчанию.
3. Добавляем в проект «WpfGuiProject» ссылку на проект «Linq2Sql»:



4. Добавляем в наш проект словарь ресурсов: из контекстного меню, вызванного нажатием правой кнопки мыши по имени нашего проекта, активируем пункт Add => Resource Dictionary… и в появившемся диалоговом окне выбираем одноимённый элемент:



5. Через контекстное меню проекта добавляем в него новый файл типа «Class», назначив ему имя «MyConverters.cs».
6. Подключаем к нашему проекту сборку «System.Data.Linq».
7. В созданном в п.5 файле пишем такой код:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Windows.Data;
  6. using Linq2Sql;
  7. using System.Xml.Linq;
  8.  
  9. namespace WpfGuiProject
  10. {
  11.   //Конвертер для класса Category. Задача этого конвертера - извлечение вложенных элементов типа Category.
  12.   public sealed class CategoryValueConverter : IValueConverter
  13.   {
  14.  
  15.     public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
  16.     {
  17.       if (value is Category)
  18.       {
  19.         Category x = (Category) value;
  20.         return (x).Categories.Where(n => n.ParrentCategoryId == x.CategoryId).OrderBy(n => n.CategoryName);
  21.       }
  22.       else
  23.       {
  24.         return null;
  25.       }
  26.     }
  27.     public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
  28.     {
  29.       throw new NotImplementedException();
  30.     }
  31.   }
  32.  
  33.   //Конвертер для класса XElement. Задача этого конвертера - извлечение вложенных элементов типа XElement, инкапсулирующих
  34.   //в себе разделы Category.
  35.   public sealed class XmlConverter :IValueConverter
  36.   {
  37.  
  38.     public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
  39.     {
  40.       if (value is XElement)
  41.       {
  42.         XElement x = (XElement)value;
  43.         return x.Elements("Category");
  44.       }
  45.       else
  46.       {
  47.         return null;
  48.       }
  49.     }
  50.     public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
  51.     {
  52.       throw new NotImplementedException();
  53.     }
  54.   }
  55. }
* This source code was highlighted with Source Code Highlighter.

Созданные в п.7 являются вторым (из трёх) ключевым моментом для решения стоящей перед нами задачи — иерархическое визуальное представление информации, реализованное при помощи привязки к данным. В коде мы определяем логику получения нужных нам дочерних элементов, которые должны участвовать в построении иерархической модели.
8. Редактируем файл «Dictionary1.xaml»:

  1. <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  2.           xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  3.           xmlns:linq2Xml="clr-namespace:System.Xml.Linq;assembly=System.Xml.Linq"
  4.           xmlns:linq="clr-namespace:Linq2Sql;assembly=Linq2Sql"
  5.           xmlns:local="clr-namespace:WpfGuiProject">
  6.   <!--Шаблон иерархического отображения экземпляров класса TreeTable В СЛУЧАЕ ПОДКЛЮЧЕНИЯ К БАЗЕ ДАННЫХ-->
  7.   <HierarchicalDataTemplate x:Key="key1" DataType="{x:Type linq:Category}">
  8.     <!--Указываю источник данных, на основании которого должно формироваться дерево разделов-->
  9.     <HierarchicalDataTemplate.ItemsSource>
  10.       <Binding Path=".">
  11.         <!--Указываю конвертер, который позволяет получить список дочерних элементов типа Category, по отношению к данному-->
  12.         <Binding.Converter>
  13.           <local:CategoryValueConverter/>
  14.         </Binding.Converter>
  15.       </Binding>
  16.     </HierarchicalDataTemplate.ItemsSource>
  17.     <!--Формирую визуальное представление элемента, отображаемого в дереве разделов-->
  18.     <DockPanel>
  19.       <TextBlock Text="{Binding Path=CategoryName}">
  20.         <TextBlock.ToolTip>
  21.           <Binding Path="Description"/>
  22.         </TextBlock.ToolTip>
  23.       </TextBlock>
  24.       <!--Количество книг, размещённых непосредственно в разделе-->
  25.       <TextBlock Name="txtLeft" Text=" (" Grid.Column="1"/>
  26.       <TextBlock Name="txtCount" Text="{Binding Path=Books.Count}" Grid.Column="2"/>
  27.       <TextBlock Name="txtRight" Text=")" Grid.Column="3"/>
  28.     </DockPanel>
  29.   </HierarchicalDataTemplate>
  30.   <!--Шаблон отображения экземпляров класса Book В СЛУЧАЕ ПОДКЛЮЧЕНИЯ К БАЗЕ ДАННЫХ-->
  31.   <DataTemplate DataType="{x:Type linq:Book}">   
  32.     <TextBlock Text="{Binding Path=BookName}">
  33.       <TextBlock.ToolTip>
  34.         <Binding Path="ToolTipText"/>
  35.       </TextBlock.ToolTip>
  36.     </TextBlock>
  37.   </DataTemplate>
  38.  
  39.   <!--Шаблон иерархического отображения данных В СЛУЧАЕ ПОДКЛЮЧЕНИЯ К XML-ФАЙЛУ -->
  40.   <HierarchicalDataTemplate x:Key="key2" DataType="{x:Type linq2Xml:XElement}">
  41.     <!--Указываю источник данных, на основании которого должно формироваться дерево разделов-->
  42.     <HierarchicalDataTemplate.ItemsSource>
  43.       <Binding Path=".">
  44.         <!--Указываю конвертер, который позволяет получить список дочерних элементов типа Category, по отношению к данному-->
  45.         <Binding.Converter>
  46.           <local:XmlConverter/>
  47.         </Binding.Converter>
  48.       </Binding>
  49.     </HierarchicalDataTemplate.ItemsSource>
  50.     <!--Формирую визуальное представление элемента, отображаемого в дереве разделов-->
  51.     <TextBlock Text="{Binding Path=Attribute[CategoryName].Value}">
  52.         <TextBlock.ToolTip>
  53.           <Binding Path="Attribute[ToolTipText].Value"/>
  54.         </TextBlock.ToolTip>
  55.     </TextBlock>
  56.   </HierarchicalDataTemplate>
  57.  
  58.   <!--Шаблон отображения экземпляров класса Book В СЛУЧАЕ ПОДКЛЮЧЕНИЯ К XML-файлу -->
  59.   <DataTemplate x:Key="key3" DataType="{x:Type linq2Xml:XElement}">
  60.     <Grid>
  61.       <Grid.RowDefinitions>
  62.         <RowDefinition />
  63.         <RowDefinition Height="Auto" />
  64.       </Grid.RowDefinitions>
  65.       <TextBlock Text="{Binding Path=Attribute[BookName].Value}">
  66.         <TextBlock.ToolTip>
  67.           <Binding Path="Attribute[ToolTipText].Value"/>
  68.         </TextBlock.ToolTip>
  69.       </TextBlock>
  70.     </Grid>
  71.   </DataTemplate>
  72. </ResourceDictionary>
* This source code was highlighted with Source Code Highlighter.


Xml-разметка, созданная в п.8 является третьим ключевым моментом. В данной разметке с помощью класса HierarchicalDataTemplate определены — шаблон отображения данных и конвертер, который следует использовать, для получения дочерних элементов по отношению к текущему.

Теперь начинаем формировать графическое представление нашего окна.

9. Редактируем содержимое файла «MainWindow.xaml»:

  1. <Window x:Class="WpfGuiProject.MainWindow"
  2.     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.     xmlns:linq="clr-namespace:Linq2Sql;assembly=Linq2Sql"
  5.     xmlns:local="clr-namespace:WpfGuiProject"
  6.     Title="Иерархическое отображение данных с помощью привязки" Height="350" Width="525">
  7.   <!--Подключаем файл ресурсов-->
  8.   <Window.Resources>
  9.     <ResourceDictionary>
  10.       <ResourceDictionary.MergedDictionaries>
  11.         <ResourceDictionary Source="Dictionary1.xaml"/>
  12.       </ResourceDictionary.MergedDictionaries>
  13.     </ResourceDictionary>
  14.   </Window.Resources>
  15.   <!--Формируем визуальное содержимое окна-->
  16.   <Grid>
  17.     <Grid.ColumnDefinitions>
  18.       <ColumnDefinition Width="50*" />
  19.       <ColumnDefinition Width="Auto" />
  20.       <ColumnDefinition Width="50*" />
  21.     </Grid.ColumnDefinitions>
  22.     <Grid.RowDefinitions>
  23.       <RowDefinition Height="Auto" />
  24.       <RowDefinition Height="100*" />
  25.       <RowDefinition Height="Auto" />
  26.     </Grid.RowDefinitions>
  27.     <!--Отображаем древовидную структуру-->
  28.     <TreeView Name="treeStructure" Margin="2" Grid.Row="1"/>
  29.     <!--Отображаем примечания для выбранного в иерархии элемента-->
  30.     <GroupBox Header="Примечание" Margin="2" Grid.Row="2">
  31.       <TextBlock Name="selectedNodeDescription" TextWrapping="Wrap" Text="{Binding ElementName=treeStructure, Path=SelectedItem.Description}" />
  32.     </GroupBox>
  33.     <GridSplitter Width="5" HorizontalAlignment="Center" VerticalAlignment="Stretch" Grid.Column="1" Grid.RowSpan="3"/>
  34.     <ListBox Name="listBooks" Margin="2" Grid.Column="2" Grid.RowSpan="2" ItemsSource="{Binding ElementName=treeStructure, Path=SelectedItem.Books}"/>
  35.     <GroupBox Header="Примечание" Margin="2" Grid.Column="2" Grid.Row="2">
  36.       <TextBlock Name="selectedBookDescription" TextWrapping="Wrap" Text="{Binding ElementName=listBooks, Path=SelectedItem.Description}" />
  37.     </GroupBox>
  38.     <!--Предоставляем пользователю выбрать интересующий его источник данных. Данный блок обязательно должен размещаться ПОСЛЕ XAML-разметки, в которой определён
  39.     элемент treeStructure. Это требование обусловлено тем, что XAML-разметка элементов RadioButton содержит в себе регистрацию
  40.     события Checked, в теле которого, в свою очередь присутствует код, использующий элемент treeStructure, а по правилам XAML-документов, нельзя использовать элемент
  41.     раньше, чем он будет определён-->
  42.     <GroupBox Header="Источник данных" Margin="2">
  43.       <StackPanel Margin="2">
  44.         <RadioButton Name="rbDatabase" Content="База данных MS SQL Server" IsChecked="True" Checked="Change_DataSource"/>
  45.         <RadioButton Name="rbXmlFile" Content="XML-файл" IsChecked="False" Checked="Change_DataSource"/>
  46.       </StackPanel>
  47.     </GroupBox>
  48.   </Grid>
  49. </Window>
* This source code was highlighted with Source Code Highlighter.


Данная разметка сформирует такое окно:



10. Добавляем в наш проект файл «XMLFile1.xml», который будет являться альтернативным источником данных:



Наполняем созданный нами файл данными, прописанными в xml-формате:

  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <MyXmlDocument>
  3.  <Category CategoryName="Компьютерная литература" Description ="Различная литература по компьютерной тематике" ToolTipText="Мой раздел">
  4.   <Category CategoryName="Windows-приложения" Description ="Данная ОС наиболее популярна" ToolTipText="Моя ось">
  5.    <Category CategoryName="MS Office 2007" Description ="Офисный пакет программ" ToolTipText="Мой офис"/>
  6.    <Category CategoryName="Продукция компании Autodesk" Description ="Различные САПР" ToolTipText="Мой выбор">
  7.     <Category CategoryName="AutoCAD" Description ="Самый распространённый САПР" ToolTipText="Мой САПР">
  8.      <Book BookName ="AutoCAD 2010. Полещук Н.Н." Description ="Моя книжка по данному САПР" ToolTipText="Справочник"/>
  9.      <Book BookName ="AutoCAD 2007, библия пользователя. (автора не помню)" Description ="Ещё одна моя книжка по данному САПР" ToolTipText="Старая книга"/>
  10.     </Category>
  11.     <Category CategoryName="Revit" Description ="новое поколение САПР (BIM)" ToolTipText="Пока ещё не мой САПР"/>    
  12.    </Category>
  13.   </Category>
  14.   <Category CategoryName="Linux-приложения" Description ="Свободно распространяемая ОС" ToolTipText="Интересно, но не сильно распространено"/>
  15.  </Category>
  16.  <Category CategoryName="Классика" Description ="Классическая литература" ToolTipText="Полезно для общего развития">
  17.   <Category CategoryName="Повести и рассказы" Description ="Стихи отечественных авторов" ToolTipText="Для души">
  18.    <Book BookName ="Капитанская дочка. А.С. Пушкин" Description ="Школьный курс" ToolTipText="Читал когда-то..."/>
  19.   </Category>
  20.   <Category CategoryName="Поэзия" Description ="Повести и рассказы отечественных авторов" ToolTipText="Это тоже для души"/>
  21.  </Category> 
  22. </MyXmlDocument>
* This source code was highlighted with Source Code Highlighter.


11. Вносим изменения в файл «MainWindow.xaml.cs»:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Windows;
  6. using System.Windows.Controls;
  7. using System.Windows.Data;
  8. using System.Windows.Documents;
  9. using System.Windows.Input;
  10. using System.Windows.Media;
  11. using System.Windows.Media.Imaging;
  12. using System.Windows.Navigation;
  13. using System.Windows.Shapes;
  14.  
  15. //Добавляем ссылки на нужные нам пространства имён
  16. using Linq2Sql;
  17. using System.Xml.Linq;
  18.  
  19. namespace WpfGuiProject
  20. {
  21.   /// <summary>
  22.   /// Interaction logic for MainWindow.xaml
  23.   /// </summary>
  24.   public partial class MainWindow : Window
  25.   {
  26.     XElement xml;
  27.     public MainWindow()
  28.     {
  29.       InitializeComponent();
  30.       xml = XElement.Load(@"..\..\XMLFile1.xml");
  31.     }
  32.  
  33.     //В этом методе заключается вся логика по указанию источника данных, которые следует отображать в окне
  34.     private void Change_DataSource(object sender, RoutedEventArgs e)
  35.     {
  36.       if (rbDatabase.IsChecked == true) //В качестве источника данных выбрана база данных
  37.       {
  38.         listBooks.ItemTemplate = null;
  39.         treeStructure.ItemsSource = new MyTestDbDataContext().Categories.Where(n => n.ParrentCategoryId == null);
  40.         treeStructure.ItemTemplate = (HierarchicalDataTemplate)FindResource("key1");
  41.         //Настраиваем привязку примечаний
  42.         DescriptionBinding(selectedNodeDescription, "SelectedItem.Description", treeStructure);
  43.         DescriptionBinding(selectedBookDescription, "SelectedItem.Description", listBooks);
  44.         //Настраиваем привязку отображения книг
  45.         Binding bind = new Binding("SelectedItem.Books") {Source = treeStructure };
  46.         listBooks.SetBinding(ItemsControl.ItemsSourceProperty, bind);
  47.       }
  48.       else //В качестве источника данных выбран xml-файл
  49.       {
  50.         treeStructure.ItemsSource = xml.Elements("Category");
  51.         treeStructure.ItemTemplate = (HierarchicalDataTemplate)FindResource("key2");
  52.         //Настраиваем привязку примечаний
  53.         DescriptionBinding(selectedNodeDescription, "SelectedItem.Attribute[Description].Value", treeStructure);
  54.         DescriptionBinding(selectedBookDescription, "SelectedItem.Attribute[Description].Value", listBooks);
  55.         //Настраиваем привязку отображения книг
  56.         listBooks.ItemTemplate = (DataTemplate)FindResource("key3");
  57.         Binding bind = new Binding("SelectedItem.Elements[Book]") { Source = treeStructure };
  58.         listBooks.SetBinding(ItemsControl.ItemsSourceProperty, bind);
  59.  
  60.       }
  61.     }
  62.  
  63.     /// <summary>
  64.     /// Настройка привязки отображения данных
  65.     /// </summary>
  66.     /// <param name="textBlock">Текстовый объект, который должен отображать текст примечания</param>
  67.     /// <param name="pathValue">значение Path привязки</param>
  68.     /// <param name="source">ссылка на объект-источник, из которого считываются данные через свойство Path</param>
  69.     void DescriptionBinding(TextBlock textBlock, string pathValue, Control source)
  70.     {
  71.       textBlock.SetBinding(TextBlock.TextProperty, new Binding(pathValue) { Source = source });
  72.     }
  73.   }
  74. }
* This source code was highlighted with Source Code Highlighter.


Теперь можно запускать наше приложение на исполнение и наслаждаться результатами работы… Итак жмём клавишу F5 и смотрим, что мы имеем…

А. Источником является база данных:



Б. Источником является Xml-файл:



Исходный код решения (Solution) в формате MS Visual Studio 2010 можно скачать здесь.
Tags:
Hubs:
Total votes 41: ↑24 and ↓17+7
Comments17

Articles