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

84

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

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

Мы уже рассмотрели пример применения элемента управления TreeView для отображения данных карты сайта. В этом примере использовалась возможность привязки элемента управления TreeView к иерархическим источникам данных. Однако элемент управления TreeView можно также заполнить с помощью привязки к обычному источнику данных (в этом случае получается только один уровень узлов) или за счет самостоятельного создания узлов - либо программно, либо через объявление .aspx.

Последний вариант является самым простым. Например, добавляя дескрипторы <asp:TreeNode> в раздел <Nodes> элемента управления TreeView, можно создать несколько узлов:

<asp:TreeView ID="TreeView1" runat="server">
	<Nodes>
	    <asp:TreeNode Text="Products">
	        <asp:TreeNode Text="Hardware" />
	        <asp:TreeNode Text="Software" />
	    </asp:TreeNode>
	    <asp:TreeNode Text="Services" />
	</Nodes>
</asp:TreeView>

А вот как добавить TreeNode программно при загрузке страницы:

protected void Page_Load(object sender, System.EventArgs e)
{
        TreeNode newNode = new TreeNode("Consulting");

        // Добавить в качестве дочернего узла второго корневого узла
        // (узла Services в предыдущем примере)
        TreeView1.Nodes[1].ChildNodes.Add(newNode);
}

При первом отображении элемента управления TreeView будут показаны все узлы. Этим поведением можно управлять, устанавливая свойство TreeView.ExpandDepth. Например, если ExpandDepth равно 2, то будут показаны только первые три уровня (уровень 0, уровень 1 и уровень 2). Для определения количества уровней, включенных в TreeView (в свернутом или развернутом состоянии) служит свойство MaxDataBindDepth. По умолчанию это свойство имеет значение -1, при котором видимо все дерево. Однако если, например, установить его в 2, то под начальным узлом будут отображаться только два узла. Узлы можно также программно разворачивать и сворачивать, присваивая свойству TreeNode.Expanded значение true или false.

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

Объект TreeNode

Каждый узел в дереве представлен объектом TreeNode. Как вы уже знаете, каждый объект TreeNode имеет связанный с ним фрагмент текста, отображаемый в дереве. Объект TreeNode предлагает также свойства навигации, такие как ChildNodes (коллекция узлов, которые он содержит) и Parent (контейнерный узел, расположенный в дереве на уровень выше). Наряду с этим объект TreeNode предоставляет набор свойств, перечисленных в таблице ниже:

Свойства TreeNode
Свойство Описание
Text

Текст, отображаемый в дереве для данного узла

ToolTip

Текст контекстной подсказки, который появляется при наведении указателя мыши на текст узла

Value

Хранит неотображаемое значение с дополнительными данными об узле (например, уникальный идентификатор, который будет использоваться при обработке событий щелчков для идентификации узла или поиска дополнительной информации)

NavigateUrl

Если установить это свойство, оно автоматически переадресует пользователя по соответствующему URL-адресу после выбора узла, в противном случае придется повторно отреагировать на событие TreeView.SelectedNodeChanged, чтобы решить, какое действие нужно выполнить

Target

Если свойство NavigateUrl установлено, то это свойство задает искомое окно или фрейм для ссылки. Если свойство Tagret не установлено, то в текущем окне браузера открывается новая страница. TreeView также имеет свойство Target, с помощью которого можно применить цель по умолчанию для всех экземпляров TreeNode

ImageUrl

Изображение, которое отображается рядом с этим узлом

ImageToolTip

Текст контекстной подсказки для изображения, отображаемого рядом с узлом

Необычной особенностью объекта TreeNode является то, что он может использоваться в одном из двух режимов. В режиме выбора щелчок на узле приводит к обратной отправке страницы и генерации события TreeView.SelectedNodeChanged. Это режим по умолчанию для всех узлов. В режиме навигации щелчок на узле приводит к переходу на новую страницу без генерации события SelectedNodeChanged. Объект TreeNode перейдет в режим навигации, если свойству NavigateUrl присвоить значение, отличное от пустой строки. Объект TreeNode, привязанный к данным карты сайта, работает в режиме навигации, поскольку каждый узел карты сайта предоставляет информацию об URL-адресе.

