Технология Microsoft ADO.NET

       

Понятие DataSet, DataTable и DataColumn


Итак, DataSet представляет собой буфер для хранения данных из базы. Этот буфер предназначен для хранения структурированной информации, представленной в виде таблиц, поэтому первым, самым очевидным вложенным объектом DataSet является DataTable. Внутри одного объекта DataSet может храниться несколько загруженных таблиц из базы данных, помещенных в соответствующие объекты DataTable. Всякая таблица состоит из столбцов (называемых также полями или колонками) и строк. Для обращения к ним и для управления столбцами и строками в объекте DataTable предназначены специальные объекты - DataColumn и DataRow. Между таблицами, как мы знаем, могут быть связи - здесь они представлены объектом DataRelation. Наконец, в таблицах есть первичные и вторичные ключи - объект Constraint со своими двумя подклассами UniqueConstraint и ForeighKeyConstraint описывают их. Я все вр емя говорю "представлены", "описывают", избегая слов "отображают" и "определяются" - дело в том, что нельзя ставить знак равенства между, например, объектом DataRelation и связью таблиц. В загруженных таблицах не формируются автоматически все нужные объекты - кое-где мы должны делать это самостоятельно. Сами объекты имеют также довольно тонкую и сложную структуру, поэтому это было бы довольно грубым приближением. Однако, на первых порах, для понимания сути полезно держать в голове следующие "формулы":

DataSet = одна или несколько таблиц = один или несколько объектов DataTable. DataTable = таблица. DataColumn = столбец, поле, колонка. DataRow = строка. DataTable = таблица = несколько полей, столбцов, колонок = несколько объектов DataColumn. DataTable = таблица = несколько строк = несколько объектов DataRow. DataRelation = связь между таблицами.

Возникает вопрос: для чего нужны эти объекты, если мы прекрасно обходились и без них для вывода содержимого таблицы, например в элемент DataGrid? Дело в том, что для простого отображения информации создавать эти объекты не требуется, но тогда все данные будут однородными текстовыми переменными, подобно таблицам в документе Microsoft Word.
DataSet не может сам сформировать структуру данных - тип переменных, первичные и вторичные ключи, связи между таблицами. Для управления структурой, для сложного отображения (например, вывод информации с привязкой к элементам, создаваемым в режиме работы приложения) и нужно определение этих объектов.

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


Рис. 8.1.  Структура базы Tests (диаграмма "QuestVar")

Каждый вопрос будет содержать несколько ответов, для синхронного перемещения по структуре нужна связь по полю questID. Галочка "Allow Nulls" (Разрешить пустые значения) снята для всех полей - это означает, что все поля будут обязательными для заполнения. Структура таблиц Questions и Variants приводится в таблице 8.1:

Таблица 8.1. Структура таблиц базы данных TestsТаблица Поле Описание поля
Questions - таблица вопросов questID Номер вопроса
question Текст вопроса
questType Тип вопроса (с одним правильным вариантом ответа или с несколькими)
Variants - таблица вариантов ответов id Номер ответа
questID Номер вопроса
variant Текст варианта ответа
isRight Является ли данный вопрос верным
Для нас не столь важно количество вопросов, поэтому их всего будет пять, относящихся к теме операционной системы Windows XP. Одному вопросу может соответствовать один правильный вариант ответа - в этом случае в поле questType указывается значение "0". Далее на форме эти варианты будут выводиться с элементами RadioButton. Для вопросов, у которых есть несколько правильных вариантов ответов, указывается в поле questType значение "1". На форме такие варианты будут выводиться с чекбоксами. В таблицах 8.2 и 8.3 приводится содержимое таблиц Questions и Variants.



