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

Объекты DataTable, DataRow и DataColumn

Разбить на страницы
Показывать лекцию целиком

Объекты DataTable, DataRow и DataColumn

Понятие 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:

Структура таблиц базы данных Tests
Таблица Поле Описание поля
Questions - таблица вопросов questID Номер вопроса
question Текст вопроса
questType Тип вопроса (с одним правильным вариантом ответа или с несколькими)
Variants - таблица вариантов ответов id Номер ответа
questID Номер вопроса
variant Текст варианта ответа
isRight Является ли данный ответ верным

Для нас не столь важно количество вопросов, поэтому их всего будет пять, относящихся к теме операционной системы Windows XP. Одному вопросу может соответствовать один правильный вариант ответа - в этом случае в поле questType указывается значение "0". Далее на форме эти варианты будут выводиться с элементами RadioButton. Для вопросов, у которых есть несколько правильных вариантов ответов, указывается в поле questType значение "1". На форме такие варианты будут выводиться с чекбоксами. В таблицах 8.2 и 8.3 приводится содержимое таблиц Questions и Variants.

Вопросы таблицы Questions
questID 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
Ответы таблицы "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 элемента (...), запускается редактор Table Collection Editor (рис. 8.2), нажимаем кнопку "Add" и вводим следующие значения свойств:

TableName Questions
Name dtQuestions

Значение (рис 8.2) (...), появляется редактор 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();
}

Создаем два экземпляра класса HashtableДополнительные сведения об этом классе см. в конце Лекции 9.:

// Экземпляр 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).

Программное создание объектов DataTable и DataColumn. Ограничения Unique Constraint и ForeignConstraint

Еще в первой лекции мы узнали, что все объекты вкладки Data или все объекты ADO .NET можно создавать программно. Скопируйте папку приложения Tests и назовите ее "ProgrammTests". Открываем проект, удаляем объект DataSet с панели компонент формы. Для создания этого объекта программно достаточно следующей строчки кода:

DataSet dsTests = new DataSet();

Поскольку созданный экземпляр dsTests будет использоваться в нескольких методах, эту строку следует написать в классе формы. Здесь dsTests - это название объекта DataSet, которое мы затем используем в коде - это известно нам с первой лекции. Среда Visual Studio .NET однако, сгенерировала следующий фрагмент кода (его можно найти в исходном проекте "Tests"):

this.dsTests = new System.Data.DataSet();
...
// 
// dsTests
// 
this.dsTests.DataSetName = "NewDataSet"; 
this.dsTests.Locale = new System.Globalization.CultureInfo("ru-RU");

Как всегда, автоматический код избыточен - здесь указывается культура ru-RU. Дело в том, что у меня установлена русская версия Windows XP и соответствующие региональные настройки.

Среда определила еще свойство DataSetName = "NewDataSet", эквивалентное описание будет иметь следующий вид:

DataSet dsTests = new DataSet("NewDataSet");

Что же это за еще одно название объекта DataSet? Свойство DataSetName используется для работы с XSD-схемами, пока про это название (до лекции 10) мы можем просто забыть.

Создадим теперь объект DataTable для таблицы Questions:

DataTable dtQuestions = dsTests.Tables.Add("Questions");
//Или
//DataTable dtQuestions = new DataTable("Questions");
//dsTests.Tables.Add(dtQuestions);

Здесь мы создаем экземпляр dtQuestions объекта DataTable, затем вызываем метод Add свойства Tables объекта dsTests, которому передаем название таблицы Questions. Далее создаем поля в объекте dtQuestions:

DataColumn dсQuestID = dtQuestions.Columns.Add("questID", typeof(Int32));
dсQuestID.Unique = true;
DataColumn dcQuestion = dtQuestions.Columns.Add("question");
DataColumn dcQuestType = dtQuestions.Columns.Add("questType", typeof(Int32));

Мы создаем поля, нужные для отражения соответствующих столбцов в таблице Questions. Перегруженный метод Add свойства Columns объекта dtQuestions позволяет задавать название столбца и его тип данных (рис. 8.7):

(рис 8.7) Создание поля

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

Точно так же создаем объект DataTable для таблицы Variants и соответствующие поля:

//Cоздаем таблицу "Variants"
DataTable dtVariants = dsTests.Tables.Add("Variants");
	
//Заполняем поля таблицы "Variants"
DataColumn dcID = dtVariants.Columns.Add("id", typeof(Int32));
dcID.Unique = true;
dcID.AutoIncrement = true;
DataColumn dcVariantQuestID = dtVariants.Columns.Add("questID", typeof(Int32));
DataColumn dcVariant = dtVariants.Columns.Add("variant");
DataColumn dcIsRight = dtVariants.Columns.Add("isRight", typeof(Boolean));

Здесь мы дополнительно установили свойству AutoIncrement объекта dcID значение true. Свойство AutoIncrement (Счетчик) позволяет создать счетчик для поля, аналогичный типу данных "Счетчик" в Microsoft Access.

Теперь приступим к созданию связи между таблицами. В базе данных Microsoft SQL Tests между родительской таблицей Questions и дочерней Variants была установлена связь по полю questID, которое было в обеих таблицах. При программном создании объектов для поля questID таблицы Questions был создан объект dсQuestID, для этого же поля таблицы Variants создан объект dcVariantQuestID. В коде создание отношения между таблицами будет иметь следующий вид:

DataRelation drQuestionsVariants = new DataRelation("QuestionsVariants",
 dсQuestID, dcVariantQuestID);
dsTests.Relations.Add(drQuestionsVariants);

Здесь в конструкторе drQuestionsVariants - название экземпляра объекта (класса) DataRelation, а QuestionsVariants - свойство relationName - название связи, которая будет содержаться в объекте drQuestionsVariants. Другими словами, drQuestionsVariants - название экземпляра DataRelation, которое мы будем использовать в коде, а свойство relationName - всего лишь название отражаемой связи, которую можно удалить или переименовать.

Итак, мы создали все объекты для отображения таблиц, полей и даже связей между таблицами.

Теперь нам осталось определить некоторые свойства таблиц, называемые ограничениями. Свойство ограничения ( Constraint ) объекта DataTable бывает двух типов - UniqueConstraint и ForeignKeyConstraint. Свойство UniqueConstraint определяет первичный ключ таблицы, например, в таблице Questions ключевым полем является questID. Объект dсQuestID представляет это поле:

DataColumn dсQuestID = dtQuestions.Columns.Add("questID", typeof(Int32));

Ограничение UniqueConstraint, налагаемое на объект dсQuestID, запрещает появление дублированных строк:

UniqueConstraint UC_dtQuestions = new UniqueConstraint(dсQuestID);
dtQuestions.Constraints.Add(UC_dtQuestions);

Однако при создании объекта dсQuestID мы ведь уже определяли его уникальность:

dсQuestID.Unique = true;

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