В следующем примере элемент управления TreeView заполняется результатами запроса к базе данных. Способность TreeView к отображению иерархических данных желательно использовать для создания списка типа "главный-детальный". Поскольку ASP.NET не включает никакого элемента управления источником данных, который мог бы осуществлять запрос к базе данных и предлагать результаты в виде иерархического источника данных, применять связывание данных нельзя. Вместо этого нужно программно запросить таблицу и вручную создать структуру TreeNode.

Вот как выглядит код, в котором реализуется этот подход:

protected void Page_Load(object sender, System.EventArgs e)
{
        if (!Page.IsPostBack)
        {
            // Извлечь DataSet с помощью вспомогательного метода
            DataSet ds = GetProductsAndCategories();

            // Циклический проход по записям категорий
            foreach (DataRow row in ds.Tables["Categories"].Rows)
            {
                // Использовать конструктор, который требует только текст 
                // и неотображаемое значение
                TreeNode nodeCategory = new TreeNode(
                    row["CategoryName"].ToString(),
                    row["CategoryID"].ToString());

                TreeView1.Nodes.Add(nodeCategory);

                // Получить дочерние элементы (товары) для данного
                // родительского элемента (категории)
                DataRow[] childRows = row.GetChildRows(ds.Relations[0]);

                // Циклический проход no всем товарам данной категории
                foreach (DataRow childRow in childRows)
                {
                    TreeNode nodeProduct = new TreeNode(
                        childRow["ProductName"].ToString(),
                        childRow["ProductID"].ToString());
                    nodeCategory.ChildNodes.Add(nodeProduct);
                }

                // Сохранить свернутое состояние всех категорий
                nodeCategory.Collapse();
            }
        }
}

// Вспомогательный метод, возвращающий DataSet для двух таблиц
// Category и Products тестовой базы данных Northwind
private DataSet GetProductsAndCategories()
{
        string connectionString =
            WebConfigurationManager.ConnectionStrings["Northwind"].ToString();

        SqlConnection connection = new SqlConnection(connectionString);
        string sqlCat = "SELECT CategoryID, CategoryName FROM Categories";
        string sqlProd = "SELECT ProductID, ProductName, CategoryID FROM Products";

        SqlDataAdapter da = new SqlDataAdapter(sqlCat, connection);
        DataSet ds = new DataSet();
        try
        {
            connection.Open();

            // Заполнить DataSet
            da.Fill(ds, "Categories");

            // Добавить таблицу products
            da.SelectCommand.CommandText = sqlProd;
            da.Fill(ds, "Products");
        }
        finally
        {
            connection.Close();
        }

        // Добавить отношение между Categories и Products
        DataRelation relat = new DataRelation("CatProds",
          ds.Tables["Categories"].Columns["CategoryID"],
          ds.Tables["Products"].Columns["CategoryID"]);

        ds.Relations.Add(relat);
        return ds;
}

При щелчке на узле можно обработать событие SelectedNodeChanged, чтобы показать информацию об узле:

protected void TreeView1_SelectedNodeChanged(object sender, EventArgs e)
{
        if (TreeView1.SelectedNode == null) return;
        if (TreeView1.SelectedNode.Depth == 0)
            Label1.Text = "Вы выбрали категорию с CategoryID: ";
        else
            Label1.Text = "Вы выбрали товар с ProductID: ";

        Label1.Text += "<b>" + TreeView1.SelectedNode.Value + "</b>";
}

Результат выполнения этого кода показан на рисунке ниже:

Заполнение элемента управления TreeView информацией из базы данных