Таблица 8.2. Вопросы таблицы QuestionsquestID Question questType
1 Для переустановки операционной системы Windows XP вам необходимо экспортировать банк сообщений программы Microsoft Outlook Express, расположенный по адресу: 0
2 При компиляции программы в среде Microsoft Visual Studio .NET возникает систематическая ошибка в модуле AssemblyInfo из-за неудачного выбора имени пользователя и организации (были использованы кавычки) при установке системы (Для просмотра: "Мой компьютер - правая кнопка - Свойства - вкладка "Общие""). Вам необходимо изменить эти параметры 0
3 Выберите группы, состоящие из файлов, размер которых после архивирования составляет 5-10% от исходного 1
4 Укажите ряд, состоящий из агрегатных (агрегаторных) функций SQL 0
5 Вы изменили ключ BootExecute в разделе реестра [HKEY_LOCAL_MACHINE \ SYSTEM \ CurrentControlSet \ Control \ Session Manager]. В результате было выполнено следующее действие: 0
Таблица 8.3. Ответы таблицы "Variants"id questID Variant isRight
1 1 C:\Program Files\Outlook Express\Mail 0
2 1 C:\Documents and Settings\Имя_Пользователя\ Local Settings\Application Data\Identities\ {F4CB90C4-3FD5-406B-83FB-85E644627B87}\Microsoft\Outlook Express 1
3 1 C:\WINDOWS\system32\Microsoft\Outlook Express\Bases 0
4 1 C:\Documents and Settings\Default User\Cookies 0
5 2 "Мой компьютер - правая кнопка - Свойства - Вкладка "Общие" на подписи - правая кнопка - Свойства - Переименовать" 0
6 2 [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion] затем меняем параметры RegisteredOwner и RegisteredOrganization 1
7 2 [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT] удаляем раздел CurrentVersion 0
8 2 [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run] 0
9 3 *.mpeg, *.mdb, *. Htm 0
10 3 *.xls, *.txt , *.mht 1
11 3 *. jpeg, *.gif, *.mp3 0
12 3 *. doc, *. xml, *.bmp 1
13 4 update, insert, sum 0
14 4 where, like, create 0
15 4 count, min, max 1
16 4 select, avg, into 0
17 5 Отключилась проверка на ошибки всех дисковых разделов при загрузке Windows ХР 1
18 5 Установлен таймер на автоматическое отключение 0
19 5 Отключены все диагностические сообщения 0
20 5 Система перестанет загружаться 0


В поле isRight таблицы Variants правильные ответы отмечаются значением "1". Понятно, что для практического применения тестовой программы следует ограничить доступ к базе данных Tests. Мы, однако, будем использовать подключения без пароля.

Создайте новое Windows-приложение и назовите его "Tests". Устанавливаем следующие свойства формы:

Form1, форма, свойство Значение
FormBorderStyle FixedSingle
MaximizeBox False
Size 550; 350
Text Тест


Перетаскиваем на форму TextBox, GroupBox, Label и пять кнопок, устанавливаем следующие значения свойств элементов:

label1, свойство Значение
Location 16;8
Text Вопрос:
textBox1, свойство Значение
Name txtQuestion
Location 12; 24
Multiline True
Size 520; 90
TabIndex 8
Text
groupBox1, свойство Значение
Name GbVariants
Location 12; 120
Size 520; 150
Text Варианты ответов
button1, свойство Значение
Name btnFirst
Location 24; 280
Text <<
button2, свойство Значение
Name BtnPrev
Location 99; 280
Text <
button3, свойство Значение
Name BtnNext
Location 174; 280
TabIndex 0
Text >
button4, свойство Значение
Name BtnLast
Location 249; 280
Text >
button5, свойство Значение
Name BtnCheck
Location 360; 280
Size 150; 23
Text Результат
Интерфейс приложения готов. Чекбоксы или элементы radioButton мы будем добавлять в режиме запуска приложения - в самом деле, длина всех ответов разная и делать привязку к статичным элементам нет смысла. Переключаемся на вкладку Data панели инструментов Toolbox и перетаскиваем на форму объект DataSet. В появившемся окне Add DataSet выбираем значение "Untyped DataSet" и нажимаем "OK". Появляется панель компонентов, с которой мы уже неоднократно имели дело. Свойству Name устанавливаем значение dsTests.

В свойстве Tables элемента DataSet нажимаем на кнопку
(...), запускается редактор Table Collection Editor (рис. 8.2), нажимаем кнопку "Add" и вводим следующие значения свойств:

TableName Questions
Name dtQuestions

увеличить изображение
Рис. 8.2.  Запуск редактора Table Collection Editor

Значение dtQuestions свойства Name указывает название созданного объекта DataTable, а значение Questions свойства TableName указывает название таблицы, которая будет помещена в DataTable.


Добавим поля к объекту DataTable. В редакторе Tables Collection Editor в поле свойства Columns нажимаем на кнопку
(...), появляется редактор Columns Collection Editor (рис. 8.3), нажимаем кнопку Add. Всего нужно будет создать три поля: questID, question и questType:

Поле Column1, свойство Значение
ColumnName questID
DataType System.Int32
Unique True
Name dсQuestID
Поле Column2, свойство Значение
ColumnName question
Name dcQuestion
Поле Column3, свойство Значение
ColumnName questType
DataType System.Int32
Name dcQuestType

увеличить изображение
Рис. 8.3.  Запуск редактора Columns Collection Editor