Второе ограничение - ForeignKeyConstraint - определяет, как должны себя вести дочерние записи при изменении родительских записей и наоборот. Конечно, интерфейс нашего приложения вообще не подразумевает внесение изменений, но ADO .NET требует точного описания объектов для управления ими. Ограничение ForeignKeyConstraint содержит следующие три правила:

  • UpdateRule - применяется при изменении родительской строки;
  • DeleteRule - применяется при удалении родительской строки;
  • AcceptRejectRule - применяется при вызове метода AcceptChanges объекта DataTable, для которого определено ограничение.
  • Для этих правил могут применяться следующие значения:

  • Cascade - каскадное обновление связанных записей;
  • None - изменения в родительской таблице не отражаются в дочерних записях;
  • SetDefault - полю внешнего ключа в дочерних записях присваивается значение, заданное в свойстве DefaultValue этого поля;
  • SetNull - полю внешнего ключа в дочерних записях присваивается значение Null.
  • Значением по умолчанию для правил UpdateRule и DeleteRule является Cascade, для правила AcceptRejectRule - None. Дополнительно, правило AcceptRejectRule принимает значения только Cascade или None.

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

    ForeignKeyConstraint FK_QuestionsVariants = 
     new ForeignKeyConstraint(dtQuestions.Columns["questID"],
     dtVariants.Columns["questID"]);
    dtVariants.Constraints.Add(FK_QuestionsVariants);

    Здесь задается вначале родительская колонка, а затем дочерняя (рис. 8.8). Добавлять созданное ограничение следует к объекту DataTable, представляющему дочернюю таблицу (в данном случае - объект dtVariants )

    (рис 8.8) Создание ограничения FK_QuestionsVariants

    Этот фрагмент кода оставляет значения правил UpdateRule, DeleteRule и AcceptRejectRule заданными по умолчанию, т.е. Cascade и None, что соответствует значениям настройки с помощью мастера Relation (рис. 8.9):

    (рис 8.9) Мастер Relation и соответствующий фрагмент кода

    Если бы нам потребовалось задать значение одному из правил, отличное по умолчанию, мы бы просто применили другой вариант конструктора (рис. 8.10):

    (рис 8.10) В этом конструкторе можно задать значения правил RejectRule, DeleteRule и UpdateRule

    ПолностьюНе забудьте в классе формы создать объект DataSet: DataSet dsTests = new DataSet(); метод LoadDataBase в проекте ProgrammTests будет выглядеть так:

    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);
    	//dsTests.EnforceConstraints = true;
    	//Cоздаем таблицу "Questions"
    	DataTable dtQuestions = dsTests.Tables.Add("Questions");
    	//Или
    	//DataTable dtQuestions = new DataTable("Questions");
    	//dsTests.Tables.Add(dtQuestions);
    	//Заполняем поля таблицы "Questions"
    	DataColumn dсQuestID =
    	 dtQuestions.Columns.Add("questID", typeof(Int32));
    	dсQuestID.Unique = true;
    	//Или
    	//UniqueConstraint UC_dtQuestions = new UniqueConstraint(dсQuestID);
    	//dtQuestions.Constraints.Add(UC_dtQuestions);
    	DataColumn dcQuestion = dtQuestions.Columns.Add("question");
    	DataColumn dcQuestType =
    	 dtQuestions.Columns.Add ("questType", typeof(Int32));
    	//Cоздаем таблицу "Variants"
    	DataTable dtVariants = dsTests.Tables.Add("Variants");
    	//Заполняем поля таблицы "Variants"
    	DataColumn dcID = dtVariants.Columns.Add("id", typeof(Int32));
    	dcID.Unique = true;
    	dcID.AutoIncrement = true;
    	DataColumn dcVariantQuestID =
    	 dtVariants.Columns.Add("questID", typeof(Int32));
    	DataColumn dcVariant = dtVariants.Columns.Add("variant");
    	DataColumn dcIsRight = dtVariants.Columns.Add("isRight", typeof(Boolean));
    	//Создаем ограничение
    	ForeignKeyConstraint FK_QuestionsVariants = 
    	 new ForeignKeyConstraint(dtQuestions.Columns["questID"],
    	 dtVariants.Columns["questID"]);
    	dtVariants.Constraints.Add(FK_QuestionsVariants);
    	//Создаем отношение
    	DataRelation drQuestionsVariants = new DataRelation("QuestionsVariants",
    	 dсQuestID, dcVariantQuestID);
    	dsTests.Relations.Add(drQuestionsVariants);
    	conn.Open();
    	//Заполняем таблицу "Questions" данными из questAdapter
    	questAdapter.Fill(dsTests.Tables["Questions"]);
    	//Заполняем таблицу "Variants" данными из variantsAdapter
    	variantsAdapter.Fill(dsTests.Tables["Variants"]);
    	conn.Close();
    }

    Обратим внимание на несколько деталей этого метода. Значение true свойства EnforceConstraints объекта dsTests разрешает использование ограничений. По умолчанию в созданном объекте DataSet это свойство и так принимает значение true, поэтому этот фрагмент кода закомментирован. Следует иметь в виду, что для снятия всех ограничений достаточно установить свойству EnforceConstraints значение false. Ограничение FK_QuestionsVariants создается перед отношением drQuestionsVariants - вначале следует создавать ограничения, а затем определять отношения. Соединение conn открывается как можно позже - непосредственно перед заполнением данными объектов DataAdapter, - и тут же закрывается. Открыть его в начале метода и закрыть в конце было бы нерациональным.

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

    Создание столбцов, основанных на выражении

    При создании базы данных не следует помещать в нее значения, которые могут быть получены из уже имеющихся данных. В первой лекции мы создали базу данных BDTur_firm.mdb, в которой есть таблица "Туры". В этой таблице имеется поле "цена", где указывается стоимость туров. На практике может понадобиться значение цены, выраженное в различных валютах, значение с учетом налогов, значение со скидкой и т.п. Для каждого конкретного случая можно получить дополнительное поле, не вводя его в саму базу данных. Технология ADO .NET позволяет создавать объекты DataColumn, основанные на выражении. Создайте новое Windows-приложение и назовите его CustomExpression. Перетаскиваем на форму элемент управления DataGrid, свойству Dock устанавливаем значение Fill. Переходим в код формы, подключаем пространство имен:

    using System.Data.OleDb;

    В классе формы определяем строки CommandText и ConnectionString:

    string commandText = "SELECT Информация, [Код тура], Название, Цена FROM Туры";
    string connectionString = @"Provider=""Microsoft.Jet.OLEDB.4.0"";Data
     Source=""D:\Uchebnik\Code\Glava1\BDTur_firm.mdb"";User ID=Admin;
     Jet OLEDB:Encrypt Database=False";

    В конструкторе формы программно создаем все объекты для вывода таблицы "Туры" в элемент управления DataGrid:

    public Form1()
    {	
    	InitializeComponent();
    	OleDbConnection conn = new OleDbConnection (connectionString);
    	OleDbCommand myCommand = new OleDbCommand();
    	myCommand.Connection = conn;
    	myCommand.CommandText = commandText;
    	OleDbDataAdapter dataAdapter = new OleDbDataAdapter();
    	dataAdapter.SelectCommand = myCommand;
    	DataSet dsTours = new DataSet();
    	DataTable dtTours = dsTours.Tables.Add("Туры");
    	DataColumn dсIDtour = dtTours.Columns.Add("Код тура", typeof(Int32));
    	dсIDtour.Unique = true;
    	DataColumn dcName = dtTours.Columns.Add("Название");
    	DataColumn dcPrice = dtTours.Columns.Add("Цена", typeof(Decimal));
    	DataColumn dcInformation = dtTours.Columns.Add("Информация");
    	conn.Open();
    	dataAdapter.Fill(dsTours.Tables["Туры"]);
    	conn.Close();
    	dataGrid1.DataSource = dsTours.Tables["Туры"].DefaultView;
    }

    Запускаем приложение (рис. 8.11):

    (рис 8.11) Вывод содержимого таблицы "Туры"

    Теперь добавим два объекта DataColumn, в которых будет вычисляться налог и скидка, после добавления объекта dcPrice:

    ...
    DataColumn dcPrice = dtTours.Columns.Add("Цена", typeof(Decimal));
    DataColumn dcPriceNDS = dtTours.Columns.Add("Цена c НДС", typeof(Decimal));
    dcPriceNDS.Expression = "Цена*0.15+Цена";
    DataColumn dcPricewithDiscount = dtTours.Columns.Add("Цена cо скидкой",
     typeof(Decimal));
    dcPricewithDiscount.Expression = "Цена-Цена*0.10";
    ...

    Свойство Expression созданного объекта DataColumn задает выражения для всех значений заданного поля (рис. 8.12):

    (рис 8.12) Значения полей "Цена с НДС" и "Цена со скидкой" получены в результате вычислений

    Свойство Expression поддерживает также агрегатные функции, объединение строк, ссылки на родительские и дочерние таблицы.

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

    Объект DataRow

    Содержимое объекта DataSet представляет собой набор записей, который представлен объектами DataRow. В запущенном приложении содержимое объекта DataSet доступно для изменений, например, если данные выводятся в элемент управления DataGrid, то, перемещаясь по отдельным клеткам, можно править значения как в обычной электронной таблице. При этом происходит изменение объекта DataRow, соответствующее заданной записи. Рассмотрим программное создание и изменение записей. Создайте новое Windows-приложение и назовите его ProgrammDataRow. Перетаскиваем на форму элемент управления DataGrid, свойству Dock устанавливаем значение Fill. В конструкторе формы создаем экземпляр dtTours и поля, соответствующие таблице "Туры":

    public Form1()
    {
    	InitializeComponent();
    	DataTable dtTours = new DataTable();
    	DataColumn dсIDtour = dtTours.Columns.Add("Код тура", typeof(Int32));
    	dсIDtour.Unique = true;
    	DataColumn dcName = dtTours.Columns.Add("Название");
    	DataColumn dcPrice = dtTours.Columns.Add("Цена", typeof(Decimal));
    	DataColumn dcInformation = dtTours.Columns.Add("Информация");
    	DataView myDataView = new DataView(dtTours);
    	dataGrid1.DataSource = myDataView;
    }

    Для того чтобы привязать созданные данные к элементу управления DataTable содержит объект DataView, причем этот объект, используемый по умолчанию, называется DataTable.DefaultView. Мы уже сталкивались с ним неоднократно, например, в предыдущем проекте CustomExpression для вывода данных:

    dataGrid1.DataSource = dsTours.Tables["Туры"].DefaultView;

    Один объект DataTable может иметь несколько объектов DataView - это удобно для вывода одних и тех же данных, отфильтрованных или отсортированных различным образом. В следующей лекции мы рассмотрим подробно DataView. Запускаем приложение (рис. 8.13). Мы видим готовую структуру таблицы "Туры":

    (рис 8.13) Структура таблицы "Туры"

    Мы не будем сейчас подключаться к какой-либо базе данных - попробуем заполнить таблицу записями программно. Для добавлений одной новой записи перед созданием экземпляра myDataView вставляем следующий фрагмент кода:

    DataRow myRow = dtTours.NewRow();
    myRow["Код тура"] = 1;
    myRow["Название"] = "Кипр";
    myRow["Цена"] = 25000;
    myRow["Информация"] = "В стоимость двух взрослых путевок
     входит цена одной детской (до 7 лет)";
    dtTours.Rows.Add(myRow);

    Запускаем приложение (рис. 8.14). В таблице появилась первая запись.

    (рис 8.14) Добавление записи в таблицу

    Добавим еще одну запись:

    DataRow myRow2 = dtTours.NewRow();
    myRow2["Код тура"] = 2;
    myRow2["Название"] = "Греция";
    myRow2["Цена"] = 32000;
    myRow2["Информация"] = "В августе и сентябре действуют
     специальные скидки";
    dtTours.Rows.Add(myRow2);

    Название, указываемое в квадратных скобках объектов myRow или myRow2, представляет собой имя столбца, которые мы определили в самом начале. К столбцу можно обращаться и по индексу - закомментируйте добавление двух записей и внесите следующий код:

    DataRow myRow = dtTours.NewRow();
    myRow[0] = 1;
    myRow[1] = "Кипр";
    myRow[2] = 25000;
    myRow[3] = "В стоимость двух взрослых путевок входит
     цена одной детской (до 7 лет)";
    dtTours.Rows.Add(myRow);
    
    DataRow myRow2 = dtTours.NewRow();
    myRow2[0] = 2;
    myRow2[1] = "Греция";
    myRow2[2] = 32000;
    myRow2[3] = "В августе и сентябре действуют специальные скидки";
    dtTours.Rows.Add(myRow2);

    Нумерация столбцов начинается с нуля. Более удобный способ добавления записей - применение свойства ItemArray объекта DataRow, где можно задавать значения полей в виде массива:

    DataRow myRow3 = dtTours.NewRow();
    myRow3.ItemArray = new object[]{3,"Таиланд", 30000, null};
    dtTours.Rows.Add(myRow3);

    Здесь мы указали значение поля "Информация", равное null, - таким образом можно пропускать неизвестные поля (рис. 8.15):

    (рис 8.15) Вставка записи с одним значением null

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

    Конечно, вставка записей вручную в объект DataSet, не связанный с хранилищем данных, имеет не очень большой смысл. Поэтому давайте рассмотрим, как вставлять (и изменять) данные в уже загруженный кэш данных. Скопируйте папку проекта CustomExpression и переименуйте ее в WorkWithRow. Чтобы не тратить время на привязку действий к элементам управления, будем далее писать код после отображения данных в элементе DataGrid:

    dataGrid1.DataSource = dsTours.Tables["Туры"].DefaultView;

    Для изменения, например, пятой строки, мы указываем в свойстве Rows объекта dtTours ее индекс (4, нумерация начинается с нуля), затем вызываем метод BeginEdit для начала редактирования, устанавливаем группу свойств и в заключение принимаем изменения, вызывая метод EndEdit:

    DataRow myRow=dtTours.Rows[4];
    myRow.BeginEdit();
    myRow["Код тура"] = 5;
    myRow["Название"] = "Турция";
    myRow["Цена"] = "27000";
    myRow["Информация"] = "Осенние скидки с 15 октября";
    myRow.EndEdit();

    Тот же самый результат мы получим с помощью свойства ItemArray:

    DataRow myRow=dtTours.Rows[4];
    myRow.BeginEdit();
    myRow.ItemArray = new object[]{5,"Турция", 27000, null, null,
     "Осенние скидки с 15 октября"};
    myRow.EndEdit();

    Здесь мы установили для третьего и четвертого полей, которые являются вычисляемыми, значения null, подразумевая, что они останутся по умолчанию, а при запуске заполнятся своими значениями (рис. 8.16):

    (рис 8.16) Пропущенные вычисляемые поля заполняются своими значениями

    Для удаления заданной записи нужно создать объект DataRow, которому передается индекс строки, а затем вызвать метод Remove свойства Rows объекта DataTable:

    DataRow myRow2 = dtTours.Rows[0];
    dtTours.Rows.Remove(myRow2);

    Этого достаточно для удаления строки, но для того, чтобы пометить заданную строку как удаленную, вызываем метод Delete:

    myRow2.Delete();

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

    (рис 8.17) Первая строка, имеющая индекс 0, была удалена

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

    Свойство RowState

    При работе с данными приходится постоянно вносить изменения в записи - добавлять, редактировать или удалять. Объект DataRow обладает свойством RowState, позволяющим отслеживать текущий статус строки. Создайте новое приложение, назовите его "RowState". Перетаскиваем на форму элементы RichTextBox, Splitter, DataGrid, устанавливаем для них следующие свойства:

    richTextBox1, свойство Значение
    Dock Bottom
    Text
    splitter1, свойство Значение
    Dock Bottom
    dataGrid1, свойство Значение
    Dock Fill

    В конструкторе формы мы создаем всего одно поле, затем одну запись, статус которой будем отслеживать:

    public Form1()
    {
    	InitializeComponent();
    	DataTable dtTours = new DataTable("Туры");
    	DataColumn IDtour = new DataColumn("Код тура", typeof(Int32));
    	dtTours.Columns.Add(IDtour);
    	dataGrid1.DataSource = dtTours;
    	DataRow myRow;
    	// Создаем новую, отсоединенную запись
    	myRow = dtTours.NewRow();
    	richTextBox1.Text += Convert.ToString("Новая запись: "
    	 + myRow.RowState);
    	//Добавляем запись в объект DataTable
    	dtTours.Rows.Add(myRow);
    	richTextBox1.Text += Convert.ToString("\nДобавление записи: " +
    	 myRow.RowState);
    	//Принимаем все изменения в объекте DataTable
    	dtTours.AcceptChanges();
    	richTextBox1.Text += Convert.ToString("\nМетод AcceptChanges: " +
    	 myRow.RowState);
    	//Редактируем запись
    	myRow["Код тура"] = 1;
    	richTextBox1.Text += Convert.ToString("\nРедактирование строки: " +
    	 myRow.RowState);
    	//Удаляем строку
    	myRow.Delete();
    	richTextBox1.Text += Convert.ToString("\nУдаление: " +
    	 myRow.RowState);
    	//Отменяем все изменения в объекте DataTable
    	dtTours.RejectChanges();
    	richTextBox1.Text += Convert.ToString("\nМетод RejectChanges: " +
    	 myRow.RowState);
    }

    Запускаем приложение. В текстовое поле выводится статус записи myRow (рис. 8.18):

    (рис 8.18) Приложение RowState

    Значение Detached означает, что запись не относится к объекту DataTable. После добавления ее статус изменяется на Added - теперь она существует в объекте DataTable, но ее нет в базе данных. Конечно, здесь мы не рассматривали взаимодействие с источником записей, но это же значение будет у записей, добавляемых в DataGrid после вывода данных из базы при наличии подключения. Вызывая метод AcceptChanges объекта DataTable, мы принимаем все изменения, поэтому статус DataRow изменяется на Unchanged - теперь запись считается "своей", она не была изменена после вызова метода. После вызова метода Delete запись помечается удаленной - она еще не полностью удалена, в случае отмены изменений статус будет восстановлен. Действительно, вызывая метод RejectChanges объекта DataTable, мы восстанавливаем запись до состояния Unchanged.

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

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

    Свойство RowVersion

    Свойство RowVersion предназначено для извлечения значения записи (объекта DataRow), зависящего от совершенных изменений. Возможны следующие версии записи:

  • Current - текущее значение
  • Default - значение по умолчанию
  • Original - первоначальное значение
  • Proposed - предполагаемое значение
  • Создайте новое приложение, назовите его "RowVersion". Свойству Size формы устанавливаем значение 500; 300. Из окна Toolbox перетаскиваем следующие элементы и устанавливаем их свойства:

    panel1, свойство Значение
    Dock Bottom
    dataGrid1, свойство Значение
    Dock Left
    CaptionText Туры
    splitter1, свойство Значение
    Dock Left
    richTextBox1, свойство Значение
    Name rtbReport
    Dock Fill
    Text
    button1, свойство Значение
    Name BtnBeginEdit
    Location 14; 21
    Text Begin Edit
    button2, свойство Значение
    Name btnEndEdit
    Location 106; 21
    Text End Edit
    button3, свойство Значение
    Name btnCancelEdit
    Location 198; 21
    Text Cancel Edit
    button4, свойство Значение
    Name btnDelete
    Location 290; 21
    Text Удалить
    button5, свойство Значение
    Name btnClearReport
    Location 382; 21
    Size 96; 23
    Text Очистить отчет

    Переходим к коду. В классе формы объявляем объекты DataRow и DataTable:

    DataRow myRow;
    DataTable dtTours;

    В конструкторе формы создаем запись, определяем источник данных для элемента DataGrid, а также отключаем его доступность:

    public Form1()
    {			
    	InitializeComponent();
    	dtTours = new DataTable("Туры");
    	DataColumn IDtour = new DataColumn("Название", typeof(string));
    	dtTours.Columns.Add(IDtour);
    	myRow = dtTours.NewRow();
    	dtTours.Rows.Add(myRow);
    	myRow["Название"] = "Таиланд";
    	dataGrid1.DataSource = dtTours;
    	dataGrid1.Enabled = false;
    }

    Создаем метод TestRowVersion, в котором будет проверяться свойство RowVersion записи:

    private void TestRowVersion()
    {
    	if(myRow.HasVersion(DataRowVersion.Original))
    	rtbReport.Text += String.Format("Значение original: {0}\n",
    	 myRow["Название", DataRowVersion.Original]);
    	if(myRow.HasVersion(DataRowVersion.Current))
    	rtbReport.Text += String.Format("Значение current: {0}\n",
    	 myRow["Название", DataRowVersion.Current]);
    	if(myRow.HasVersion(DataRowVersion.Default))
    	rtbReport.Text += String.Format("Значение default: {0}\n",
    	 myRow["Название", DataRowVersion.Default]);
    	if(myRow.HasVersion(DataRowVersion.Proposed))
    	rtbReport.Text += String.Format("Значение proposed: {0}\n",
    	 myRow["Название", DataRowVersion.Proposed]);
    }

    Метод HasVersion позволяет определить, поддерживает ли объект myRow версию данных, указываемую в скобках. В случае подтверждения будет выполняться код оператора - выводится в элемент rtbReport соответствующее сообщение.

    В обработчике кнопки "Begin Edit" вызываем метод BeginEdit, устанавливаем новое значение записи:

    private void btnBeginEdit_Click(object sender, System.EventArgs e)
    {
    	myRow.BeginEdit();
    	myRow["Название"] = "Франция";
    	rtbReport.Text += "BeginEdit\n";
    	TestRowVersion();
    }

    В обработчике кнопки "End Edit" завершаем редактирование записи:

    private void btnEndEdit_Click(object sender, System.EventArgs e)
    {
    	myRow.EndEdit();
    	rtbReport.Text += "EndEdit\n";
    	TestRowVersion();
    }

    В обработчике кнопки "Cancel Edit" отказываемся от внесенных изменений:

    private void btnCancelEdit_Click(object sender, System.EventArgs e)
    {
    	myRow.CancelEdit();
    	rtbReport.Text += "CancelEdit\n";
    	TestRowVersion();
    }

    В обработчике кнопки "Удалить" удаляем объект myRow:

    private void btnDelete_Click(object sender, System.EventArgs e)
    {
    	myRow.Delete();
    	rtbReport.Text += "Запись удалена\n";
    	TestRowVersion();
    }

    В обработчике кнопки "Очистить отчет" просто удаляем содержимое текстового поля:

    private void btnClearReport_Click(object sender, System.EventArgs e)
    {
    	this.rtbReport.Text = "";
    }

    Запускаем приложение. После нажатия кнопки "BeginEdit" мы начинаем редактирование записи, вводится новое значение - "Франция". Оно становится значением по умолчанию "Default" и предполагаемым "Proposed", значение "Таиланд" является текущим "Current" (рис. 8.19, А). Отменяем редактирование, нажимая кнопку "Cancel Edit". При этом значение "Таиланд" становится текущим "Current" и по умолчанию " Default" (рис. 8.19, Б). Снова начинаем редактирование - картина повторяется (рис. 8.19, В). На этот раз завершаем его, нажимая кнопку "End Edit" - новое значение "Франция" становится текущим "Current" и по умолчанию " Default" (рис. 8.19, Г). Нажимаем кнопку "Удалить" - при этом удаляется сам объект myRow и дальнейшее изменение его з начений оказывается невозможным (рис. 8.19, Д).

    (рис 8.19) Приложение "RowVersion"

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

    Подобная функциональность вряд может нас устроить в реальных приложениях - в самом деле, мы вставляем только заранее определенное значение "Франция". Скопируйте папку приложения RowVersion и назовите ее RowVersion2. Добавим на форму элементы "надпись" и "текстовое поле":

    label1, свойство Значение
    location 24; 16
    Size 72; 24
    Text Название
    textbox1, свойство Значение
    Name txtmyRowName
    location 104; 16
    Size 344; 20
    Text

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

    private void btnBeginEdit_Click(object sender, System.EventArgs e)
    		{
    			myRow.BeginEdit();
    			rtbReport.Text += "BeginEdit\n";
    			TestRowVersion();
    			txtmyRowName.Enabled = true;
    		}
    
    private void btnEndEdit_Click(object sender, System.EventArgs e)
    		{
    			myRow["Название"] = txtmyRowName.Text;
    			myRow.EndEdit();
    			rtbReport.Text += "EndEdit\n";
    			TestRowVersion();
    			txtmyRowName.Enabled = false;
    		}
    
    private void btnCancelEdit_Click(object sender, System.EventArgs e)
    		{
    			myRow.CancelEdit();
    			rtbReport.Text += "CancelEdit\n";
    			TestRowVersion();
    			txtmyRowName.Enabled = false;
    		}

    Обработчик кнопки "Удалить" изменим так же - теперь будет удаляться текущий экземпляр myRow и тут же создаваться новый:

    private void btnDelete_Click(object sender, System.EventArgs e)
    		{
    			myRow.Delete();
    			rtbReport.Text += "Запись удалена\n";
    			TestRowVersion();
    			myRow = dtTours.NewRow();
    			dtTours.Rows.Add(myRow);
    		}

    В конструкторе формы отключим доступность текстового поля:

    public Form1()
    		{
    			...			
    			txtmyRowName.Enabled = false;
    		}

    Запускаем приложение. Выполняя знакомую последовательность действий, получаем похожий отчет; теперь при удалении записи можно продолжить работу - объект myRow будет воссоздан (рис. 8.20):

    (рис 8.20) Приложение RowVersion2

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

    События объекта DataTable

    Объект DataTable содержит ряд событий, которые могут применяться для слежения за происходящими изменениями. Наиболее часто используются следующие события:

  • ColumnChanged - наступает после изменения содержимого поля таблицы;
  • ColumnChanging - происходит в течение редактирования содержимого поля таблицы;
  • RowChanged - наступает после изменения объекта DataRow (записи);
  • RowChanging - происходит в течение редактирования объекта DataRow ;
  • RowDeleted - наступает после удаления объекта DataRow ;
  • RowDeleting - происходит при удалении объекта DataRow.
  • Скопируйте папку приложения RowVersion, назовите ее "Data TableEvents". В конструкторе формы добавим обработку четырех событий объекта DataTable:

    public Form1()
    {			
    	...
    	dtTours.RowChanging += new DataRowChangeEventHandler(dtTours_RowChanging);
    	dtTours.RowChanged += new DataRowChangeEventHandler(dtTours_RowChanged);
    	dtTours.RowDeleting += new DataRowChangeEventHandler(dtTours_RowDeleting);
    	dtTours.RowDeleted += new DataRowChangeEventHandler(dtTours_RowDeleted);
    }

    В соответствующих методах просто выводим сообщение в текстовое поле:

    private void dtTours_RowChanging(object sender, DataRowChangeEventArgs e)
    {
    	rtbReport.Text += String.Format("Событие - изменение записи\n",
    	 e.Row["Название"]);
    }
    
    private void dtTours_RowChanged(object sender, DataRowChangeEventArgs e)
    {
    	rtbReport.Text += "\nСобытие - запись изменена\n";
    }
    
    private void dtTours_RowDeleting(object sender, DataRowChangeEventArgs e)
    {
    	rtbReport.Text += String.Format("Событие - удаление записи\n",
    	 e.Row["Название"]);
    }
    
    private void dtTours_RowDeleted(object sender, DataRowChangeEventArgs e)
    {
    	rtbReport.Text += "\nСобытие - запись удалена\n";
    }

    Запускаем приложение. Нажимаем кнопку "Begin Edit", затем "End Edit" - происходят события RowChanging и RowChanged. Удаляем запись - происходят события RowDeleting и RowDeleted (рис. 8.21).

    (рис 8.21) Проект "DataTableEvents"

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

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

    Страницы:

    Объекты DataTable, DataRow и DataColumn

    Понятие 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:

    Структура таблиц базы данных Tests
    Таблица Поле Описание поля
    Questions - таблица вопросов questID Номер вопроса
    question Текст вопроса
    questType Тип вопроса (с одним правильным вариантом ответа или с несколькими)
    Variants - таблица вариантов ответов id Номер ответа
    questID Номер вопроса
    variant Текст варианта ответа
    isRight Является ли данный ответ верным

    Для нас не столь важно количество вопросов, поэтому их всего будет пять, относящихся к теме операционной системы Windows XP. Одному вопросу может соответствовать один правильный вариант ответа - в этом случае в поле questType указывается значение "0". Далее на форме эти варианты будут выводиться с элементами RadioButton. Для вопросов, у которых есть несколько правильных вариантов ответов, указывается в поле questType значение "1". На форме такие варианты будут выводиться с чекбоксами. В таблицах 8.2 и 8.3 приводится содержимое таблиц Questions и Variants.

    Вопросы таблицы Questions
    questID 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
    Ответы таблицы "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 элемента (...), запускается редактор Table Collection Editor (рис. 8.2), нажимаем кнопку "Add" и вводим следующие значения свойств:

    TableName Questions
    Name dtQuestions

    Значение (рис 8.2) (...), появляется редактор 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();
    }

    Создаем два экземпляра класса HashtableДополнительные сведения об этом классе см. в конце Лекции 9.:

    // Экземпляр 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).

    Программное создание объектов DataTable и DataColumn. Ограничения Unique Constraint и ForeignConstraint

    Еще в первой лекции мы узнали, что все объекты вкладки Data или все объекты ADO .NET можно создавать программно. Скопируйте папку приложения Tests и назовите ее "ProgrammTests". Открываем проект, удаляем объект DataSet с панели компонент формы. Для создания этого объекта программно достаточно следующей строчки кода:

    DataSet dsTests = new DataSet();

    Поскольку созданный экземпляр dsTests будет использоваться в нескольких методах, эту строку следует написать в классе формы. Здесь dsTests - это название объекта DataSet, которое мы затем используем в коде - это известно нам с первой лекции. Среда Visual Studio .NET однако, сгенерировала следующий фрагмент кода (его можно найти в исходном проекте "Tests"):

    this.dsTests = new System.Data.DataSet();
    ...
    // 
    // dsTests
    // 
    this.dsTests.DataSetName = "NewDataSet"; 
    this.dsTests.Locale = new System.Globalization.CultureInfo("ru-RU");

    Как всегда, автоматический код избыточен - здесь указывается культура ru-RU. Дело в том, что у меня установлена русская версия Windows XP и соответствующие региональные настройки.

    Среда определила еще свойство DataSetName = "NewDataSet", эквивалентное описание будет иметь следующий вид:

    DataSet dsTests = new DataSet("NewDataSet");

    Что же это за еще одно название объекта DataSet? Свойство DataSetName используется для работы с XSD-схемами, пока про это название (до лекции 10) мы можем просто забыть.

    Создадим теперь объект DataTable для таблицы Questions:

    DataTable dtQuestions = dsTests.Tables.Add("Questions");
    //Или
    //DataTable dtQuestions = new DataTable("Questions");
    //dsTests.Tables.Add(dtQuestions);

    Здесь мы создаем экземпляр dtQuestions объекта DataTable, затем вызываем метод Add свойства Tables объекта dsTests, которому передаем название таблицы Questions. Далее создаем поля в объекте dtQuestions:

    DataColumn dсQuestID = dtQuestions.Columns.Add("questID", typeof(Int32));
    dсQuestID.Unique = true;
    DataColumn dcQuestion = dtQuestions.Columns.Add("question");
    DataColumn dcQuestType = dtQuestions.Columns.Add("questType", typeof(Int32));

    Мы создаем поля, нужные для отражения соответствующих столбцов в таблице Questions. Перегруженный метод Add свойства Columns объекта dtQuestions позволяет задавать название столбца и его тип данных (рис. 8.7):

    (рис 8.7) Создание поля

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

    Точно так же создаем объект DataTable для таблицы Variants и соответствующие поля:

    //Cоздаем таблицу "Variants"
    DataTable dtVariants = dsTests.Tables.Add("Variants");
    	
    //Заполняем поля таблицы "Variants"
    DataColumn dcID = dtVariants.Columns.Add("id", typeof(Int32));
    dcID.Unique = true;
    dcID.AutoIncrement = true;
    DataColumn dcVariantQuestID = dtVariants.Columns.Add("questID", typeof(Int32));
    DataColumn dcVariant = dtVariants.Columns.Add("variant");
    DataColumn dcIsRight = dtVariants.Columns.Add("isRight", typeof(Boolean));

    Здесь мы дополнительно установили свойству AutoIncrement объекта dcID значение true. Свойство AutoIncrement (Счетчик) позволяет создать счетчик для поля, аналогичный типу данных "Счетчик" в Microsoft Access.

    Теперь приступим к созданию связи между таблицами. В базе данных Microsoft SQL Tests между родительской таблицей Questions и дочерней Variants была установлена связь по полю questID, которое было в обеих таблицах. При программном создании объектов для поля questID таблицы Questions был создан объект dсQuestID, для этого же поля таблицы Variants создан объект dcVariantQuestID. В коде создание отношения между таблицами будет иметь следующий вид:

    DataRelation drQuestionsVariants = new DataRelation("QuestionsVariants",
     dсQuestID, dcVariantQuestID);
    dsTests.Relations.Add(drQuestionsVariants);

    Здесь в конструкторе drQuestionsVariants - название экземпляра объекта (класса) DataRelation, а QuestionsVariants - свойство relationName - название связи, которая будет содержаться в объекте drQuestionsVariants. Другими словами, drQuestionsVariants - название экземпляра DataRelation, которое мы будем использовать в коде, а свойство relationName - всего лишь название отражаемой связи, которую можно удалить или переименовать.

    Итак, мы создали все объекты для отображения таблиц, полей и даже связей между таблицами.

    Теперь нам осталось определить некоторые свойства таблиц, называемые ограничениями. Свойство ограничения ( Constraint ) объекта DataTable бывает двух типов - UniqueConstraint и ForeignKeyConstraint. Свойство UniqueConstraint определяет первичный ключ таблицы, например, в таблице Questions ключевым полем является questID. Объект dсQuestID представляет это поле:

    DataColumn dсQuestID = dtQuestions.Columns.Add("questID", typeof(Int32));

    Ограничение UniqueConstraint, налагаемое на объект dсQuestID, запрещает появление дублированных строк:

    UniqueConstraint UC_dtQuestions = new UniqueConstraint(dсQuestID);
    dtQuestions.Constraints.Add(UC_dtQuestions);

    Однако при создании объекта dсQuestID мы ведь уже определяли его уникальность:

    dсQuestID.Unique = true;

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

    Второе ограничение - ForeignKeyConstraint - определяет, как должны себя вести дочерние записи при изменении родительских записей и наоборот. Конечно, интерфейс нашего приложения вообще не подразумевает внесение изменений, но ADO .NET требует точного описания объектов для управления ими. Ограничение ForeignKeyConstraint содержит следующие три правила:

  • UpdateRule - применяется при изменении родительской строки;
  • DeleteRule - применяется при удалении родительской строки;
  • AcceptRejectRule - применяется при вызове метода AcceptChanges объекта DataTable, для которого определено ограничение.
  • Для этих правил могут применяться следующие значения:

  • Cascade - каскадное обновление связанных записей;
  • None - изменения в родительской таблице не отражаются в дочерних записях;
  • SetDefault - полю внешнего ключа в дочерних записях присваивается значение, заданное в свойстве DefaultValue этого поля;
  • SetNull - полю внешнего ключа в дочерних записях присваивается значение Null.
  • Значением по умолчанию для правил UpdateRule и DeleteRule является Cascade, для правила AcceptRejectRule - None. Дополнительно, правило AcceptRejectRule принимает значения только Cascade или None.

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

    ForeignKeyConstraint FK_QuestionsVariants = 
     new ForeignKeyConstraint(dtQuestions.Columns["questID"],
     dtVariants.Columns["questID"]);
    dtVariants.Constraints.Add(FK_QuestionsVariants);

    Здесь задается вначале родительская колонка, а затем дочерняя (рис. 8.8). Добавлять созданное ограничение следует к объекту DataTable, представляющему дочернюю таблицу (в данном случае - объект dtVariants )

    (рис 8.8) Создание ограничения FK_QuestionsVariants

    Этот фрагмент кода оставляет значения правил UpdateRule, DeleteRule и AcceptRejectRule заданными по умолчанию, т.е. Cascade и None, что соответствует значениям настройки с помощью мастера Relation (рис. 8.9):

    (рис 8.9) Мастер Relation и соответствующий фрагмент кода

    Если бы нам потребовалось задать значение одному из правил, отличное по умолчанию, мы бы просто применили другой вариант конструктора (рис. 8.10):

    (рис 8.10) В этом конструкторе можно задать значения правил RejectRule, DeleteRule и UpdateRule

    ПолностьюНе забудьте в классе формы создать объект DataSet: DataSet dsTests = new DataSet(); метод LoadDataBase в проекте ProgrammTests будет выглядеть так:

    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);
    	//dsTests.EnforceConstraints = true;
    	//Cоздаем таблицу "Questions"
    	DataTable dtQuestions = dsTests.Tables.Add("Questions");
    	//Или
    	//DataTable dtQuestions = new DataTable("Questions");
    	//dsTests.Tables.Add(dtQuestions);
    	//Заполняем поля таблицы "Questions"
    	DataColumn dсQuestID =
    	 dtQuestions.Columns.Add("questID", typeof(Int32));
    	dсQuestID.Unique = true;
    	//Или
    	//UniqueConstraint UC_dtQuestions = new UniqueConstraint(dсQuestID);
    	//dtQuestions.Constraints.Add(UC_dtQuestions);
    	DataColumn dcQuestion = dtQuestions.Columns.Add("question");
    	DataColumn dcQuestType =
    	 dtQuestions.Columns.Add ("questType", typeof(Int32));
    	//Cоздаем таблицу "Variants"
    	DataTable dtVariants = dsTests.Tables.Add("Variants");
    	//Заполняем поля таблицы "Variants"
    	DataColumn dcID = dtVariants.Columns.Add("id", typeof(Int32));
    	dcID.Unique = true;
    	dcID.AutoIncrement = true;
    	DataColumn dcVariantQuestID =
    	 dtVariants.Columns.Add("questID", typeof(Int32));
    	DataColumn dcVariant = dtVariants.Columns.Add("variant");
    	DataColumn dcIsRight = dtVariants.Columns.Add("isRight", typeof(Boolean));
    	//Создаем ограничение
    	ForeignKeyConstraint FK_QuestionsVariants = 
    	 new ForeignKeyConstraint(dtQuestions.Columns["questID"],
    	 dtVariants.Columns["questID"]);
    	dtVariants.Constraints.Add(FK_QuestionsVariants);
    	//Создаем отношение
    	DataRelation drQuestionsVariants = new DataRelation("QuestionsVariants",
    	 dсQuestID, dcVariantQuestID);
    	dsTests.Relations.Add(drQuestionsVariants);
    	conn.Open();
    	//Заполняем таблицу "Questions" данными из questAdapter
    	questAdapter.Fill(dsTests.Tables["Questions"]);
    	//Заполняем таблицу "Variants" данными из variantsAdapter
    	variantsAdapter.Fill(dsTests.Tables["Variants"]);
    	conn.Close();
    }

    Обратим внимание на несколько деталей этого метода. Значение true свойства EnforceConstraints объекта dsTests разрешает использование ограничений. По умолчанию в созданном объекте DataSet это свойство и так принимает значение true, поэтому этот фрагмент кода закомментирован. Следует иметь в виду, что для снятия всех ограничений достаточно установить свойству EnforceConstraints значение false. Ограничение FK_QuestionsVariants создается перед отношением drQuestionsVariants - вначале следует создавать ограничения, а затем определять отношения. Соединение conn открывается как можно позже - непосредственно перед заполнением данными объектов DataAdapter, - и тут же закрывается. Открыть его в начале метода и закрыть в конце было бы нерациональным.

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

    Создание столбцов, основанных на выражении

    При создании базы данных не следует помещать в нее значения, которые могут быть получены из уже имеющихся данных. В первой лекции мы создали базу данных BDTur_firm.mdb, в которой есть таблица "Туры". В этой таблице имеется поле "цена", где указывается стоимость туров. На практике может понадобиться значение цены, выраженное в различных валютах, значение с учетом налогов, значение со скидкой и т.п. Для каждого конкретного случая можно получить дополнительное поле, не вводя его в саму базу данных. Технология ADO .NET позволяет создавать объекты DataColumn, основанные на выражении. Создайте новое Windows-приложение и назовите его CustomExpression. Перетаскиваем на форму элемент управления DataGrid, свойству Dock устанавливаем значение Fill. Переходим в код формы, подключаем пространство имен:

    using System.Data.OleDb;

    В классе формы определяем строки CommandText и ConnectionString:

    string commandText = "SELECT Информация, [Код тура], Название, Цена FROM Туры";
    string connectionString = @"Provider=""Microsoft.Jet.OLEDB.4.0"";Data
     Source=""D:\Uchebnik\Code\Glava1\BDTur_firm.mdb"";User ID=Admin;
     Jet OLEDB:Encrypt Database=False";

    В конструкторе формы программно создаем все объекты для вывода таблицы "Туры" в элемент управления DataGrid:

    public Form1()
    {	
    	InitializeComponent();
    	OleDbConnection conn = new OleDbConnection (connectionString);
    	OleDbCommand myCommand = new OleDbCommand();
    	myCommand.Connection = conn;
    	myCommand.CommandText = commandText;
    	OleDbDataAdapter dataAdapter = new OleDbDataAdapter();
    	dataAdapter.SelectCommand = myCommand;
    	DataSet dsTours = new DataSet();
    	DataTable dtTours = dsTours.Tables.Add("Туры");
    	DataColumn dсIDtour = dtTours.Columns.Add("Код тура", typeof(Int32));
    	dсIDtour.Unique = true;
    	DataColumn dcName = dtTours.Columns.Add("Название");
    	DataColumn dcPrice = dtTours.Columns.Add("Цена", typeof(Decimal));
    	DataColumn dcInformation = dtTours.Columns.Add("Информация");
    	conn.Open();
    	dataAdapter.Fill(dsTours.Tables["Туры"]);
    	conn.Close();
    	dataGrid1.DataSource = dsTours.Tables["Туры"].DefaultView;
    }

    Запускаем приложение (рис. 8.11):

    (рис 8.11) Вывод содержимого таблицы "Туры"

    Теперь добавим два объекта DataColumn, в которых будет вычисляться налог и скидка, после добавления объекта dcPrice:

    ...
    DataColumn dcPrice = dtTours.Columns.Add("Цена", typeof(Decimal));
    DataColumn dcPriceNDS = dtTours.Columns.Add("Цена c НДС", typeof(Decimal));
    dcPriceNDS.Expression = "Цена*0.15+Цена";
    DataColumn dcPricewithDiscount = dtTours.Columns.Add("Цена cо скидкой",
     typeof(Decimal));
    dcPricewithDiscount.Expression = "Цена-Цена*0.10";
    ...

    Свойство Expression созданного объекта DataColumn задает выражения для всех значений заданного поля (рис. 8.12):

    (рис 8.12) Значения полей "Цена с НДС" и "Цена со скидкой" получены в результате вычислений

    Свойство Expression поддерживает также агрегатные функции, объединение строк, ссылки на родительские и дочерние таблицы.

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

    Объект DataRow

    Содержимое объекта DataSet представляет собой набор записей, который представлен объектами DataRow. В запущенном приложении содержимое объекта DataSet доступно для изменений, например, если данные выводятся в элемент управления DataGrid, то, перемещаясь по отдельным клеткам, можно править значения как в обычной электронной таблице. При этом происходит изменение объекта DataRow, соответствующее заданной записи. Рассмотрим программное создание и изменение записей. Создайте новое Windows-приложение и назовите его ProgrammDataRow. Перетаскиваем на форму элемент управления DataGrid, свойству Dock устанавливаем значение Fill. В конструкторе формы создаем экземпляр dtTours и поля, соответствующие таблице "Туры":

    public Form1()
    {
    	InitializeComponent();
    	DataTable dtTours = new DataTable();
    	DataColumn dсIDtour = dtTours.Columns.Add("Код тура", typeof(Int32));
    	dсIDtour.Unique = true;
    	DataColumn dcName = dtTours.Columns.Add("Название");
    	DataColumn dcPrice = dtTours.Columns.Add("Цена", typeof(Decimal));
    	DataColumn dcInformation = dtTours.Columns.Add("Информация");
    	DataView myDataView = new DataView(dtTours);
    	dataGrid1.DataSource = myDataView;
    }

    Для того чтобы привязать созданные данные к элементу управления DataTable содержит объект DataView, причем этот объект, используемый по умолчанию, называется DataTable.DefaultView. Мы уже сталкивались с ним неоднократно, например, в предыдущем проекте CustomExpression для вывода данных:

    dataGrid1.DataSource = dsTours.Tables["Туры"].DefaultView;

    Один объект DataTable может иметь несколько объектов DataView - это удобно для вывода одних и тех же данных, отфильтрованных или отсортированных различным образом. В следующей лекции мы рассмотрим подробно DataView. Запускаем приложение (рис. 8.13). Мы видим готовую структуру таблицы "Туры":

    (рис 8.13) Структура таблицы "Туры"

    Мы не будем сейчас подключаться к какой-либо базе данных - попробуем заполнить таблицу записями программно. Для добавлений одной новой записи перед созданием экземпляра myDataView вставляем следующий фрагмент кода:

    DataRow myRow = dtTours.NewRow();
    myRow["Код тура"] = 1;
    myRow["Название"] = "Кипр";
    myRow["Цена"] = 25000;
    myRow["Информация"] = "В стоимость двух взрослых путевок
     входит цена одной детской (до 7 лет)";
    dtTours.Rows.Add(myRow);

    Запускаем приложение (рис. 8.14). В таблице появилась первая запись.

    (рис 8.14) Добавление записи в таблицу

    Добавим еще одну запись:

    DataRow myRow2 = dtTours.NewRow();
    myRow2["Код тура"] = 2;
    myRow2["Название"] = "Греция";
    myRow2["Цена"] = 32000;
    myRow2["Информация"] = "В августе и сентябре действуют
     специальные скидки";
    dtTours.Rows.Add(myRow2);

    Название, указываемое в квадратных скобках объектов myRow или myRow2, представляет собой имя столбца, которые мы определили в самом начале. К столбцу можно обращаться и по индексу - закомментируйте добавление двух записей и внесите следующий код:

    DataRow myRow = dtTours.NewRow();
    myRow[0] = 1;
    myRow[1] = "Кипр";
    myRow[2] = 25000;
    myRow[3] = "В стоимость двух взрослых путевок входит
     цена одной детской (до 7 лет)";
    dtTours.Rows.Add(myRow);
    
    DataRow myRow2 = dtTours.NewRow();
    myRow2[0] = 2;
    myRow2[1] = "Греция";
    myRow2[2] = 32000;
    myRow2[3] = "В августе и сентябре действуют специальные скидки";
    dtTours.Rows.Add(myRow2);

    Нумерация столбцов начинается с нуля. Более удобный способ добавления записей - применение свойства ItemArray объекта DataRow, где можно задавать значения полей в виде массива:

    DataRow myRow3 = dtTours.NewRow();
    myRow3.ItemArray = new object[]{3,"Таиланд", 30000, null};
    dtTours.Rows.Add(myRow3);

    Здесь мы указали значение поля "Информация", равное null, - таким образом можно пропускать неизвестные поля (рис. 8.15):

    (рис 8.15) Вставка записи с одним значением null

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

    Конечно, вставка записей вручную в объект DataSet, не связанный с хранилищем данных, имеет не очень большой смысл. Поэтому давайте рассмотрим, как вставлять (и изменять) данные в уже загруженный кэш данных. Скопируйте папку проекта CustomExpression и переименуйте ее в WorkWithRow. Чтобы не тратить время на привязку действий к элементам управления, будем далее писать код после отображения данных в элементе DataGrid:

    dataGrid1.DataSource = dsTours.Tables["Туры"].DefaultView;

    Для изменения, например, пятой строки, мы указываем в свойстве Rows объекта dtTours ее индекс (4, нумерация начинается с нуля), затем вызываем метод BeginEdit для начала редактирования, устанавливаем группу свойств и в заключение принимаем изменения, вызывая метод EndEdit:

    DataRow myRow=dtTours.Rows[4];
    myRow.BeginEdit();
    myRow["Код тура"] = 5;
    myRow["Название"] = "Турция";
    myRow["Цена"] = "27000";
    myRow["Информация"] = "Осенние скидки с 15 октября";
    myRow.EndEdit();

    Тот же самый результат мы получим с помощью свойства ItemArray:

    DataRow myRow=dtTours.Rows[4];
    myRow.BeginEdit();
    myRow.ItemArray = new object[]{5,"Турция", 27000, null, null,
     "Осенние скидки с 15 октября"};
    myRow.EndEdit();

    Здесь мы установили для третьего и четвертого полей, которые являются вычисляемыми, значения null, подразумевая, что они останутся по умолчанию, а при запуске заполнятся своими значениями (рис. 8.16):

    (рис 8.16) Пропущенные вычисляемые поля заполняются своими значениями

    Для удаления заданной записи нужно создать объект DataRow, которому передается индекс строки, а затем вызвать метод Remove свойства Rows объекта DataTable:

    DataRow myRow2 = dtTours.Rows[0];
    dtTours.Rows.Remove(myRow2);

    Этого достаточно для удаления строки, но для того, чтобы пометить заданную строку как удаленную, вызываем метод Delete:

    myRow2.Delete();

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

    (рис 8.17) Первая строка, имеющая индекс 0, была удалена

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

    Свойство RowState

    При работе с данными приходится постоянно вносить изменения в записи - добавлять, редактировать или удалять. Объект DataRow обладает свойством RowState, позволяющим отслеживать текущий статус строки. Создайте новое приложение, назовите его "RowState". Перетаскиваем на форму элементы RichTextBox, Splitter, DataGrid, устанавливаем для них следующие свойства:

    richTextBox1, свойство Значение
    Dock Bottom
    Text
    splitter1, свойство Значение
    Dock Bottom
    dataGrid1, свойство Значение
    Dock Fill

    В конструкторе формы мы создаем всего одно поле, затем одну запись, статус которой будем отслеживать:

    public Form1()
    {
    	InitializeComponent();
    	DataTable dtTours = new DataTable("Туры");
    	DataColumn IDtour = new DataColumn("Код тура", typeof(Int32));
    	dtTours.Columns.Add(IDtour);
    	dataGrid1.DataSource = dtTours;
    	DataRow myRow;
    	// Создаем новую, отсоединенную запись
    	myRow = dtTours.NewRow();
    	richTextBox1.Text += Convert.ToString("Новая запись: "
    	 + myRow.RowState);
    	//Добавляем запись в объект DataTable
    	dtTours.Rows.Add(myRow);
    	richTextBox1.Text += Convert.ToString("\nДобавление записи: " +
    	 myRow.RowState);
    	//Принимаем все изменения в объекте DataTable
    	dtTours.AcceptChanges();
    	richTextBox1.Text += Convert.ToString("\nМетод AcceptChanges: " +
    	 myRow.RowState);
    	//Редактируем запись
    	myRow["Код тура"] = 1;
    	richTextBox1.Text += Convert.ToString("\nРедактирование строки: " +
    	 myRow.RowState);
    	//Удаляем строку
    	myRow.Delete();
    	richTextBox1.Text += Convert.ToString("\nУдаление: " +
    	 myRow.RowState);
    	//Отменяем все изменения в объекте DataTable
    	dtTours.RejectChanges();
    	richTextBox1.Text += Convert.ToString("\nМетод RejectChanges: " +
    	 myRow.RowState);
    }

    Запускаем приложение. В текстовое поле выводится статус записи myRow (рис. 8.18):

    (рис 8.18) Приложение RowState

    Значение Detached означает, что запись не относится к объекту DataTable. После добавления ее статус изменяется на Added - теперь она существует в объекте DataTable, но ее нет в базе данных. Конечно, здесь мы не рассматривали взаимодействие с источником записей, но это же значение будет у записей, добавляемых в DataGrid после вывода данных из базы при наличии подключения. Вызывая метод AcceptChanges объекта DataTable, мы принимаем все изменения, поэтому статус DataRow изменяется на Unchanged - теперь запись считается "своей", она не была изменена после вызова метода. После вызова метода Delete запись помечается удаленной - она еще не полностью удалена, в случае отмены изменений статус будет восстановлен. Действительно, вызывая метод RejectChanges объекта DataTable, мы восстанавливаем запись до состояния Unchanged.

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

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

    Свойство RowVersion

    Свойство RowVersion предназначено для извлечения значения записи (объекта DataRow), зависящего от совершенных изменений. Возможны следующие версии записи:

  • Current - текущее значение
  • Default - значение по умолчанию
  • Original - первоначальное значение
  • Proposed - предполагаемое значение
  • Создайте новое приложение, назовите его "RowVersion". Свойству Size формы устанавливаем значение 500; 300. Из окна Toolbox перетаскиваем следующие элементы и устанавливаем их свойства:

    panel1, свойство Значение
    Dock Bottom
    dataGrid1, свойство Значение
    Dock Left
    CaptionText Туры
    splitter1, свойство Значение
    Dock Left
    richTextBox1, свойство Значение
    Name rtbReport
    Dock Fill
    Text
    button1, свойство Значение
    Name BtnBeginEdit
    Location 14; 21
    Text Begin Edit
    button2, свойство Значение
    Name btnEndEdit
    Location 106; 21
    Text End Edit
    button3, свойство Значение
    Name btnCancelEdit
    Location 198; 21
    Text Cancel Edit
    button4, свойство Значение
    Name btnDelete
    Location 290; 21
    Text Удалить
    button5, свойство Значение
    Name btnClearReport
    Location 382; 21
    Size 96; 23
    Text Очистить отчет

    Переходим к коду. В классе формы объявляем объекты DataRow и DataTable:

    DataRow myRow;
    DataTable dtTours;

    В конструкторе формы создаем запись, определяем источник данных для элемента DataGrid, а также отключаем его доступность:

    public Form1()
    {			
    	InitializeComponent();
    	dtTours = new DataTable("Туры");
    	DataColumn IDtour = new DataColumn("Название", typeof(string));
    	dtTours.Columns.Add(IDtour);
    	myRow = dtTours.NewRow();
    	dtTours.Rows.Add(myRow);
    	myRow["Название"] = "Таиланд";
    	dataGrid1.DataSource = dtTours;
    	dataGrid1.Enabled = false;
    }

    Создаем метод TestRowVersion, в котором будет проверяться свойство RowVersion записи:

    private void TestRowVersion()
    {
    	if(myRow.HasVersion(DataRowVersion.Original))
    	rtbReport.Text += String.Format("Значение original: {0}\n",
    	 myRow["Название", DataRowVersion.Original]);
    	if(myRow.HasVersion(DataRowVersion.Current))
    	rtbReport.Text += String.Format("Значение current: {0}\n",
    	 myRow["Название", DataRowVersion.Current]);
    	if(myRow.HasVersion(DataRowVersion.Default))
    	rtbReport.Text += String.Format("Значение default: {0}\n",
    	 myRow["Название", DataRowVersion.Default]);
    	if(myRow.HasVersion(DataRowVersion.Proposed))
    	rtbReport.Text += String.Format("Значение proposed: {0}\n",
    	 myRow["Название", DataRowVersion.Proposed]);
    }

    Метод HasVersion позволяет определить, поддерживает ли объект myRow версию данных, указываемую в скобках. В случае подтверждения будет выполняться код оператора - выводится в элемент rtbReport соответствующее сообщение.

    В обработчике кнопки "Begin Edit" вызываем метод BeginEdit, устанавливаем новое значение записи:

    private void btnBeginEdit_Click(object sender, System.EventArgs e)
    {
    	myRow.BeginEdit();
    	myRow["Название"] = "Франция";
    	rtbReport.Text += "BeginEdit\n";
    	TestRowVersion();
    }

    В обработчике кнопки "End Edit" завершаем редактирование записи:

    private void btnEndEdit_Click(object sender, System.EventArgs e)
    {
    	myRow.EndEdit();
    	rtbReport.Text += "EndEdit\n";
    	TestRowVersion();
    }

    В обработчике кнопки "Cancel Edit" отказываемся от внесенных изменений:

    private void btnCancelEdit_Click(object sender, System.EventArgs e)
    {
    	myRow.CancelEdit();
    	rtbReport.Text += "CancelEdit\n";
    	TestRowVersion();
    }

    В обработчике кнопки "Удалить" удаляем объект myRow:

    private void btnDelete_Click(object sender, System.EventArgs e)
    {
    	myRow.Delete();
    	rtbReport.Text += "Запись удалена\n";
    	TestRowVersion();
    }

    В обработчике кнопки "Очистить отчет" просто удаляем содержимое текстового поля:

    private void btnClearReport_Click(object sender, System.EventArgs e)
    {
    	this.rtbReport.Text = "";
    }

    Запускаем приложение. После нажатия кнопки "BeginEdit" мы начинаем редактирование записи, вводится новое значение - "Франция". Оно становится значением по умолчанию "Default" и предполагаемым "Proposed", значение "Таиланд" является текущим "Current" (рис. 8.19, А). Отменяем редактирование, нажимая кнопку "Cancel Edit". При этом значение "Таиланд" становится текущим "Current" и по умолчанию " Default" (рис. 8.19, Б). Снова начинаем редактирование - картина повторяется (рис. 8.19, В). На этот раз завершаем его, нажимая кнопку "End Edit" - новое значение "Франция" становится текущим "Current" и по умолчанию " Default" (рис. 8.19, Г). Нажимаем кнопку "Удалить" - при этом удаляется сам объект myRow и дальнейшее изменение его з начений оказывается невозможным (рис. 8.19, Д).

    (рис 8.19) Приложение "RowVersion"

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

    Подобная функциональность вряд может нас устроить в реальных приложениях - в самом деле, мы вставляем только заранее определенное значение "Франция". Скопируйте папку приложения RowVersion и назовите ее RowVersion2. Добавим на форму элементы "надпись" и "текстовое поле":

    label1, свойство Значение
    location 24; 16
    Size 72; 24
    Text Название
    textbox1, свойство Значение
    Name txtmyRowName
    location 104; 16
    Size 344; 20
    Text

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

    private void btnBeginEdit_Click(object sender, System.EventArgs e)
    		{
    			myRow.BeginEdit();
    			rtbReport.Text += "BeginEdit\n";
    			TestRowVersion();
    			txtmyRowName.Enabled = true;
    		}
    
    private void btnEndEdit_Click(object sender, System.EventArgs e)
    		{
    			myRow["Название"] = txtmyRowName.Text;
    			myRow.EndEdit();
    			rtbReport.Text += "EndEdit\n";
    			TestRowVersion();
    			txtmyRowName.Enabled = false;
    		}
    
    private void btnCancelEdit_Click(object sender, System.EventArgs e)
    		{
    			myRow.CancelEdit();
    			rtbReport.Text += "CancelEdit\n";
    			TestRowVersion();
    			txtmyRowName.Enabled = false;
    		}

    Обработчик кнопки "Удалить" изменим так же - теперь будет удаляться текущий экземпляр myRow и тут же создаваться новый:

    private void btnDelete_Click(object sender, System.EventArgs e)
    		{
    			myRow.Delete();
    			rtbReport.Text += "Запись удалена\n";
    			TestRowVersion();
    			myRow = dtTours.NewRow();
    			dtTours.Rows.Add(myRow);
    		}

    В конструкторе формы отключим доступность текстового поля:

    public Form1()
    		{
    			...			
    			txtmyRowName.Enabled = false;
    		}

    Запускаем приложение. Выполняя знакомую последовательность действий, получаем похожий отчет; теперь при удалении записи можно продолжить работу - объект myRow будет воссоздан (рис. 8.20):

    (рис 8.20) Приложение RowVersion2

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

    События объекта DataTable

    Объект DataTable содержит ряд событий, которые могут применяться для слежения за происходящими изменениями. Наиболее часто используются следующие события:

  • ColumnChanged - наступает после изменения содержимого поля таблицы;
  • ColumnChanging - происходит в течение редактирования содержимого поля таблицы;
  • RowChanged - наступает после изменения объекта DataRow (записи);
  • RowChanging - происходит в течение редактирования объекта DataRow ;
  • RowDeleted - наступает после удаления объекта DataRow ;
  • RowDeleting - происходит при удалении объекта DataRow.
  • Скопируйте папку приложения RowVersion, назовите ее "Data TableEvents". В конструкторе формы добавим обработку четырех событий объекта DataTable:

    public Form1()
    {			
    	...
    	dtTours.RowChanging += new DataRowChangeEventHandler(dtTours_RowChanging);
    	dtTours.RowChanged += new DataRowChangeEventHandler(dtTours_RowChanged);
    	dtTours.RowDeleting += new DataRowChangeEventHandler(dtTours_RowDeleting);
    	dtTours.RowDeleted += new DataRowChangeEventHandler(dtTours_RowDeleted);
    }

    В соответствующих методах просто выводим сообщение в текстовое поле:

    private void dtTours_RowChanging(object sender, DataRowChangeEventArgs e)
    {
    	rtbReport.Text += String.Format("Событие - изменение записи\n",
    	 e.Row["Название"]);
    }
    
    private void dtTours_RowChanged(object sender, DataRowChangeEventArgs e)
    {
    	rtbReport.Text += "\nСобытие - запись изменена\n";
    }
    
    private void dtTours_RowDeleting(object sender, DataRowChangeEventArgs e)
    {
    	rtbReport.Text += String.Format("Событие - удаление записи\n",
    	 e.Row["Название"]);
    }
    
    private void dtTours_RowDeleted(object sender, DataRowChangeEventArgs e)
    {
    	rtbReport.Text += "\nСобытие - запись удалена\n";
    }

    Запускаем приложение. Нажимаем кнопку "Begin Edit", затем "End Edit" - происходят события RowChanging и RowChanged. Удаляем запись - происходят события RowDeleting и RowDeleted (рис. 8.21).

    (рис 8.21) Проект "DataTableEvents"

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

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

    Вернуться к учебному плану