Упростить код страницы в этом примере можно несколькими способами. Один из них заключается в привязке к XML-данным, а не к реляционным данным. Поскольку SQL Server 2000 и последующие его версии могут выполнять XML-запросы с помощью конструкции FOR XML, можно извлечь данные, оформленные в специфической XML-разметке, а затем привязать их посредством элемента управления XmlDataSource. Единственным ухищрением будет то, что поскольку XmlDataSource предполагает, что вы будете привязываться к файлу, свойство Data понадобится установить вручную с применением XML-данных, полученных из базы данных.

Заполнение узлов по запросу

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

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

Чтобы использовать заполнение по запросу, нужно присвоить значение true свойству PopulateOnDemand любого элемента управления TreeView, который в текущий момент имеет необходимое для заполнения содержимое. Когда пользователь развернет эту ветвь, элемент управления TreeView инициирует событие TreeNodePopulate, которое можно будет применять для добавления следующего уровня узлов. При желании этот уровень узлов может содержать другой уровень узлов, заполняемых по запросу.

Хотя программная модель остается неизменной, TreeView поддерживает два способа заполнения узлов по запросу. Если свойство TreeView.PopulateNodesFromClient имеет значение true (по умолчанию), то TreeView выполняет обратный вызов на стороне клиента, чтобы извлечь необходимые узлы из события, не отправляя обратно всю страницу целиком. Если PopulateNodesFromClient установлено в false или если оно равно true, но TreeView обнаруживает, что текущий браузер не поддерживает обратные вызовы на стороне клиента, то TreeView выполняет обычную обратную отправку для получения того же результата. Единственное отличие заключается в том, что вся страница будет обновлена в браузере, генерируя не такой гладкий интерфейс. (Он позволяет также генерировать другие страничные события, например, события изменения элемента управления.)

Средство заполнения по запросу можно применить в предыдущем примере. Вместо того чтобы заполнять все дерево во время загрузки страницы, можно начать с добавления только узлов категорий, настраивая их для заполнения по запросу:

protected void Page_Load(object sender, EventArgs e)
{
        if (!Page.IsPostBack)
        {
            DataTable dtCategories = GetCategories();

            // Изначально заполнить только категории
            foreach (DataRow row in dtCategories.Rows)
            {
                TreeNode nodeCategory = new TreeNode(
                    row["CategoryName"].ToString(),
                    row["CategoryID"].ToString());

                // Использовать средство заполнения no запросу 
                // для дочерних узлов этого узла
                nodeCategory.PopulateOnDemand = true;

                nodeCategory.Collapse();
                TreeView1.Nodes.Add(nodeCategory);
            }
        }
}

// Вспомогательный метод извлекающий только категории
private DataTable GetCategories()
{
        string connectionString = 
            WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
        SqlConnection con = new SqlConnection(connectionString);

        string sqlCat = "SELECT CategoryID, CategoryName FROM Categories";

        SqlDataAdapter da = new SqlDataAdapter(sqlCat, con);
        DataSet ds = new DataSet();
        try
        {
            con.Open();
            da.Fill(ds, "Categories");
        }
        finally
        {
            con.Close();
        }

        return ds.Tables["Categories"];
}

Теперь необходимо отреагировать на событие TreeNodePopulate, чтобы заполнить категорию при ее разворачивании. В рассматриваемом примере единственными узлами, которые сами заполняются по запросу, являются категории. Если же будет несколько уровней узлов, в которых используется заполнение по запросу, можно проверить TreeNode.Depth, чтобы определить, какой тип узла развернут:

protected void TreeView1_TreeNodePopulate(object sender, TreeNodeEventArgs e)
{
        int categoryID = Int32.Parse(e.Node.Value);
        DataTable dtProducts = GetProducts(categoryID);

        foreach (DataRow row in dtProducts.Rows)
        {
            TreeNode nodeProduct = new TreeNode(
                row["ProductName"].ToString(),
                row["ProductID"].ToString());

            e.Node.ChildNodes.Add(nodeProduct);
        }
}

// Вспомогательный метод извлекающий продукты из
// таблицы Products по CategoryID
private DataTable GetProducts(int categoryID)
{
        string connectionString = 
            WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
        SqlConnection con = new SqlConnection(connectionString);

        string sqlProd = "SELECT ProductID, ProductName, CategoryID FROM Products WHERE CategoryID=@CategoryID";

        SqlDataAdapter da = new SqlDataAdapter(sqlProd, con);
        da.SelectCommand.Parameters.AddWithValue("@CategoryID", categoryID);
        DataSet ds = new DataSet();
        try
        {
            con.Open();
            da.Fill(ds, "Products");
        }
        finally
        {
            con.Close();
        }
        return ds.Tables["Products"];
}

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

Стили элемента управления TreeView

Элемент управления TreeView имеет подробную модель стилей, которая позволяет полностью управлять его внешним видом. Каждый стиль применяется к типу узла. Стили представляются классом TreeNodeStyle, который унаследован от обычного класса Style. Подобно другим многофункциональным элементам управления, с помощью стилей можно настраивать цвета фона и изображения, шрифты и границы. Кроме того, класс TreeNodeStyle добавляет свойства стилей, специфичные для узлов (таблица ниже). Эти свойства связаны с изображением узла и пространством вокруг него:

Свойства стилей TreeNodeStyle
Свойство Описание
ImageUrl

URL-адрес изображения, отображаемого рядом с узлом

NodeSpacing

Интервал (в пикселях) между текущим узлом и узлами, расположенными над ним и под ним

VerticalSpacing

Интервал (в пикселях) между верхней и нижней частями текста узла и границей вокруг текста

HorizontalSpacing

Интервал (в пикселях) между левой и правой частью текста узла и границей вокруг текста

ChildNodesPadding

Интервал {в пикселях) между последним дочерним узлом развернутого родительского узла и следующим родственным узлом

Поскольку элемент управления TreeView генерируется в виде HTML-таблицы, можно установить заполнение различных элементов, чтобы управлять интервалами вокруг текста, между узлами и т.д. Еще одним свойством, вступающим в игру, является TreeView.NodeIndent, которое определяет количество пикселей отступа (слева) в каждом последующем уровне иерархии дерева. На рисунке ниже показано, как эти настройки применяются к одному узлу:

TreeViewNode

Элемент управления TreeView позволяет также сконфигурировать свою внутреннюю визуализацию с помощью высокоуровневых свойств. Линии узлов в дереве можно убрать с помощью свойства TreeView.ShowExpandCollapse. Можно также использовать свойства CollapseImageUrl и ExpandImageUrl, чтобы установить индикаторы свернутого и развернутого состояний элемента управления TreeView (обычно они отображаются в виде знака "минус" и "плюс" соответственно), и свойство NoExpandImageUrl, чтобы указать, что будет отображаться рядом с узлами, не имеющими дочерних узлов.

Наконец, можно отобразить флажки рядом с каждым из узлов (установив свойство TreeView.ShowCheckBoxes в true) или рядом с отдельными узлами (установив свойство TreeNode.ShowCheckBox в true). Чтобы узнать, выбран ли данный узел, нужно проверить свойство TreeNode.Checked.

Применение стилей к типам узлов

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

Чтобы применить настройки стиля узла ко всем узлам дерева, можно использовать свойство TreeView.NodeStyle. Отдельные области элемента управления TreeView изолируются с использованием более специфического стиля:

Свойства стилей TreeView
Свойство Описание
NodeStyle

Применяется ко всем узлам

RootNodeStyle

Применяется только к узлам первого уровня (корневой узел)

ParentNodeStyle

Применяется к любому узлу, содержащему другие узлы, кроме корневых узлов

LeafNodeStyle

Применяется к любому узлу, не содержащему дочерних узлов и не являющемуся корневым узлом

SelectedNodeStyle

Применяется к выбранному на данный момент узлу

HoverNodeStyle

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

