Для демонстрации способа хранения дерева в базе данных, а также для того, чтобы на конкретном примере изучить некоторые новые для меня методы работы с базами данных в приложениях C#, я разработал приложение ArticlesApp.
Приложение ArticlesApp представляет собой простейшую информационную систему, предназначенную для хранения текстовой информации. В базе данных этой системы Articles хранятся статьи, организованные иерархическим образом.
В левой части этого окна имеется дерево, созданное с использованием элемента управления TreeView. Окно этого дерева отображает заголовки статей, а также так называемые веса сортировки заголовков, указанные в круглых скобках. Эти веса понадобятся мне далее в программе для корректного отображения иерархичности данных.
Замечу, что сразу после запуска приложения, когда в базе данных нет ни одной записи, дерево заголовков не содержит ни одного элемента. Если щелкнуть окно дерева правой клавишей мыши, на экране появится контекстное меню со строками Добавить статью, Удалить статью и Правка статьи. При нажатии на эти кнопки вызываются методы, позволяющие управлять содержимым базы данных.
Выбор строки Добавить статью приведет к тому, что на экране появится диалоговое окно Form2, специально созданное для этой цели. Оно содержит поле типа textbox для хранения заголовка статьи, поле типа RichTextBoxдля хранения самого текста статьи. Также имеется поле типа numericUpDown для хранения веса сортировки и 2 кнопки Сохранить и Отменить.Таким образом, при помощи этого окна можно добавить в базу данных новую статью, определив для нее заголовок, тело и вес сортировки.
Если в дереве нет ни одного элемента, то при первом использовании строки Добавить статью контекстного меню в дерево будет добавлен корневой элемент. Для того чтобы добавить в дерево дочерний элемент, нужно вначале выделить левой клавишей мыши заголовок родительского элемента, а потом, щелкнув этот заголовок правой клавишей мыши, выбрать из контекстного меню строку Добавить статью. Редактирование любого элемента выполняется аналогично. Для выполнения этой операции нужно выделить элемент, а затем, щелкнув его правой клавишей мыши, выбрать из контекстного меню строку Правка статьи. С помощью строки Удалить статью можно удалить элемент дерева. Замечу, что программа удаляет только элементы, не имеющие дочерних элементов. Попытки удалить элемент с дочерними элементами моё приложение игнорирует.
Для создания приложения ArticlesApp мною была создана база данных Articles на MicrosoftSQLServer 2008. Она содержит 2 таблицы и 3 хранимые процедуры.
Таблица Tree предназначена для хранения структуры дерева статей. В ней создано четыре столбца с именами id, parent_id, title и weight. Столбец id является первичным ключом. Данную таблицу я создал с помощью sql-запроса следующего содержания:
CREATE TABLE [dbo].[Tree] (
[id] [int] IDENTITY (1, 1) NOT NULL ,
[parent_id] [int] NOT NULL ,
[title] [varchar] (50) COLLATE Cyrillic_General_CI_AS NOT NULL ,
[weight] [int] NOT NULL
) ON [PRIMARY].
Здесь столбец id хранит идентификаторы узлов дерева, а столбец parent_id — идентификаторы родительских узлов. Таким образом, вместе с каждым узлом хранится идентификатор его родительского узла.
Поля title и weight предназначены, соответственно, для хранения заголовка статьи и веса сортировки, назначенного этой статье. Вес сортировки необходим для визуализации заголовков статей в виде дерева. Чем выше вес сортировки, тем ниже располагается данная статья в дереве заголовков.
Можно было бы хранить тексты документов в самой таблице Tree, однако это привело бы к неэффективному расходованию памяти.
В самом деле, при отображении дерева мне фактически нужно загрузить в память все содержимое таблицы Tree. Однако в каждый момент времени пользователь просматривает или редактирует только одну статью, поэтому нет никакой необходимости загружать эти данные в память вместе со структурой дерева.
Для хранения текстов статей я создал отдельную таблицу Documents, содержащую столбцы id, document и tree_id. Первый из этих столбцов является ключевым. Ниже представлен sql-запрос, с помощью которого я создал таблицу Documents:
CREATE TABLE [dbo].[Documents] (
[id] [int] IDENTITY (1, 1) NOT NULL ,
[document] [varchar] (5000) COLLATE Cyrillic_General_CI_AS NOT NULL ,
[tree_id] [int] NOT NULL
) ON [PRIMARY]
В столбце id таблицы Documents хранятся уникальные идентификаторы статей, которые напрямую не используются в моём приложении.
Столбец tree_id хранит идентификатор узла дерева, соответствующего данной статье. Этот столбец является внешним ключом для таблицы Tree.
И, наконец, столбец document хранит текст самой статьи.
Часть работы с базой данных моё приложение будет выполнять при помощи команд SQL, оформленных в виде объектов класса SqlCommand. Однако на примере этого приложения я покажу как можно работать с хранимыми процедурами сервера Microsoft SQL Server.
Хранимая процедура sp_InsertDocument предназначена для добавления нового документа в таблицу Documents:
CREATE PROCEDURE [dbo].[sp_InsertDocument]
@tree_id AS INT,
@document AS VARCHAR(2000)
AS
INSERT INTO dbo.Documents(tree_id, document) VALUES (@tree_id, @document);
RETURN @@identity
Этой процедуре необходимо передать два параметра @tree_id и @document. Первый из этих параметров предназначен для передачи идентификатора узла, в который добавляется статья, а второй — для передачи текста этой статьи. Процедура возвращает идентификатор добавленной строки @@identity.
Хранимая процедура sp_ InsertNode вставляет новую строку в таблицу Tree, возвращая идентификатор новой строки:
CREATE PROCEDURE [dbo].[sp_InsertNode]
@parent_id AS INT,
@title AS VARCHAR(50),
@weight AS INT
AS
INSERT INTO dbo.Tree(parent_id, title, weight) VALUES (@parent_id, @title, @weight);
RETURN @@identity
Этой процедуре нужно передать через входные параметры идентификатор родительского узла @parent_id (равный 0 для корневого узла), заголовок статьи @title и вес сортировки @weight.
При помощи хранимой процедуры sp_UpdateDocument моё приложение обновляет тексты статей, хранящиеся в таблице Documents:.
ALTER PROCEDURE [dbo].[sp_UpdateDocument]
@tree_id as int,
@document AS VARCHAR(2000)
AS
UPDATE dbo.Documents SET document = @document WHERE (tree_id = @tree_id)
В качестве параметра этой хранимой процедуре необходимо передать идентификатор узла @tree_id обновляемой статьи, а также текст статьи @document.
Прежде всего, я обеспечил приложение возможностью соединения с базой данных Articles. С этой целью мною был добавлен программный компонент SqlConnection. Идентификатор этого компонента будет храниться в поле sqlConnection1 класса Form1.
Чтобы приложение могло корректным образом соединиться с базой данных Articles свойство ConnectionString объекта SqlConnection1 должно быть отредактировано следующим образом:
DataSource=.\SQLEXPRESS;AttachDbFilename=D:\Work\ArticlesApp\ArticlesApp\Articles.mdf;Integrated Security=True;Connect Timeout=30;User Instance=True
Для того чтобы приложение могло загружать содержимое таблицы Tree базы данных Articles, хранящей структуру дерева статей, я добавил в него адаптер SqlDataAdapter, использующий соединение SqlConnection1. Таким образом ссылка на адаптер данных будет хранится в поле SqlDataAdapter1.
После добавления адаптера необходимо создать набор данных DataSet. Выбрав на панели инструментов одноимённый компонент, я создал набор данных DataSet1, содержащий обе таблицы базы данных Articles.
Создание дерева начинается с того, что пользователь запускает приложение, щелкает правой клавишей мыши пустое окно дерева и выбирает из контекстного меню строку Добавить статью. В результате на экране появляется диалоговое окно, показанное на рисунке ниже, где пользователь может ввести информацию для узла дерева:
Реализация процесса добавления статьи состоит в написании обработчика события для кнопки Добавить статью контекстного меню contextMenuStrip1. Ниже приведён обработчик для этого события:
privatevoid добавитьToolStripMenuItem_Click(objectsender, EventArgse)
{
if (treeView1.SelectedNode != null)
{
int id = (int)treeView1.SelectedNode.Tag;
AddNode(id);
UpdateTree();
}
else
{
// Пустойсписок
if (treeView1.Nodes.Count == 0)
{
AddNode(0);
UpdateTree();
}
}
}
При самом первом запуске приложения и пустой базе данных в дереве treeView1 не выделено ни одного элемента, т.к. их там попросту нет. Соответственно, количество узлов дерева treeView1.Nodes.Count равно нулю. В этом случае моё приложение вызывает два метода:
AddNode(0);
UpdateTree();
3.5.1 Метод AddNode
Метод AddNode, определенный в моём приложении, создает узел дерева. В качестве единственного параметра этому методу нужно передать идентификатор родительского узла. Так как в первый раз пользователь создает корневой узел, то передаем методу AddNode нулевое значение.
Что же касается метода UpdateTree, то он тоже определен в моём приложении. Его задачей является наполнение окна дерева treeView1 содержимым таблицы Tree базы данных Articles. Я вызываю этот метод всякий раз после внесения изменений в структуру дерева (т.е. после добавления или удаления узлов дерева).
Для того чтобы содержимое дерева отображалось сразу после запуска приложения, я добавил вызов метода UpdateTree в конструктор класса Form1:
public Form1()
{
InitializeComponent();
UpdateTree();
}
В том случае, если в дереве есть узлы, и пользователь выделил какой-либо узел левой клавишей мыши или при помощи клавиатуры, наш обработчик событий добавитьToolStripMenuItem_Click выполняет следующие действия:
if (treeView1.SelectedNode != null)
{
int id = (int)treeView1.SelectedNode.Tag;
AddNode(id);
UpdateTree();
}
Вначале он извлекает из свойства treeView1.SelectedNode.Tag идентификатор строки таблицы Tree, соответствующий выделенному узлу. Этот идентификатор записывается в данное свойство методом UpdateTree в процессе построения дерева.
Замечу, что данный идентификатор обозначает узел, являющийся родительским по отношению к создаваемому узлу. Обработчик событий добавитьToolStripMenuItem_Click передает этот идентификатор методу AddNode, а затем перерисовывает обновленное дерево методом UpdateTree: