Электронная библиотека книг Александра Фролова и Григория Фролова.
Shop2You.ru Создайте свой интернет-магазин
Библиотека
Братьев
Фроловых
[Назад] [Содержание] [Дальше]

Визуальное проектирование приложений C#


А.В. Фролов, Г.В. Фролов

Глава 9. Интеграция с MS SQL Server.. 2

Приложение SQLTestApp. 2

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

Создание таблицы Contacts. 4

Идентификатор для подключения к базе данных. 6

Создание проекта приложения. 10

Добавление адаптера данных SqlDataAdapter 10

Программный код для адаптера и соединения. 16

Создание локального набора DataSet 22

Редактирование содержимого набора DataSet 24

Загрузка набора данных. 24

Сохранение отредактированного набора данных. 25

Парольный доступ к системе. 25

Приложение LoginApp. 26

Таблица Users. 27

Создание проекта приложения. 27

Соединение с базой данных. 27

Запросы SQL. 28

Адаптер SqlDataAdapter 29

Элемент управления DataGrid. 30

Подключение пользователя. 32

Обновление таблицы Users. 36

Хранение дерева в базе данных.. 36

Приложение ArticlesApp. 36

База данных Articles. 37

Таблица Tree. 38

Таблица Documents. 38

Хранимая процедура sp_InsertDocument 38

Хранимая процедура sp_ InsertNode. 39

Хранимая процедура sp_ UpdateDocument 39

Создание проекта приложения ArticlesApp. 39

Соединение с базой данных. 40

Добавление адаптера SqlDataAdapter. 40

Создание набора данных DataSet 41

Добавление контекстного меню.. 41

Создание узла дерева.. 41

Метод AddNode. 43

Диалоговое окно для ввода данных добавляемого узла. 43

Открытие и закрытие соединения с базой данных. 44

Использование хранимых процедур. 44

Параметры хранимых процедур. 44

Запуск хранимой процедуры.. 45

Получение значений выходных параметров. 45

Добавление текста документа. 45

Диалоговая форма редактирования документа. 45

Свойство Title. 45

Свойство Weight 46

Свойство Document 46

Построение дерева. 46

Метод UpdateTree. 46

Метод CreateNodes. 47

Редактирование узла дерева.. 48

Обработчик событий меню Edit 49

Извлечение идентификатора редактируемого узла. 50

Извлечение данных редактируемого узла дерева. 50

Получение текст редактируемой статьи. 50

Извлечение заголовка и веса сортировки. 51

Обновление информации узла в базе данных. 51

Удаление узла дерева.. 52

Отслеживание перемещений по дереву.. 54

 

Глава 9. Интеграция с MS SQL Server

В предыдущей главе нашей книги мы научились работать с компонентами DataSet и DataGtid, с использованием которых можно относительно легко создавать базы данных и отображать их содержимое в формах приложений C#.

Теперь нашей задачей будет научиться интегрировать приложения с сервером Microsoft SQL Server, используя как технологию рассоединенной (disconnected) обработки, так и классическую клиент-серверную технологию. Мы познакомимся с программными компонентами, предусмотренными в Microsoft .NET Framework специально для решения этой задачи. Это так называемые управляемые поставщики (managed providers), реализованные в виде ряда интерфейсов. Мы уже рассказывали о них в разделе «Провайдеры данных для управляемого кода» 8 главы.

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

Приложение SQLTestApp

Материал этой главы мы будем изучать на нескольких примерах приложений, первое из которых называется SQLTestApp. В этом приложении мы привяжем локальный набор DataSet к базе данных, расположенной на сервере Microsoft SQL Server. Отображение и редактирование таблицы Contacts, созданной в рамках локального набора DataSet, будет осуществляться с помощью рассмотренного в предыдущей главе элемента управления DataGrid.

Для загрузки содержимого таблицы Contacts из базы данных Microsoft SQL Server, а также для сохранения изменений, сделанных в этой таблице локально, в окне приложения мы создадим кнопки Загрузить и Обновить (рис. 9-1).

Рис. 9-1. Окно приложения SQLTestApp

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

Прежде всего, необходимо создать базу данных на сервере Microsoft SQL Server. Мы полагаем, что Вы предварительно установили сервер Microsoft SQL Server версии 2000 или 7.0 на тот же самый компьютер, где находится Ваше приложение, или на другой компьютер Вашей локальной сети.

Описание процедуры установки и настройки сервера Microsoft SQL Server выходит за рамки нашей книги, однако на эту тему уже издано немало литературы. Тем не менее, мы подробно расскажем о выполнении всех шагов, необходимых для создания баз данных наших приложений.

Итак, запустите программу SQL Server Enterprise Manager (рис. 9-2), которая входит в состав клиентских утилит сервера Microsoft SQL Server. Эти утилиты могут устанавливаться как вместе с сервером SQL, так и на  отдельный компьютер.

Рис. 9-2. Утилита SQL Server Enterprise Manager

Последовательно раскройте в левой части окна утилиты SQL Server Enterprise Manager каталоги Microsoft SQL Servers и SQL Server Group, а затем каталоги, расположенные на Вашем сервере SQL. На рис. 9-2 SQL Server называется DR4.

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

Щелкните правой клавишей мыши папку Databases, а затем выберите из контекстного меню строку New Database. На экране появится диалоговое окно, показанное на рис. 9-3.

Рис. 9-3. Создание базы данных PhoneBook

Введите в поле Name строку PhoneBook (название создаваемой базы данных), а затем щелкните кнопку OK. В результате будет создана новая база данных. Раскройте ее папку, как это показано на рис. 9-4.

Рис. 9-4. Содержимое базы данных PhoneBook

Создание таблицы Contacts

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

Чтобы создать таблицу, щелкните правой клавишей мыши строку Tables в раскрытой папке PhoneBook (рис. 9-4), а затем выберите из контекстного меню строку New Table. На экране появится редактор структуры таблицы, показанный на рис. 9-5.

Рис. 9-5. Редактирование структуры таблицы Contacts

Введите в столбце редактора Column Name имена столбцов таблицы Contacts. Столбец id будет использован для хранения уникальных идентификаторов строк, а в столбцах FirstName и LastName будут храниться, соответственно, имена и фамилии людей.

Столбец редактора Data Type определяет тип данных, хранящихся в столбце таблицы. Первый столбец id должен иметь числовой тип int, а два других — текстовый тип с переменной длиной varchar.

В столбце Length укажите размер полей, как это показано на рис. 9-5.

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

На следующем этапе необходимо сделать столбец id первичным ключом в создаваемой таблице Contacts. С этой целью щелкните соответствующую строку в окне редактора правой клавишей мыши и затем выберите из контекстного меню строку Set Primary Key. Около строки параметров id появится изображение маленького ключа (рис. 9-6).

Рис. 9-6. Настройка параметров столбца id

Далее, выберите из списка Identity для столбца id значение Yes (как это показано на рис. 9-6). В результате столбец id будет содержать уникальные идентификаторы строк таблицы.

Кроме того, измените значение параметра Identity Seed, задающее начальное значение столбца id, с 1 на 0. Именно такое значение используется по умолчанию для идентификации строк таблиц, хранящихся в наборе данных DataSet.

Отредактировав параметры таблицы, сохраните ее структуру, щелкнув кнопку с изображением дискеты. Эта кнопка находится в левом верхнем углу окна редактора. В результате на экране появится диалоговое окно Choose Name, где Вам нужно будет указать имя таблицы (рис. 9-7). Наша таблица должна называться Contacts.

Рис. 9-7. Сохранение структуры таблицы

Нажмите здесь кнопку OK, а затем закройте окно редактора структуры таблицы.

Ниже мы привели сценарий SQL, который создает нужную нам таблицу Contacts при помощи команд SQL:

CREATE TABLE [dbo].[Contacts] (
  [id] [int] IDENTITY (1, 1) NOT NULL ,
  [FirstName] [varchar] (50) COLLATE Cyrillic_General_CI_AS NULL ,
  [LastName] [varchar] (50) COLLATE Cyrillic_General_CI_AS NULL
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[Contacts] WITH NOCHECK ADD
  CONSTRAINT [PK_Contacts] PRIMARY KEY  CLUSTERED
  (
     [id]
  ) ON [PRIMARY]
GO

Вы можете использовать этот сценарий для автоматического создания таблицы в базе данных при помощи утилиты Query Analyzer, входящей в состав клиентских утилит сервера Microsoft SQL Server.

Идентификатор для подключения к базе данных

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

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

В окне просмотра содержимого базы данных PhoneBook раскройте список пользователей базы данных Users, показанный на рис. 9-8.

Рис. 9-8. Пользователи базы данных PhoneBook

По умолчанию в этом списке есть только один пользователь с идентификатором dbo. Это владелец базы данных (Data Base Owner), который ее создал. Если Вы создавали базу данных, подключившись к серверу с правами администратора, то только администратор будет иметь доступ к базе данных.

Для добавления нового пользователя щелкните строку Users правой клавишей мыши, а затем выберите из контекстного меню строку New Database User. Вы увидите на экране диалоговое окно, показанное на рис. 9-9.

Рис. 9-9. Добавление нового пользователя

Выберите в списке Login name, задающем идентификатор пользователя, строку <new> для создания нового идентификатора. Вы увидите диалоговое окно создания нового идентификатора, показанное на рис. 9-10.

Рис. 9-10. Добавление нового идентификатора

Введите в поле Name строку c_sharp_app. Этот идентификатор будут использовать наши приложения для подключения к базе данных PhoneBook.

Далее Вам необходимо отметить флажок SQL Server Authentication, чтобы использовать сервер SQL для аутентификации (проверки) данного идентификатора. Введите пароль доступа в поле Password.

На последнем шаге выберите в списке Database базу данных PhoneBook, а в списке Language — строку Russian. После аутентификации пользователь (т.е. наше приложение) будет по умолчанию работать с базой данных PhoneBook, используя русские национальные настройки.

Далее раскройте вкладку Database Access (рис. 9-11) для настройки прав доступа пользователя c_sharp_app к базам данных.

Рис. 9-11. Настройка доступа к базе данных

Отметьте флажок базы данных PhoneBook, разрешив доступ пользователя c_sharp_app к этой базе данных, а затем щелкните кнопку OK.

После этого у Вас будет еще раз запрошен пароль, введенный ранее. Используйте здесь тот же самый пароль, что вы указывали в поле Password диалогового окна, показанного на рис. 9-10. Далее закройте окно создания нового пользователя.

Если Вы все сделали правильно, в списке пользователей базы данных появится новая строка (рис. 9-12).

Рис. 9-12. Добавлен новый пользователь

На следующем этапе Вы должны задать права доступа пользователя c_sharp_app к таблице Contacts. Для этого щелкните имя таблицы в списке таблиц базы данных PhoneBook (рис. 9-13), а затем выберите из контекстного меню строку Properties.

Рис. 9-13. Таблицы нашей базы данных

На экране появится диалоговое окно свойств таблицы Contacts, в которой Вам нужно будет щелкнуть кнопку Permissions. На экране появится окно с вкладкой Permissions, где Вы сможете задать права доступа пользователя по выполнению отдельных операций с таблицей Contacts (рис. 9-14).

Рис. 9-14. Настройка прав доступа к таблице Contacts

Отметьте в этом окне флажки SELECT, INSERT, UPDATE и DELETE, разрешив пользователю c_sharp_app выполнение операций извлечения, вставки обновления и удаления строк, соответственно.

Создание проекта приложения

После создания базы данных PhoneBook Вам нужно создать новое приложение с именем SQLTestApp, пользуясь для этого мастером проектов.

Добавление адаптера данных SqlDataAdapter

Чтобы обеспечить связь между таблицей локального набора данных DataSet и базой данных, размещенной на сервере Microsoft SQL Server, нам нужно добавить в проект приложения программный компонент адаптера данных SqlDataAdapter.

Методы Fill и Update, предусмотренные в классе SqlDataAdapter, выполняют, соответственно, прямую и обратную передачу данных между набором DataSet и таблицей, хранящейся в базе данных сервера SQL.

При этом метод Fill наполняет набор DataSet данными из сервера SQL, а метод Update обновляет базу сервера SQL данными из локального набора DataSet. Для передачи данных используются соответствующие строки, составленные на языке Transact-SQL.

Когда компонент SqlDataAdapter используется для доступа к серверу Microsoft SQL Server, то вместе с ним применяются компоненты SqlConnection и SqlCommand. Первый из них обеспечивает подключение к серверу SQL, а второй — выполнение команд SQL.

В компоненте SqlDataAdapter имеются также свойства SelectCommand, InsertCommand, DeleteCommand, UpdateCommand и TableMappings, нужные в процессе загрузки и обновления данных.

Нам нужно добавить в наше приложение адаптер SqlDataAdapter, перетащив его значок в окно формы из вкладки Data инструментальной панели Toolbox системы разработки приложений Microsoft visual Studio .NET (рис. 9-15).

Рис. 9-15. Значок адаптера SqlDataAdapter

Как только Вы это сделаете, на экране появится первое окно мастера конфигурирования адаптера данных (рис. 9-16).

Рис. 9-16. Мастер конфигурирования адаптера данных

Щелкните в этом окне кнопку Next для продолжения работы мастера конфигурирования.

В следующем диалоговом окне Вам нужно будет создать соединение с источником данных (рис. 9-17).

Рис. 9-17. Создание соединения с источником данных

Чтобы создать соединение, щелкните кнопку New Connection. На экране появится диалоговое окно Data Link Properties, открытое на вкладке Connection (рис. 9-18).

Рис. 9-18. Выбор параметров соединения с источником данных

В списке Select or enter a server name Вы должны указать сервер SQL, к которому будет подключаться Ваше приложение. Если это тот сервер работает на том же компьютере, что и приложение, укажите здесь строку localhost.

Далее, отметьте флажок Use a specific user name and password, после чего введите в поле User name строку c_sharp_app. Задайте также в поле Password пароль пользователя c_sharp_app.

Чтобы позволить приложению сохранить пароль, отметьте флажок Allow saving password. Избегайте установки этого флажка, если к безопасности предъявляются повышенные требования.

Далее отметьте флажок Select the database on the server и выберите в списке, расположенном под этим флажком, нашу базу данных PhoneBook.

На этом создание соединения с источником данных завершено. Обязательно проверьте его работоспособность, щелкнув кнопку Test Connection. Если все нормально, Вы получите сообщение с текстом Test connection succeed.

Проверив соединение, щелкните кнопку OK. На экране появится диалоговое окно SQL Server Login (рис. 9-19).

Рис. 9-19. Подключение к серверу SQL

В поле Password этого окна Вам нужно ввести пароль пользователя c_sharp_app, а затем щелкнуть кнопку OK. Теперь в списке соединений появится название нового, только что созданного Вами соединения (рис. 9-20).

Рис. 9-20. Выбрано созданное и проверенное соединение

Щелкните кнопку Next для перехода к следующему окну мастера соединений.

На экране появится диалоговое окно с флажками, в котором Вам нужно будет определить тип запроса, т.е. способ, которым будет пользоваться адаптер SqlDataAdapter для извлечения данных из сервера SQL (рис. 9-21).

Рис. 9-21. Выбор типа запроса

Для извлечения данных Вы можете использовать запрос, составленный на языке SQL, новую или существующую хранимую процедуру сервера Microsoft SQL Server. В нашем случае мы воспользуемся простой командой SELECT языка SQL, поэтому отметьте флажок Use SQL statements, а затем щелкните кнопку Next.

На экране появиться очередное окно мастера, показанное на рис. 9-22. Это окно позволит Вам ввести текст сценария SQL для извлечения данных.

Рис. 9-22. Окно ввода сценария SQL

Введите в этом окне следующую строку:

SELECT id, FirstName, LastName FROM Contacts

Щелкнув кнопку Query Builder, Вы сможете создать строку запроса SQL при помощи мастера сценариев SQL, главное окно которого показано на рис. 9-23.

Рис. 9-23. Мастер сценариев SQL

Нужного нам результата можно добиться, просто отметив флажки всех строк таблицы Contacts.

Щелкнув кнопку Next в окне мастера, показанном на рис. 9-22, Вы перейдете к финальному окну, показанному на рис. 9-24.

Рис. 9-24. Адаптер создан

Для завершения процедуры создания адаптера щелкните в нем кнопку Finish.

Программный код для адаптера и соединения

Рассмотрим программный код, добавленный мастером в проект в результате всех наших действий.

Прежде всего, обратите внимание, что теперь к проекту был автоматически добавлен программный компонент sqlConnection1. Этот компонент обеспечивает соединение с источником данных.

Мастер создал в классе Form1 несколько полей для хранения ссылок на объекты адаптера sqlDataAdapter1, соединения sqlConnection1, а также команд SQL, предназначенных для выборки (sqlSelectCommand1), вставки (sqlInsertCommand1), обновления (sqlUpdateCommand1) и удаления (sqlDeleteCommand1) строк таблицы:

private System.Data.SqlClient.SqlDataAdapter sqlDataAdapter1;
private System.Data.SqlClient.SqlCommand sqlSelectCommand1;
private System.Data.SqlClient.SqlCommand sqlInsertCommand1;
private System.Data.SqlClient.SqlCommand sqlUpdateCommand1;
private System.Data.SqlClient.SqlCommand sqlDeleteCommand1;
private System.Data.SqlClient.SqlConnection sqlConnection1;

Инициализация всех этих компонентов осуществляется методом InitializeComponent, исходный текст которого по умолчанию скрыт от разработчика. Давайте раскроем эти «секреты».

Прежде всего, в процессе инициализации метод InitializeComponent создает перечисленные выше объекты при помощи соответствующих конструкторов:

this.sqlDataAdapter1 = new System.Data.SqlClient.SqlDataAdapter();
this.sqlSelectCommand1 = new System.Data.SqlClient.SqlCommand();
this.sqlInsertCommand1 = new System.Data.SqlClient.SqlCommand();
this.sqlUpdateCommand1 = new System.Data.SqlClient.SqlCommand();
this.sqlDeleteCommand1 = new System.Data.SqlClient.SqlCommand();
this.sqlConnection1 = new System.Data.SqlClient.SqlConnection();

Далее выполняется настройка свойств адаптера sqlDataAdapter1.

Программа подключает к адаптеру объекты, при помощи которых выполняются операции удаления, вставки, выборки и обновления данных:

this.sqlDataAdapter1.DeleteCommand = this.sqlDeleteCommand1;
this.sqlDataAdapter1.InsertCommand = this.sqlInsertCommand1;
this.sqlDataAdapter1.SelectCommand = this.sqlSelectCommand1;
this.sqlDataAdapter1.UpdateCommand = this.sqlUpdateCommand1;

После этого необходимо настроить свойство TableMappings адаптера. Это свойство задает соответствие между таблицами источника данных (сервера SQL) и таблицами локального набора данных DataSet.

В нашем случае источник данных представляет собой таблицу Contacts. Эта таблица отображается на таблицу набора DataSet с именем Table:

this.sqlDataAdapter1.TableMappings.AddRange(
  new System.Data.Common.DataTableMapping[]
  {
     new System.Data.Common.DataTableMapping(
       "Table", "Contacts",
       new System.Data.Common.DataColumnMapping[]
       {
          new System.Data.Common.DataColumnMapping("id", "id"),
          new System.Data.Common.DataColumnMapping("FirstName",
            "FirstName"),
          new System.Data.Common.DataColumnMapping("LastName",
            "LastName")
       }
     )
  }
);

При этом программа отображает каждый столбец источника данных на одноименный столбец набора данных DataSet.

Уже после создания адаптера Вы сможете изменить отображение, отредактировав свойство TableMappings. Это можно сделать с помощью редактора, показанного на рис. 9-25.

Рис. 9-25. Редактирование отображения таблиц

Здесь список SourceTable задает исходную таблицу локального набора данных DataSet, а список Dataset table — таблицу источника данных.

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

//
// sqlSelectCommand1
//
this.sqlSelectCommand1.CommandText =
  "SELECT id, FirstName, LastName FROM dbo.Contacts";
this.sqlSelectCommand1.Connection = this.sqlConnection1;

Текст команды, составленный на языке SQL, записывается в свойство CommandText объекта sqlSelectCommand1. Так как команда имеет отношение к вполне определенному соединению с источником данных, в свойство Connection записывается ссылка на соединение sqlConnection1. Об инициализации соединения мы расскажем чуть позже.

Аналогичным образом готовятся и остальные команды.

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

//
// sqlInsertCommand1
//
this.sqlInsertCommand1.CommandText =
  "INSERT INTO dbo.Contacts(FirstName, LastName) VALUES (@FirstName, @LastName); SEL" +
  "ECT id, FirstName, LastName FROM dbo.Contacts WHERE (id = @@IDENTITY)";
this.sqlInsertCommand1.Connection = this.sqlConnection1;

Обратите внимание, что для вставки используются параметры @FirstName и @LastName. Они определяют содержимое ячеек вставляемых строк.

Параметры добавляются в свойство sqlInsertCommand1.Parameters при помощи метода Add:

this.sqlInsertCommand1.Parameters.Add(
  new System.Data.SqlClient.SqlParameter("@FirstName",
     System.Data.SqlDbType.VarChar, 50, "FirstName"));

this.sqlInsertCommand1.Parameters.Add(
  new System.Data.SqlClient.SqlParameter("@LastName",
     System.Data.SqlDbType.VarChar, 50, "LastName"));

При этом каждый параметр команды SQL создается в виде объекта класса System.Data.SqlClient.SqlParameter.

В качестве первого параметра (извиняемся за тавтологию) конструктору передается имя параметра команды SQL, снабженное префиксом @. Второй параметр задает тип данных, третий — размер данных, и, наконец, четвертый — имя соответствующего столбца в источнике данных. Заметим, что в классе System.Data.SqlClient.SqlParameter есть еще несколько перегруженных конструкторов, описание которых Вы найдете в документации.

Команда обновления данных также имеет параметры:

//
// sqlUpdateCommand1
//
this.sqlUpdateCommand1.CommandText =
  "UPDATE dbo.Contacts SET FirstName = @FirstName, LastName = @LastName WHERE (id = " +
  "@Original_id) AND (FirstName = @Original_FirstName) AND (LastName = @Original_La" +
  "stName); SELECT id, FirstName, LastName FROM dbo.Contacts WHERE (id = @id)";

this.sqlUpdateCommand1.Connection = this.sqlConnection1;

this.sqlUpdateCommand1.Parameters.Add(
  new System.Data.SqlClient.SqlParameter("@FirstName",
     System.Data.SqlDbType.VarChar, 50, "FirstName"));

this.sqlUpdateCommand1.Parameters.Add(
  new System.Data.SqlClient.SqlParameter("@LastName",
     System.Data.SqlDbType.VarChar, 50, "LastName"));

this.sqlUpdateCommand1.Parameters.Add(
  new System.Data.SqlClient.SqlParameter("@id",
     System.Data.SqlDbType.Int, 4, "id"));

this.sqlUpdateCommand1.Parameters.Add(
  new System.Data.SqlClient.SqlParameter("@Original_id",
     System.Data.SqlDbType.Int, 4,
     System.Data.ParameterDirection.Input, false, ((System.Byte)(0)),
     ((System.Byte)(0)), "id", System.Data.DataRowVersion.Original,
     null));

this.sqlUpdateCommand1.Parameters.Add(
  new System.Data.SqlClient.SqlParameter("@Original_FirstName",
     System.Data.SqlDbType.VarChar, 50,
     System.Data.ParameterDirection.Input, false, ((System.Byte)(0)),
     ((System.Byte)(0)), "FirstName",
     System.Data.DataRowVersion.Original, null));

this.sqlUpdateCommand1.Parameters.Add(
  new System.Data.SqlClient.SqlParameter("@Original_LastName",
     System.Data.SqlDbType.VarChar, 50,
     System.Data.ParameterDirection.Input, false, ((System.Byte)(0)),
     ((System.Byte)(0)), "LastName",
     System.Data.DataRowVersion.Original, null));

Как видите, для команды обновления требуется больше параметров. Наряду с параметрами, задающими новые значения для ячеек обновляемой строки @FirstName, @LastName и @id, необходимо передать исходные значения упомянутых ячеек посредством параметров @Original_FirstName, @Original_LastName и @Original_id.

Для передачи параметров с исходными значениями используется перегруженный вариант конструктора класса System.Data.SqlClient.SqlParameter, имеющий 10 параметров.

В частности, четвертый параметр задает направление передачи данных как System.Data.ParameterDirection.Input. Таким образом, здесь добавляются не выходные, а входные параметры. После выполнения команды эти параметры будут содержать исходные значения ячеек, какими они были до выполнения команды.

Команда удаления также имеет входные параметры:

//
// sqlDeleteCommand1
//
this.sqlDeleteCommand1.CommandText =
  "DELETE FROM dbo.Contacts WHERE (id = @Original_id) AND (FirstName = @Original_Fir" +
  "stName) AND (LastName = @Original_LastName)";

this.sqlDeleteCommand1.Connection = this.sqlConnection1;

this.sqlDeleteCommand1.Parameters.Add(
  new System.Data.SqlClient.SqlParameter("@Original_id",
     System.Data.SqlDbType.Int, 4,
     System.Data.ParameterDirection.Input, false, ((System.Byte)(0)),
     ((System.Byte)(0)), "id", System.Data.DataRowVersion.Original,
     null));

this.sqlDeleteCommand1.Parameters.Add(
  new System.Data.SqlClient.SqlParameter("@Original_FirstName",
     System.Data.SqlDbType.VarChar, 50,
     System.Data.ParameterDirection.Input, false, ((System.Byte)(0)),
     ((System.Byte)(0)), "FirstName",
     System.Data.DataRowVersion.Original, null));

this.sqlDeleteCommand1.Parameters.Add(
  new System.Data.SqlClient.SqlParameter("@Original_LastName",
     System.Data.SqlDbType.VarChar, 50,
     System.Data.ParameterDirection.Input, false, ((System.Byte)(0)),
     ((System.Byte)(0)), "LastName",
     System.Data.DataRowVersion.Original, null));

Через эти параметры команда передает исходные значения ячеек удаляемой строки.

Команды и параметры удобно редактировать не в исходном тексте приложения, а при помощи соответствующих графических средств.

Для редактирования параметров, например, команды обновления данных, раскройте список этих параметров в окне свойств объекта sqlDataAdapter1, как это показано на рис. 9‑26.

Рис. 9-26. Редактирование параметров команды обновления данных

Для изменения команды SQL, выполняющей обновление данных, отредактируйте здесь свойство CommandText. При этом на экране появится редактор запросов Query Builder, окно которого  показано на рис. 9-27.

Рис. 9-27. Редактор запросов Query Builder

С помощью этого редактора можно внести все необходимые изменения в текст команды SQL.

Для изменения параметров команды SQL отредактируйте свойство Parameters. При этом на экране появится редактор параметров, показанный на рис. 9-28.

Рис. 9-28. Редактор параметров команды SQL

И, наконец, рассмотрим программный код, который используется для установки параметров соединения с источником данных:

//
// sqlConnection1
//
this.sqlConnection1.ConnectionString =
  "data source=localhost;initial catalog=PhoneBook;
password=12345;persist security i" +
  "nfo=True;user id=c_sharp_app;workstation id=DR4;
packet size=4096";

Как видите, в свойство ConnectionString объекта sqlConnection1 записывается довольно длинная строка, содержащая значения параметров соединения. Каждый такой параметр задается при помощи строк имени и значения, объединенных оператором присваивания. Отдельные параметры в этой строке разделены символом точка с запятой.

Назначения некоторых параметров строки соединения мы привели в табл. 9-1.

Таблица 9-1. Параметры строки соединения

Имя

Описание

data source

Этот параметр может также задаваться как ServerAddress, Addr или Network Address.
Он задает имя или сетевой адрес узла сети, на котором работает сервер SQL

initial catalog

Может задаваться  как Database.

Этот параметр определяет имя базы данных, с которой осуществляется соединение

password

Может задаваться как Pwd.

Задает пароль для подключения к базе данных.

persist security info

Когда значение этого параметра равно false, парольная информация не включается в параметры соединения

user id

Идентификатор для подключения к серверу SQL

workstation id

Локальное имя рабочей станции, подключающейся к серверу SQL

packet size

Размер пакетов данных (в байтах), используемых для обмена данными с сервером SQL

Application Name

Имя приложения или провайдера данных .Net SqlClient Data Provider

AttachDBFilename

Может задаваться как extended properties или Initial File Name.

Полный путь и имя файла базы данных. При использовании этого параметра имя базы данных должно быть задано с использованием параметра Database

Connect Timeout

Может задаваться как Connection Timeout.

Продолжительность периода ожидания ответа от сервера перед выдачей сообщения об ошибке

Connection Lifetime

Время жизни соединения

Connection Reset

Режим автоматического завершения соединения

Current Language

Текущий национальный язык для сервера SQL

Network Library

Может задаваться как Net.

Этот параметр задает сетевую библиотеку, используемую для установки соединения с сервером SQL. Здесь можно указывать значения dbnmpntw  (Named Pipes), dbmsrpcn (Multiprotocol), dbmsadsn (Apple Talk), dbmsgnet (VIA), dbmsipcn (Shared Memory), dbmsspxn (IPX/SPX), и dbmssocn (TCP/IP)

Создание локального набора DataSet

Вернемся к нашему приложению SQLTestApp. На данный момент мы создали базу данных и проект приложения, а также добавили в проект адаптер данных и настроили соединение с источником данных. Теперь нашей задачей будет создание локального набора данных DataSet, отображаемого на базу данных, размещенную на сервере SQL.

Для этого щелкните правой клавишей мыши форму Вашего приложения и выберите из контекстного меню строку Generate DataSet. На экране появится одноименное диалоговое окно, показанное на рис. 9-29.

Рис. 9-29. Создание набора данных DataSet

Вам нужно отметить флажок New. При необходимости Вы также можете изменить имя набора данных в поле, расположенном справа от этого флажка.

Убедитесь также, что в списке Choose which table(s) to add to the dataset, определяющим таблицы, добавляемые в набор данных, выбрана таблица Contacts. Отметьте также флажок Add this dataset to the designer.

После того как Вы щелкните кнопку OK, будет создан и добавлен в проект приложения набор данных dataSet11.

Помимо этого к проекту приложения будет добавлена схема набора данных в виде файла DataSet1.xsd. Этот файл содержит описание таблицы на языке XML.

Если попытаться открыть файл DataSet1.xsd для редактирования, то в окне Microsoft Visual Studio .NET эта схема появится в виде таблицы (рис. 9-30).

Рис. 9-30. Редактирование схемы набора данных в визуальном режиме

Ячейки этой таблицы поддаются редактированию. На вкладке XML (рис. 9-30) Вы сможете  отредактировать файл схемы:

<?xml version="1.0" standalone="yes" ?>
<xs:schema id="DataSet1" targetNamespace="http://www.tempuri.org/DataSet1.xsd" xmlns:mstns="http://www.tempuri.org/DataSet1.xsd" xmlns="http://www.tempuri.org/DataSet1.xsd" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" attributeFormDefault="qualified" elementFormDefault="qualified">
  <xs:element name="DataSet1" msdata:IsDataSet="true" msdata:Locale="ru-RU">
     <xs:complexType>
       <xs:choice maxOccurs="unbounded">
          <xs:element name="Contacts">
            <xs:complexType>
              <xs:sequence>
                 <xs:element name="id" msdata:ReadOnly="true" msdata:AutoIncrement="true" type="xs:int" />
                 <xs:element name="FirstName" type="xs:string" />
                 <xs:element name="LastName" type="xs:string" />
              </xs:sequence>
            </xs:complexType>
          </xs:element>
       </xs:choice>
     </xs:complexType>
     <xs:unique name="Constraint1" msdata:PrimaryKey="true">
       <xs:selector xpath=".//mstns:Contacts" />
       <xs:field xpath="mstns:id" />
     </xs:unique>
  </xs:element>
</xs:schema>

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

Редактирование содержимого набора DataSet

Теперь нам нужно предусмотреть в нашем приложении средство просмотра и редактирования созданного на предыдущем этапе набора данных DataSet. Проще всего это сделать при помощи элемента управления DataGrid, о котором мы подробно рассказывали в предыдущей главе.

Итак, перетащите мышью элемент управления DataGrid из панели Toolbox и расположите его внутри формы приложения.

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

Прежде всего, установите значение свойства DataSource, равным dataSet11.

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

Загрузка набора данных

Добавьте в форму приложения кнопку с надписью Загрузить, а затем создайте для нее следующий обработчик событий:

private void button1_Click(object sender, System.EventArgs e)
{
  try
  {
     dataSet11.Clear();
     sqlDataAdapter1.Fill(dataSet11);
  }
  catch(Exception ex)
  {
     MessageBox.Show(ex.Message, "
Ошибка");
  }
}

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

Далее он загружает данных из источника данных в локальный набор данных dataSet11  с помощью адаптера sqlDataAdapter1 и метода Fill.

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

Сохранение отредактированного набора данных

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

Создайте для этой кнопки следующий обработчик событий:

private void button2_Click(object sender, System.EventArgs e)
{
  try
  {
     sqlDataAdapter1.Update(dataSet11);
  }
  catch(Exception ex)
  {
     MessageBox.Show(ex.Message, "Ошибка");
  }
}

Он сохраняет содержимое набора данных dataSet11 в источнике данных при помощи адаптера sqlDataAdapter1 и метода Update.

Теперь Вы можете испытать приложение в работе (рис. 9-31).

Рис. 9-31. Приложение SQLTestApp в действии

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

Парольный доступ к системе

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

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

Приложение LoginApp

В этом разделе мы создадим приложение LoginApp, предназначенное для создания и редактирования таблицы идентификаторов и паролей пользователей. Сама таблица с названием Users при этом хранится на сервере SQL. Для доступа к ней мы используем адаптер SqlDataAdapter, рассмотренный в предыдущих разделах этой главы, а также класс SqlDataReader.

При этом адаптер SqlDataAdapter мы будем использовать для получения и обновления содержимого таблицы Users, а класс SqlDataReader — для проверки идентификатора и пароля пользователя.

Внешний вид окна приложения LoginApp показан на рис. 9-32.

Рис. 9-32. Приложение LoginApp

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

При самом первом подключении к системе идентификатор и пароль вводить не надо. При этом наше приложение автоматически создаст в таблице Users строку администратора с идентификатором admin и паролем password, отображая на экране сообщение об этом событии (рис. 9-33). Данную парольную информацию можно будет затем использовать для первого подключения к системе, изменив ее в дальнейшем.

Рис. 9-33. Эта учетная запись создается автоматически

Если в таблице Users уже есть хотя бы одна запись, необходимо ввести правильный идентификатор и пароль. В этом случае содержимое таблицы Users будет отображено в окне приложения с помощью адаптера SqlDataAdapter и элемента управления DataGrid.

Помимо идентификаторов и паролей пользователей таблица Users хранит код доступа каждого пользователя. В нашем приложении пользователям с кодом доступа 80 разрешается модифицировать таблицу Users, а всем остальным пользователям — только просматривать содержимое этой таблицы.

Для сохранения изменений в таблице Users, расположенной на сервере SQL, мы предусмотрели кнопку Обновить. Эта кнопка, однако, будет разблокирована только в том случае, если код доступа пользователя равен 80.

Таблица Users

Прежде чем приступить к созданию приложения LoginApp Вам нужно будет создать в базе данных PhoneBook (с которой мы работали раньше) новую таблицу Users.

В таблице Users нужно предусмотреть столбцы id, Login, Password и assess. На рис. 9-34 мы показали таблицу Users в окне дизайнера таблиц утилиты SQL Server Enterprise Manager.

Рис. 9-34. Таблица Users

Столбец id предназначен для хранения первичного ключа. Столбцы Login и Password хранят, соответственно, идентификатор пользователя и пароль. Что же касается столбца access, то он хранит код доступа в виде целого числа. Если этот код равен 80, пользователю разрешается модифицировать таблицу Users, а если нет — только просматривать.

Ниже мы привели сценарий SQL, с помощью которого можно создать таблицу Users и определить для нее первичный ключ:

CREATE TABLE [dbo].[Users] (
  [id] [int] IDENTITY (0, 1) NOT NULL ,
  [Login] [varchar] (50) COLLATE Cyrillic_General_CI_AS NOT NULL ,
  [Password] [varchar] (50) COLLATE Cyrillic_General_CI_AS NOT NULL ,
  [access] [int] NOT NULL
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[Users] WITH NOCHECK ADD
  CONSTRAINT [PK_Users] PRIMARY KEY  CLUSTERED
  (
     [id]
  ) ON [PRIMARY]
GO

Создание проекта приложения

Создайте приложения LoginApp при помощи мастера проектов, а затем  добавьте в его главное окно поля ввода идентификатора пользователя и пароля, элемент управления DadaGrid, а также кнопки Войти и Обновить.

Расположение этих элементов управления в окне приложения показано на рис. 9-32.

Соединение с базой данных

Затем откройте вкладку Data инструментальной панели Toolbox системы Microsoft Visual Studio .NET и перетащите из нее в окно приложения значок компонента SqlConnection, обеспечивающего соединение с базой данных. Настраивая свойство ConnectionString, созданного при этом объекта sqlConnection1, Вы можете создать новое соединение, как это было описано ранее в этой главе, или использовать соединение, созданное ранее для приложения SQLTestApp.

Запросы SQL

Далее перетащите два раза в окно приложения компонент SqlCommand. В результате будет создано два объекта, предназначенных для выполнения команд SQL.

Первый из них с идентификатором sqlCommand1 предназначен для выборки информации из таблицы Users. Ниже мы показали строку команды SQL, которую нужно записать в свойство CommandText:

SELECT id, login, password, access FROM dbo.Users

Объект sqlCommand2 содержит команду с параметрами, предназначенную для поиска в таблице Users строки пользователя с заданным идентификатором и паролем:

SELECT id, login, password, access FROM dbo.Users
  WHERE (login = @login) AND (password = @password)

Вы можете подготовить эту команду с помощью мастера Query Builder (рис. 9-35).

Рис. 9-35. Подготовка команды для объекта sqlCommand2

Окно этого мастера появится на экране при редактировании свойства CommandText объекта sqlCommand2.

Здесь Вам нужно в окне Users отметить флажки всех столбцов таблицы.

Кроме того, в столбце Criteria задайте два условия для полей Login и Password:

= @login
= @password

Когда текст запроса SQL будет готов, щелкните кнопку OK чтобы закрыть окно мастера Query Builder.

Так как текст команды SQL ссылается на параметры @login и @password, нам нужно определить эти параметры, отредактировав свойство Parameters объекта sqlCommand2. Эта операция выполняется с помощью редактора SqlParameter Collection Editor (рис. 9-36).

Рис. 9-36. Свойства параметра @login

Каждый параметр имеет набор свойств. Эти свойства можно редактировать в правой части окна редактора.

Установите свойства параметра @login, как это показано на рис. 9-36. Установку свойства параметра @password мы показали на рис. 9-37.

Рис. 9-37. Свойства параметра @password

Как видите, отличия есть только в свойствах SourceColumn и ParameterName, задающих столбец таблицы и название параметра, соответственно.

Адаптер SqlDataAdapter

Для редактирования содержимого таблицы Users в приложении LoginApp мы будем использовать те же самую технологию, что и в ранее рассмотренном приложении SQLTestApp, основанную на использовании адаптера SqlDataAdapter в паре с элементом управления DataGrid.

Итак, перетащите значок компонента  SqlDataAdapter в окно приложения LoginApp. При этом в приложении будет создан объект sqlDataAdapter1.

Во втором окне мастера адаптеров выберите то же самое соединение, что было использовано для объекта sqlConnection1. Настройте свойства адаптера аналогично тому, как Вы это делали для приложения SQLTestApp (рис. 9-38), но укажите в командах SQL таблицу Users.

Рис. 9-38. Настройка свойств адаптера sqlDataAdapter1

Кроме того, создайте набор данных DataSet, аналогичный набору данных из приложения SQLTestApp, в котором будет храниться содержимое таблицы Users. Отображение таблиц, задаваемое свойством TableMappings объекта sqlDataAdapter1, должно быть таким, как показано на рис. 9-39.

 

Рис. 9-39. Настройка отображения таблиц

Ссылка на созданный набор данных класса DataSet будет храниться в поле dataSet11.

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

Как мы уже говорили, элемент управления DataGrid будет использоваться в нашем приложении для просмотра и редактирования содержимого набора данных dataSet11. В этом наборе данных будет находиться содержимое таблицы Users, скопированной с сервера SQL.

Настраивая свойства элемента управления DataGrid, укажите в свойстве DataSource ссылку на объект dataSet11, а в свойстве DataMember — таблицу Users.

Для того чтобы снабдить отображаемую таблицу заголовком, задайте текст заголовка в свойстве CaptionText. Напомним, что шрифт заголовка можно изменить, редактируя свойство CaptionFont.

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

Для того чтобы скрыть столбец id, который не содержит никакой информации, интересной для пользователя приложения, создайте стиль таблицы, отредактировав свойство TableStyles. Добавьте один стиль, как это показано на рис. 9-40.

Рис. 9-40. Добавление стиля таблицы

Укажите в свойстве MappingName имя таблицы Users.

Далее Вам нужно создать три стиля для каждого столбца таблицы, отредактировав свойство GridColumnStyles (рис. 9-41).

Рис. 9-41. Стиль для столбца идентификаторов пользователя

Здесь Вы должны указать привязку каждого столбца при помощи свойства MappingName, а также задать заголовок столбца, отредактировав свойство HeaderText. В результате получится таблица со столбцами Идентификатор, Пароль и Доступ (рис. 9-32).

Подключение пользователя

Согласно логике работы приложения LoginApp, когда пользователь щелкает кнопку Войти, выполняется проверка идентификатора и пароля пользователя. Если предъявленная парольная информация отсутствует в таблице Users, а также, если идентификатор и пароль не соответствуют друг другу, приложение выводит сообщение на экран с текстом «Доступ запрещен».

Когда идентификатор и пароль указан правильно, приложение проверяет код доступа пользователя. Как мы уже говорили, пользователю с кодом доступа 80 разрешается редактирование таблицы Users, а пользователям с другими кодами доступа — только просмотр этой таблицы.

Чтобы реализовать такое поведение, создайте для кнопки Войти следующий обработчик событий:

private void button1_Click(object sender, System.EventArgs e)
{
  SqlDataReader myReader;

  try
  {
     sqlConnection1.Open();
     myReader = sqlCommand1.ExecuteReader();
      
     if(!myReader.Read())
     {
       sqlConnection1.Close();
       myReader.Close();
       CreateAdminLogin();
     }
     else
     {
       myReader.Close();

       sqlCommand2.Parameters["@login"].Value = LoginTextBox.Text;
       sqlCommand2.Parameters["@password"].Value =
          passwordTextBox.Text;

       myReader = sqlCommand2.ExecuteReader();

       if(!myReader.Read())
       {
          MessageBox.Show("Доступ запрещен", "Ошибка");
          myReader.Close();
          sqlConnection1.Close();
          return;
       }

       if(LoginTextBox.Text == myReader.GetString(1) &&
          passwordTextBox.Text == myReader.GetString(2))
       {
          if(myReader.GetInt32(3) != 80)
          {
            dataGrid1.ReadOnly = true;
            button2.Enabled = false;
          }
          else
          {
            dataGrid1.ReadOnly = false;
            button2.Enabled = true;
          }

          myReader.Close();

          dataSet11.Clear();
          sqlDataAdapter1.Fill(dataSet11);
       }
       else
       {
          MessageBox.Show("Доступ запрещен", "Ошибка");
       }
       myReader.Close();
       sqlConnection1.Close();
     }
  }
  catch(Exception ex)
  {
     MessageBox.Show(ex.Message, "Ошибка");
  }
}

Рассмотрим работу этого обработчика в деталях.

Прежде всего, заметим, что мы применили здесь класс SqlDataReader. Этот класс очень удобен для извлечения результатов запросов SQL в режиме последовательного чтения.

Получив управление, обработчик события button1_Click открывает соединение sqlConnection1, о котором мы говорили раньше:

sqlConnection1.Open();

Параметры этого соединения мы задавали во время проектирования нашего приложения.

Далее наш обработчик событий исполняет команду SQL, хранящуюся в объекте  sqlCommand1:

SqlDataReader myReader;

myReader = sqlCommand1.ExecuteReader();

В результате выполнения метода ExecuteReader создается объект класса SqlDataReader, позволяющий последовательно прочитать все строки результата выполнения команды SQL.

Напомним, что эта команда выбирает все строки таблицы Users, возвращая столбцы id, login, password и access. Для выборки нужно использовать метод SqlDataReader.Read, вызывая его в цикле.

Когда все строки, полученные в результате выполнения команды SQL, будут получены, метод SqlDataReader.Read возвратит значение false. Мы используем это обстоятельство в самом начале работы обработчика событий button1_Click, для того чтобы определить, имеются ли в таблице Users какие-либо записи:

if(!myReader.Read())
{
  sqlConnection1.Close();
  myReader.Close();
  CreateAdminLogin();
}
else
{
  …
}

Если таблица Users пустая (как бывает при самом первом запуске приложения), обработчик закрывает соединение методом sqlConnection1.Close, а также закрывает объект SqlDataReader. После этого он вызывает метод CreateAdminLogin, предназначенный для создания самой первой учетной записи администратора в таблице Users. Исходный текст этого метода мы рассмотрим чуть позже.

Теперь мы рассмотрим ситуацию, когда в таблице Users уже имеются какие-то записи. Вы можете добавить новые записи в эту таблицу, например, вручную с помощью утилиты Microsoft SQL Server Enterprise Manager.

Прежде всего, в этом случае мы закрываем ненужный нам больше объект SqlDataReader:

myReader.Close();

Теперь нам нужно выполнить команду sqlCommand2, которая выбирает из таблицы Users учетные записи с заданным идентификатором пользователя и паролем. Перед выполнением этой команды необходимо подготовить параметры @login и @password, воспользовавшись для этого свойством Value контейнера sqlCommand2.Parameters:

sqlCommand2.Parameters["@login"].Value = LoginTextBox.Text;
sqlCommand2.Parameters["@password"].Value = passwordTextBox.Text;
myReader = sqlCommand2.ExecuteReader();

В результате выполнения этого запроса будет создан объект класса SqlDataReader. Если таблица Users не содержит ни одной записи с заданным идентификатором пользователя и паролем, то метод myReader.Read вернет значение false:

if(!myReader.Read())
{
  MessageBox.Show("Доступ запрещен", "Ошибка");
  myReader.Close();
  sqlConnection1.Close();
  return;
}

Это означает, что был задан неправильный идентификатор или пароль пользователя. В этом случае наш обработчик событий выводит сообщение об отказе в доступе, закрывает объект myReader и соединение sqlConnection1, а затем возвращает управление.

Теперь мы рассмотрим случай, когда идентификатор и пароль был введен правильно.

Наша программа выполняет дополнительную проверку, сравнивая данные из ячеек строки, считанной методом myReader.Read, со значениями, введенными пользователем в полях Идентификатор и Пароль:

if(LoginTextBox.Text == myReader.GetString(1) &&
  passwordTextBox.Text == myReader.GetString(2))
{
  …
}
else
{
  MessageBox.Show("Доступ запрещен", "Ошибка");
}
myReader.Close();
sqlConnection1.Close();

Обратите внимание на то, как мы извлекаем значения ячеек прочитанной строки — для получения текстовой строки мы вызываем метод myReader.GetString, указывая ей в качестве параметра индекс столбца. Самый первый столбец нашей таблицы id имеет индекс 0, второй столбец Login — индекс 1 и т.д.

В табл. 9-2 мы перечислили некоторые методы класса SqlDataReader, предназначенные для извлечения из ячеек строки данных различных типов, встроенных в язык программирования C#.

Таблица 9-2. Методы класса SqlDataReader для извлечения содержимого ячеек строки стандартных типов данных

Имя метода

Тип возвращаемого значения

GetBoolean

bool

GetByte

byte

GetChar

char

GetDateTime

DateTime

GetDecimal

decimal

GetDouble

double

GetFloat

float

GetGuid

Guid

GetInt16

short

GetInt32

int

Getint64

long

GetString

string

Метод GetValue позволяет получить данные в естественном формате, т.е. в том формате, в котором они хранятся в ячейке строки.

Специально для работы с серверами баз данных, такими как Microsoft SQL Server, этот набор был расширен методами, возвращающими данные в форматах этих серверов. Некоторые из этих методов перечислены в табл. 9-3.

Таблица 9-3. Методы класса SqlDataReader для извлечения содержимого ячеек строки типов данных серверов SQL

Имя метода

Тип возвращаемого значения

Описание

GetSqlBinary

SqlBinary

Поток двоичных данных переменной длины

GetSqlBoolean

SqlBoolean 

Целое значение 0 или 1

GetSqlByte

SqlByte 

Целое значение без знака размером 8 разрядов и значением от 0 до 255

GetSqlDateTime

SqlDateTime

Дата и время, исчисляемое в периодах системного таймера от 1 января 1753 года

GetSqlDecimal

SqlDecimal

Числовое значение с фиксированной точкой в диапазоне от -1038 -1 до 10 38 –1

GetSqlDouble

SqlDouble

Числовое значение с плавающей точкой в диапазоне от -1.79E +308 до 1.79E +308

GetSqlGuid

SqlGuid

Глобальный уникальный идентификатор GUID

GetSqlInt16

SqlInt16

16-разрядное целое со знаком

GetSqlInt32

SqlInt32

32-разрядное целое со знаком

GetSqlInt64

SqlInt64

64-разрядное целое со знаком

GetSqlMoney

SqlMoney

Денежные суммы в диапазоне от -263 (‑922 337 203 685 477,5808) до 2 63 -1 (+922 337 203 685 477,5807)

GetSqlSingle

SqlSingle

Числовое значение с плавающей точкой в диапазоне от -3.40E +38 до 3.40E +38

GetSqlString

SqlString

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

Вернемся к нашему приложению.

Итак, обработчик события button1_Click, создаваемого кнопкой Войти, определил, что пользователь ввел правильный идентификатор и пароль. Теперь ему нужно получить код доступа ткущего пользователя. Он извлекает этот код из столбца access, имеющего индекс 3, при помощи метода GetInt32:

if(myReader.GetInt32(3) != 80)
{
  dataGrid1.ReadOnly = true;
  button2.Enabled = false;
}
else
{
  dataGrid1.ReadOnly = false;
  button2.Enabled = true;
}

Если код доступа не равен 80, обработчик события приравнивает свойству dataGrid1.ReadOnly значение true. Это запрещает редактирование строк, отображаемых в окне элемента управления DataGrid. Кроме этого, обработчик блокирует кнопку Обновить, приравнивая свойству  button2.Enabled значение false.

В том случае, когда код доступа пользователя равен 80, обработчик события разрешает редактирование строк и разблокирует кнопку Обновить, изменяя соответствующим образом свойства dataGrid1.ReadOnly и button2.Enabled.

Завершив проверку прав доступа пользователя, обработчик событий закрывает объект myReader, т.к. он больше не понадобится:

myReader.Close();

Дальнейшие действия обработчика button1_Click заключаются в очистке содержимого набора данных dataSet11 с последующим его наполнением из таблицы Users сервера SQL:

dataSet11.Clear();
sqlDataAdapter1.Fill(dataSet11);

Эта работа выполняется с помощью адаптера sqlDataAdapter1. Соответствующие процедуры были подробно описаны в разделе «Приложение SQLTestApp» этой главы.

Обновление таблицы Users

Для обновления таблицы Users, отредактированной пользователем с кодом доступа 80, мы применяем адаптер sqlDataAdapter1 и метод Update. Вот исходный текст обработчика событий кнопки Обновить:

private void button2_Click(object sender, System.EventArgs e)
{
  try
  {
     sqlDataAdapter1.Update(dataSet11);
  }
  catch(Exception ex)
  {
     MessageBox.Show(ex.Message, "Ошибка");
  }
}

Точно такая же методика обновления применялась и для таблицы Contacts в упомянутом ранее приложении SQLTestApp.

Хранение дерева в базе данных

Очень часто возникает задача хранения информации, организованной иерархически. Реляционные базы данных, такие как Microsoft SQL Server, позволяют хранить информацию в виде иерархического дерева, причем для представления такого дерева нужна всего одна таблица.

Каждая строка таблицы, хранящей структуру дерева, соответствует одному узлу этого дерева. При этом в таблице должно быть, как минимум, два столбца. Первый из них должен содержать уникальные идентификаторы строки (т.е. идентификаторы узлов), а второй — идентификатор соответствующего родительского узла. Для корневого узла в качестве идентификатора родительского узла обычно используется нулевое или какое-либо другое особое значение.

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

Приложение ArticlesApp

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

Главное окно приложения ArticlesApp  показано на рис. 9-42.

Рис. 9-42. Главное окно приложения ArticlesApp

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

Заметим, что сразу после запуска приложения, когда в базе данных нет ни одной записи, дерево заголовков не содержит ни одного элемента. Если щелкнуть окно дерева правой клавишей мыши, на экране появится контекстное меню со строками Add, Delete и Edit.

Выбор строки Add приведет к тому, что на экране появится диалоговое окно, показанное на рис. 9-43.

Рис. 9-43. Добавление новой или редактирование существующей статьи

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

Если в дереве нет ни одного элемента, то при первом использовании строки Add контекстного меню в дерево будет добавлен корневой элемент.

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

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

С помощью строки Delete можно удалить элемент дерева. Заметим, что программа удаляет только элементы, не имеющие дочерних элементов. Попытки удалить элемент с дочерними элементами наше приложение игнорирует.

Для чего нужен вес сортировки?

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

База данных Articles

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

Таблица Tree

Таблица Tree предназначена для хранения структуры дерева. В ней необходимо создать четыре столбца с именами id, parent_id, title и weight. Столбец id должен быть первичным ключом.

Вот сценарий SQL, при помощи которого можно создать таблицу Tree:

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]
GO

ALTER TABLE [dbo].[Tree] WITH NOCHECK ADD
  CONSTRAINT [PK_Tree] PRIMARY KEY CLUSTERED
  (
     [id]
  ) ON [PRIMARY]
GO

Здесь столбец id хранит идентификаторы узлов дерева, а столбец parent_id — идентификаторы родительских узлов. Таким образом, вместе с каждым узлом хранится идентификатор его родительского узла.

Поля title и weight предназначены, соответственно, для хранения заголовка статьи и веса сортировки, назначенного этой статье.

Таблица Documents

Мы могли бы хранить тексты документов непосредственно в таблице Tree, однако это привело бы к неэффективному расходованию памяти.

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

Для хранения текстов статей мы создали отдельную таблицу Documents, содержащую столбцы id, document и tree_id. Первый из этих столбцов является ключевым:

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]
GO

ALTER TABLE [dbo].[Documents] WITH NOCHECK ADD
  CONSTRAINT [PK_Documents] PRIMARY KEY CLUSTERED
  (
     [id]
  ) ON [PRIMARY]
GO

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

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

И, наконец, столбец document хранит текст самой статьи.

Хранимая процедура sp_InsertDocument

Часть работы с базой данных наше приложение будет выполнять при помощи команд 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
GO

Этой процедуре необходимо передать два параметра @tree_id и @document. Первый из этих параметров предназначен для передачи идентификатора узла, в который добавляется статья, а второй — для передачи текста этой статьи.

Процедура возвращает идентификатор добавленной строки @@identity.

Хранимая процедура sp_ InsertNode

Хранимая процедура 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
GO

Этой процедуре нужно передать через входные параметры идентификатор родительского узла @parent_id (равный 0 для корневого узла), заголовок статьи @title и вес сортировки @weight.

Хранимая процедура sp_ UpdateDocument

При помощи хранимой процедуры sp_UpdateDocument наше приложение обновляет тексты статей, хранящиеся в таблице Documents:

CREATE PROCEDURE [dbo].[sp_UpdateDocument]
@tree_id AS INT,
@document AS VARCHAR(2000)
AS

UPDATE dbo.Documents SET document = @document  WHERE (tree_id = @tree_id)
GO

В качестве параметра этой хранимой процедуре необходимо передать идентификатор узла @tree_id обновляемой статьи, а также текст статьи @document.

Создание проекта приложения ArticlesApp

В окно нашего приложения нужно поместить элемент управления TreeView, разделитель Splitter, а также редактор текста RichTextBox. Дерево TreeView должно занимать левую часть окна, а редактор RichTextBox — правую. Соответствующие рекомендации по настройке свойств элементов управления TreeView и RichTextBox мы приводили в 7 главе.

Помимо только что перечисленных элементов управления в проект приложения ArticlesApp нужно будет добавить множество других программных компонентов, предназначенных главным образом для работы с сервером базы данных (рис. 9-44).

Рис. 9-44. Компоненты приложения ArticlesApp

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

Соединение с базой данных

Прежде всего, обеспечьте приложение возможностью соединения с базой данных Articles. С этой целью добавьте программный компонент SqlConnection. Идентификатор этого компонента будет храниться в поле sqlConnection1 класса Form1.

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

data source=localhost;initial catalog=Articles;password=12345;
persist security info=True;user id=c_sharp_app;
workstation id=FROLOV;packet size=4096

Вероятно, у Вас будет другое значение идентификатора рабочей станции workstation id, а также, возможно, идентификатор пользователя user id и пароль password.

Добавление адаптера SqlDataAdapter

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

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

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

Создание набора данных DataSet

После добавления адаптера создайте набор данных DataSet, воспользовавшись строкой Generate Dataset контекстного меню. Это меню появится на экране, если щелкнуть правой клавишей мыши область окна дизайнера форм, занимаемую значками программных компонентов (нижняя часть окна, показанного на рис. 9-44).

В результате должен быть создан набор данных dataSet11. Кроме того, в проект будут автоматически добавлены компоненты класса SqlCommand, предназначенные для чтения, обновления, удаления и добавления данных в таблицу Tree. Эти команды предназначены для совместной работы с адаптером sqlDataAdapter1, но могут использоваться и независимо от него.

Добавление контекстного меню

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

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

Если выделить левой клавишей мыши компонент contextMenu1 в области значков программных компонентов дизайнера форм, то в верхней части формы появится меню Context Menu, показанное на рис. 9-45.

Рис. 9-45. Редактирование контекстного меню

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

Создайте в контекстном меню строки Add, Delete и Edit. Первая из этих строк будет использована для создания узлов дерева, вторая — для удаления этих узлов, а третья — для редактирования информации, хранящейся в узле дерева (заголовка, веса сортировки и текста статьи).

Создание узла дерева

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

Сейчас мы реализуем эту часть приложения, отвечающую за добавление новых узлов в дерево.

Прежде всего, создайте обработчик событий для строки Add контекстного меню contextMenu1, щелкнув его дважды левой клавишей мыши. Отредактируйте исходный текст обработчика следующим образом:

private void menuItem1_Click(object sender, System.EventArgs e)
{
  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();

Метод AddNode, определенный в нашем приложении, создает узел дерева. В качестве единственного параметра этому методу нужно передать идентификатор родительского узла. Так как в первый раз мы создаем корневой узел, то передаем методу AddNode нулевое значение.

Что же касается метода UpdateTree, то он тоже определен в нашем приложении. Его задачей является наполнение окна дерева treeView1 содержимым таблицы Tree базы данных Articles. Мы вызываем этот метод всякий раз после внесения изменений в структуру дерева (т.е. после добавления или удаления узлов дерева).

Для того чтобы содержимое дерева отображалось сразу осле запуска приложения, мы добавили вызов метода UpdateTree в конструктор класса Form1:

public Form1()
{
  //
  // Required for Windows Form Designer support
  //
  InitializeComponent();

  //
  // TODO: Add any constructor code after InitializeComponent call
  //
  UpdateTree();
}

Методы AddNode и UpdateTree мы рассмотрим в деталях позже, а пока вернемся к обработчику событий menuItem1_Click, созданному нами для строки Add контекстного меню.

В том случае, если в дереве есть узлы, и пользователь выделил какой-либо узел левой клавишей мыши или при помощи клавиатуры, наш обработчик событий menuItem1_Click выполняет следующие действия:

if(treeView1.SelectedNode != null)
{
  int id = (int)treeView1.SelectedNode.Tag;
  AddNode(id);
  UpdateTree();
}

Вначале он извлекает из свойства treeView1.SelectedNode.Tag идентификатор строки таблицы Tree, соответствующий выделенному узлу. Этот идентификатор записывается в данное свойство методом UpdateTree в процессе построения дерева.

Заметим, что данный идентификатор обозначает узел, являющийся родительским по отношению к создаваемому узлу. Обработчик событий menuItem1_Click передает этот идентификатор методу AddNode, а затем перерисовывает обновленное дерево методом UpdateTree:

AddNode(id);
UpdateTree();

Метод AddNode

Создайте в классе Form1 метод AddNode. Как мы только что говорили, этот метод предназначен для создания нового узла в дереве заголовков статей. Он не только добавляет новый узел в окно элемента управления treeView1, но и создает все необходимые записи в базе данных Articles.

Ниже мы привели полный исходный текст метода AddNode:

public void AddNode(int id)
{
  Form2 dialog = new Form2();
  if(DialogResult.Yes == dialog.ShowDialog())
  {
     sqlConnection1.Open();
     try
     {
       SqlCommand cmd = new SqlCommand("sp_InsertNode",
          sqlConnection1);
       cmd.CommandType = CommandType.StoredProcedure;

       SqlParameter param = cmd.Parameters.Add("RETURN_VALUE",
          SqlDbType.Int);

       param.Direction = ParameterDirection.ReturnValue;
         
       cmd.Parameters.Add("@parent_id", SqlDbType.Int).Value = id;
       cmd.Parameters.Add("@title", SqlDbType.VarChar).Value =
          dialog.Title;
       cmd.Parameters.Add("@weight", SqlDbType.Int).Value =
          dialog.Weight;

       cmd.ExecuteNonQuery();

       int tree_id = (int)cmd.Parameters["RETURN_VALUE"].Value;

       cmd = new SqlCommand("sp_InsertDocument", sqlConnection1);
       cmd.CommandType = CommandType.StoredProcedure;

       param = cmd.Parameters.Add("RETURN_VALUE", SqlDbType.Int);
       param.Direction = ParameterDirection.ReturnValue;
         
       cmd.Parameters.Add("@tree_id", SqlDbType.Int).Value = tree_id;
       cmd.Parameters.Add("@document", SqlDbType.VarChar).Value =
          dialog.Document;

       cmd.ExecuteNonQuery();

       int document_id = (int)cmd.Parameters["RETURN_VALUE"].Value;
     }
     catch(Exception ex)
     {
       MessageBox.Show(ex.Message, "Ошибка");
     }
     sqlConnection1.Close();
  }
}

Займемся описанием этого исходного текста.

Диалоговое окно для ввода данных добавляемого узла

В самом начале работы метод AddNode отображает на экране диалоговое окно, показанное на рис. 9-43. Вам нужно будет создать форму для данного окна, но этим мы займемся позже.

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

Form2 dialog = new Form2();
if(DialogResult.Yes == dialog.ShowDialog())
{
  …
}

Открытие и закрытие соединения с базой данных

Для добавления данных, прежде всего, открывается соединение с базой данных:

sqlConnection1.Open();

Все дальнейшие операции выполняются в теле оператора try-catch, что позволяет перехватывать ошибки и отображать текст сообщений об ошибках на экране:

try
{
  …
}
catch(Exception ex)
{
  MessageBox.Show(ex.Message, "
Ошибка");
}

sqlConnection1.Close();

После обновления содержимого базы данных метод AddNode закрывает соединение с базой данных.

Использование хранимых процедур

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

Прежде всего, наша программа вызывает хранимую процедуру sp_InsertNode, предназначенную для добавления новой строки в таблицу Tree, хранящую структуру дерева.  Напомним, что этой процедуре нужно передать через входные параметры идентификатор родительского узла @parent_id, заголовок статьи @title и вес сортировки @weight.

Вызов хранимой процедуры начинается с создания объекта класса SqlCommand:

SqlCommand cmd = new SqlCommand("sp_InsertNode",  sqlConnection1);

Далее программа должна задать тип команды в свойстве CommandType в виде константы CommandType.StoredProcedure:

cmd.CommandType = CommandType.StoredProcedure;

Это означает, что команда содержит не строку SQL, а имя хранимой процедуры.

Параметры хранимых процедур

На следующем этапе необходимо добавить параметры хранимой процедуры.  Наша хранимая процедура sp_InsertNode имеет три входных и один выходной параметр.

Через выходной параметр со специальным именем RETURN_VALUE хранимая процедура возвращает идентификатор добавленной строки:

SqlParameter param = cmd.Parameters.Add("RETURN_VALUE",
  SqlDbType.Int);

В качестве первого параметра методу Add передается имя параметра хранимой процедуры, а в качестве второго — тип данных, соответствующих этому параметру.

Чтобы указать, что этот параметр является выходным, мы записываем константу ParameterDirection.ReturnValue в свойство параметра с именем Direction:

param.Direction = ParameterDirection.ReturnValue;

Если этого не сделать, то по умолчанию параметр будет входным.

Вот как мы указываем входные параметры для хранимой процедуры sp_InsertNode:

cmd.Parameters.Add("@parent_id", SqlDbType.Int).Value = id;
cmd.Parameters.Add("@title", SqlDbType.VarChar).Value = dialog.Title;
cmd.Parameters.Add("@weight", SqlDbType.Int).Value = dialog.Weight;

Обратите внимание на то, что тип числовых данных указан как SqlDbType.Int, а тип строчных данных — как SqlDbT ype.VarChar.

Параметру хранимой процедуры @parent_id мы присваиваем значение идентификатора родительского узла, который передается при вызове методу AddNode. Что же касается параметров @title и @weight, то для их инициализации мы извлекаем значения из свойств Title и Weight, определенных нами в классе Form2 диалогового окна ввода данных узла (рис. 9-43).

Запуск хранимой процедуры

Для запуска хранимой процедуры на выполнение мы вызываем метод ExecuteNonQuery:

cmd.ExecuteNonQuery();

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

Получение значений выходных параметров

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

Вот как мы извлекаем значение, возвращаемое хранимой процедурой sp_InsertNode:

int tree_id = (int)cmd.Parameters["RETURN_VALUE"].Value;

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

Добавление текста документа

Для добавления текста документа, извлеченного из свойства dialog.Document диалогового окна класса Form2 мы вызываем хранимую процедуру sp_InsertDocument:

cmd = new SqlCommand("sp_InsertDocument", sqlConnection1);
cmd.CommandType = CommandType.StoredProcedure;

Эта процедура имеет один выходной параметр и два входных:

param = cmd.Parameters.Add("RETURN_VALUE", SqlDbType.Int);
param.Direction = ParameterDirection.ReturnValue;
         
cmd.Parameters.Add("@tree_id", SqlDbType.Int).Value = tree_id;
cmd.Parameters.Add("@document", SqlDbType.Text).Value =
  dialog.Document;

Выходной параметр получает получить идентификатор новой строки в таблице Documents. Мы извлекаем его только для примера, но в приложении не используем. Что же касается входных параметров, то хранимой процедуре sp_InsertDocument передается идентификатор узла @tree_id обновляемой статьи, а также текст статьи @document.

Обратите внимание, что параметр @document имеет тип SqlDbType.Text.

Хранимая процедура  sp_InsertDocument запускается при помощи метода ExecuteNonQuery, после чего извлекается результат ее выполнения:

cmd.ExecuteNonQuery();
int document_id = (int)cmd.Parameters["RETURN_VALUE"].Value;

Диалоговая форма редактирования документа

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

Для того чтобы программа могла инициализировать поля формы, а также получать значения, введенные в ней пользователем, необходимо создать в классе Form2 свойства Title, Weight и Document. Каждое из этих свойств должно содержать методы доступа set и get.

Свойство Title

Свойство Title должно быть определено следующим образом:

public string Title
{
  get
  {
     return textBox1.Text;
  }
  set
  {
     textBox1.Text = value;
  }
}

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

Свойство Weight

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

Вот исходный текст методов доступа свойства Weight:

public int Weight
{
  get
  {
     return (int)numericUpDown1.Value;
  }
  set
  {
     numericUpDown1.Value = value;
  }
}

Свойство Document

Текст статей мы создаем и редактируем при помощи элемента управления richTextBox1. Вот исходный текст методов доступа свойства Document, с помощью которого наша программа может отображать исходный текст статьи, а также загружать отредактированный вариант статьи:

public string Document
{
  get
  {
     return richTextBox1.Text;
  }
  set
  {
     richTextBox1.Text = value;
  }
}

Построение дерева

Для построения дерева заголовков статей в окне элемента управления TreeView в нашем приложении определен метод UpdateTree, на который мы уже ссылались ранее, а также метод CreateNodes.

Метод UpdateTree

Метод UpdateTree считывает структуру дерева из базы данных Articles и отображает ее в окне элемента управления treeView1 класса TreeView.

Вот исходный текст этого метода:

public void UpdateTree()
{
  dataSet11.Clear();
  sqlDataAdapter1.Fill(dataSet11);

  treeView1.Nodes.Clear();
  CreateNodes(0,(TreeNode)null);
  treeView1.ExpandAll();
}

Получив управление, метод UpdateTree очищает набор данных dataSet11, а затем наполняет его из таблицы Tree базы данных Aticles, пользуясь для этого адаптером sqlDataAdapter1 и методом Fill.

На следующем этапе метод UpdateTree удаляет все элементы из дерева treeView1.

Заполнение дерева treeView1 содержимым набора данных dataSet11 осуществляется методом CreateNodes. Далее все узлы заполненного дерева раскрываются, для чего программа вызывает метод treeView1.ExpandAll.

Метод CreateNodes

Теперь нам нужно изучить метод CreateNodes, который создает в дереве treeView1 узел, соответствующий одной статье. Ниже мы привели исходный текст этого метода:

public void CreateNodes(int iParent, TreeNode pNode)
{
  DataView dvwData = new DataView(dataSet11.Tables[0]);
  dvwData.RowFilter =  "[parent_id] = " + iParent;
      
  foreach(DataRowView Row in dvwData)
  {
     int id = Int32.Parse(Row["id"].ToString());

     if(pNode == null)
     {
       TreeNode zNode = treeView1.Nodes.Add(Row["title"].ToString() +
          " (" + Row["weight"].ToString() + ")");
       zNode.Tag = id;

       CreateNodes(id, zNode);
     }
     else
     {
       if(id == iParent)
       {
          return;
       }
       TreeNode zNode = pNode.Nodes.Add(Row["title"].ToString() +
          " (" + Row["weight"].ToString() + ")");

       zNode.Tag = id;

       CreateNodes(id, zNode);
     }
  }     
}

Метод CreateNodes имеет два параметра.

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

Второй параметр используется для передачи ссылки на родительский узел дерева treeView1, который является объектом класса TreeNode. При создании корневого узла этот параметр должен иметь значение null.

Как работает метод CreateNodes?

Прежде всего, он создает представление (view) таблицы dvwData, хранящейся в наборе данных dataSet11. Это представление включает в себя подмножество строк таблицы, столбец parent_id которых содержит идентификатор родительского узла, переданного методу CreateNodes в качестве первого параметра. Иными словами, здесь происходит отбор дочерних узлов заданного родительского узла.

Вот как создается это представление:

DataView dvwData = new DataView(dataSet11.Tables[0]);
dvwData.RowFilter =  "[parent_id] = " + iParent;

В свойстве dvwData.RowFilter мы указываем нужное нам условие отбора строк в самой первой (и единственной) таблице набора данных dataSet11.

После того как все нужные строки будут отобраны, запускается цикл по всем строкам полученного представления:

foreach(DataRowView Row in dvwData)
{
  …
}

Внутри этого цикла мы извлекаем идентификатор каждой строки id и сохраняем ее в одноименной переменной. Для преобразования извлеченного значения в число мы используем метод Int32.Parse:

int id = Int32.Parse(Row["id"].ToString());

Дальнейшие действия, выполняемые нашей программой, зависят от значения второго параметра метода CreateNodes.

Вот как добавляется корневой узел дерева:

if(pNode == null)
{
  TreeNode zNode = treeView1.Nodes.Add(Row["title"].ToString() +
     " (" + Row["weight"].ToString() + ")");

  zNode.Tag = id;
  CreateNodes(id, zNode);
}

Здесь мы с помощью метода treeView1.Nodes.Add добавляем в дерево treeView1 новый элемент. В качестве текстовой строки, отображаемой в окне дерева, мы используем заголовок статьи, извлеченный из столбца title текущей извлеченной строки. К этому заголовку мы дописываем в круглых скобках значение веса сортировки, взятого из столбца weight.

Далее наша программа записывает в свойство Tag идентификатор узла дерева, взяв его из столбца id. Пользуясь этим свойством, приложение сможет однозначно сопоставить элементы дерева, выделенные пользователем в окне элемента управления treeView1, и строки таблицы Tree, хранящей информацию об узлах дерева в базе данных Microsoft SQL Server.

И, наконец, метод CreateNodes вызывает сам себя рекурсивно для добавления в дерево всех узлов, дочерних для только что добавленного элемента. Обратите внимание, что теперь в качестве второго параметра этому методу передается ссылка на только что созданный узел дерева zNode.

Теперь настало время рассмотреть действия метода CreateNodes при рекурсивном вызове, когда значение второго параметра не равно null.

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

if(id == iParent)
{
  return;
}

Если идентификатор узла равен идентификатору родительского узла, работа метода завершается оператором return.

В противном случае мы создаем узел дерева, записывая идентификатор узла id в свойство zNode.Tag:

TreeNode zNode = pNode.Nodes.Add(Row["title"].ToString() +
     " (" + Row["weight"].ToString() + ")");

zNode.Tag = id;

Далее следует рекурсивный вызов метода CreateNodes:

CreateNodes(id, zNode);

Редактирование узла дерева

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

Обработчик событий меню Edit

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

private void menuItem3_Click(object sender, System.EventArgs e)
{
  // Пустой список
  if(treeView1.Nodes.Count != 0)
  {
     int id = (int)treeView1.SelectedNode.Tag;
         
     Form2 dialog = new Form2();

     sqlConnection1.Open();

     SqlDataReader myReader;
     string strCmd = String.Format(
       "SELECT document FROM Documents WHERE tree_id = {0}", id);
     SqlCommand cmd = new SqlCommand(strCmd, sqlConnection1);
     myReader = cmd.ExecuteReader();

     if(myReader.Read())
     {
       dialog.Document = myReader.GetString(0).ToString();
     }
     myReader.Close();

     sqlGetNodeCommand.Parameters["@id"].Value = id;
     myReader = sqlGetNodeCommand.ExecuteReader();

     if(myReader.Read())
     {
       dialog.Title = myReader.GetString(2).ToString();
       dialog.Weight = myReader.GetInt32(3);

       if(DialogResult.Yes == dialog.ShowDialog())
       {
          try
          {
            sqlUpdateNodeCommand.Parameters["@id"].Value = id;
            sqlUpdateNodeCommand.Parameters["@title"].Value =
              dialog.Title;
            sqlUpdateNodeCommand.Parameters["@weight"].Value =
              dialog.Weight;

            myReader.Close();

            sqlUpdateNodeCommand.ExecuteNonQuery();

            cmd = new SqlCommand("sp_UpdateDocument",
              sqlConnection1);

            cmd.CommandType = CommandType.StoredProcedure;

            cmd.Parameters.Add("@tree_id", SqlDbType.Int).Value = id;
            cmd.Parameters.Add("@document",SqlDbType.VarChar).Value=
              dialog.Document;

            cmd.ExecuteNonQuery();

            richTextBox1.Text = dialog.Document;
          }
          catch(Exception ex)
          {
            MessageBox.Show(ex.Message, "Ошибка");
          }
       }
         
       sqlConnection1.Close();
       UpdateTree();
     }
  }
}

Получив управление, обработчик событий menuItem3_Click, прежде всего, проверяет, есть ли какие-либо элементы в окне дерева:

if(treeView1.Nodes.Count != 0)
{
  …
}

Если дерево пустое, обработчик возвращает управление, не предпринимая никаких действий.

Извлечение идентификатора редактируемого узла

В том случае, когда в дереве есть узлы, обработчик получает идентификатор выделенного узла, извлекая его из свойства treeView1.SelectedNode.Tag:

int id = (int)treeView1.SelectedNode.Tag;

Напомним, что этот идентификатор равен содержимому столбца id строки таблицы Tree, описывающей данный узел. Этот идентификатор записывается в свойство treeView1.SelectedNode.Tag во время заполнения окна элемента управления treeView1.

Идентификатор id потребуется нам для извлечения из таблицы Tree информации об узле дерева, подлежащей редактированию.

Для редактирования мы создаем форму класса Form2:

Form2 dialog = new Form2();

Это та же самая форма, которую мы использовали для создания новых узлов дерева (рис. 9-43).

Извлечение данных редактируемого узла дерева

Далее нам нужно заполнить поля формы текущей информацией из таблиц Tree и Document, соответствующей данному узлу с идентификатором id. С этой целью мы открываем соединения с базой данных sqlConnection1 и создаем объект класса SqlDataReader:

sqlConnection1.Open();
SqlDataReader myReader;

Получение текст редактируемой статьи

На первом шаге нам нужно создать команду SQL класса SqlCommand, с помощью которой мы могли бы извлечь текст редактируемой статьи из таблицы Documents. При этом нам необходимо найти в этой таблице такую строку, для которой в столбце tree_id хранилось бы значение, равное идентификатору редактируемого узла дерева id.

Необходимую команду SQL мы создаем «на лету», формируя соответствующую текстовую строку с помощью класса String.Format:

string strCmd = String.Format(
  "SELECT document FROM Documents WHERE tree_id = {0}", id);

SqlCommand cmd = new SqlCommand(strCmd, sqlConnection1);

После выполнения команды методом ExecuteReader обработчик событий извлекает текст документа методом myReader.GetString, а затем сохраняет его в свойстве dialog.Document диалогового окна редактирования узлов дерева:

myReader = cmd.ExecuteReader();

if(myReader.Read())
{
  dialog.Document = myReader.GetString(0).ToString();
}

myReader.Close();

Далее обработчик закрывает объект SqlDataReader, чтобы подготовить его к выполнению новой команды.

Извлечение заголовка и веса сортировки

Для того чтобы извлечь заголовок редактируемой статьи и вес сортировки, мы создали команду sqlGetNodeCommand. Ниже приведен текст команды SQL, который нужно записать в свойство CommandText объекта  sqlGetNodeCommand:

SELECT id, parent_id, title, weight FROM dbo.Tree WHERE (id = @id)

Через параметр @id команде передается идентификатор узла дерева, для которого нужно извлечь информацию из таблицы Tree:

sqlGetNodeCommand.Parameters["@id"].Value = id;

Далее подготовленная команда запускается на выполнение с помощью метода ExecuteReader:

myReader = sqlGetNodeCommand.ExecuteReader();

Если программа нашла в таблице Tree нужный нам узел, то заголовок статьи и вес сортировки этого узла записывается, соответственно, в свойства dialog.Title и dialog.Weight диалогового окна редактирования:

if(myReader.Read())
{
  dialog.Title = myReader.GetString(2).ToString();
  dialog.Weight = myReader.GetInt32(3);
  …
}

Обновление информации узла в базе данных

Когда пользователь завершает редактирование данных в форме, показанной на рис. 9-43, при помощи кнопки Сохранить, метод dialog.ShowDialog возвращает значение DialogResult.Yes. В этом случае обработчик событий menuItem3_Click обновляет информацию об узле в базе данных:

if(DialogResult.Yes == dialog.ShowDialog())
{
  try
  {
     // Обновление базы данных
     …
  }
  catch(Exception ex)
  {
     MessageBox.Show(ex.Message, "Ошибка");
  }
}

Обновление выполняется с помощью команды sqlUpdateNodeCommand и хранимой процедуры sp_UpdateDocument, исходный текст которой мы уже приводили ранее в этой главе.

Свойство CommandText команды sqlUpdateNodeCommand должно быть установлено следующим образом:

UPDATE dbo.Tree SET title = @title, weight = @weight WHERE (id = @id)

Команде sqlUpdateNodeCommand нужно передать три параметра:

sqlUpdateNodeCommand.Parameters["@id"].Value = id;
sqlUpdateNodeCommand.Parameters["@title"].Value = dialog.Title;
sqlUpdateNodeCommand.Parameters["@weight"].Value = dialog.Weight;

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

Команда выполняется обычным образом с помощью метода ExecuteNonQuery:

myReader.Close();
sqlUpdateNodeCommand.ExecuteNonQuery();

Перед выполнением этой команды мы закрываем ненужный нам больше объект myReader.

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

cmd = new SqlCommand("sp_UpdateDocument", sqlConnection1);
cmd.CommandType = CommandType.StoredProcedure;

Для этой хранимой процедуры необходимо задать два параметра:

cmd.Parameters.Add("@tree_id", SqlDbType.Int).Value = id;
cmd.Parameters.Add("@document",SqlDbType.VarChar).Value =
  dialog.Document;

Первый из этих параметров задает идентификатор обновляемой статьи, а второй — новый текст статьи.

После подготовки параметров хранимая процедура sp_UpdateDocument запускается на выполнение при помощи метода ExecuteNonQuery:

cmd.ExecuteNonQuery();

С целью демонстрации различных методов обновления базы данных мы искусственно разделили процесс обновления таблиц Tree и Documents. Первая из этих таблиц выполняется при помощи команды SQL, созданной в виде объекта класса SqlCommand, а вторая — с помощью хранимой процедуры sp_UpdateDocument.

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

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

richTextBox1.Text = dialog.Document;

Далее этот обработчик закрывает соединение с базой данных и перерисовывает дерево:

sqlConnection1.Close();
UpdateTree();

Удаление узла дерева

Чтобы удалить статью, пользователь должен выделить его заголовок в окне дерева, затем щелкнуть этот заголовок правой клавишей мыши и выбрать из контекстного меню строку Delete.

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

private void menuItem2_Click(object sender, System.EventArgs e)
{
  if(treeView1.SelectedNode != null)
  {
     int id = (int)treeView1.SelectedNode.Tag;
     DeleteNode(id);
     UpdateTree();
  }
}

Если в окне дерева есть узлы, выделенные пользователем (т.е. если значение свойства treeView1.SelectedNode не равно null), то обработчик событий menuItem2_Click получает идентификатор узла, подлежащего к удалению. Этот идентификатор считывается из свойства treeView1.SelectedNode.Tag, куда он был записан во время заполнения дерева.

Далее узел удаляется методом DeleteNode, определенным в нашем приложении, после чего окно дерева обновляется при помощи метода UpdateTree.

Ниже мы привели исходный текст метода DeleteNode:

public void DeleteNode(int id)
{
  SqlDataReader myReader;

  sqlConnection1.Open();
  try
  {
     sqlFindChildsCommand.Parameters["@parent_id"].Value = id;
     myReader = sqlFindChildsCommand.ExecuteReader();
     if(!myReader.Read())
     {
       myReader.Close();

       sqlDeleteRowCommand1.Parameters["@id"].Value = id;
       sqlDeleteRowCommand1.ExecuteNonQuery();

       string strCmd = String.Format(
          "DELETE FROM Documents WHERE tree_id = {0}", id);
       SqlCommand cmd = new SqlCommand(strCmd, sqlConnection1);
       cmd.ExecuteNonQuery();

       richTextBox1.Text = "";  
     }
     else
     {
       myReader.Close();
     }
  }
  catch(Exception ex)
  {
     MessageBox.Show(ex.Message, "Ошибка");
  }
  sqlConnection1.Close();
}

Получив управление, этот метод создает объект myReader класса SqlDataReader и открывает соединение с базой данных:

SqlDataReader myReader;
sqlConnection1.Open();

Дальнейшая работа выполняется внутри блока try-catch, в задачу которого входит перехват и обработка исключений.

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

Для поиска всех дочерних узлов выделенного пользователем узла метод DeleteNode использует команду  sqlFindChildsCommand.

Вот текст этой команды, который нужно записать в свойство CommandText объекта sqlFindChildsCommand:

SELECT id FROM dbo.Tree WHERE (parent_id = @parent_id)

Через параметр @parent_id мы передаем этой команде идентификатор удаляемого узла:

sqlFindChildsCommand.Parameters["@parent_id"].Value = id;
myReader = sqlFindChildsCommand.ExecuteReader();

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

if(!myReader.Read())
{
  // Удаление узла
  …
}

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

Для удаления строки из таблицы Tree мы используем команду sqlDeleteRowCommand1, предварительно закрыв объект myReader:

myReader.Close();

sqlDeleteRowCommand1.Parameters["@id"].Value = id;
sqlDeleteRowCommand1.ExecuteNonQuery();

Свойство объекта sqlDeleteRowCommand1 должно содержать следующий текст команды SQL:

DELETE FROM dbo.Tree WHERE (id = @id)

Идентификатор удаляемого узла передается команде sqlDeleteRowCommand1 в качестве единственного параметра.

Для удаления текста статьи мы использовали другой подход — сформировали исходный текст команды SQL в памяти при помощи метода String.Format:

string strCmd = String.Format(
  "DELETE FROM Documents WHERE tree_id = {0}", id);

SqlCommand cmd = new SqlCommand(strCmd, sqlConnection1);
cmd.ExecuteNonQuery();

Полученная в результате этих действий команда исполняется методом ExecuteNonQuery.

И, наконец, последнее, что делает метод DeleteNode после удаления информации из базы данных, это очистка окна элемента управления RichEdit, отображающего содержимое текущего документа, выделенного пользователем в окне дерева:

richTextBox1.Text = "";

О том, как текст выделенного документа попадает в это окно, мы расскажем в следующем разделе.

Отслеживание перемещений по дереву

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

private void treeView1_AfterSelect(object sender,
  System.Windows.Forms.TreeViewEventArgs e)
{
  int id = (int)treeView1.SelectedNode.Tag;

  sqlConnection1.Open();
  try
  {
     SqlDataReader myReader;

     string strCmd = String.Format(
       "SELECT document FROM Documents WHERE tree_id = {0}", id);

     SqlCommand cmd = new SqlCommand(strCmd, sqlConnection1);
     myReader = cmd.ExecuteReader();

     if(myReader.Read())
     {
       richTextBox1.Text = myReader.GetString(0).ToString();
     }
     myReader.Close();
  }
  catch(Exception ex)
  {
     MessageBox.Show(ex.Message, "
Ошибка");
  }

  sqlConnection1.Close();
}

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

Что делает наш обработчик события treeView1_AfterSelect?

Прежде всего, он получает идентификатор узла, выделенного пользователем, считывая его из свойства treeView1.SelectedNode.Tag:

int id = (int)treeView1.SelectedNode.Tag;

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

Для того чтобы получить текст статьи, мы открываем соединение с базой данных. Операция выполняется в блоке try-catch, после чего соединение закрывается:

sqlConnection1.Open();
try
{
  …
}
catch(Exception ex)
{
  MessageBox.Show(ex.Message, "
Ошибка");
}
sqlConnection1.Close();

Чтобы извлечь текст статьи из таблицы Documents, мы формируем строку в памяти, пользуясь методом String.Format:

string strCmd = String.Format(
  "SELECT document FROM Documents WHERE tree_id = {0}", id);

SqlCommand cmd = new SqlCommand(strCmd, sqlConnection1);

Для запуска команды мы создаем объект myReader класса SqlDataReader, а затем вызываем метод ExecuteReader:

SqlDataReader myReader;
myReader = cmd.ExecuteReader();

Результат выполнения команды записывается в свойство richTextBox1.Text, после чего текст статьи появляется в правой части главного окна нашего приложения:

if(myReader.Read())
{
  richTextBox1.Text = myReader.GetString(0).ToString();
}

Необходимое финальное действие — закрытие ненужного больше объекта myReader:

myReader.Close();

Разумеется, для чтения текста статьи Вы можете использовать хранимые процедуры сервера Microsoft SQL Server. Использование хранимых процедур дает дополнительные преимущества при создании приложений с базами данных. В частности, разработчик может настраивать права доступа к таким процедурам. Кроме того, хранимые процедуры можно готовить и отлаживать вне контекста приложения. Эту работу можно поручить специалисту по серверу Microsoft SQL Server, который знает все о базах данных, но ничего о создании приложений на языке программирования C#.

[Назад] [Содержание] [Дальше]