Стили в этой таблице перечислены в порядке от наиболее общего до специфичного. Это означает, например, что настройки стиля SelectedNodeStyle замещают любые конфликтующие настройки в RootNodeStyle. (Если не хотите, чтобы узел можно было выбирать, установите свойство TreeNode.SelectActionNone.) Однако настройки RootNodeStyle, ParentNodeStyle и LeafNodeStyle никогда не конфликтуют, т.к. определения для корневых, родительских и дочерних узлов, взаимно исключают друг друга. Не может существовать узел, который, например, будет одновременно родительским и корневым узлом - TreeView просто обозначит его как корневой узел.

Применение стилей к уровням узлов

Применение стилей к различным типам узлов само по себе интересно, хотя более полезной особенностью является возможность применения стилей на основе уровня узла. Объясняется это тем, что большинство деревьев используют жесткую иерархию (например, первый уровень узлов представляет категории, второй уровень - товары, третий уровень - заказы и т.д.). В рассматриваемом случае важно определить не наличие дочерних узлов в данном узле, а его глубину.

Единственная проблема состоит в том, что элемент управления TreeView теоретически может иметь неограниченное количество уровней узлов. В связи с этим нет смысла предлагать такие свойства, как FirstLevelStyle, SecondLevelStyle и т.д. Наоборот, TreeView имеет коллекцию LevelStyles, которая может иметь столько элементов, сколько необходимо. Уровень выводится из позиции стиля в коллекции, поэтому первый элемент считается корневым уровнем, второй элемент - вторым уровнем узла и т.д. Чтобы такая система могла работать, понадобится повторить этот порядок и включить пустой заполнитель стиля, если необходимо пропустить уровень, не изменяя форматирование.

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

<asp:TreeView ID="TreeView1" runat="server">
	<LevelStyles>
	    <asp:TreeNodeStyle ChildNodesPadding="10" Font-Bold="true" Font-Size="20px" 
	        ForeColor="LimeGreen" />
	    <asp:TreeNodeStyle ChildNodesPadding="5" Font-Bold="true" Font-Size="16px" />
	</LevelStyles>
</asp:TreeView>

Если применить этот код к списку категорий и товаров, показанному в предыдущих примерах, отобразится страница вроде показанной на рисунке ниже:

Элемент управления TreeView без отступов

Изображения элемента управления TreeView

Как уже известно, изображение для одиночного узла можно задать с помощью свойства TreeViewNode.ImageUrl. К счастью, когда требуется назначить последовательный набор изображений для всего дерева, применять такой детальный подход не придется. Вместо этого можно использовать свойства TreeView для установки изображений для всех узлов. Можно выбрать картинку, которая будет отображаться рядом со всеми свернутыми узлами (CollapsedUrl), всеми развернутыми узлами (ExpandImageUrl) и всеми узлами, которые не имеют дочерних узлов и поэтому не могут быть развернуты (NoExpandImageUrl). Если задать эти свойства и определить изображение для определенного узла с помощью свойства TreeViewNode.ImageUrl, то преимущество будет отдано изображению, определенному для узла.

Элемент управления TreeView имеет коллекцию изображений, которыми можно пользоваться, чтобы не тратить время на создание специальных изображений для узлов. Для доступа к этим изображениям предназначено свойство TreeView.ImageSet, которое принимает одно из 16 значений из перечисления TreeViewImageSet. Каждый набор включает изображение для свернутого и развернутого узла, а также для узла, не имеющего дочерних узлов. Применяя свойство ImageSet, можно отказаться от использования любых других свойств, связанных с изображениями.

Некоторые доступные варианты свойства ImageSet показаны на рисунке ниже. Например, значение TreeViewImageSet.Faq создает дерево со значками в стиле справки, отображающими вопросительный знак (для узлов, которые не имеют дочерних узлов), или вопросительный знак, расположенный поверх папки (для узлов с дочерними узлами):

Разные представления TreeView

Чтобы вручную не создавать эти стили можете выбрать в смарт-теге TreeView раздел AutoFormat и выбрать нужный стиль:

Создание стилей с помощью Visual Studio
Пройди тесты
Лучший чат для C# программистов