Завершив работу с редактором Columns Collection Editor, нажимаем кнопку Close. Мы закончили создание объекта DataTable для таблицы Questions. Аналогичные действия надо проделать, чтобы создать DataTable для таблицы Variants (рис. 8.4) и соответствующих полей id, questID, variant и isRight:

TableName Variants
Name dtVariants
Поле Column1, свойство Значение
ColumnName id
DataType System.Int32
Unique True
Name dcID
Поле Column2, свойство Значение
ColumnName questID
DataType System.Int32
Name dcVariantQuestID
Поле Column3, свойство Значение
ColumnName variant
Name dcVariant
Поле Column4, свойство Значение
ColumnName isRight
DataType System.Boolean
Name dcIsRight

увеличить изображение
Рис. 8.4.  Создание объектов DataTable и DataColumn для таблицы Variants

В базе данных Tests таблицы Questions и Variants мы связали по полю questID. Теперь при проектировании схемы базы нам следует также создать это отношение. В окне Properties объекта DataSet нажимаем на кнопку (_) в поле свойства Relations. В появившемся редакторе Relations Collection Editor нажимаем кнопку "Add" для добавления связи. Называем отношение "QuestionsVariants", а в качестве ключевого поля указываем questID (рис. 8.5):


Рис. 8.5.  Создание отношения QuestionsVariants

Мы закончили работу с визуальными средствами. Программное создание объектов DataTable, DataColumn, DataRelation (см.


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

Переходим в код формы, подключаем пространство имен:

using System.Data.SqlClient;

В классе создаем перечисление QuestionType - вопросу с одним правильным вариантом будет соответствовать постоянная SingleVariant, вопросу с несколькими вариантами - MultiVariant:

public enum QuestionType { SingleVariant, MultiVariant }

Создаем перечисление Direction для навигации по вопросам:

public enum Direction { First, Prev, Next, Last }

В методе LoadDataBase заполняем объект DataSet данными из базы данных:

private void LoadDataBase() { SqlConnection conn = new SqlConnection("Data Source=.;Initial Catalog=Tests;Integrated Security=SSPI;");

SqlDataAdapter questAdapter = new SqlDataAdapter("select * from questions", conn); SqlDataAdapter variantsAdapter = new SqlDataAdapter("select * from variants", conn); conn.Open(); //Заполняем таблицу "Questions" данными из questAdapter questAdapter.Fill(dsTests.Tables["Questions"]); //Заполняем таблицу "Variants" данными из variantsAdapter variantsAdapter.Fill(dsTests.Tables["Variants"]); conn.Close(); }

Создаем два экземпляра класса Hashtable1):

// Экземпляр clientAnswers для хранения ответов пользователей Hashtable clientAnswers = new Hashtable(); // Экземпляр keys для хранения правильных ответов Hashtable keys = new Hashtable();

Создаем метод InitAnswersKeysTable, в котором связываем элементы экземпляров Hashtable с записями таблиц

private void InitAnswersKeysTable() { // Создаем цикл, длина которого равна числу записей в таблице "Questions" for(int i = 0; i < dsTests.Tables["Questions"].Rows.Count; i++) { // Выбираем записи из таблицы "Questions". DataRow drquestion = dsTests.Tables["Questions"].Rows[i];



// Выбираем записи из таблицы "Variants". DataRow[] drvariants = drquestion.GetChildRows (dsTests.Relations["QuestionsVariants"]); // Устанавливаем значение j, равное false для всех вариантов. bool[] answers = new bool[drvariants.Length]; for(int j = 0; j < answers.Length; j++) answers[j] = false;

// Добавляем значения к экземплярам Hashtable keys.Add(drquestion, drvariants); clientAnswers.Add(drquestion, answers); } }

Объект DataRow предназначен для просмотра и изменения содержимого отдельной записи в объекте DataTable. Для обращения к конкретной записи используется свойство Rows[i], где i - номер записи. Метод GetChildRows позволяет обращаться к дочерним записям, он принимает название отношения. Здесь мы фактически обращаемся к записям таблицы Variants. В экземпляре keys ключами будут записи из таблицы Questions, а значениями - элементы массива записей с вариантами ответов. В экземпляре clientAnswers ключами также будут записи из таблицы Questions, а значениями - элементы массива типа bool, зависящие от ответа пользователя.

В классе формы создаем экземпляр cmTest класса CurrencyManager для перемещения по записям:

CurrencyManager cmTest = null;

В методе InitDefaultSettings определяем настройки по умолчанию:

private void InitDefaultSettings() { //В свойство Tag каждой кнопки помещаем константу из перечисления Direction btnFirst.Tag = Direction.First; btnPrev.Tag = Direction.Prev; btnNext.Tag = Direction.Next; btnLast.Tag = Direction.Last;

//Для всех кнопок будет один обработчик btnFirst_Click btnFirst.Click += new EventHandler(btnFirst_Click); btnPrev.Click += new EventHandler(btnFirst_Click); btnNext.Click += new EventHandler(btnFirst_Click); btnLast.Click += new EventHandler(btnFirst_Click); //Вызываем метод LoadDataBase(); //Определяем действия для случая, //если нет записей в таблице "Questions" if(dsTests.Tables["Questions"].Rows.Count == 0) { txtQuestion.Text = "Нет данных о вопросах"; btnFirst.Enabled= false; btnPrev.Enabled= false; btnNext.Enabled= false; btnLast.Enabled= false; btnCheck.Enabled= false; } else { //Вызываем метод.


InitAnswersKeysTable(); //Связываем эземпляр cmTest с содержимым таблицы "Questions" cmTest = (CurrencyManager)this.BindingContext[dsTests, "Questions"]; //Определяем обработчик для события cmTest.PositionChanged += new EventHandler(cmTest_PositionChanged); ShowQuestion(dsTests.Tables["questions"].Rows[0]); //Включаем доступность кнопок ">" и ">>" btnFirst.Enabled= false; btnPrev.Enabled= false; btnNext.Enabled= true; btnLast.Enabled= true; } }

Создаем метод cmTest_PositionChanged, для обработки события PositionChanged объекта cmTest, в котором определяем доступность кнопок навигации:

private void cmTest_PositionChanged(object sender, EventArgs e) { if (cmTest.Position == 0) { btnPrev.Enabled = false; btnFirst.Enabled = false; btnNext.Enabled = true; btnLast.Enabled = true; } else if(cmTest.Position == dsTests.Tables["questions"].Rows.Count - 1) { btnNext.Enabled = false; btnLast.Enabled = false; btnPrev.Enabled = true; btnFirst.Enabled = true; } else { btnPrev.Enabled = true; btnFirst.Enabled = true; btnNext.Enabled = true; btnLast.Enabled = true; } }

Создаем метод btnFirst_Click - общий обработчик для всех кнопок навигации:

private void btnFirst_Click(object sender, EventArgs e) { Button btn = (Button)sender; Direction direction = (Direction)btn.Tag;

switch (direction) { case Direction.First: cmTest.Position = 0; break; case Direction.Prev: --cmTest.Position; break; case Direction.Next: ++cmTest.Position; break; case Direction.Last: cmTest.Position = dsTests.Tables["questions"].Rows.Count - 1; break; }

int rowIndex = cmTest.Position; //Вызываем метод ShowQuestion, который выводит вопросы на форму ShowQuestion(dsTests.Tables["questions"].Rows[rowIndex]); }

Вызываем метод InitDefaultSettings в конструкторе формы:

public Form1() { InitializeComponent(); InitDefaultSettings(); }

В методе ShowQuestion выводим вопрос на форму:

private void ShowQuestion(DataRow drquestion) { txtQuestion.Text = drquestion["question"].ToString(); //Вызываем метод ShowVariants, который выводит на форму варианты ответов ShowVariants(drquestion); }



В методе ShowVariants в зависимости от типа вопроса формируется набор элементов RadioButton или CheckBox c вариантами ответов, который затем выводится в элемент gbVariants:

private void ShowVariants(DataRow question) { //Удаляем все элементы из GroupBox gbVariants.Controls.Clear(); //Снова создаем экземпляр childVariants //для обращения к записям таблицы "Variants" DataRow[] childVariants = question.GetChildRows (dsTests.Relations["QuestionsVariants"]);

//Определяем тип вопроса bool[] vars = (bool[])clientAnswers[question]; int i = 0; QuestionType questType = (QuestionType)question["questType"]; switch(questType) { //Если вопрос имеет всего один правильный вариант, //на форме будут созданы элементы Radiobutton case QuestionType.SingleVariant: foreach(DataRow childVariant in childVariants) { RadioButton rb = new RadioButton(); #region Ищем выбранный ответ в таблице ответов bool selectedAnswer = (bool)vars[i++]; rb.Checked = selectedAnswer; #endregion //Определяем свойства созданного элемента RadioButton rb.Text = childVariant["variant"].ToString(); rb.Tag = childVariant; rb.CheckedChanged += new EventHandler(rb_CheckedChanged); int y = (gbVariants.Controls.Count == 0)?20: ((RadioButton)gbVariants.Controls[gbVariants.Controls.Count - 1]).Bottom + 2; //Определяем размеры создаваемых элементов RadioButton //500 - ширина в пикселях, rb.Height+5 - высота rb.Size = new Size(500, rb.Height+5); rb.Location = new Point(10, y); gbVariants.Controls.Add(rb); } break; //Если вопрос имеет несколько правильных вариантов, //на форме будут созданы элементы CheckBox case QuestionType.MultiVariant: foreach(DataRow childVariant in childVariants) { CheckBox chb = new CheckBox(); #region Ищем выбранный ответ в таблице ответов bool selectedAnswer = (bool)vars[i++]; chb.Checked = selectedAnswer; #endregion //Определяем свойства созданного элемента RadioButton chb.Text = childVariant["variant"].ToString(); chb.Tag = childVariant; chb.CheckedChanged += new EventHandler(chb_CheckedChanged); int y = (gbVariants.Controls.Count == 0)?20: ((CheckBox)gbVariants.Controls[gbVariants.Controls.Count - 1]).Bottom + 2; //Определяем размеры создаваемых элементов RadioButton //500 - ширина в пикселях, chb.Height+5 - высота chb.Size = new Size( 500, chb.Height+5); chb.Location = new Point(10, y); gbVariants.Controls.Add(chb); } break; } }



Когда пользователь отметит галочкой элементы CheckBox или выберет RadioButton, будет происходить событие CheckedChanged. В этом событии мы фиксируем положение отмеченного элемента - при возврате к решенному вопросу пользователь будет видеть свои ответы:

private void rb_CheckedChanged(object sender, EventArgs e) { RadioButton rb = (RadioButton)sender; if(!rb.Checked) return; //Создаем объект drvariant класса DataRow, с которым связываем //свойство Tag элемента RadioButton DataRow drvariant = (DataRow)rb.Tag; //Отмечаем текущее положение объекта cmTest int questIndex = cmTest.Position; DataRow drquestion = dsTests.Tables["Questions"].Rows[questIndex]; int answIndex = gbVariants.Controls.IndexOf(rb); //Выводим элемент RadioButton отмеченным, если он уже был выбран bool[] answers = (bool[])clientAnswers[drquestion]; for(int i = 0; i < answers.Length; i++) { if(i == answIndex) answers[i] = rb.Checked; else answers[i] = !rb.Checked; } }

private void chb_CheckedChanged(object sender, EventArgs e) { CheckBox chb = (CheckBox)sender; //Создаем объект drvariant класса DataRow, с которым связываем //свойство Tag элемента CheckBox DataRow drvariant = (DataRow)chb.Tag; //Отмечаем текущее положение объекта cmTest int rowIndex = cmTest.Position; DataRow drquestion = dsTests.Tables["Questions"].Rows[rowIndex]; int answIndex = gbVariants.Controls.IndexOf(chb); //Выводим элемент CheckBox отмеченным, если он уже был выбран bool[] answers = (bool[])clientAnswers[drquestion]; for(int i = 0; i< answers.Length; i++) { if (i == answIndex) { answers[i] = chb.Checked; break; } } }

Переключаемся в режим дизайна, щелкаем на кнопке "Результат" - в обработчике события подсчитываем количество правильных ответов:

private void btnCheck_Click(object sender, System.EventArgs e) { //Создаем счетчик double counter = 0; //Перебираем все ключи экземпляра key класса Hashtable, //в котором хранятся ответы пользователя foreach(object key in keys.Keys) { bool flag = true; DataRow[] drvariants = (DataRow[])keys[key]; bool[] answers = (bool[])clientAnswers[key]; int i = 0; foreach(DataRow variant in drvariants) { if(((bool)variant["isRight"]) == answers[i++]) continue; else { flag = false; break; } } if (flag) ++counter; } //Делим количество правильных ответов на общее число ответов, //результат умножаем на 100 int result = (int)(counter / dsTests.Tables["questions"].Rows.Count * 100); MessageBox.Show(String.Format("Вы ответили правильно на {0}% вопросов.", result), "Результат тестирования", MessageBoxButtons.OK, MessageBoxIcon.Information); }

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


увеличить изображение
Рис. 8.6.  Готовое приложение Tests. А - вид формы с двумя правильными ответами, Б - вид формы с одним правильным ответом, В - результат тестирования

В программном обеспечении к курсу вы найдете приложение Tests (Code\Glava4\Tests).


Содержание раздела