Элемент управления TreeView в Silverlight

Элемент управления TreeView идеально подходит для отображения данных иерархической структуры. Несмотря на то, что вы можете выполнить многое при помощи данного элемента управления, в своей простейшей форме он достаточно легок в использовании, как только вы разберетесь в основной рекурсивности, требуемой им.

Мы рассмотрим два примера: сырой TreeView, а затем с использованием DataBinding вместе с небольшой специализацией. Вот что мы будем строить:

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

Простейшим способом получения информации является ее создание в памяти (хотя вашим источником может быть Xml-файл, полученный из базы данных и т.д.).

Информация иерархической структуры

Класс Data, созданный мною, объявляется следующим образом:

public class Data
{
   public string Name { get; set; }
   public Data Parent { get; set; }
   public List<Data> Children;
   public int Depth  { get { return Parent == null ? 0 : Parent.Depth +1; } }  

Я создал стандартный конструктор, хотя его функциональность минимальна, при этом я мог создать его при помощи компилятора. Метод CreateChild добавляет нового потомка к текущему объекту Data.

public Data()
{
   Name=string.Empty;
   Parent = null;
   Children = new List<Data>();
}

public Data CreateChild( string name )
{
   Data child = new Data() { Name = name, Parent = this};
   Children.Add(child);
   return child;
}

Наконец, класс Data предоставляет статический метод, который возвращает  иерархический набор данных про Соединенные Штаты,

public static Data CreateDataSample()
{
   Data root = new Data() { Name = "United States" };
   Data states = root.CreateChild( "States" );
   states.CreateChild( "Alabama" ).CreateChild( "Montgomery" );
   states.CreateChild( "Alaska" ).CreateChild( "Juneau" );
   states.CreateChild( "Arizona" ).CreateChild( "Phoenix" );
   states.CreateChild( "Arkansas" ).CreateChild( "Little Rock" );
   Data presidents = root.CreateChild( "Presidents" );
   Data W = presidents.CreateChild( "Washington" );
   Data WTerm = W.CreateChild( "Took office" );
   WTerm.CreateChild( "1789" );
   WTerm.CreateChild( "1793" );
   Data A = presidents.CreateChild( "Adams" ).CreateChild( "Took office" ).CreateChild( "1797" );
   Data J = presidents.CreateChild( "Jefferson" );
   Data JTerm = J.CreateChild( "Took office" );
   JTerm.CreateChild( "1801" );
   JTerm.CreateChild( "1805" );
   return root;
}

Обратите внимание на то, что CreateChild возвращает объект Data, и мы используем это в третьей строке, где мы создаем наследника (Child) штатов (States), чье название (Name) является Алабама (Alabama), и затем создаем наследника для данного объекта Data с названием Монтгомери (Montgomery), столица Алабамы.

В конце концов, мы возвращаем только корневой объект Data, но структура, которую мы только что создали, выглядит следующим образом:

А вот полный код Xaml страницы, которая отображает данную информацию,

<UserControl x:Class="TreeView.TreeViewPage"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:control="clr-namespace:Microsoft.Windows.Controls;
                 assembly=Microsoft.Windows.Controls"
             Width="600"
             Height="600">
  <Grid x:Name="LayoutRoot"
        Background="White">
    <control:TreeView x:Name="LeftTreeView"
                      HorizontalAlignment="Left"
                      Background="Wheat" />
    <control:TreeView x:Name="RightTreeView"
                      HorizontalAlignment="Right"
                      Background="Beige" />
  </Grid>
</UserControl>

Самое интересное, конечно же, происходит в фоновом коде.

public partial class TreeViewPage : UserControl
{
   public TreeViewPage()
   {
      InitializeComponent();
      Data root = Data.CreateDataSample();
      FillTree( LeftTreeView.Items, root );
      FillBoundTree( RightTreeView.Items, root );
   }

Рекурсия

После вызова InitializeComponent мы вызываем наш статический метод, который возвращает нам корневой узел (United States), обладающий всей структурой данных. Мы передаем два аргумента в частный (private) вспомогательный метод FillTree:

  1. Items Collection элемента управления TreeView, объявленный в Xaml
  2. Экземпляр Data , который служит в качестве корня дерева

Вспомогательный метод был создан для того, чтобы предоставить нам возможность рекурсии, поскольку процесс добавления узлов к дереву, по сути, (почти всегда) является рекурсивным (Я написал "почти всегда рекурсивным", потому что вы, конечно же, можете сделать это и без рекурсии, и даже без привязки к данным, но есть некоторые вещи, которые так и напрашиваются на использование рекурсии, и это как раз тот случай).

 private void FillTree( ItemCollection itemColl, Data dataNode )
 {
    TreeViewItem tvi = new TreeViewItem();
    itemColl.Add( tvi );
    tvi.Header = dataNode.Name;
    //….

FillTree создает экземпляр TreeViewItem (узел в TreeView) и добавляет его к набору, который был передан (в первом случае, к набору элементов TreeView).

Затем он устанавливает свойство header элементов TreeView в свойство Name полученного объекта Data. На данном этапе мы можем все описать следующим образом:

 

Стоит отметить то, что TreeViewItem не содержит копию объекта Data, а вместо этого свойство Header, принадлежащее TreeViewItem, получает свою строку из DataObject.

Метод FillTree затем итеративно проходит по каждому потомку в свойстве Children текущего объекта Data (которые вы затем сохрание как List<Data>).

    foreach ( Data childDataNode in dataNode.Children )
    {
       FillTree( tvi.Items, childDataNode );
    }
 }  // end method

Для каждого найденного наследника, оно осуществляет рекурсию, передавая набор Items нового элемента TreeView и, одиного за другим потомка текущего объекта Data,

Тем самым, при первой рекурсии метода параметрами являются набор Items, принадлежащий TreeViewItem, чей заголовок будет United States, и States - объект Data.

В таком случае название каждого объекта будет добавлено к TreeViewItem в соответствующем "уровне" дерева.

Последней строкой конструктор TreeViewPage вызывает FillBoundTree

public TreeViewPage()
{
   InitializeComponent();
   Data root = Data.CreateDataSample();
   FillTree( LeftTreeView.Items, root );
   FillBoundTree( RightTreeView.Items, root );
}

Привязка данных

Второй метод выглядит как FillTree, но работает немного по-другому.

private void FillBoundTree( ItemCollection itemColl, Data dataNode )
{
   TreeViewItem tvi = new TreeViewItem();
   itemColl.Add( tvi );
   tvi.DataContext = dataNode;
   tvi.Header = new TreeViewNode();
   foreach ( Data childDataNode in dataNode.Children )
   {
      FillBoundTree( tvi.Items, childDataNode );
   }
}

Ключевым моментом тут являются выделенные две строки. В данном случае вместо копирования текста поля Name в Header, мы устанавливаем DataContext (который оповещает о том, что мы делаем какую-то привязку данных) и мы устанавливаем Header в экземпляр TreeViewNode.

TreeViewNode является элементом управления. Вот полный Xaml-код TreeViewNode,

<UserControl x:Class="TreeView.TreeViewNode"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Width="300" >
    <Grid x:Name="LayoutRoot" Background="Bisque">
    <Border BorderBrush="Black"
            BorderThickness="4">
      <StackPanel Orientation="Horizontal">
        <TextBlock Text="{Binding Name}"
                   Margin="3" />
        <TextBlock Text="{Binding Depth}"
                   Margin="3"/>
      </StackPanel>
    </Border>
  </Grid>
</UserControl>

(Ничего не нужно делать в TreeViewNode.cs)

Основой пользовательского элемента управления является пара объектов TextBlock, один из которых привязан к свойству Name, а другой к свойству Depth объекта Data. Так как каждый объект Data передается в метод FillBoundTree, то он служит в качестве DataContext, из которого затем будут извлечены данные параметры.