П. В. Шумаков
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Руководство разработчика с примерами на C#
МОСКВА "ДИАЛОГ-МИФИ" 2003
ББК 32.97 Ш96
Шумаков П. В. Ш96 ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET. Руководство разработчика с примерами на C#. – М.: ДИАЛОГ-МИФИ, 2003. – 528 с. ISBN 5-86404-184-х Никакое приложение – кроме, быть может, школьных да хитромудрых системных, в силу их специфики, – не может обойтись без хранения данных в сторонних хранилищах – будь то гигабайтовые базы на SQL Server, небольшие базы Access или плоские таблицы dBase. Поэтому доступ к внешним источникам данных и их хранение в приложении является одной из существеннейших прикладных проблем, решаемых при создании приложений. В этой книге рассказывается, как работать с данными из приложений, создаваемых на платформе .NET Framework.
ООО “Издательство ДИАЛОГ-МИФИ” 115409, Москва, ул. Москворечье, 31, корп. 2. Т.: (495) 320-30-77, 320-43-77 Http://www.dialog-mifi.ru. E-mail:
[email protected]
ISBN 5-86404-184-х
© П. В. Шумаков, 2003 © Оригинал-макет, оформление обложки. ООО “Издательство ДИАЛОГ-МИФИ”, 2003
ВВЕДЕНИЕ Работа с данными нужна всем. Никакое приложение – кроме, быть может, школьных да хитромудрых системных, в силу их специфики, – не может обойтись без хранения данных в сторонних хранилищах – будь то гигабайтовые базы на SQL Server, небольшие базы Access или плоские таблицы dBase. Поэтому доступ к внешним источникам данных и их хранение в приложении является одной из существеннейших прикладных проблем, решаемых при создании приложений. В этой книге рассказывается, как работать с данными из приложений, создаваемых на платформе .NET Framework. Названная платформа не может не вызывать пристального внимания разработчиков программного обеспечения. Поскольку именно эту платформу корпорация Microsoft провозгласила своим магистральным направлением на ближайшие годы.
.NET Framework и единая библиотека типов В 2002 г. Microsoft официально выпустила .NET Framework – новую платформу разработки приложений. В ее основе лежат очень привлекательные для разбработчика принципы создания приложений. При проектировании на любых алгоритмических языках разработчик использует библиотеку 1 типов, констант, классов, методов и пр., входящую именно в состав .NET Framework, а не являющуюся подмножеством конкретной среды разработки, как это было ранее. Поэтому сразу пропадает причина, мешающая компоновке кода, написанного на различных языках – будь то, C#, Visual Basic.NET или C++.NET. Например, каким бы ключевым словом ни называлось 32-разрядное знаковое целое в C# и Visual Basic, физически переменные этого типа реализуются объектами класса System.Int32 из библиотеки .NET Framework. Поэтому они абсолютно совместимы и их можно передавать в качестве параметров в код на любом языке – там эти параметры будут однозначно интерпретированы. А перевод значения, содержащегося в строковой переменной (тип System.String), к верхнему регистру (к заглавным литерам) производится методом ToUpper объекта System.String на всех .NETязыках, хотя синтаксис обращения к ToUpper на C#, C++.NET и VB.NET будет, конечно, разным. Код приложения с алгоритмического языка транслируется в промежуточный код на языке IL (Intermediate Languague), который затем выполняется .NET Framework в режиме немедленной компиляции (Just-In-Time) в машинный язык, хотя, впрочем, можно заготавливать и предварительно компилированные приложения или их фрагменты.
1
Framework Class Library, FCL.
3
В условиях, когда разные исходные языки используют одну и ту же библиотеку типов, классов и пр., стыковка воедино полученного затем IL-кода становится тривиальной задачей. Поскольку если написать на различных языках два фрагмента, выполняющих одни и те же функции, то в идеале компиляторы с этих языков должны произвести одинаковый IL-код. Перевод программного текста приложения в IL-код – это целиком задача компилятора. Для языков, поставляемых в составе средств разработки Microsoft Visual Studio .NET (в том числе C#, C++.NET, VB.NET), такие компиляторы уже созданы и поставляются вместе с Visual Studio. Чтобы разработчики других языков могли создавать приложения под .NET Framework, были опубликованы требования к компиляторам из любого языка в IL-код. И ныне приходится слышать о том, что процессы написания таких компиляторов набирают силу. В этом нет ничего удивительного. Не все хотят учить новые языки, например С#, как бы логичен и строен он ни был. Поэтому вряд ли стоит удивляться, встретив впоследствии код на каком-нибудь Кобол.NET или Fortran.NET. Упрощается и межплатформенный перенос единожды созданного кода: IL-код переносится "как есть", а как его интерпретировать в машинный код конкретной платформы – это уж дело той версии .NET Framework, которая "заточена" именно под эту платформу.
.NET Framework и доступ к данным В .NET Framework для доступа к данным применяется ADO.NET 1. Некоторые понимают под этой аббревиатурой технологию доступа к данным, другие библиотеку – подмножество библиотеки NET Framework (а именно пространства System.Data, System.Data.SqlClient, System.Data.OleDb, где размещены используемые при доступе к данным классы, интерфейсы, делегаты и перечисления). В общем случае ADO.NET – это и то и другое. Все, что находится в System.Data и вложеных в него пространствах, подчинено строгой реализации идеологии обмена данными в .NET Framework. Эта идеология – отражение Web-модели построения приложений, использующих язык XML для обмена данными и протокол HTTP как базовый транспорт для доставки сообщений. Web-приложения построены на двухстороннем обмене данными между клиентским приложением-потребителем данных и сервером, их предоставляющим. Сформировав запрос на предоставление данных, клиент связывается с приложением сервера и отсылает ему запрос. Далее соединение между ними разрывается, поскольку время, потребное серверу для удовлетворение запроса, заранее неизвестно. Сформировав ответ, сервер устанавливает соединение с клиентом, отсылает ему ответ и разрывает соединение. Клиент обрабатывает данные в отсоединенном режиме, т. е. будучи не связан 1 Свое название ADO.NET заимствовала от "старой" технологии доступа к данным корпорации Microsoft – ActiveX Data Objects, развитие которой завершилось совместно с развитием среды разработки Visual Studio 6. "Старое" ADO применялось для построения клиентсерверных приложений и, как увидит в процессе чтения книги читатель, кроме сходства названия у "старого" ADO и ADO.NET довольно мало общего.
4
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
с каким-либо внешним источником данных. Возможно, впоследствии клиент снова инициирует установку соединения с этим или другим сервером и цикл обмена данными повторится. В настоящее время на платформе Windows под управлением .NET Framework можно создавать приложения двух видов: Web и Windows. Хотя принципы обмена данными из приложений перечисленных видов отличны, в ADO.NET обмен данными и их использование зиждется на отсоединенном использовании данных в приложении и хранении последним своего состояния между сеансами соединения с источником данных. В .NET выделяют два вида обмена данными между приложениями и источником данных. Они рассматриваются ниже.
Отсоединенный режим с двухсторонним обменом данными Для отсоединенного режима с двухсторонним обменом данными характерно чтение данных потоком с источника данных, длительное автономное использование данных в отсоединенном режиме и пакетное обновление обратно в источник. Наиболее часто такой обмен применяют в приложениях Windows. Общая схема взаимодействия приложения и источника показана ниже. Сначала приложение соединяется с источником данных и считывает оттуда данные (рис. 1) , затем разрывает соединение и обрабатывает данные в отсоединенном режиме (рис. 2). После окончания обработки соединение с источником устанавливается вновь и приложение производит пакетное обновление данных из источника (рис. 3).
рис. 1
рис. 2
рис. 3
В качестве базового принимают требование минимизации числа возможных соединений между приложением и источником данных. В идеале их должно быть два – в начале сеанса работы, при считывании данных, и в конце, при пакетном обновлении данных их приложения в источник. Введение
5
Для обеспечения такой "идеальной" модели в приложения стараются "заливать" максимальный объем данных, необходимых для решаемых приложением задач. Если выдвигается предположение, что те или иные данные понадобятся приложению в процессе его работы, то, чтобы лишний раз не устанавливать соединение с источником, эти данные заливают в приложение на начальной стадии его работы. Если же объемы таблиц в источнике данных невелики, то в приложение полностью заливают все содержимое таких таблиц, не особенно ломая при этом голову. Заливка больших объемов данных в клиентские приложения сегодня никого не удивляет ввиду значительного размера оперативной и дисковой памяти у современных "настольных" компьютеров. Для обеспечения описанного выше режима работы структура приложения в ADO.NET должна отвечать структуре, показанной на рис. 4. П Р И Л О Ж Е Н И Е Таблица данных
Таблица данных
DataTable
DataTable
...
Связи между таблицами DataRelation
Набор данных
Fill Заливка записей
Update Актуализация изменений
Команда данных
Команда данных
Команда данных
Команда данных
SqlCommand, OleDbCommand
SqlCommand, OleDbCommand
SqlCommand, OleDbCommand
SqlCommand, OleDbCommand
SELECT...FROM...
INSERT INTO...
UPDATE...SET...
DELETE...FROM...
Execute
Соединение SqlConection, OleDbConection
Execute... Выполнение запросов
Провайдеры
рис. 4
6
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Центральной структурой данных в приложении ADO.NET является набор данных – объект типа DataSet. Эта хранящаяся в памяти реляционная структура включает в себя некоторое число таблиц данных – объектов типа DataTable. Между таблицами набора могут иметься отношения. Каждое реализуется объектом DataRelation, а все отношения между таблицами набора хранятся в коллекции DataRelations этого набора. В наборе же хранятся и ограничения, накладываемые на отдельные столбцы таблиц. Одна из главных особенностей набора данных – его автономность по отношению к конкретному источника данных. Своих средств для взаимодействия с источником данных у набора нет. Набору все равно, кто, каким образом, когда и откуда занесет данные в его таблицы. В различные таблицы набора можно занести данные не только из разных источников данных, но и из разнотипных. Например, в одну таблицу можно занести данные из базы MS SQL Server, в другую таблицу – из Access, в третью – из текстового файла. Первичное соединение приложения с источником данных выполняется при помощи объекта типа SqlConnection или OleDbConnection. После заливки соединение разрывают, вплоть до того момента, когда записи, измененные в приложении, потребуется записать обратно в источник. Заливка данных из источника данных в набор данных приложения производится адаптером данных – объектом типа SqlDataAdapter или OleDbDataAdapter. Адаптер служит как бы мостом между набором данных и источником. Адаптер содержит объект команды данных, выполняющей извлечение данных из источника. Команда данных – это объект типа SqlCommand или OleDbCommand, выполняющий запрос на сервере. Для считывания данных применяется запрос, содержащий оператор SELECT. Приложение инициирует заливку данных, вызывая метод Fill адаптера данных. Команда отсылает источнику данных запрос, содержащий оператор SELECT. Источник выполняет этот запрос и возвращает выбранные записи, если они есть. Записи поступают в приложение и размещаются в таблице данных (объект DataTable). Отметим, что каждый адаптер обслуживает только одну таблицу набора данных. Сколько в наборе таблиц, столько должно быть и адаптеров, хотя для простоты приведенный выше рисунок и содержит изображение только одного адаптера. После заливки соединение с источником данных разрывается. Наступает этап обработки данных в приложении. По окончании этого этапа таблицы набора данных могут содержать некоторое количество измененных записей, в том числе старых откорректированных и удаленных, а также вновь добавленных. Источник данных ничего не знает об этих изменениях до их актуализации. В процессе актуализации в приложении происходит формирование потоков измененных записей, устанавливается повторное соединение с источником данных и производится перезапись изменений в источник. Вслед за этим соединение вновь разрывается. Актуализацию изменений производит также адаптер. Актуализация начинается, когда приложение вызывает метод Update адаптера. За актуализацию конкретного вида изменений отвечает отдельная команда данных. Каждая из них выбирает в наборе данных приложения записи с изменением конкретного вида и отсылает их источнику Введение
7
данных совместно с соответствующим запросом – оператором UPDATE, INSERT или DELETE. Рассмотренный выше принцип взаимодействия приложения и источника данных существенно отличается от принятого ранее в клиент-серверных системах, где между приложением и источником существовала постоянная связь. Считывание данных в приложение из источника производилось всякий раз по потребности. Запросы на выборку данных строились таким образом, чтобы получить только те записи, которые будут реально востребованы в приложении. Запись изменений из приложения в источник выполнялась для каждой измененной записи, либо для группы записей, либо, реже, пакетом для всех измененных в приложении записей.
Режим однонаправленного использования данных "только-на-чтение" В этом режиме приложение единожды считывают данные и формируют по ним какие-либо отчеты в режиме "только-на-чтение", перебирая полученные записи последовательно по направлению от первой к последней. Соединение с источником данных устанавливается перед считыванием данных и сохраняется в процессе считывания (рис. 5). Затем приложение интерпретирует данные в отсоединенном режиме (рис. 6), после чего вновь запрашивает у источника другие данные (рис. 7).
рис. 5
рис. 6
рис. 7
Схема приложения, функционирующего в режиме однонаправленного чтения, показана ниже (рис. 8). В таких приложениях функциональность, предоставляемая набором данных, избыточна, поэтому наборы данных в таких приложениях не применяют. Не применяют и адаптеры данных. Когда нужно считать из источника поток записей, применяют команду данных, отсылающую к источнику на выполнение оператор SELECT. Далее записи из полученного от источника потока по одной считываются в режиме "только-на8
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
чтение" ридером – объектом типа SqlDataReader, OleDbDataReader или, если приложение имеет дело с потоком данных в формате XML, типа XmlTextReader. П Р И Л О Ж Е Н И Е Ридер SqlDataReader, OleDbDataReader XmlTextReader
Read Чтение записей из потока
Команда данных
Команда данных
Команда данных
Команда данных
SqlCommand, OleDbCommand XmlTextReader
SqlCommand, OleDbCommand XmlTextReader
SqlCommand, OleDbCommand XmlTextReader
SqlCommand, OleDbCommand XmlTextReader
SELECT...FROM...
INSERT INTO...
UPDATE...SET...
DELETE...FROM...
ExecuterReader Считывание потока записей
ExecuterNonQuery Выполнение запросов Соединение SqlConection, OleDbConection
Команда данных SqlCommand, OleDbCommand CREATE TABLE...
Провайдеры
рис. 8
Если подобное приложение осуществляет перезапись данных обратно в источник (рис. 9), то использует для этого отдельные команды данных, выполняющие в источнике операторы INSERT, UPDATE и DELETE. В ряде случаев применяют также команды, изменяющие структуру данных в источнике, например посредством операторов CREATE TABLE, ALTER TABLE и пр.
рис. 9
Подобный принцип обмена данными с источником характерен, как говорилось ранее, для Web-приложений. Введение
9
Обзор объектов ADO.NET Выше нами рассмотрены основные принципы построения приложений ADO.NET. Рассмотрим теперь, какие объекты из библиотеки ADO.NET можно использовать в этих приложениях. В ряде случаев для одной и той же цели в ADO.NET заготовлена пара объектов. Выбор одного из них зависит от типа провайдера, обеспечивающего обмен с источником данных. Провайдер SQL Server позволяет взаимодействовать с базами данных SQL Server версий от 7.0 и выше. При этом используются объекты, чье имя начинается с префикса Sql, например SqlConnection. Провайдер OLE DB позволяет взаимодействовать с прочими источниками данных, доступных посредством драйверов OLE DB. В этом случае используются объекты, чье имя начинается с префикса OleDb. Ниже, при первом упоминании класса объекта, используется полное имя класса, включающее пространство имен, в котором этот объект задан в библиотеке.NET Framework. Все перечисленные ниже объекты хранятся в пространствах System.Data, System.Data.SqlClient, System.Data.OleDb библиотеки .NET Framework. Объекты классов System.Data.SqlClient.SqlConnection и System.Data.OleDb.OleDbConnection используются для установки соединения с источником данных. Объекты классов System.Data.OleDb.OleDbTransaction и System.Data.SqlClient.SqlTransaction реализуют транзакцию. Объекты класса System.Data.DataSet реализуют набор данных. Набор данных включает коллекции: • Tables – таблиц данных (объекты класса System.Data.DataTable); • Relations – отношений между таблицами набора данных (объекты System.Data.DataRelation). Объекты класса System.Data.DataTable реализуют таблицу данных. Каждый такой объект включает коллекции: • Columns – столбцов данных (объектов класса System.Data.DataColumn); • Rows – строк данных (объектов класса System.Data.DataRow); • Constraints – ограничений (объекты типа System.Data.ForeignKey Constraint для ограничений внешнего ключа и объекты типа System.Data. UniqueConstraint для ограничений столбца с уникальными значениями). Объекты класса System.Data.DataColumn реализуют отдельный столбец таблицы данных. Объекты класса System.Data.DataRow реализуют отдельную строку таблицы данных. Объекты класса System.Data.DataView реализуют просмотры – отфильтрованные и(или) особым образом отсортированные подмножества записей отдельной таблицы данных. Объекты класса System.Data.DataRowView реализуют отдельную строку просмотра. 10
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Объекты класса System.Data.DataViewManager реализуют менеджеры просмотров. Менеджер просмотра всегда создается для какого-либо набора данных и содержит коллекцию индивидуальных настроек сортировки и фильтрации для каждой из таблиц данных этого набора. Объекты класса System.Data.DataViewSetting реализуют настройки отдельной таблицы набора данных в менеджере просмотров. Объекты классов System.Data.OleDb.OleDbCommand или System.Data. SqlClient.SqlCommand реализуют функциональность команд данных. В ADO.NET команда данных задает запрос к источнику данных. Запрос может возвращать: • поток записей (оператор SELECT); • скалярное значение (оператор вида SELECT агрегатная_функция); • значения в выходных параметрах (когда команда используется для обращения к хранимым процедурам базы данных); • число затронутых запросом записей (запросы с использованием операторов INSERT, UPDATE, DELETE) и вообще не возвращать никакого результата (запросы с операторами типа CREATE TABLE). Коллекция Parameters команды данных может содержать параметры (объекты классов System.Data.SqlClient.SqlParameter или System.Data.OleDb.OleDbParameter). Если команда данных выполняет непараметрический запрос, коллекция Parameters пуста. Объекты классов System.Data.SqlClient.SqlDataReader или System.Data.OleDb.OleDbDataReader также называются ридерами. Они используются совместно с командами данных, возвращающими поток записей как результат выполнения в источнике данных оператора SELECT. Ридер последовательно считывает полученный поток записей в режиме "только-на-чтение" по одной записи.
Об этой книге Эта книга призвана служить руководством по изучению и использованию ADO.NET при построении Windows-приложений .NET 1. Основная цель руководства – не только научить читателя применять соответствующие объекты и очертить круг их использования, но и служить справочником в дальнейшем. Этому способствует построение материала по принципу "как сделать", наличие примера для каждого существенного аспекта применения технологии и изолированность примеров друг от друга. Книга ориентирована на пользователя или программиста, имеющего некоторое знакомство с C# и общее представление о разработке баз данных на SQL Server и(или) Access.
1
"Крен" в сторону Windows-приложений объясним: в настоящее время, по крайней мере в России, Windows-приложений пишется все же больше, чем Web-приложений. Притом создание последних имеет явно выраженную специфику, для рассмотрения которой понадобится еще одна книга. Введение
11
Материал сгруппирован по темам и излагается последовательно. Книга разбита на части. Каждая часть посвящена отдельному разделу ADO.NET и содержит ряд связанных либо близких по смыслу глав. Для читателя, абсолютно незнакомого с ADO.NET, в первой части книги содержится 9 примеров, изложенных по принципу "делай как я". Каждый пример иллюстрирует создание приложения определенного типа. После проработки материала читатель сможет составить устойчивые понятия о том, что можно делать в ADO.NET и какими средствами. Последующие части книги посвящены использованию объектов и технологий ADO.NET для построения Windows-приложений. Каждый существенный аспект применения ADO.NET сопровождается примером. К такому примеру, как правило, привязаны минимум два снимка, иллюстрирующие начальное состояние данных, структур данных и пр. и их конечное состояние в результате выполнения примера. Примеры не связаны между собой. Практически все примеры используют одну или две таблицы из тривиальной базы данных "ВУЗ". Таблицы содержат минимум данных. Автор намеренно избегал применения в примерах таких средств .NET и C#, которые отвлекали бы читателя от тематики ADO.NET, например наследования, перегрузок, возни с атрибутами, хитрых приведений типов и пр. Для спецификации объектов ADO.NET и их свойств, методов, событий, а также перечислений используется нотация C# . Язык C# выбран не случайно. Во-первых, корпорация Microsoft позиционирует C# как свой основной язык для платформы .NET Framework. Во-вторых, автор солидарен с бытующим в кругах .NET-программистов мнением, согласно которому разработчики C#, создавая новый язык с учетом достоинств и недостатков "старых" языков, смогли соединить в нем в мощь, лаконичность и простоту. В общем, я стремился написать книгу, которую захотел бы прочитать об ADO.NET сам. Надеюсь, что она принесет читателю пользу и тот не пожалеет о потраченном времени. Желаю читателям этой книги успехов на всех поприщах и всего наилучшего. С уважением, П. Шумаков, MCSD. (
[email protected])
12
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
1 ЧАСТЬ 1 ПРИМЕРЫ ПОСТРОЕНИЯ ПРИЛОЖЕНИЙ БАЗ ДАННЫХ С ИСПОЛЬЗОВАНИЕМ ADO.NET 1.1. Примеры построения приложений, использующих ADO.NET Когда мы изучаем новый программный продукт, нет ничего полезнее, чем для начала выполнить с применением этого продукта несколько примеров, шаг за шагом следуя инструкциям, изложенным в документации. Получаемый при этом объем информации трудно переоценить. Во-первых, становится понятен общий алгоритм работы с программной средой – на какие кнопки куда нажимать и для чего нужны те или иные окошки на экране. Во-вторых, в процессе выполнения примеров обычно создаются разноплановые приложения, каждое из которых решает некую специфическую задачу. Остается только соотнести полученные таким образом сведения со своим прежним опытом и имеющимися потребностями. В-третьих, мы избавляемся от томящего чувства неизвестности: продукт перестает быть таинственным и ужасным, завеса тайны приоткрывается и слегка понятно, куда идти дальше. Уверен, что каждый, если захочет, сможет продолжить этот список. В этой части книги читателю, только начинающему знакомство с ADO.NET, предлагается немного попрактиковаться и создать несколько приложений для доступа к данным. Те, кто уже знаком с ADO.NET, могут эту часть книги пропустить.
1.1.1. Перечень примеров – приложений с использованием ADO.NET • • •
• • •
В этой части книги мы попрактикуемся в создании следующих приложений. Данные в приложениях Windows: доступ к таблице базы данных из формы приложения Windows; доступ из формы приложения Windows к таблицам базы данных, связанным отношением "один-ко-многим"; использование в форме приложения Windows параметрических запросов для доступа к данным. Данные в приложениях Web: доступ к записям из таблицы данных на странице Web Forms; Web Forms и доступ к данным в режиме "только-на-чтение"; обновление данных в Web-форме с использованием команды данных; 13
• •
обновление данных в Web-форме с использованием элемента управления DataGrid; постраничный доступ к данным в Web-форме.
•
XML: чтение данных в формате XML в набор данных приложения.
1.1.2. Базы данных, используемые в примерах приложений В приводимых ниже примерах использованы две базы данных под управлением SQL Server и Access. Для учебных целей в SQL Server используется база данных Northwind. Она входит в комплект поставки SQL Server. Для работы с Access создадим базу данных VUZ.mdb и заполним две ее таблицы несколькими строками данных.
1.1.2.1. Учебная база Northwind (SQL Server) База данных Northwind содержит таблицу Customers, записи которой описывают покупателей (клиентов). Схема таблицы показана на рис. 1.1. В примерах используются столбцы CustomerID (идентификатор клиента), CompanyName (имя компании клиента), City (город, где расположена организация клиента).
рис. 1.1
1.1.2.2. Учебная база VUZ.mdb (Access) Информация в учебной базе описывает специфику деятельности вуза. База содержит две таблицы: Kaf (кафедры вуза) и Prep (преподаватели кафедры). Таблицы связаны отношением "один-ко-многим": одной кафедре может соответствовать несколько преподавателей. Однако в базе данных эта связь физически никак не реализуется в силу ненадобности: все равно мы будем связывать эти таблицы в наборе данных приложений. Спецификации столбцов таблицы Kaf показаны на рис. 1.2.
14
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
рис. 1.2
Первичный ключ таблицы Kaf построен по столбцу KK. Спецификации столбцов таблицы Prep показаны на рис. 1.3.
рис. 1.3
Первичный ключ таблицы Prep построен по столбцам KK и KP. Занесем в созданные таблицы ровно такой минимум данных, который позволит использовать их в примерах. Содержимое таблицы Kaf показано на рис. 1.4, а таблицы Prep – на рис. 1.5.
рис. 1.4
рис. 1.5
1.2. Доступ к данным из Windows Form Создадим приложение Windows с единственной формой. В приложении создадим набор данных nd1, состоящий из единственной таблицы – tKaf. В форме приложения 1. Примеры построения приложений баз данных с использованием ADO.NET
15
создадим сетку данных, отображающую записи из таблицы Kaf набора данных nd1. Зальем в таблицу Kaf набора данных записи из таблицы Kaf базы данных VUZ.mdb. Реализуем в форме возможность изменения данных таблицы и запоминания изменененных записей в базе данных VUZ.mdb. 1. Создадим новое приложение Windows. Для этого в главном меню среды разработки Visual Studio .NET выберем элемент меню File | New | Project. 2. В появившемся окне (рис. 1.6) в поле Project Types выберем Visual C# Projects, а в окне Templates – Windows Application. В поле Name зададим имя приложения, а в поле Location – каталог, где приложение будет размещаться. Для выбора каталога можно использовать кнопку Browse. Нажмем кнопку OK.
рис. 1.6
3. Установим соединение с базой данных VUZ.mdb. В окне Toolbox выберем закладку Data и в ней элемент управления OleDbConnection (рис. 1.7) и мышью перетащим этот элемент в форму приложения. 4. В нижней части формы приложения появится объект с именем oleDbConnection1. Это объект соединения с источником данных OLE DB. Сделаем этот объект текущим в форме, для чего щелкнем по нему мышью. В окне Properties (свойства) найдем свойство ConnectionString (строка соединения) и в поле данных нажмем кнопку выпадающего списка. В появившемся списке выберем строку New Connection (рис. 1.8). 5. В появившемся на экране окне Data Link Properties (параметры связи с данными) перейдем на закладку Provider и выберем Microsoft Jet 4.0 OLE DB Provider (рис. 1.9). 16
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
рис. 1.8 рис. 1.7
6. Перейдем на закладку Connection (соединение) и в поле Select or enter a database name (выберите или введите имя базы данных) укажем имя базы данных VUZ.mdb с указанием полного пути к ней. Для выбора того каталога, где на вашем компьютере размещается эта база, воспользуемся кнопкой эллипса (…). Остальные поля оставим так, как они показаны на рис. 1.10, и нажмем кнопку OK. Перед этим можно воспользоваться кнопкой Test Conntction, чтобы убедиться в успешности установки соединения.
рис. 1.9
рис. 1.10
1. Примеры построения приложений баз данных с использованием ADO.NET
17
7. После нажатия кнопки OK управление возвратится в форму. Теперь свойство ConnectionString объекта соединения oleDbConnection1 будет содержать параметры соединения – строку вида Provider=Microsoft.Jet.OLEDB.4.0;Password="";User ID=Admin;Data Source=D:\BAZA\VUZ.mdb;Mode=Share Deny None;Extended Properties="";Jet OLEDB:System database="";Jet OLEDB:Registry Path="";Jet OLEDB:Database Password="";Jet OLEDB:Engine Type=5;Jet OLEDB:Database Locking Mode=1;Jet OLEDB:Global Partial Bulk Ops=2;Jet OLEDB:Global Bulk Transactions=1;Jet OLEDB:New Database Password="";Jet OLEDB:Create System Database=False;Jet OLEDB:Encrypt Database=False;Jet OLEDB:Don't Copy Locale on Compact=False;Jet OLEDB:Compact Without Replica Repair=False;Jet OLEDB:SFP=False
8. Создадим адаптер данных – промежуточное звено между набором данных (еще нами не созданным) и таблицей Kaf базы данных VUZ.mdb. В окне Toolbox на закладке Data выберем элемент управления типа OleDbDataAdapter и мышью перетащим его в форму. На экране появится заставка мастера создания и конфигурирования адаптеров данных (Data Adapter Configuration Wizard). Нажмем кнопку Next. В появившемся окне выбора соединения перейдем в поле Which data connection should the data adapter use? (какое соединение должен использовать конфигурируемый адаптер данных?) и в выпадающем списке выберем созданное нами ранее соединение с базой данных VUZ.mdb (рис. 1.11). Нажмем кнопку Next.
рис. 1.11
18
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
9. В следующем окне выберем радиокнопку Use SQL Statements и нажмем кнопку Next. 10. В следующем окне (Generate the SQL Statements) в поле данных введем оператор SELECT KK, Nazv, Fakultet, Korpus, Sostav FROM Kaf Нажмем кнопку Next. В следующем по порядку окне нажмем кнопку Finish. Адаптер сконфигурирован. 11. Под формой, рядом с ранее созданным объектом соединения, будет добавлен объект адаптера с именем oleDbDataAdapter1. Выберем адаптер в форме мышью и перейдем в окно свойств (Properties). Изменим имя объекта адаптера на aKaf. 12. Сгенерируем набор данных, состоящий из одной таблицы данных. Выберем в форме адаптер aKaf и под окном свойств отыщем ссылку Generate Dataset (сгенерировать набор данных). В появившемся окне Generate Dataset установим значения полей, как это показано на рис. 1.12, и нажмем кнопку OK.
рис. 1.12
13. В приложение будет добавлен класс набора данных с именем nd. В этом можно убедиться, обратившись к окну структуры приложения (Solution Explorer). В форму будет добавлен экземпляр этого набора данных – объект с именем nd1. 1. Примеры построения приложений баз данных с использованием ADO.NET
19
Он разместится в форме в области объектов данных, рядом с ранее созданными нами объектами соединения и адаптера данных. 14. Зальем в таблицу Kaf набора данных nd1 записи из одноименной таблицы в базе данных VUZ.mdb. Для этого дважды щелкнем мышью по полю формы и в обработчике события загрузки формы зададим код: private void Form1_Load(object sender, System.EventArgs e) { aKaf.Fill(nd1); }
15. Перейдем в окно Toolbox на закладку Windows Forms, выберем элемент управления DataGrid и мышью перетащим его в форму. В форму добавится объект с именем dataGrid1. Выберем его мышью и перейдем в окно свойств. В поле данных свойства DataSource из выпадающего списка выберем набор данных nd1 (рис. 1.13, а), а в поле данных свойства DataMember – таблицу Kaf (рис. 1.13, б).
а
б рис. 1.13
16. Форма данных приобретет вид, показанный на рис. 1.14.
рис. 1.14
20
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
17. Выполним приложение. Для запуска приложения на выполнение нажмем кнопку F5. Вид формы с сеткой данных обеспечивающей доступ к записям таблицы Kaf набора данных nd1, показан на рис. 1.15.
рис. 1.15
18. Добавим в форму две экранные кнопки для запоминания в базе данных VUZ.mdb изменений, внесенных в записи таблицы Kaf набора данных nd1, а также для отмены этих изменений. В окне Toolbox на закладке Windows Forms выберем элемент управления Button и перетащим его в форму. В окне свойств изменим имя кнопки с button1 на btnUpdate (свойство Name), а текст кнопки – на Запомнить (свойство Text). Аналогичным образом добавим в форму вторую кнопку. Назовем ее btnReject, в качестве текста зададим Отменить. Форма приложения приобретет вид, показанный на рис. 1.16.
рис. 1.16
19. Дважды щелкнем по кнопке Запомнить и зададим следующий текст для обработчика нажатия кнопки: private void btnUpdate_Click(object sender, System.EventArgs e) { aKaf.Update(nd1, "Kaf"); }
20. Дважды щелкнем по кнопке Отменить и зададим следующий текст для обработчика нажатия кнопки: 1. Примеры построения приложений баз данных с использованием ADO.NET
21
private void btnReject_Click(object sender, System.EventArgs e) { nd1.Kaf.RejectChanges(); }
21. Выполним приложение. Для запуска приложения на выполнение нажмем кнопку F5. Исходный вид формы приложения показан на рис. 1.17.
рис. 1.17
22. Перейдем к третьей записи и изменим значение столбца Fakultet с Общий на ОБЩИЙ (рис. 1.18).
рис. 1.18
23. Нажмем кнопку Отменить. Форма снова приобретет вид, показанный выше на рис. 1.17, – внесенные в данные изменения будут отменены. 24. Повторим изменение – вновь изменим в третьей записи значение столбца Fakultet с Общий на ОБЩИЙ. Не будем ни подтверждать, ни отменять изменений, а попросту закроем приложение. Затем запустим его заново. Данные вновь будут представлены в виде, показанном выше на рис. 1.17. И это неудивительно: в отсоединенном от базы данных VUZ.mdb-приложении мы внесли изменения в данные и эти изменения в базе не запомнили. 25. Вновь повторим изменение – вновь изменим в третьей записи значение столбца Fakultet с Общий на ОБЩИЙ. Нажмем кнопку Запомнить. Закроем приложение и вновь запустим его на выполнение. Изменения, внесенные в записи таблицы Kaf набора данных nd1 теперь запомнены в базе данных VUZ.mdb, и заливка данных из ба22
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
зы в набор данных стартовавшего на выполнение приложения подтверждает это (рис. 1.19).
рис. 1.19
26. Добавим в форму приложения еще одну экранную кнопку – с именем btnRefresh и текстом Освежить. Дважды щелкнув по кнопке мышью, зададим следующий обработчик события нажатия кнопки: private void btnRefresh_Click(object sender, System.EventArgs e) { aKaf.Fill(nd1, "Kaf"); }
27. Метод Fill адаптера данных "освежает" содержимое таблицы набора данных актуальным состоянием записей из соответствующей таблицы базы данных. Ведь в реальной жизни другой пользователь, работающий параллельно с нами, мог изменить данные в базе независимо от нас. Запустим приложение на выполнение (рис. 1.20).
рис. 1.20
28. Имитируем действия стороннего пользователя. Затем зайдем в среду Access, выберем в ней нашу базу VUZA.mdb, затем таблицу Kaf. Изменим во второй строке таблицы изменения – название факультета укажем ТЕХНИЧЕСКИЙ вместо Технический. Закроем Access. Нажмем в приложении кнопку Освежить и увидим, что в таблице Kaf набора данных nd1 теперь содержатся не устаревшие, а актуальные данные (рис. 1.21). 1. Примеры построения приложений баз данных с использованием ADO.NET
23
рис. 1.21
В этой главе мы довольно подробно рассмотрели базовые операции по работе с данными в форме Windows: • установку соединения с источником (базой) данных; • создание адаптера данных – посредника между источником данных и таблицей набора данных; • заливку набора данных из таблицы источника (базы) данных в таблицу набора данных – методом Fill адаптера; • связывание таблицы набора данных и элемента управления DataGrid в форме; • изменение данных в сетке данных элемента управления DataGrid, запоминание и отмену сделанных изменений; • освежение таблицы набора данных актуальным содержимым из соответствующей таблицы источника данных. При рассмотрении дальнейших примеров, во избежание повторений, мы уже не будем столь подробно останавливаться на перечисленных аспектах проектирования форм данных.
1.3. Доступ из формы приложения Windows к таблицам базы данных, связанным отношением "один-ко-многим" В базе данных VUZ.mdb содержатся две таблицы, связанные отношением "одинко-многим": Kaf (родительская) и Prep (дочерняя). Рассмотрим, как в приложении Windows предоставить пользователю доступ к данным из двух связанных таблиц.
1.3.1. Создание приложения 1. Создадим новое приложение Windows. Для этого в главном меню среды разработки Visual Studio .NET выберем элемент меню File | New | Project . 2. В появившемся окне New Project в поле Project Types выберем Visual C# Projects, а в окне Templates – Windows Application. В поле Name зада24
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
дим имя приложения, а в поле Location – каталог, где приложение будет размещаться. Для выбора каталога можно использовать кнопку Browse. Нажмем кнопку OK.
1.3.2. Создание адаптеров данных Для создания адаптеров данных воспользуемся окном Server Explorer. В стандартной конфигурации окон в среде разработки Visual Studio это окно размещается слева и активизируется после выбора иконки . 1. Выберем из списка соединений в окне Server Explorer соединение с базой данных VUZ.mdb. Оно запоминается Visual Studio .NET после того, как мы его создали в предыдущем примере. 2. Раскроем узел соединения, щелкнув по крестику рядом с узлом. Выберем подузел Tables и раскроем его. Выберем таблицу Kaf (рис. 1.22).
рис. 1.22
3. Перетащим мышью таблицу Kaf в форму. 4. В форму приложения будет добавлен объект соединения (тип OleDbConnection, имя по умолчанию oleDbConnection1) и объект адаптера данных (тип OleDbDataAdapter, имя по умолчанию oleDbDataAdapter1). 5. Изменим имя адаптера с oleDbDataAdapter1 на aKaf. 6. Перейдем в окно Server Explorer способом, аналогичным описанному ранее, выберем таблицу Prep и перетащим ее в форму. 7. В форму будет добавлен объект адаптера (тип OleDbDataAdapter, имя по умолчанию oleDbDataAdapter1). Изменим его имя на aPrep.
1.3.4. Генерация набора данных 1. Выберем один из адаптеров, щелкнув по нему указателем мыши. Под окном свойств (Options) найдем ссылку Generate Dataset и нажмем ее. 2. В появившемся окне Generate Dataset установим радиокнопку New (новый набор) и в поле рядом зададим имя класса набора ds. В нижнем поле отметим все таблицы – Kaf и Prep (рис. 1.23). Нажмем кнопку OK.
1. Примеры построения приложений баз данных с использованием ADO.NET
25
рис. 1.23
3. В проекте будет сгенерирован класс набора данных ds. Файл схемы ds.xsd будет добавлен в дерево проекта (окно Solution Explorer). 4. В форму будет добавлен объект набора данных с именем ds1.
1.3.5. Установка отношения между таблицами 1. В окне Solution Explorer выберем файл схемы ds.xsd и дважды щелкнем по нему мышью. 2. Откроется окно дизайнера схем XML. В схеме набора данных ds увидим, как и следовало ожидать, две таблицы – Kaf и Prep (рис. 1.24).
рис. 1.24
26
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
3. Отметим мышью таблицу Kaf и затем, удерживая кнопку Shift, – таблицу Prep. Нажмем правую кнопку мыши и в появившемся контекстном меню выберем Add. В появившемся контекстном подменю выберем Add New Relation. 4. В появившемся на экране окне Edit Relation зададим значения полей так, как показано на рис. 1.25, и нажмем кнопку OK.
рис. 1.25
5. Между таблицами будет установлено отношение "один-ко-многим". В схеме этот факт будет отображен при помощи своебразного соединения таблиц прерывистой линией с ромбиком посередине (рис. 1.26).
1. Примеры построения приложений баз данных с использованием ADO.NET
27
рис. 1.26
6. Сохраним приложение и выйдем из схемы набора данных. Для этого в структуре проекта выберем узел формы Form1.cs и дважды щелкнем по нему мышью.
1.3.6. Добавление и настройка элементов управления DataGrid 1. Перейдем в окно Toolbox (иконка ), на странице Windows Form выберем элемент управления DataGrid и перетащим его в форму. В форме будет создан объект с именем dataGrid3. 2. Перетащим в форму еще один элемент управления DataGrid. В форме будет создан объект с именем dataGrid2. 3. Изменим свойства объекта dataGrid1 так, как показано ниже в таблице. Свойство DataSource DataMember CaptionText
Значение ds1 Kaf Кафедры (Kaf)
4. Изменим свойства объекта dataGrid2 так, как показано ниже в таблице. Свойство DataSource DataMember
Значение ds1 Для выбора значения в выпадающем списке сначала раскройте узел, соответствующий таблице Kaf, а затем выберите имя отношения (Kaf.KafPrep) (см. рис. 1.26)
CaptionText
Преподаватели (Prep)
28
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
рис. 1.27
5. Разместим в форме DataGrid2 под DataGrid1.
1.3.7. Заливка записей в набор данных 1. Дважды щелкнем мышью по форме. 2. В появившемся редакторе программного кода зададим следующий обработчик события загрузки формы: private void Form1_Load(object sender, System.EventArgs e) { aKaf.Fill(ds1, "Kaf"); aPrep.Fill(ds1, "Prep"); }
1.3.8. Добавление экранных кнопок 1. Добавим в форму две экранные кнопки (объекты типа Button) для запоминания в базе данных VUZ.mdb изменений, внесенных в записи таблицы Kaf набора данных nd1, а также для отмены этих изменений. В окне Toolbox на закладке Windows Forms выберем элемент управления и перетащим его в форму. В окне свойств изменим имя кнопки с button1 на btnUpdate (свойство Name), а текст кнопки – на Запомнить (свойство Text). Аналогичным образом добавим в форму вторую кнопку. Назовем ее btnReject, в качестве текста зададим Отменить. Форма приобретет вид, показанный на рис. 1.28.
1. Примеры построения приложений баз данных с использованием ADO.NET
29
рис. 1.28
2. Дважды щелкнем мышью по экранной кнопке Запомнить и зададим следующий обработчик события нажатия кнопки: private void btnUpdate_Click(object sender, System.EventArgs e) { //---если в наборе данных что-то менялось if (ds1.HasChanges()) { //---получаем удаленные записи в дочерней таблице //---и записываем в БД DataTable dtDelChild = ds1.Prep.GetChanges(DataRowState.Deleted); if (dtDelChild != null) { aPrep.Update(dtDelChild); dtDelChild.Dispose(); } //---записываем в БД изменения в родительской таблице DataTable dtAnyChangesInParent = ds1.Kaf.GetChanges(); if (dtAnyChangesInParent != null) { aKaf.Update(dtAnyChangesInParent); dtAnyChangesInParent.Dispose(); } //---получаем добавленные записи в дочерней таблице DataTable dtAddChild = ds1.Prep.GetChanges(DataRowState.Added); if (dtAddChild != null) { aPrep.Update(dtAddChild); dtAddChild.Dispose(); } //---получаем измененные записи в дочерней таблице DataTable dtEditChild = ds1.Prep.GetChanges(DataRowState.Modified); if (dtEditChild != null) {
30
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
aPrep.Update(dtEditChild); dtEditChild.Dispose();
}
}
} //---подтверждаем изменения ds1.AcceptChanges();
3. Дважды щелкнем мышью по экранной кнопке Отменить и зададим следующий обработчик события нажатия кнопки: private void btnReject_Click(object sender, System.EventArgs e) { //---если в наборе данных что-то менялось if (ds1.HasChanges()) { //---отключаем ограничения ds1.EnforceConstraints = false; //---отменяем изменения в таблицах ds1.Kaf.RejectChanges(); ds1.Prep.RejectChanges(); //---включаем ограничения ds1.EnforceConstraints = true; } }
1.3.9. Тестирование приложения 1. Сохраним приложение. Нажмем кнопку F5. 2. После запуска приложения "погуляем" по таблице Kaf и увидем, что всякий раз при переходе на новую запись таблицы Kaf в верхнем объекте DataGrid и в нижнем объекте DataGrid показываются только те записи таблицы Prep, которые подчинены (по ключу KK) этой записи таблицы Kaf (рис. 1.29 и 1.30).
рис. 1.29
1. Примеры построения приложений баз данных с использованием ADO.NET
31
рис. 1.30
3. Внесем какие-либо изменения в записи, запомним изменения или отменим их. Словом, убедимся в том, что приложение работает должным образом и поддерживает ссылочную целостность между двумя связанными таблицами.
1.4. Использование в форме приложения Windows параметрических запросов Рассмотрим пример, иллюстрирующий выполнение из приложения Windows параметрического запроса к источнику данных.
1.4.1. Создание приложения 1. Создадим новое приложение Windows. Для этого в главном меню среды разработки Visual Studio .NET выберем элемент меню File | New | Project . 2. В появившемся окне в поле Project Types выберем Visual C# Projects, а в окне Templates – Windows Application. В поле Name зададим имя приложения, а в поле Location – каталог, где приложение будет размещаться. Для выбора каталога можно использовать кнопку Browse. Нажмем кнопку OK.
1.4.2. Создание соединения с базой данных Northwind 1. Установим соединение с базой данных VUZ.mdb. В окне Toolbox выберем закладку Data, в ней элемент управления SqlConnection и перетащим его в форму приложения. 2. В нижней части формы приложения появится объект с именем sqlConnection1. Это объект соединения с источником данных SQL Server. Сделаем этот объект текущим в форме, для чего щелкнем по нему мышью. В окне Properties (свойства) найдем свойство ConnectionString (строка соединения) 32
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
и в поле данных нажмем кнопку выпадающего списка. В появившемся списке выберем строку New Connection. 3. В появившемся на экране окне Data Link Properties (параметры связи с данными) перейдем на закладку Connection (соединение) и в поле Select or enter a server name выберем имя SQL-сервера из выпадающего списка. 4. В поле Enter information to log on to the server выберем радиокнопку Use Windows NT integration security. 5. В поле Select the database on the server выберем из выпадающего списка базу данных Northwind. 6. Нажмем кнопку OK.
1.4.3. Создание адаптера данных 1. В окне Toolbox выберем закладку Data, в ней – элемент управления SqlDataAdapter и перетащим его в форму приложения. 2. На экране появится заглавное окно визарда (помощника) DataAdapter Configuration Wizard. Нажмем кнопку Next. 3. В окне Choose Your Data Connection из выпадающего списка выберем ранее созданное соединение с базой данных Northwind. 4. В окне Choose a Query Type выберем радиокнопку Use SQL statements и нажмем кнопку Next. 5. В окне Generate the SQL Statements в поле запроса зададим следующий запрос: select CustomerID, CompanyName, City from Customers where City = @City
Нажмем кнопку Next. 6. В следующем окне нажмем кнопку Finish. 7. В форму будет добавлен объект адаптера данных с именем sqlDataAdapter1. Изменим это имя на aCustomers. 8. Выберем объект адаптера в форме и перейдем к окну свойств. Выберем свойство SelectCommand адаптера. Оно ссылается на команду данных, выполняющую выборки данных из таблицы Customers. Щелкнем мышью по крестику в квадратике рядом с именем свойства. 9. Раскроется список свойств команда данных. Выберем свойство Parameters и в поле данных нажмем кнопку эллипса (…). Просмотрим список сгенерированных параметров команды. Он должен содержать единственный параметр с именем @City. Убедившись в наличии параметра, закроем список, нажав кнопку Cancel.
1.4.4. Генерация набора данных 1. Под окном свойств отыщем сноску Generate Dataset и нажмем ее.
1. Примеры построения приложений баз данных с использованием ADO.NET
33
2. В появившемся окне Generate Dataset выберем радиокнопку New и зададим имя набора данных ds. Проследим, чтобы в поле Choose wich table(s) to add to the dataset был отмечен элемент Customers. 3. Нажмем кнопку OK. В проект будет добавлен класс набора данных ds.xsd, а на форму – его экземпляр с именем ds1.
1.4.5. Добавление элементов управления в форму 1. В окне Toolbox выберем закладку Windows Forms, в ней – элемент управления ComboBox и перетащим его в форму приложения. В форму будет добавлен объект с именем comboBox1. 2. Выберем этот объект его в форме, щелкнув по нему мышью. Перейдем в окно свойств, где выберем свойство Items и нажмем кнопку эллипса (…). 3. Зададим список значений выпадающего списка, как показано на рис. 1.31.
рис. 1.31
4. Выберем свойство DropDownStyle и из выпадающего списка выберем значение DropDownList. 5. В окне Toolbox на закладке Windows Forms выберем элемент управления Label и перетащим его в форму приложения. В форму будет добавлен объект с именем label1. В окне свойств для свойства Text зададим значение ГОРОД. Разместим в форме объект label1 перед объектом comboBox1. 6. В окне Toolbox на закладке Windows Forms выберем элемент управления DataGrid и перетащим его в форму приложения. В форму будет добавлен объект с именем dataGrid1. 7. Установим значения свойств объекта dataGrid1: DataSource = ds1; DataMember = ‘Customers’.
1.4.6. Обработчик события смены текущего элемента в ComboBox 1. Выберем в форме объект comboBox1. В окне свойств перейдем к списку событий объекта, нажав на панели кнопок окна кнопку 34
.
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
2. Выберем событие SelectedIndexChanged и в поле данных дважды шелкнем мышью. 3. В появившемся редакторе программного кода зададим следующий обработчик смены индекса текущего элемента в comboBox1: private void comboBox1_SelectedIndexChanged(object sender, System.EventArgs e) { //---значение параметра команды SelectCommand адаптера //---берется из текущего элемента ComboBox aCustomers.SelectCommand.Parameters["@City"].Value = comboBox1.Items[comboBox1.SelectedIndex].ToString(); //---очищаем набор данных от всего ds1.Customers.Clear(); //---заносим в набор схему таблицы Customers и данные aCustomers.Fill(ds1); }
1.4.7. Тестовый запуск приложения 1. Нажмем кнопку F5 и запустим приложение. 2. Будем выбирать в объекте comboBox1 города и увидим, что всякий раз при смене города в dataGrid1 будут отображаться только записи с аналогичным значением в столбце City (рис. 1.32).
рис. 1.32
1.5. Показ данных на странице Web Forms В рассматриваемом ниже примере создается Web-приложение с одной формой – Web-страницей ASP. В Web-форме создается адаптер данных для доступа к таблице Kaf базы данных VUZ.mdb. Затем генерируется набор данных nd с единственной таблицей Kaf. В форме размещается сетка данных (элемент управления WebDataGrid). При загрузке формы таблица Kaf набор данных заливается данными из одноименной таблицы в базе данных VUZ.mdb, после чего происходит связывание элемента управления WebDataGrid и таблицы набора данных. Затем форма запускается из среды Web-браузера. 1. Примеры построения приложений баз данных с использованием ADO.NET
35
На компьютере должен быть установлен Internet Information Server (IIS) версии не ниже 5.0 с установленным на нем .NET Framework. Не забудьте установить на сервере доступ для пользователя ‘Имя-сервера\ ASPNET’, где Имясервера можно узнать в строке соединения (свойство ConnectionString объекта соединения). ПРИМЕЧАНИЕ. Предполагается, что вы уже выполнили предыдущий пример и создали соединение с базой данных VUZ.mdb. Visual Studio "помнит" параметры единожды установленных соединений с источниками данных, поэтому повторно создавать новое соединение не нужно. Все, что нужно сделать при конфигурировании адаптера данных, – это выбрать нужное соединение из их списка. Если вы не выполнили предыдущий пример, то вам следует обратиться к нему хотя бы в той части, где описано создание нового соединения.
1.5.1. Создание проекта и формы 1. В главном меню Visual Studio выберите элемент File | New| Project. 2. В диалоге New Project в поле Project Types выберите Visual C# Projects; в поле Templates – ASP.NET Web Application. В поле Location введите полный URL вашего приложения, включая http://, имя сервера, имя проекта (рис. 1.33).
рис. 1.33
3. Нажмете кнопку OK. Новый проект Web будет создан на указанном вами Webсервере. Этот проект будет содержать единственную страницу Web Forms с именем WebForm1.aspx. 36
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
1.5.2. Создание и настройка набора данных 1.5.2.1. Создание адаптера данных и соединения с БД 1. В окне Toolbox на закладке Data выберите элемент управления OleDbDataAdapter и перетащите его в форму. 2. После старта визарда (помощника) для создания и конфигурирования адаптера (Data Adapter Configuration Wizard) нажмите кнопку Next. На следующей странице (Choose Your Data Connection – выбор соединения с данными) из выпадающего списка выберите соединение с базой данных VUZ.mdb (рис. 1.34) и нажмите кнопку Next.
рис. 1.34
3. На следующей странице визарда (Choose Query Type – выбор типа запроса) отметьте радиокнопку Use SQL Statements и нажмите кнопку Next. 4. На следующей странице мастера (Generate the SQL statements) в окне данных (рис. 1.35) введите показанный ниже запрос. SELECT FROM
KK, Nazv, Fakultet, Korpus, Sostav Kaf
1. Примеры построения приложений баз данных с использованием ADO.NET
37
рис. 1.35
5. Нажмите кнопку Next и на следующем окне – кнопку Finish.
1.5.2.2. Создание набора данных 1. В нижней части формы, рядом с ранее созданным объектом соединения, будет добавлен объект адаптера с именем oleDbDataAdapter1. Выберите мышью адаптер в форме и перейдите в окно свойств (Properties). Измените имя объекта адаптера на aKaf. 2. Сгенерируйте набор данных, состоящий из таблицы данных Kaf. Выберите в форме адаптер aKaf и под окном свойств отыщите ссылку Generate Dataset (сгенерировать набор данных). В появившемся окне Generate Dataset установите значения полей, как это показано на рис. 1.36, и нажмите кнопку OK. 3. В приложение будет добавлен класс набора данных с именем nd. В этом можно убедиться, обратившись к окну структуры приложения (Solution Explorer). В форму будет добавлен экземпляр этого набора данных – объект с именем nd1. Он разместится в форме в области объектов данных, рядом с ранее созданными нами объектами соединения и адаптера данных.
38
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
рис. 1.36
1.5.3. Добавление в форму элемента управления DataGrid 1. В окне Toolbox на закладке Web Forms выберите элемент управления DataGrid и перетащите его в форму. 2. Под окном Properties выберите сноску AutoFormat, затем выберите шаблон Professional1 из списка предложенных шаблонов сетки данных. 3. В свойстве DataSource грида установите как источник данных nd1. 4. В свойстве DataMember выберите Kaf.
1.5.4. Заполнение НД данными 1. Дважды щелкните мышью по поверхности формы и задайте следующий код обработчика события загрузки Web-формы, чтобы при первом открытии формы залить данные в таблицу Kaf набора данных nd1 и связать эту таблицу и элемент управления DataGrid: private void Page_Load(object sender, System.EventArgs e) { if ( !IsPostBack) { aKaf.Fill(nd1); DataGrid1.DataBind(); } } 1. Примеры построения приложений баз данных с использованием ADO.NET
39
1.5.4.1. Запуск на выполнение 1. Сохраните приложение. В окне структуры проекта (Solution Explorer) отыщите узел формы WebForm1.aspx и щелкните по иконке соответствующего ему узла правой кнопкой мыши. В появившемся контекстном меню выберите элемент Web Form и выберите Build and Browse. Содержимое Web-страницы будет показано в среде Visual Studio (рис. 1.37).
рис. 1.37
2. Можно вызвать страницу из Internet Explorer. Запустите его и в поле Adress задайте http://localhost/WebApp_OneTable/WebForm1.aspx. В окне браузера будет показана наша форма с сеткой данных, заполненной данными из таблицы Kaf набора данных nd1 Web-приложения (рис. 1.38).
рис. 1.38
40
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
1.6. Web Forms и доступ к данным в режиме "только-на-чтение" Типичный сценарий работы пользователя в приложениях Web – запросить данные у сервера, получить и далее просматривать, не внося изменений. Для этого данные нужно считать из источника данных единожды и осуществлять доступ к ним в режиме "только-на-чтение" однонаправленно – от первой записи к последней. Очевидно, что нет необходимости содержать в подобном приложении набор данных, поскольку его богатая функциональность невостребуема и потребляемые ресурсы тратятся без толку. Не нужен и адаптер данных для заливки записей в несуществующие таблицы набора. Для чтения данных из базы используют: • команду данных (в нашем случае объект OleDbCommand) – она реализует считывание данных из источника в виде потока записей; • ридер (в нашем случае объект OleDbDataReader) – он выполняет доступ к записям из потока, возвращая структуру данных, пригодную для использования в элементах управления Web-страницы (например, DataGrid, Repeater, DataList). В приводимом ниже примере им создается Web-приложение с единственной Webформой. В последней создаются объекты команды данных и ридера. Команда данных считывает данные из таблицы Kaf базы данных VUZ.mdb. Ридер выполняет доступ к считанным записям и помещает их в элемент управления DataGrid в Web-форме.
1.6.1. Создание проекта и формы 1. В главном меню Visual Studio выберем элемент File | New| Project. 2. В диалоге New Project в поле Project Types выберем Visual C# Projects; в поле Templates – ASP.NET Web Application. В поле Location введем полный URL нашего приложения, включая http://, имя сервера, имя проекта, например http://localhost/WebApp_ReadOnly. 3. Нажмем кнопку OK. Новый проект Web будет создан на указанном вами Webсервере. Этот проект будет содержать единственную страницу Web Forms с именем WebForm1.aspx.
1.6.2. Создание объекта соединения 1. В окне Toolbox на закладке Data выберем элемент управления OleDbConnection и перетащим его в форму. В форме будет создан объект с именем oleDbConnection1. 2. Выберем мышью этот объект в форме. Перейдем к окну свойств и в поле данных для свойства ConnectiionString щелкнем мышью по стрелке выпадающего списка, после чего выберем существующее соединение с базой данных VUZ.mdb.
1. Примеры построения приложений баз данных с использованием ADO.NET
41
1.6.3. Создание команды данных 1. В окне Toolbox на закладке Data выберем элемент управления OleDb Command и перетащим его в форму. В форме будет создан объект с именем oleDbCommand1. Изменим это имя на cmdSelect. 2. Выберем мышью объект команды данных и перейдем в окно свойств. Войдем в поле данных свойства Connection и нажмем мышью кнопку выпадающего списка. Из выпадающего списка выберем ранее созданный объект соединения (рис. 1.39). 3. Выберем мышью объект команды данных и перейдем в окно свойств. Войдем в поле данных свойства CommandText и нажмем кнопку эллипса (…). 4. В появившемся диалоге Add Table выберем таблицу Kaf (рис. 1.40) и нажмем кнопки Add и Close.
рис. 1.40
рис. 1.39
5. В появившемся диалоге Query Builder в самом верхнем окне будет размещен элемент, содержащий структуру таблицы Kaf. Отметим галочкой самую верхнюю строку этого элемента (All columns). Тогда в среднем окне диалога (рис. 1.41) будет сгенерирован запрос SELECT FROM
Kaf.* Kaf
6. Нажмем кнопку OK.
42
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
рис. 1.41
1.6.4. Добавление в форму элемента управления DataGrid 1. В окне Toolbox на закладке Web Forms выберем элемент управления Data Grid и перетащим его в форму. 2. Под окном Properties выберем сноску AutoFormat и затем шаблон Professional1 из списка предложенных шаблонов сетки данных.
1.6.5. Настройка обработчика события загрузки Web-страницы Дважды щелкните по поверхности формы мышью и в появившемся редакторе текста приложения занесите в обработчик события Load программный текст, содержащий все необходимые действия по считыванию данных из базы и заполнению этими данными элемента управления DataGrid в форме. Обработчик названного события должен выглядеть следующим образом: private void Page_Load(object sender, System.EventArgs e) { if (!IsPostBack) { //---открытие соединения oleDbConnection1.Open(); //---создание датаридера System.Data.OleDb.OleDbDataReader Reader;
1. Примеры построения приложений баз данных с использованием ADO.NET
43
//---выполнение команды данных и помещение //---результата в датаридер Reader = cmdSelect.ExecuteReader(); //---связывание грида и датаридера DataGrid1.DataSource = Reader; DataGrid1.DataBind(); //---закрытие ридера Reader.Close(); //---закрытие соединения oleDbConnection1.Close(); } } }
1.6.5.1. Запуск на выполнение 1. Сохраним приложение. В окне структуры проекта (Solution Explorer) отыщем узел формы WebForm1.aspx и щелкнем по иконке узла правой кнопкой мыши. В появившемся контекстном меню выберем элемент Web Form и затем Build and Browse. Содержимое Web-страницы будет показано в среде Visual Studio (рис. 1.42).
рис. 1.42
2. Можно вызвать страницу из Internet Explorer. Запустим его и в поле Adress задайте http://localhost/WebApp_ReadOnly/WebForm1.aspx. В окне браузера будет показана наша форма с сеткой данных, заполненной данными из таблицы Kaf набора данных nd1 Web-приложения (рис. 1.43). 44
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
рис. 1.43
1.7. Обновление данных в Web-форме с использованием запроса Рассмотрим, как сделать, чтобы пользователь мог не только просматривать данные на Web-страницах, но и вносить в эти данные изменения с последующим запоминанием в источнике (базе) данных. Можно использовать для этой цели элемент управление DataGrid. Такой пример рассматривается в следующей главе. Можно использовать команды данных для чтения данных в форму, а также для запоминания измененных данных в источнике данных. Такой пример рассматривается ниже.
1.7.1. Создание проекта и формы 1. В главном меню Visual Studio выберем элемент File | New| Project. 2. В диалоге New Project в поле Project Types выберем Visual C# Projects; в поле Templates – ASP.NET Web Application. В поле Location введем полный URL нашего приложения, включая http://, имя сервера, имя проекта, например http://localhost/WebApp_UpdCommand. 3. Нажмем кнопку OK. Новый проект Web будет создан на указанном вами Webсервере. Этот проект будет содержать единственную страницу Web Forms с именем WebForm1.aspx.
1.7.2. Создание объекта соединения 1. В окне Toolbox на закладке Data выберем элемент управления OleDbConnection и перетащите его в форму. В форме будет создан объект с именем oleDbConnection1. 1. Примеры построения приложений баз данных с использованием ADO.NET
45
2. Выберем мышью этот объект в форме. Перейдем к окну свойств и в поле данных для свойства ConnectiionString щелкнем мышью по стрелке раскрывающегося списка, после чего выберем существующее соединение с базой данных VUZ.mdb.
1.7.3. Создание команды данных 1. В окне Toolbox на закладке Data выберем элемент управления OleDbCommand и перетащим его в форму. 2. Выберем мышью объект команды данных и перейдем в окно свойств. Войдем в поле данных свойства Connection и нажмем мышью кнопку выпадающего списка. Из выпадающего списка выберем ранее созданный объект соединения. 3. Создадим таким образом три команды данных. Их спецификации показаны ниже в таблице. Имя объекта OleDbCommand cmdSelectAll cmdSelectByKK
Назначение Чтение всех записей из таблицы Kaf базы данных Чтение одной записи из таблицы Kaf базы данных Запоминание одной записи в таблице Kaf базы данных
cmdUpdate
Запрос (значение свойства CommandText) SELECT KK, Nazv, Fakultet, Korpus, Sostav FROM Kaf SELECT KK, Nazv, Fakultet, Korpus, Sostav FROM Kaf WHERE (KK = ?) UPDATE Kaf SET Nazv = ?, Fakultet = ?, Korpus = ?, Sostav = ? WHERE (KK = ?)
Автоматически для команд данных cmdSelectByKK и cmdUpdate будет создана коллекция параметров. Ниже рассматриваются коллекции параметров для каждой из этих команд. Команда данных cmdSelectByKK Имя параметра KK
Source Column (столбец в таблице Kaf ) KK
Source Version (версия значения) Current
OleDbType (тип данных) SmallInt
Команда данных cmdUpdate Имя параметра Nazv Fakultet Korpus Sostav Original_KK
46
Source Column (столбец в таблице Kaf ) Nazv Fakultet Korpus Sostav KK
Source Version (версия значения) Current Current Current Current Original
OleDbType (тип данных) VarWChar VarWChar VarWChar SmallInt SmallInt
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
1.7.4. Создание элементов управления в Web-форме В окне Toolbox на закладке WebForms выберем 6 элементов управления, указанных ниже в таблице, перетащим их в форму и зададим свойства. Тип элемента управления
Назначение
Значения свойство
DropDownList
Позволяет выбирать значение первичного клю- ID: ddl_KK ча (столбец KK – код кафедры) AutoPostBack: true
TextBox
Значение столбца Nazv (название кафедры)
TextBox
Значение столбца Fakultet (названфакультет) ID: txt_Fakultet
TextBox
Значение столбца Korpus (корпус)
ID: txt_Korpus
TextBox
Значение столбца Sostav (названсостав)
ID: txt_Sostav
Button
Запоминание внесенных в данные изменений
ID: btn_Update Text: "Запомнить изменения"
ID: txt_Nazv
Вид Web-формы показан на рис. 1.44.
рис. 1.44
1. Добавим в форму 5 элементов управления Label и установим значения их свойства Text следующим образом: Код кафедры, Название, Факультет, Корпус, Состав (рис. 1.45).
1. Примеры построения приложений баз данных с использованием ADO.NET
47
рис. 1.45
1.7.5. Код для обработчика события загрузки Web-формы При первой загрузке формы необходимо поместить в ComboBox с именем ddl_KK значения столбца KK всех записей таблицы Kaf базы данных VUZ.mdb. В текстовые поля (txt_Nazv, txt_Fakultet, txt_Korpus, txt_Sostav) пишутся значения соответствующих столбцов только первой записи. Дважды щелкнем мышью по поверхности кода и в текстовом редакторе кода зададим следующий обработчик события загрузки Web-формы: private void Page_Load(object sender, System.EventArgs e) { if (!IsPostBack) { // Код выполняется единожды – только во время первой // загрузки страницы //---создаем ридер System.Data.OleDb.OleDbDataReader dreader; //---открываем соединение oleDbConnection1.Open(); //---выполняем считывание всех записей из таблицы //---Kaf. Они доступны через ридер dreader = cmdSelectAll.ExecuteReader(); //---признак чтения первой записи bool firstrow = true; //---чтение ридера – в ddl_KK(ComboBox) пишутся значения //---столбца KK из всех записей, в текстовые поля //---(TextBox – txt_Nazv, ...) – только значения соотв. //---столбцов из первой записи while (dreader.Read()) {
48
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
ddl_KK.Items.Add(new ListItem(dreader[0].ToString())); if (firstrow) { txt_Nazv.Text = dreader[1].ToString(); txt_Fakultet.Text = dreader[2].ToString(); txt_Korpus.Text = dreader[3].ToString(); txt_Sostav.Text = dreader[4].ToString(); firstrow = false; }
}
} //---закрываем ридер и соединения dreader.Close(); oleDbConnection1.Close();
1.7.6. Код для обработчика события смены текущего значения в ComboBox Когда пользователь меняет текущий элемент в ComboBox, в текстовых полях (txt_Nazv, txt_Fakultet, txt_Korpus, txt_Sostav) нужно сменить значения столбцов таблицы. Для этого из таблицы Kaf базы данных VUZ.mdb нужно считать запись со значением столбца KK, которое содержится в текущем элементе ddl_KK. Для считывания записи из таблицы Kaf по значению столбца KK используется команда данных cmdSelectByKK. 1. Выберем элемент управления ddl_KK в форме при помощи мыши. 2. В окне свойств перейдем к списку событий элемента управления (нажав кнопку на панели кнопок окна свойств), выберем событие SelectedIndexChanged и дважды щелкнем мышью в поле значений события. 3. В текстовом редакторе кода зададим следующий обработчик события SelectedIndexChanged: private void ddl_KK_SelectedIndexChanged(object sender, System.EventArgs e) { //---текущее значение столбца KK string sKK = ddl_KK.SelectedItem.Text; //---заносим значение KK как значение параметра //---запроса cmdSelectByKK.Parameters["KK"].Value = sKK; //---открываем соединение oleDbConnection1.Open(); //---создаем ридер System.Data.OleDb.OleDbDataReader dreader; //---считываем из таблицы Kaf одну строку //---с заданным значением столбца KK dreader = cmdSelectByKK.ExecuteReader(CommandBehavior.SingleRow); //---пишем в текстовые поля формы значения столбцов //---считанной записи 1. Примеры построения приложений баз данных с использованием ADO.NET
49
}
if (dreader.Read()) { txt_Nazv.Text = dreader[1].ToString(); txt_Fakultet.Text = dreader[2].ToString(); txt_Korpus.Text = dreader[3].ToString(); txt_Sostav.Text = dreader[4].ToString(); //---закрываем ридер и соединение dreader.Close(); oleDbConnection1.Close();
}
1.7.7. Код для обработчика события нажатия кнопки "Запомнить изменения" Когда пользователь вносит изменения в значения столбцов записей в текстовых полях формы, то он может захотеть запомнить изменения в таблице Kaf базы данных VUZ.mdb. Для этого предназначена кнопка Запомнить изменения. Дважды щелкнем мышью кнопку и в текстовом редакторе программного кода зададим следующий обработчик события нажатия кнопки: private void btn_Update_Click(object sender, System.EventArgs e) { //---параметры команды cmdUpdate – это текущие значения //---текстовых полей формы cmdUpdate.Parameters["Nazv"].Value = txt_Nazv.Text; cmdUpdate.Parameters["Fakultet"].Value = txt_Fakultet.Text; cmdUpdate.Parameters["Korpus"].Value = txt_Korpus.Text; cmdUpdate.Parameters["Sostav"].Value = txt_Sostav.Text; //---значение параметра Original_KK берется из текущего //---значения в ComboBox cmdUpdate.Parameters["Original_KK"].Value = ddl_KK.SelectedItem.Text; //---открываем соединение oleDbConnection1.Open(); //---выполняем команду записи обновления в базу cmdUpdate.ExecuteNonQuery(); //---закрываем соединение oleDbConnection1.Close(); }
1.7.7.1. Запуск на выполнение Сохраним приложение. В окне структуры проекта (Solution Explorer) отыщем узел формы WebForm1.aspx и щелкнем по иконке узла правой кнопкой мыши. В появившемся контекстном меню выберем элемент Web Form и затем Build and Browse. Содержимое Web-страницы будет показано в среде Visual Studio. Для того чтобы просматривать содержимое страницы в Internet Explorer (рис. 1.46), правой кнопкой мыши щелкнем по узлу формы WebForm1.aspx в окне Solution Explorer и выберем элемент Browse with, а затем из списка – Internet Explorer. 50
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
рис. 1.46
Выполним какие-либо изменения в данных, запомним изменения и перегрузим форму, дабы убедиться в том, что изменения действительно попали в базу данных.
1.8. Обновление данных в Web-форме с использованием элемента управления DataGrid В Web-формах большинство элементов управления предназначено для показа данных. Поэтому нам придется потратить некоторые усилия на то, чтобы спроектировать в Web-форме возможность изменения данных.
1.8.1. Создание проекта и формы 1. В главном меню Visual Studio выберем элемент File | New| Project. 2. В диалоге New Project в поле Project Types выберем Visual C# Projects; в поле Templates – ASP.NET Web Application. В поле Location введем полный URL нашего приложения, включая http://, имя сервера, имя проекта, например http://localhost/WebApp_UpdateInGrid/WebForm1.aspx.
3. Нажмем кнопку OK. Новый проект Web будет создан на указанном вами Webсервере. Этот проект будет содержать единственную страницу Web Forms с именем WebForm1.aspx. 1. Примеры построения приложений баз данных с использованием ADO.NET
51
1.8.2. Создание адаптера данных 1. В окне Toolbox на закладке Data выберем элемент управления OleDbDataAdapter и перетащим его в форму. 2. После старта визарда Data Adapter Configuration Wizard, служащего для создания и конфигурирования адаптера, нажмем кнопку Next. На следующей странице (Choose Your Data Connection – выбор соединения с данными) из выпадающего списка выберем соединение с базой данных VUZ.mdb и нажмем кнопку Next. 3. На следующей странице мастера (Choose Query Type – выбор типа запроса) отметим радиокнопку Use SQL Statements и нажмем кнопку Next. 4. На следующей странице мастера (Generate the SQL statements) в окне данных введем показанный ниже запрос. SELECT FROM
KK, Nazv, Fakultet, Korpus, Sostav Kaf
Нажмем кнопку Next и на следующем окне – кнопку Finish.
1.8.3. Создание набора данных 1. В нижней части формы, рядом с ранее созданным объектом соединения, будет добавлен объект адаптера с именем oleDbDataAdapter1. Выберем мышью адаптер в форме и перейдем в окно свойств (Properties). Изменим имя объекта адаптера на aKaf. 2. Сгенерируем набор данных, состоящий из таблицы данных Kaf. Выберем в форме адаптер aKaf и под окном свойств отыщем ссылку Generate Dataset (сгенерировать набор данных). В появившемся окне Generate Dataset установим значения полей, как это показано на рис. 1.47, и нажмем кнопку OK. 52
рис. 1.47
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
3. В приложение будет добавлен класс набора данных с именем nd. В этом можно убедиться, обратившись к окну структуры приложения (Solution Explorer). В форму будет добавлен экземпляр этого набора данных – объект с именем nd1. Он разместится в форме в области объектов данных, рядом с ранее созданными нами объектами соединения и адаптера данных.
1.8.4. Добавление в форму элемента управления DataGrid 1. В окне Toolbox на закладке Web Forms выберем элемент управления DataGrid и перетащим его в форму. 2. Под окном Properties выберем сноску AutoFormat и выберем шаблон Professional1 из списка предложенных шаблонов. 3. В свойстве DataSource грида установим как источник данных имя набора nd1. 4. В свойстве DataMember выберем имя таблицы Kaf. 5. В свойстве DataFieldKey выберем имя столбца KK.
1.8.5. Заполнение НД данными 1. Дважды щелкнем мышью по поверхности формы и зададим следующий код обработчика, чтобы залить данные в таблицу Kaf набора данных nd1 и при первом открытии формы связать эту таблицу и элемент управления DataGrid: private void Page_Load(object sender, System.EventArgs e) { aKaf.Fill(nd1); if (!IsPostBack) { DataGrid1.DataBind(); } }
1.8.6. Добавление служебного столбца в DataGrid В применяемом в Web-формах элементе управления DataGrid редактирование, удаление, выбор и т.д. текущей записи реализуется из отдельных столбцов этого элемента управления. Создадим один такой столбец для редактирования данных и запоминания изменений (или отказа от них) в текущей записи DataGrid. 1. Выберем мышью в форме элемент управления DataGrid. 2. Под окном свойств (Properties) выберем мышью ссылку Property Builder. 3. В появившемся окне DataGrid Properties перейдем к закладке Columns; 4. В поле Available columns выберем узел Button Columns и раскроем его. 5. Выберем подузел Edit, Update, Cancel; выберем его мышью и затем нажмем размещенную справа от названного окна кнопку . Подузел Edit, Update, Cancel будет скопирован в окно Selected Columns (рис. 1.48). 1. Примеры построения приложений баз данных с использованием ADO.NET
53
рис. 1.48
6. Нажмем кнопку OK. В DataGrid появится новый столбец (рис. 1.49).
рис. 1.49
1.8.7. Обработчики события нажатия ссылок Edit и Cancel 1. Выберем элемент управления DataGrid мышью в форме и перейдем к окну свойств. В нем на панели кнопок нажмем кнопку элемента управления. 54
и перейдем к списку событий
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
2. Выберем событие EditCommand, дважды щелкнем мышью в поле данных и в появившемся редакторе программного кода зададим следующий обработчик события нажатия ссылки Edit: private void DataGrid1_EditCommand(object source, System.Web.UI.WebControls.DataGridCommandEventArgs e) { DataGrid1.EditItemIndex = e.Item.ItemIndex; DataGrid1.DataBind(); }
3. Вернемся в окно свойств и аналогичным образом зададим обработчик события CancelCommand: private void DataGrid1_CancelCommand(object source, System.Web.UI.WebControls.DataGridCommandEventArgs e) { DataGrid1.EditItemIndex = -1; DataGrid1.DataBind(); }
1.8.8. Пробный запуск приложения Сохраним приложение. В окне структуры проекта (Solution Explorer) отыщем узел формы WebForm1.aspx и щелкнем по иконке узла правой кнопкой мыши. В появившемся контекстном меню выберем элемент Web Form и затем Build and Browse. Содержимое Web-страницы будет показано в среде Visual Studio. Для того чтобы просматривать содержимое страницы в Internet Explorer, правой кнопкой мыши щелкнем по узлу формы WebForm1.aspx в окне Solution Explorer и выберем элемент Browse with, а затем из списка – Internet Explorer. Первоначально, пока мы не приступили к редактированию данных, вид DataGrid такой же, как и в предыдущих примерах, за исключением дополнительного столбца (рис. 1.50).
рис. 1.50 1. Примеры построения приложений баз данных с использованием ADO.NET
55
Выберем какую-нибудь запись и щелкнем по ссылке Edit в дополнительном столбце. Текущая запись перейдет в состояние редактирования (рис. 1.51).
рис. 1.51
Как можно заметить, для редактируемой строки служебный столбец содержит две ссылки – Update (запомнить изменения) и Cancel (отменить изменения). Обработчик события для Cancel нами задан в предыдущем разделе. Мы можем нажать на эту ссылку. Тогда все внесенные в запись изменения пропадут и DataGrid вернется в состояние, показанное выше на рис. 1.50.
1.8.9. Обработчик события нажатия ссылки Update 1. Выберем элемент управления DataGrid мышью в форме и перейдем к окну свойств. В нем на панели кнопок нажмем кнопку и перейдем к списку событий элемента управления. 2. Выберем событие UpdateCommand, дважды щелкнем мышью в поле данных и в появившемся редакторе программного кода зададим обработчик события нажатия ссылки Update, сопровождая этот процесс комментариями. 3. Получим значение первичного ключа текущей строки DataGrid, изменения в которой нужно запомнить в базе данных. Текущая строка возвращается свойством Item объекта e. Объект e доступен как параметр обработчика события UpdateCommand элемента управления DataGrid. Свойство ItemIndex возвращает первичный ключ текущей сроки: string key = DataGrid1.DataKeys[e.Item.ItemIndex].ToString();
4. Получаем значения столбцов в обрабатываемой строке DataGrid, безотносительно к тому, изменялись значения этих столбцов или нет. Обращение к столбцу носит вид e.Item.Cells[Номер].Controls[0] и возвращает ссылку на элемент управления, которую мы приводим к типу TextBox, а затем, через свойство TextBox.Text – к строковому виду. Значение каждого столбца сохраняем в отдельной переменной. 56
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
TextBox tb; tb = (TextBox) e.Item.Cells[2].Controls[0]; string sNazv = tb.Text; //столбец Nazv tb = (TextBox) e.Item.Cells[3].Controls[0]; string sFakultet = tb.Text; //столбец Fakultet tb = (TextBox) e.Item.Cells[4].Controls[0]; string sKorpus = tb.Text; //столбец Korpus tb = (TextBox) e.Item.Cells[5].Controls[0]; string sSostav = tb.Text; //столбец Sostav
5. Теперь в таблице Kaf набора данных nd1 отыщем ту запись, с которой работаем в DataGrid. Сделать это легко, поскольку нам известно значение столба первичного ключа этой записи (см. выше п. 3, переменная key): DataRow r = nd1.Kaf.FindByKK(key);
6. Обновим эту запись значениями столбцов из текущей записи DataGrid. r["Nazv"] = sNazv; r["Fakultet"] = sFakultet; r["Korpus"] = sKorpus; r["Sostav"] = System.Int16.Parse(sSostav);
7. Запишем изменения в записи таблицы Kaf в базу данных методом Update адаптера: aKaf.Update(nd1, "Kaf");
8. Выведем текущую строку DataGrid из состояние редактирования: DataGrid1.EditItemIndex = -1;
9. Повторно свяжем данные и DataGrid: DataGrid1.DataBind();
Приведем ниже полный текст обработчика нажатия ссылки Update: private void DataGrid1_UpdateCommand(object source, System.Web.UI.WebControls.DataGridCommandEventArgs e) { //---значение первичного ключа (столбец KK) текущей записи DataGrid short key = (System.Int16)DataGrid1.DataKeys[e.Item.ItemIndex]; //---TextBox – далее в него записываются значения всех //---столбцов DataGrid TextBox tb; //---значения столбцов текущей записи запоминаем в переменных tb = (TextBox) e.Item.Cells[2].Controls[0]; string sNazv = tb.Text; //столбец Nazv tb = (TextBox) e.Item.Cells[3].Controls[0]; string sFakultet = tb.Text; //столбец Fakultet tb = (TextBox) e.Item.Cells[4].Controls[0]; string sKorpus = tb.Text; //столбец Korpus tb = (TextBox) e.Item.Cells[5].Controls[0]; string sSostav = tb.Text; //столбец Sostav //---найдем соответствующую запись в таблице Kaf набора данных nd1 DataRow r = nd1.Kaf.FindByKK(key); //---обновим эту запись значениями столбцов из текущей записи ataGrid 1. Примеры построения приложений баз данных с использованием ADO.NET
57
r["Nazv"] = sNazv; r["Fakultet"] = sFakultet; r["Korpus"] = sKorpus; r["Sostav"] = System.Int16.Parse(sSostav); //---запишем измененную запись в таблицу Kaf базы данных aKaf.Update(nd1, "Kaf"); //---выведем текущую строку DataGrid из состояние редактирования DataGrid1.EditItemIndex = -1; //---обновим данные в DataGrid DataGrid1.DataBind(); }
1.8.10. Тестовый запуск приложения Запустим приложение. Нажмем ссылку Edit и изменим название кафедры во второй строке (рис. 1.52).
рис. 1.52
Нажмем ссылку Update и увидим, что изменения приняты (рис. 1.53) и должны быть записаны в базу данных. Для того чтобы окончательно убедиться в том, что запись в базу прошла успешно, перезапустим приложение.
рис. 1.53
58
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
1.9. Постраничный доступ к данным в Web-форме В зависимости от объема используемых в Web-форме данных стратегии доступа к этим данным могут различаться. Если данные невелики по объему, в Web-приложении создают набор данных, заливают его таблицы записями из источника данных и в дальнейшем, когда отображают данные в форме, берут их из набора. Когда данные велики по объему, их разбивают на порции (страницы). В этом есть большой резон. Представим, что в извлекаемой таблице находится 10 000 записей, в то время как сетка элемента управления DataGrid Web-формы одновременно вмещает, например, 10. Заранее неизвестно, будет ли пользователь просматривать все 10 000 записей или ограничится, например, первыми 20. Поэтому недостающие данные подкачиваются в форму по мере необходимости. Эффективность этого способа очевидна, поскольку минимизируются требования к объему памяти для размещения набора данных и ко времени, требуемому для заливки данных в набор. Однако этот способ более сложно реализовать программно. В данном примере используется таблица Customers базы данных Northwind (SQL Server). Эта таблица содержит достаточно записей, чтобы продемонстрировать страничное считывание данных.
1.9.1. Создание проекта и формы 1. В главном меню Visual Studio выберем элемент File | New| Project. 2. В диалоге New Project в поле Project Types выберем Visual C# Projects; в поле Templates – ASP.NET Web Application. В поле Location введем полный URL нашего приложения, включая http://, имя сервера, имя проекта, например http://localhost/WebApp_PagedFetch. 3. Нажмем кнопку OK. Новый проект Web будет создан на указанном вами Webсервере. Этот проект будет содержать единственную страницу Web Forms с именем WebForm1.aspx.
1.9.2. Создание объекта соединения 1. В окне Toolbox на закладке Data выберем элемент управления SqlConnection и перетащим его в форму. В форме будет создан объект с именем sqlConnection1. 2. Выберем мышью этот объект в форме. Перейдем к окну свойств и в поле данных для свойства ConnectionString щелкнем мышью по стрелке раскрывающегося списка, после чего выберем существующее соединение с базой данных Northwind (SQL Server). 3. Если такого соединения не существует, в выпадающем списке выберем New Connection и в появившемся окне DataLink Properties в поле Select or enter a server name выберем имя сервера SQL из выпадающего списка. 1. Примеры построения приложений баз данных с использованием ADO.NET
59
4. Если для доступа к SQL-серверу используется аутентификация Windows, в поле Enter information to log on to the server выберем радиокнопку Use Windows NT Integrated Security. В случае использования аутентификации сервера выберем радиокнопку Use a specific user name and password и в поля User Name и Password введем соответственно имя пользователя и пароль. 5. В поле Select the database on the server из выпадающего списка выберем базу данных Northwind. 6. Нажмем кнопку OK.
1.9.3. Создание команды данных 1. В окне Toolbox на закладке Data выберем элемент управления SqlCommand и перетащим его в форму. 2. Выберем мышью объект команды данных и перейдем в окно свойств. Войдем в поле данных свойства Connection и нажмем мышью кнопку выпадающего списка. Из выпадающего списка выберем ранее созданный объект соединения. 3. Создадим таким образом две команды данных. Их спецификации показаны ниже в таблице. Имя объекта SqlCommand cmdPrevious
Назначение
Запрос (значение свойства CommandText)
Чтение записей для предыдущей страницы (по отношению к текущей)
SELECT TOP 10 CustomerID, CompanyName, City FROM Customers WHERE (CustomerID >= @customerid) ORDER BY CustomerID SELECT TOP 10 CustomerID, CompanyName, City FROM Customers WHERE (CustomerID > @customerid) ORDER BY CustomerID
Чтение записей для следующей страницы (по отношению к текущей)
cmdNext
Автоматически для команд данных cmdPrevious и cmdNext будет создана коллекция параметров. Ниже рассматриваются коллекции параметров для каждой из этих команд. Команда данных cmdPrevious Имя параметра @customerid
Source Column (столбец в таблице Kaf ) CustomerID
Source Version (версия значения) Current
OleDbType (тип данных) NVarChar
Source Version (версия значения) Current
OleDbType (тип данных) NVarChar
Команда данных cmdNext Имя параметра @customerid
60
Source Column (столбец в таблице Kaf ) CustomerID
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
1.9.4. Добавление в форму элемента управления DataGrid 1. В окне Toolbox на закладке Web Forms выберем элемент управления DataGrid и перетащим его в форму. 2. Под окном Properties выберем сноску AutoFormat и выберем шаблон Professional1 из списка предложенных шаблонов. 3. На закладке Paging снимем отметку с поля Show navigation buttons. 4. Установим следующие значения свойств грида: Свойство AllowPaging Allow custom paging Page size
Значение Checked Checked 10
1.9.5. Добавление навигационных клавиш 1. В окне Toolbox на закладке Web Forms выберем два элемента управления LinkButton и перетащим их в форму. 2. Разместим эти элементы под элементом управления DataGrid. 3. Левый элемент LinkButton назовем (свойство ID) btnPrevious; свойство Text установим равным .
1.9.6. Создание процедуры показа данных в гриде 1. В приложении создадим приватную переменную CurrentPage: public class WebForm1 : System.Web.UI.Page { //---описания элементов управления private int CurrentPage; … }
2. Создадим метод FillGrid формы. Метод используется для считывания страниц данных и помещения их в DataGrid. Ссылка на команду данных, выполняющую считывание из источника данных, передается в метод FillGrid как параметр. private void FillGrid(System.Data.SqlClient.SqlCommand currentSqlCommand) { //---создание ридера System.Data.SqlClient.SqlDataReader dr; //---открытие соединения sqlConnection1.Open(); //---выполнение команды. Доступ к данным – через ридер dr = currentSqlCommand.ExecuteReader(); //---показ данных в гриде и связывание DataGrid1.DataSource = dr; DataGrid1.DataBind(); 1. Примеры построения приложений баз данных с использованием ADO.NET
61
}
//---закрытие ридера и соединения dr.Close(); sqlConnection1.Close(); //---запоминаем номер текущей страницы из переменной ViewState["CurrentPage"] = CurrentPage; //---запоминаем значение первого столбца первой записи //---страницы и связываем с номером страницы ViewState[CurrentPage.ToString()] = DataGrid1.Items[0].Cells[0].Text; //---если число записей на странице меньше, //---чем задано в свойстве PageSize грида, //---деактивизируем кнопку btnNext if (DataGrid1.Items.Count < DataGrid1.PageSize) { btnNext.Enabled = false; }
1.9.7. Инициализация самой первой страницы данных При запуске приложения должна считываться первая страница данных. Для этого в обработчике события загрузки формы нужно вызвать на выполнение метод FillGrid формы и передать ему в качестве параметра ссылку на команду данных cmdNext: private void Page_Load(object sender, System.EventArgs e) { if (!Page.IsPostBack) { cmdNext.Parameters["@customerid"].Value = ""; CurrentPage = 0; FillGrid(cmdNext); } }
1.9.8. Считывание следующей страницы данных Когда пользователь хочет организовать считывание следующей страницы данных, он нажимает кнопку Следующая> (btnNext). При этом вызывается метод FillGrid формы. В качестве параметра ему передается ссылка на команду данных cmdNext. Значение параметра @customerid названной команды устанавливается равным значению столбца CustomerID из последней записи текущей страницы. Таким образом, считывается 10 записей со значениями столбца CustomerID, превышающими значение параметра @customerid. Дважды щелкнем мышью по кнопке btnNext и в редакторе программного кода зададим следующий обработчик нажатия кнопки: private void btnNext_Click(object sender, System.EventArgs e) { //---Считываем номер текущей страницы и увеличиваем его CurrentPage = (int)(ViewState["CurrentPage"]); CurrentPage++;
62
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
}
//---считываем значение столбца CustomerID последней записи //---в текущей странице string lastid = DataGrid1.Items[9].Cells[0].Text; cmdNext.Parameters["@customerid"].Value = lastid; //---считываем следующую страницу FillGrid(cmdNext);
1.9.9. Считывание предыдущей страницы данных Когда пользователь хочет инициировать считывание предыдущей страницу данных, он нажимает кнопку . В форму будет считана следующая страница данных (рис. 1.55).
рис. 1.55
64
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Нажмем кнопку = #1/31/2002#.
Арифметические константы указываются безо всяких премудростей: KK < 11
• •
• •
Операторы В выражениях применимы следующие операторы: Арифметические: + , – , * , / , а также % модуль от деления. Строковые: + (конкатенация, или сцепление строк). Режим учета регистра символов при строковых сравнениях зависит от значения свойства CaseSensitive соответствующего объекта DataSet или DataTable. Логические: AND, OR, NOT. Сравнения: , =, , =, IN, LIKE.
Операции строковые, арифметические и – частично – логические применяются для вычисления каких-либо значений. Операции сравнения и – частично – логические применяются при задании условия фильтрации записей в ряде свойств и методов объектов ADO.NET, например методе Compute таблицы данных или свойстве RowFilter просмотра (объект DataView). Правила составления операций арифметических, логических и сравнения такие же, как и для выражений языка C#. Подробнее стоит остановиться на операторе LIKE, задающем шаблоны поиска для выражений, применяемых при фильтрации записей. 3. Структуры данных в приложении
215
В операторе LIKE задаются шаблоны * и(или) %. Они служат для указания любого количества произвольных символов. Шаблон % задает любое количество любых символов, шаблон * – только одного любого символа, показывать в просмотре все записи, столбец NAZV которых содержит любое вхождение символов 'Авт': "NAZV LIKE '%Авт%'";
Шаблон может указываться в середине строки. Например, шаблон 'К*рова' задает значения 'Корова%', 'Карова'и пр. Если контекст для сравнения сам содержит символы '*' или '%', то последние задаются внутри квадратных скобок, например "NAZV LIKE '%Авт[*]%'
Если контекст поиска содержит квадратную скобку, она также указывается внутри квадратных скобок, например [[] или []]). Сылка на столбцы записей дочерней таблицы Агрегатные функции (см. ниже) используются для вычисления значений по столбцам записей в дочерней таблице. Для обращения к агрегатным функциям применяют синтаксис Child(ИмяОбъектаОтношения).ИмяСтолбцаДочернейТаблицы
или, когда связь между таблицами единственная: Child.ИмяСтолбцаДочернейТаблицы
Примеры : "Avg(Child(OtdelyPoluchka).Oklad)"
или, без указания имени отношения OtdelyPoluchka (т. е. объекта DataRelation, заданного в схеме набора данных): "Avg(Child.Oklad)"
О применении расчетов по записям дочерней таблицы, см. 3.5.3. Агрегатные функции В выражениях можно применять следующие агрегатные функции, вычисляющие значения сразу по группе записей: Sum – сумма; Avg – среднее значение; Min – минимальное значение; Max – максимальное значение; Count – число значений; StDev – статистическое стандартное отклонение. О применении агрегатных функций см. 3.5.3 и 3.5.4. Функции В выражениях можно применять следующие функции, преобразующие единичные значения: CONVERT – преобразует значение в другой тип; LEN – возвращает длину стрококого выражения; ISNULL – проверяет, не содержит ли выражение null; 216
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
IIF – в зависимости от условия, возвращает одно из двух значений; TRIM – в строковом выражении обрезает ведущие и хвостовые пробелы и специальные символы; SUBSTRING – возвращает подстроку. Использование перечисленных выше функций подробно рассмотрено в 3.5.5.
3.5.3. Вычисление агрегатных выражений по записям дочерней таблицы Материалы данного раздела рассматриваются на следующем примере. Пример. Пусть в наборе данных nd1 между таблицами Otdel и Poluchka существует отношение типа "один-ко-многим" (имя объекта DataRelation – "OtdelyPoluchka"). Добавим в родительскую таблицу Otdel три столбца с вычисляемыми значениями. В этих столбцах каждой записи таблицы Otdel должны вычисляться агрегаты по дочерним записям в таблице Poluchka: • в столбце SumKoef – сумма коэффициентов (столбец Koef) дочерних записей; • в столбце AvgOklad – средний оклад (столбец Oklad) дочерних записей; • в столбце MinOkladColumn – минимальный оклад (столбец Oklad) среди дочерних записей. Особенность агрегатных функций: попытка их задания в окне Properties воспринимается Visual Studio .NET негативно. Поэтому лучше указывать значения свойства Expression столбцов вручную в обработчике загрузки формы: nd1.Otdely.SumKoefColumn.Expression = "Sum(Child.Koef)"; nd1.Otdely.AvgOkladColumn.Expression = "Avg(Child.Oklad)";
nd1.Otdely.MinOkladColumn.Expression = "Min(Child.Oklad)"; На рис. 3.110 показан вид формы приложения, где в каждой записи родительской таблицы рассчитаны агрегаты по соответствующим столбцам дочерних записей.
рис. 3.110
3. Структуры данных в приложении
217
3.5.4. Вычисление агрегата по записям текущей таблицы Если задать выражение, значением которого будет агрегат, вычисляемый по записям, хранящимся в этой же таблице, то на первый взгляд мы мало чего добьемся. Например, если выражением вычисляемого столбца будет "Min(Oklad)", т. е. минимальное значение столбца Oklad таблицы, то это значение будет единым для всех записей таблицы. Однако если ввести еще один вычисляемый столбец, в котором текущий оклад будет делиться на минимальный оклад, то получим некий коэффициент и результат будет значимым. Пример. Пусть таблица Poluchka содержит столбцы FIO и Oklad. Добавим в таблицу столбец MinOklad (минимальный оклад во всей таблице), чье значение вычисляется по выражению "Min Oklad)", и столбец K (коэффициент), значение которого вычисляется выражением "Oklad/Min Oklad". Резульрис. 3.111 тат показан на рис. 3.111.
3.5.5. Функции в выражениях 3.5.5.1. Функция CONVERT Описание Синтаксис Параметры
Преобразует значение выражения в некоторый тип Convert(expression, type) expression – выражение, результат которого нужно преобразовать; type – тип данных .NET Framework
Пример. Создадим новый столбец, конвертирующий номер отдела (столбец Otdel) в строковое значение и сцепляющий его со строкой "Отдел №". Результат показан на рис. 3.112. рис. 3.112 //---создадим новый столбец DataColumn newColumn = new DataColumn(); newColumn.ColumnName = "Convert"; //---назначим выражение newColumn.Expression = "'Отдел № ' + CONVERT(Otdel, 'System.String')"; //---добавим столбец к набору данных nd1.Poluchka.Columns.Add(newColumn);
218
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
3.5.5.2. Функция LEN Описание Синтаксис Параметры
Возвращает длину выражения, результатом которого является строковое значение LEN(expression) Expression – выражение, длина результата которого должна быть возвращена как результат работы функции
Пример. Создадим новый столбец, вычисляющий длину выражения, результатом которого будет сцепление строки "Товарищ " и значения столбца FIO. Результат показан на рис. 3.113.
рис. 3.113 //---создадим новый столбец DataColumn newColumn = new DataColumn(); newColumn.ColumnName = "Len"; //---назначим выражение newColumn.Expression = "LEN('Товарищ ' + FIO)"; //---добавим столбец к набору данных nd1.Poluchka.Columns.Add(newColumn);
3.5.5.3. Функция ISNULL Описание Синтаксис Параметры
Проверяет, не является ли результатом вычисления выражения значение null; если является, возвращается замещающее значение; если не является, возвращается результат вычисления выражения. ISNULL(expression, replacementvalue) expression – выражение, исследуемое на значение null; replacementvalue – значение, которое возвращается функцией, если expression все же содержит значение null.
Пример. Создадим новый столбец, тестирующий на null столбец Koef. Если в названном столбце есть значение, то в новом столбце выводить его. В противном случае (когда в Koef – пустое значение), выдавать сообщение "НЕТ ЗНАЧЕНИЯ!". Результат показан на рис. 3.114. //---создадим новый столбец DataColumn newColumn = new DataColumn(); newColumn.ColumnName = "IsNull"; 3. Структуры данных в приложении
219
//---назначим выражение newColumn.Expression = "ISNULL(Koef, 'НЕТ ЗНАЧЕНИЯ!')"; //---добавим столбец к набору данных nd1.Poluchka.Columns.Add(newColumn);
рис. 3.114
3.5.5.4. Функция IIF Описание Синтаксис
В зависимости от условия, возвращает одно из двух значений IIF(expr, truepart, falsepart)
Параметры
expr – вычисляемое логическое выражение; truepart – выражение, чье значение возвращается функцией, если вычисления результат выражения expr – истина; falsepart – выражение, чье значение возвращается функцией, если вычисления результат выражения expr – ложь
Пример. Создадим новый столбец, где будет помещаться некая субъективная оценка должностного оклада служащего. Если произведение столбцов Koef и Oklad дает значение, превышающее 350, то выдается "Богато". В противном случае – "Бедно". Результат показан на рис. 3.115. //---создадим новый столбец DataColumn newColumn = new DataColumn(); newColumn.ColumnName = "IIF"; //---назначим выражение newColumn.Expression = "IIF((Koef * Oklad) > 350, 'Богато', 'Бедно')"; //---добавим столбец к набору данных nd1.Poluchka.Columns.Add(newColumn);
рис. 3.115
220
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
3.5.5.5. Функция TRIM Описание
Отсекает от строкового значения ведущие и хвостовые пробелы и символы типа \r,\n,\t
Синтаксис
TRIM(expression)
Параметры
Expression – значение, возвращающее строковое значение, где нужно произвести усечение
Пример. Создадим новый столбец, где будет помещаться содержимое столбца FIO, очищенное от ведущих и хвостовых пробелов и служебных символов. Результат показан на рис. 3.116. //---создадим новый столбец DataColumn newColumn = new DataColumn(); newColumn.ColumnName = "TRIM"; //---назначим выражение newColumn.Expression = "TRIM(FIO)"; //---добавим столбец к набору данных nd1.Poluchka.Columns.Add(newColumn);
рис. 3.116
3.5.5.6. Функция SUBSTRING Описание Синтаксис
Выделяет подстроку из строкового значения SUBSTRING(expression, start, length)
Параметры
Expression – выражение, результатом которого является строковое значение, откуда производится выделение подстроки; start – начальный символ выделяемого значения; length – длина в символах выделяемого значения
Пример. Создадим новый столбец, где будет помещаться подстрока из первых четырех символов значения в столбце FIO. Результат показан на рис. 3.117. //---создадим новый столбец DataColumn newColumn = new DataColumn(); newColumn.ColumnName = "SUBSTRING"; //---назначим выражение newColumn.Expression = "SUBSTRING(FIO, 1, 4)"; //---добавим столбец к набору данных nd1.Poluchka.Columns.Add(newColumn);
3. Структуры данных в приложении
221
рис. 3.117
3.6. Слияние наборов данных Иногда данные, находящиеся в различных наборах данных приложения, нужно – полностью или частично – слить воедино. Для этого намечают результирующий набор, в который будут добавляться данные, и выполняют метод Merge этого набора. Общий формат метода: РезультирующийНаборДанных.Merge(ИсточникДанныхДляСлияния);
Рассмотрим различные разновидности метода Merge.
3.6.1. Слияние набора данных и массива записей Чтобы залить в результирующий набор данных записи из массива записей, используется следующая разновидность метода Merge: public void Merge( DataRow[] rows );
Записи из массива rows rows добавляются в результирующем наборе в ту таблицу данных, у которой имя совпадает с той таблицей, к которой принадлежат записи из массива rows. Если такая таблица в результирующем наборе есть, но ее структура не совпадает со структурой добавляемых записей, то в результирующую таблицу перед слиянием данных добавляются недостающие столбцы. Если такой таблицы в результирующем наборе нет, она в нем создается, а затем уже происходит копирование записей из массива.
222
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Пример. Пусть набор-поставщик данных dsSource1 и результирующий набор dsTarget1 включают таблицы с именем Kaf с примерно одинаковой структурой (рис. 3.118).
рис. 3.118
Запомним в массиве записи таблицы Kaf набора данных dsTarget1: DataRow[] drAr = new DataRow[2]; drAr[0] = dsSource1.Tables["Kaf"].Rows[0]; drAr[1] = dsSource1.Tables["Kaf"].Rows[1];
и сольем с набором данных dsTarget1. В схему таблицы Kaf результирующего набора будет добавлен столбец Sostav, после чего выполнится слияние (рис. 3.119). dsTarget1.Merge(drAr);
рис. 3.119
3.6.2. Слияние набора данных и другого набора данных Слить записи из исходного набора данных с записями результирующего набора можно, используя следующую разновидность метода Merge результирующего набора: public void Merge( DataSet dataSet );
В результирующий набор данных добавляются записи изо всех таблиц исходного набора данных dataSet. Для каждой такой таблицы исходного набора отыскивается 3. Структуры данных в приложении
223
одноименная таблица в результирующем наборе. Если она есть, то все записи из таблицы исходного набора копируются в одноименную таблицу результирующего набора. В случае необходимости в схему результирующей таблицы добавляются недостающие столбцы. Если одноименной таблицы в результирующем наборе не найдено, то она создается непосредственно перед копированием записей. Пример. Пусть результирующий набор dsTarget1 содержит только таблицу Kaf, а исходный набор dsSource1 содержит таблицы Kaf и Prep, как это показано на рис. 3.120. Произведем слияние двух наборов:
рис. 3.120
dsTarget1.Merge(dsSource1);
После слияния в результирующем наборе dsTarget1 изменилось не только содержание таблицы Kaf, но и добавилась как есть таблица Prep из исходного набора dsSource1 (рис. 3.121). рис. 3.121
3.6.3. Слияние набора данных и таблицы набора данных Слить результирующий набор данных с записями из отдельной таблицы данных можно, выполнив следующую разновидность метода Merge результирующего набора: public void Merge( DataTable Table );
Если таблица, одноименная таблице Table, есть в результирующем наборе данных, то в нее копируются все записи из Table. В случае необходимости структура результирующей таблицы дополняется недостающими столбцами. 224
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Если таблицы, одноименной Table, в результирующем наборе нет, то перед слиянием данных она в результирующем наборе создается. Пример. Копирование таблицы Prep набора данных dsSource1 в набор данных dsTarget1. dsTarget1.Merge(dsSource1.Prep);
3.6.4. Управление изменением схемы результирующего набора данных Зачастую таблица исходного набора данных и одноименная ей таблица в результирующем наборе содержат записи различной структуры. Возникает вопрос, как поступать при копировании записей – добавлять в результирующую таблицу те столбцы, которых в них ранее не было, или не добавлять. Для управления режимом изменения схемы таблиц результирующего набора данных применяется параметр MissingSchemaAction, задаваемый для следующих разновидностей метода Merge: public void Merge( DataRow[] rows, bool PreserveChanges, MissingSchemaAction missingSchemaAction ); public void Merge( DataSet dataSet, bool PreserveChanges, MissingSchemaAction missingSchemaAction ); public void Merge( DataTable table, bool PreserveChanges, MissingSchemaAction missingSchemaAction );
Перечисление System.Data.MissingSchemaAction задает следующие значения: Значение Add
AddWithKey Error Ignore
Описание Принято по умолчанию. Добавляет недостающие столбцы в схему результирующей таблицы Добавляет в схему результирующей таблицы необходимую информацию о столбцах и первичном ключе. Более подробно о том, как информация о первичном ключе добавляется в таблицу данных, см. в описании метода FillSchema набора данных Возбуждает исключение Игнорирует недостающие столбцы и не добавляет их в результирующую таблицу
3. Структуры данных в приложении
225
Пример. Пусть в наборе-поставщике и результирующем наборе данных схемы таблиц Kaf различаются, поскольку у результирующей таблицы отсутствует столбец Sostav (рис. 3.122). Результаты слияния с различными значениями параметра MissingSchema Action представлены ниже. На рис. 3.123 показан результат слияния с добавлением недостающих столбцов. dsTarget1.Merge(dsSource1, true, MissingSchemaAction.Ignore);
рис. 3.122
На рис. 3.124 показан результат слияния с игнорированием недостающих столбцов. dsTarget1.Merge(dsSource1, true, MissingSchemaAction.Ignore);
рис. 3.123
На рис. 3.125 показан результат слияния с генерацией недостающих столбцов.
рис. 3.124
рис. 3.125
226
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
3.6.5. Управление версиями одинаковых записей При слиянии данных может возникнуть ситуация, когда таблица исходного набора и одноименная ей таблица в результирующем наборе содержат записи с одинаковыми значениями столбцов первичного ключа. Такие записи в результирующей таблице могут либо перезаписываться, либо оставляться как есть. Режимом перезаписи одинаковых записей в результирующей таблице, при слиянии данных, управляет параметр PreserveChanges, задаваемый для следующих разновидностей метода Merge: public void Merge( DataSet dataSet, bool PreserveChanges ); public void Merge( DataRow[] rows, bool PreserveChanges, MissingSchemaAction missingSchemaAction ); public void Merge( DataSet dataSet, bool PreserveChanges, MissingSchemaAction missingSchemaAction ); public void Merge( DataTable table, bool PreserveChanges, MissingSchemaAction missingSchemaAction );
Когда PreserveChanges равно true, в результирующей таблице оставляется старая версия записи, а аналогичная ей запись из исходной таблицы игнорируется). Когда PreserveChanges равно false, в результирующей таблице старая версия записи перезаписывается новой версией записи из исходной таблицы. Пример. Пусть таблицы Kaf набора-поставщика и результирующего набора данных (рис. 3.126) содержат запись с одинаковым первичным ключом (значение поля KK = 3). Если произвести слияние с параметром PreserveChanges = true, в результирующей таблице останется старая версия записи, о чем можно судить по значению null добавленного в схему таблицы столбца Sostav (рис. 3.127).
рис. 3.126
3. Структуры данных в приложении
рис. 3.127
227
Если произвести слияние с параметром PreserveChanges = false, в результирующий набор данных будет записана та версия записи, которая до слияния хранилась в наборе-поставщике (рис. 3.128). Об этом можно судить по значению 22 в столбце Sostav результирующей таблицы.
рис. 3.128
3.6.6. Слияние данных и метод ApplyChanges При слиянии данных достаточно часто ощущается потребность в применении метода AcceptChanges, выполняющего подтверждение изменений в записях результирующей таблицы или результирующего набора данных в целом: public void AcceptChanges();
Причина кроется в следующем. Если слияние сопровождается добавлением столбцов в таблицу результирующего набора данных, то записи результирующей таблицы будут помечены как модифицированные, т. е. свойство RowsState таких строк возвратит значение Modified. Это может создать массу неудобств при следующем выполнении метода Update, поскольку такие записи будет трудно отличить от записей, измененных пользователем вручную либо программно, а не в результате слияния. Чтобы пометить подобные записи как неизменявшиеся и привести их свойство RowsState к значению Unchanged, выполняют подтверждение изменений методом AcceptChanges либо результирующей таблицы, либо всего результирующего набора данных в целом. Отметим, что при этом перезаписывание изменений в источник (базу) данных не производится. Если это необходимо, нужно явно вызвать на выполнение метод Update того адаптера, с которым связана результирующая таблица данных. Пример. Пусть имеем таблицу Kaf в исходном и результирующем наборах данных dsSource1 и dsTarget1 (рис. 3.129). Выполним слияние наборов методом dsTarget1.Merge(dsSource1, true);
и получим результат, показанный на рис. 3.130. Запись таблицы Kaf результирующего набора (со значением столбца KK, равным 3) будет помечена как измененная (RowState = Modified) в силу добавления в нее нового столбца и, следовательно, нового значения этого столбца. Поэтому требуется выполнить метод 228
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
dsTarget1.AcceptChanges();
для приведения этой записи в состояние Unchanged.
рис. 3.129
3. Структуры данных в приложении
рис. 3.130
229
4 ЧАСТЬ 4 СВЯЗЫВАНИЕ В ПРИЛОЖЕНИИ ОБЪЕКТОВ-ИСТОЧНИКОВ ДАННЫХ И ЭЛЕМЕНТОВ УПРАВЛЕНИЯ 4.1. Связывание данных и элементов управления Windows Forms 4.1.1. Связывание элементов управления и объектов-источников данных При взаимодействии с данными форма Windows использует объекты двух видов. Одни потребляют данные, другие эти данные предоставляют. К потребителям данных относятся различные элементы управления Windows Form. Перичислим основные: Имя класса System.Windows.Forms.DataGrid
Пример
System.Windows.Forms.Label System.Windows.Forms.TextBox System.Windows.Forms.ListBox
System.Windows.Forms.ComboBox
230
System.Windows.Forms.CheckedListBox
System.Windows.Forms.CheckBox
Источниками данных 1 для этих элементов управления могут выступать, например, объекты данных следующих классов: Имя класса System.Data.DataColumn System.Data.DataTable System.Data.DataView System.Data.DataViewManager System.Data.DataSet System.Array System.Collections.ArrayList
Описание Столбец таблицы набора данных Таблица набора данных Просмотр таблицы набора данных Менеджер связанных просмотров набора данных Набор данных Массив Коллекция
Чтобы элемент управления (потребитель) мог использовать данные из какого-либо источника, между ними нужно установить связь. Процесс установки такой связи между элементом управления и объектов данных в форме Windows называется связыванием (binding). На рис. 4.1 показан пример связывания элементов управления формы с таблицей Kaf набора данных nd1: • элемент управления DataGrid связан с таблицей набора данных и служит для представления значений столбцов в строках таблицы и для доступа к ним; • элемент управления Label служит для визуализации значения столбца KK ("Код Кафедры"); • элемент управления TextBox служит для визуализации значения столбца Nazv ("Название") и предоставления к нему доступа; • элемент управления CheckBox служит для визуализации значения столбца Osnovnaya ("Основная") и предоставления к нему доступа; • элемент управления ListBox служит для визуализации значения столбца Fakultet ("Факультет") и выбора одного значения из имеющегося их списка; • элементы управления ComboBox и CheckedListBox служат для визуализации значения столбца Korpus ("Корпус") и выбора одного значения из имеющегося их списка.
1
В данном случае имеет место дублирование терминологии: "источником данных" данных зовется и внешнее хранилище данных, и объект (DataSet, DataTable, …), хранящий данные в приложении. Здесь автор следовал традиции первоисточника – документации по ADO.NET. Разрешить подобный казус довольно просто: всякий раз, когда говорится о соединении или о функциях адаптера данных, под источником данных понимается внешнее по отношению к приложению хранилище данных. Когда рассматривается связывание в форме Windows какого-либо элемента управления с данными, источником данных называется объект данных приложения – набор данных, таблица данных, просмотр данных, менеджер просмотров и, редко, массив. 4. Связывание в приложении. объектов-источников данных и элементов управления
231
рис. 4.1
В этой главе рассматриваются особенности связывания элементов управления и объектов данных в формах Windows.
4.1.2. Разделы источников данных Для ряда источников данных при связывании их с элементами управления требуется уточнять параметры связи, дополнительно указывая раздел данных (Data Member) Ниже перечисляются разделы данных для основных источников данных. Источник данных Набор данных (объект DataSet) Набор данных (объект DataSet) Таблица набора данных (объект DataTable) Менеджер просмотров (объект DataViewManager)
Раздел данных Таблица набора данных (объект DataTable) Отношение между таблицами (объект DataRelation) Столбец (объект DataColumn) Таблица набора данных (объект DataTable)
4.1.3. Требования к объектам-источникам данным В качестве источника данных в формах Windows может выступать далеко не всякий объект данных. Иными словами, не с каждым объектом данных можно связать элемент управления формы Windows. Объект-источник данных как минимум должен поддерживать интерфейс System.Collections.IList, обеспечивающий наиболее простую функциональность обработки данных. Класс, наследующий от IList, – это массив или коллекция, т. е. индексированный список элементов типа object. Типы элементов должны быть одинаковы, поскольку первый элемент используется для определения типа всего списка. Этот класс доступен для применения только во время выполнения.
232
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Более богатая функциональность у классов, поддерживающий типизированный интерфейс IList. Такой объект хранит список типизированных элементов (а не элементов "вообще типа object"), и свойство Item возвращает значение соответствующего типа. Все более сложную функциональность объектов данных обеспечивает поддержка ими интерфейсов System.ComponentModel.IbindingList, System.ComponentModel.IeditableObject, а также IdataErrorInfo из того же пространства имен.
4.1.4. Модель связывания Windows Form В форме Windows для каждого объекта-источника данных (например, таблицы данных, массив, коллекции) автоматически создается объект типа System.Windows.Forms.CurrencyManager. Он связывает, посредством коллекции Bindings, источник данных с одним или несколькими элементами управления. Параметры каждой связи источника данных и элемента управления формы задаются одним элементом в коллекции Bindings. Все объекты CurrencyManager объединяются в форме Windows объектом System.Windows.Forms.BindingContext. Обращение к объекту CurrencyManager происходит через индексаторы объекта BindingContext: public BindingManagerBase this[ object dataSource ] {get;} public BindingManagerBase this[ object dataSource, string dataMember ] {get;}
где: • dataSource задает источник данных; • dataMember задает раздел источника данных. Возвращаемый индексатором объект типа System.Windows.Forms.BindingManagerBase, базового для типа System.Windows.Forms.CurrencyManager, обладает свойством public override int Position {get; set;}
возвращающим целочисленное значение, которое используется для синхронизации источника данных и всех связанных с ними элементами управления. Свойство Position задает ту запись источника данных, значения столбцов которой будут представляться во всех элементах управления, связанных с этим источником данных. Некоторые элементы управления, например DataGrid, ListBox, ComboBox, изменяют значения свойства Position, когда пользователь перемещает указатель с одной записи на другую. Другие элементы управления (TextBox, Label и пр.) такой способностью не обладают. Рассмотрим рис. 4.2 и 4.3. Пусть таблица данных Kaf связана с элементами управления DataGrid, Label, TextBox, CheckBox. При изменении текущей строки в элементе управления DataGrid изменяется и значение позиции соответствующего объекта CurrencyManager, что приводит к пересинхронизации значений в других связанных элементах управления. 4. Связывание в приложении. объектов-источников данных и элементов управления
233
рис. 4.2
рис. 4.3
Изменение свойства Position объекта CurrencyNamager сопровождается наступлением события PositionChanged для этого объекта. Пример. Используя событие PositionChanged, при смене позиции в таблице Kaf набора данных ds1 выводить номер позиции в элементе управления типа Label с именем lbl_CurPosition. //---переменная под объект CurrencyManager private System.Windows.Forms.CurrencyManager cm; … //---получаем CurrencyManager, связанный с табл. Kaf набора данных ds1 cm = (System.Windows.Forms.CurrencyManager)this.BindingContext[ds1, "Kaf"]; //---связываем его с обработчиком события PositionChanged cm.PositionChanged += new EventHandler(this.KafPosition_Changed);
234
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
… //---обработчик события смены позиции в таблице Kaf private void KafPosition_Changed(object sender, EventArgs e) { lbl_CurPosition.Text = cm.Position.ToString(); }
4.1.5. Связывание отдельных элементов управления • •
Связать источник данных и элемент управления можно двумя способами: на этапе разработки приложения – путем задания значений свойств DataSource и DataMember (или DisplayMember); программно на этапе выполнения приложения, добавив элемент – объект типа Binding в коллекцию DataBindings элемента управления.
4.1.5.1. Связывание при помощи свойств DataSource и DataMember (или DisplayMember) Связывание элемента управления DataGrid При связывании элемента управления DataGrid источник данных задается в свойстве DataSource. Раздел источника данных может указываться в свойстве DataMember. Если источником данных служит просмотр или таблица данных, то значение свойства DataMember элемента управления DataGrid указывать необязательно. Если в качестве источника данных для DataGrid выступает набор данных или менеджер просмотров, то значение свойства DataMember элемента управления DataGrid можно как указывать, так и не указывать. Если название свойства DataMember указано, то DataGrid показывается содержимое раздела данных (таблицы данных, или отношения для набора данных, или просмотра для менеджера просмотров). Если названное свойство не указано, то DataGrid изначально представляется с пустой структурой, т. е. не имеет столбцов (рис. 4.4). Если раскрыть узел, нажав на крестик в единственной ячейке грида, то получим список имен всех таблиц, входящих в набор данных (рис. 4.5).
рис. 4.4
рис. 4.5
Выбрав в списке имя одной из таблиц, в DataGrid получим нормальное представление записей и столбцов этой таблицы (рис. 4.6). Обратим внимание на то, что в строке заголовка DataGrid справа показывается символ стрелки. Если нажать на нее, мы снова вернемся к состоянию грида, показанному выше на рис. 4.5. 4. Связывание в приложении. объектов-источников данных и элементов управления
235
рис. 4.6
Связывание элементов управления ListBox, CheckedListBox, ComboBox При связывании элементов управления ListBox, CheckedListBox, ComboBox, источник данных задается в свойстве DataSource. Раздел источника данных – столбец данных указывается в свойстве DisplayMember элемента управления. Если источником данных является набор данных (например, ds1), то в свойстве DataMember указывается имя таблицы (просмотра) и имя столбца, разделенное точкой (Kaf.Korpus). Если в качестве источника данных выступает таблица или просмотр (например, ds1.Kaf), то в свойстве DataMember указывается просто имя столбца (например, Korpus).
4.1.5.2. Связывание при помощи коллекции DataBindings Всякий элемент управления, производный от класса dows.Forms.Control, содержит свойство DataBindings:
System.Win-
public ControlBindingsCollection DataBindings {get;} Оно возвращает ссылку на коллекцию типа System.Windows.Forms.ControlBindingsCollection, содержащую элементы типа System.Windows.Forms.Binding. Каждый объект типа Binding задает характеристики связи между источником данных и элементом управления. Конструктор объекта этого класса имеет вид public Binding( string propertyName, object dataSource, string dataMember );
Параметры: propertyName – название того свойства элемента управления, которое связывается с источником данных; dataSource – источник данных; dataMember – раздел источника данных. Ниже приводятся типовые значения параметра propertyName наиболее употребительных элементов управления, связываемых при помощи их собственной коллекции DataBindings.
236
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Элемент управления System.Windows.Forms.Label System.Windows.Forms.TextBox System.Windows.Forms.CheckBox
Название связываемого свойства элемента управления Text Text Checked
Добавление объекта Binding в коллекцию ControlBindingsCollection производится методом Add той коллекции. Например, связать элемент управления textBox1 (типа TextBox) со столбцом Nazv таблицы Kaf набора данных ds1: textBox1.DataBindings.Add( new System.Windows.Forms.Binding("Text", ds1,"Kaf.Nazv"));
Для элементов управления Label, TextBox, CheckBox связывание можно производить и не программно, а в окне свойств (Properties), для этого нужно выбрать в списке свойств DataBindings и, щелкнув по крестику в квадратике рядом со значением свойства, раскрыть нижележащий список. Далее следует перейти к имени связываемого свойства и выбрать источник и раздел данных из выпадающего списка рис. 4.7).
рис. 4.7
4.1.5.3. Пример связывания на этапе выполнения приложения Рассмотрим пример связывание источника данных и элементов управления во время выполнения приложения. Пример. Пусть в приложении задан набор данных ds1, в нем – таблица Kaf. Свяжем таблицу Kaf с элементом управления DataGrid, столбцы таблицы – с элементами управления типа ListBox, CheckedListBox, ComboBox, Label, TextBox, CheckBox. Связывание будем производить по нажатии экранной кнопки Связать. Зададим следующий обработчик нажатия этой кнопки: private void bntSvtyazyat_Click_2(object sender, System.EventArgs e) { //---DataGrid this.dataGrid1.DataSource = this.ds1; this.dataGrid1.DataMember = "Kaf"; //---ListBox this.lbx_Fakultet.DataSource = this.ds1; this.lbx_Fakultet.DisplayMember = "Kaf.Fakultet"; //---CheckedListBox 4. Связывание в приложении. объектов-источников данных и элементов управления
237
this.cheklb_Korpus.DataSource = this.ds1; this.cheklb_Korpus.DisplayMember = "Kaf.Korpus"; //---ComboBox this.cb_Korpus.DataSource = this.ds1; this.cb_Korpus.DisplayMember = "Kaf.Korpus"; //---Label this.lbl_KK.DataBindings.Add( new System.Windows.Forms.Binding("Text", this.ds1, "Kaf.KK")); //---TextBox this.txtNazv.DataBindings.Add( new System.Windows.Forms.Binding("Text", this.ds1,"Kaf.Nazv")); //---CheckBox this.ckbx_Osnovnaya.DataBindings.Add( new System.Windows.Forms.Binding("Checked", this.ds1, "Kaf.Osnovnaya")); }
Запустим приложение. Элементы формы изначально не связаны ни с каким источником данных (рис. 4.8). Эта ситуация изменяется после нажатия кнопки Связать (рис. 4.9).
рис. 4.8
рис. 4.9
238
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
4.1.6. Cвойство Position и текущая строка в DataGrid Пусть источником данных для элемента управления DataGrid служит таблица данных либо источник данных – набор и раздел источника данных – таблица. Тогда свойство Position элемента CurrencyManager для этой таблицы будет совпадать с индексом текущей строки в элементе управления DataGrid (свойство CurrentRowIndex). Этим обстоятельством можно широко пользоваться при переходе от строки DataGrid (объекта типа DataViewRow) к строке данных (объект типа DataRow) в коллекции Rows таблицы данных. Пример. При смене позиции в таблице Kaf набора данных ds1 выводить: • •
номер позиции в элементе управления типа Label с именем lbl_CurPosition; номер строки в элементе управления DataGrid в элементе управления типа Label с именем lbl_gridRowNumber.
//---переменная под объект CurrencyManager private System.Windows.Forms.CurrencyManager cm; … //---получаем CurrencyManager, связанный с таблицей Kaf //––-набора данных ds1 cm = (System.Windows.Forms.CurrencyManager)this.BindingContext[ds1, "Kaf"]; … //---обработчик события смены ячейки в DataGrid1 private void dataGrid1_CurrentCellChanged(object sender, System.EventArgs e) { lbl_CurPosition.Text = cm.Position.ToString(); lbl_gridRowNumber.Text = dataGrid1.CurrentRowIndex.ToString(); }
Пару раз сменим текущую строку в элементе управления DataGrid (рис. 4.10 и 4.11) и убедимся в совпадении номера позиции в источнике данных и индекса текущей строки в DataGrid.
рис. 4.10 4. Связывание в приложении. объектов-источников данных и элементов управления
239
рис. 4.11
Итак, номер позиции в объекте CurrencyManager, созданного для источника данных – объекта DataRow, можно использовать для доступа к соответствующей записи таблицы (объект DataRow), если названный номер позиции рассматривать как индекс в коллекции Rows таблицы данных. Пример. Пользователь выбирает в элементе управления DataGrid одну из записей таблицы Kaf и нажимает экранную кнопку ПРИВЕСТИ К ВЕРХНЕМУ РЕГИСТРУ. Значения всех строковых столбцов этой записи должны быть приведены к верхнему регистру. Зададим следующий обработчик нажатия названной кнопки: private void btn_Click(object sender, System.EventArgs e) { //---индекс выбранной строки в таблице Kaf int nCurrentIndex = this.BindingContext[ds1, "Kaf"].Position; //---выбранная строка в таблице Kaf DataRow drCurrentRow = ds1.Kaf.Rows[nCurrentIndex]; //---приводим значения строковых столбцов выбранной строки //---к верхнему регистру for (int i = 0; i < ds1.Kaf.Columns.Count; i++) { if (drCurrentRow[i].GetType().ToString() == "System.String") { drCurrentRow[i] = drCurrentRow[i].ToString().ToUpper(); } } }
Запустим приложение, выберем какую-либо запись в DataGrid (рис. 4.12) и нажмем кнопку ПРИВЕСТИ К ВЕРХНЕМУ РЕГИСТРУ. Результат показан на рис. 4.13.
240
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
рис. 4.12
рис. 4.13
Однако подобный порядок вещей нарушается, котда для DataGrid источником данных служит не таблица данных, а какой-либо другой объект данных – просмотр, менеджер просмотров, отношение. Пример. Пусть имеем в приложении две таблицы – Kaf и Prep, связанные отношением с именем KafPrep, где Kaf – родительская, а Prep – дочерняя. Пусть в форме заданы два элемента управления DataGrid: первый показывает записи таблицы Kaf, второй – только те записи Prep, которые подчинены текущей записи таблицы Kaf. Поэтому в свойстве DataMember второго элемента DataGrid задается не имя таблицы Prep, а имя отношения – Kaf.KafPrep. //---переменная под объект CurrencyManager private System.Windows.Forms.CurrencyManager cmPrep; … //---получаем CurrencyManager, связанный с таблицей Prep набора данных ds1 cmPrep = (System.Windows.Forms.CurrencyManager) this.BindingContext[ds1, "Prep"]; … //---обработчик события смены ячейки в DataGrid2 private void dataGrid2_CurrentCellChanged(object sender, System.EventArgs e) { lbl_CurPosition.Text = cmPrep.Position.ToString(); lbl_gridRowNumber.Text = dataGrid2.CurrentRowIndex.ToString(); }
4. Связывание в приложении. объектов-источников данных и элементов управления
241
Пару раз сменим текущую строку в элементе управления DataGrid (рис. 4.14 и 4.15) и посмотрим, как соотносятся номера позиции в объекте CurrencyManager, созданном для таблицы Prep, и индекс текущей строки в элементе управления dataGrid2.
рис. 4.14
рис. 4.15
Как видно из приведенных рисунков, номер позиции для дочерней таблицы Prep не затрагивается действиями, производимыми над элементом управления DataGrid. Чтобы перейти от текущей строки такого DataGrid к соответствующему объекту DataRow, полезно придерживаться такой последовательности действий. Сначала необходимо получить объект CurrencyManager, с которым связан этот DataGrid. System.Windows.Forms.CurrencyManager cm;
cm = (System.Windows.Forms.CurrencyManager)this.BindingContext [dataGrid2.DataSource, dataGrid2.DataMember]; 242
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Свойство Current этого объекта CurrencyManager возвращает ссылку на объект типа DataRowView (строку просмотра, содержимое столбцов которого соответствует текущей строке в DataGrid). DataRowView drv = (DataRowView)cm.Current; Итак, в DataGrid данные показываются из некоего просмотра. Текущая строка в DataGrid ассоциирована с записью просмотра. Отсюда, если мы знакомы с просмотрами данных, можно сделать несколько выводов. От строки просмотра мы можем перейти к самому просмотру, к таблице данных и к той строке таблицы данных, которая ассоциирована с этой строкой просмотра. //---CurrencyManager, связанный с DataGrid2 System.Windows.Forms.CurrencyManager cm; cm = (System.Windows.Forms.CurrencyManager) this.BindingContext[dataGrid2.DataSource, dataGrid2.DataMember]; //---текущий объект CurrencyManager – это строка просмотра DataRowView drv = (DataRowView)cm.Current; //---просмотр, связанный с DataGrid DataView v = drv.DataView; //---таблица данных, связанная с этим просмотром
DataTable t = drv.DataView.Table;
//---строка таблицы данных, ассоциированная со строкой просмотра
DataRow r = drv.Row;
Если в данные нужно внести изменения, то с одинаковым результатом можно изменять значения столбцов и в записи просмотра (объект DataRowView), и в записи данных (объект DataRow). Пример. Пусть элемент управления dataGrid2 содержит записи из таблицы Prep, подчиненные текущей записи родительской таблицы Kaf. Свойство DataMember элемента управления dataGrid2 ссылается на отношение (объект DataRelation) между этими таблицами. Под dataGrid2 размещается экранная кнопка ПРИВЕСТИ К ВЕРХНЕМУ РЕГИСТРУ (рис. 4.16). По ее нажатии запись в таблице Prep, соответствующая текущей строке в dataGrid2, должна быть изменена: значения всех ее строковых столбцов необходимо привести к верхнему регистру.
рис. 4.16 4. Связывание в приложении. объектов-источников данных и элементов управления
243
Напишем обработчик события нажатия кнопки ПРИВЕСТИ К ВЕРХНЕМУ РЕГИСТРУ. Хотя с практической точки зрения это излишне, получим ссылки на все, что можно: на запись просмотра, ассоциированную с текущей строкой в dataGrid2; на просмотр; на таблицу данных (будем считать, что мы не знаем, из какой таблицы берутся данные в dataGrid2) и строку таблицы данных. //---CurrencyManager, связанный с DataGrid2 System.Windows.Forms.CurrencyManager cm; cm = (System.Windows.Forms.CurrencyManager)this.BindingContext [dataGrid2.DataSource, dataGrid2.DataMember]; //---текущий объект CurrencyManager – это строка просмотра DataRowView drv = (DataRowView)cm.Current; //---просмотр, связанный с DataGrid DataView v = drv.DataView; //---таблица данных, связанная с этим просмотром DataTable t = drv.DataView.Table; //---строка таблицы данных, ассоциированная со строкой просмотра
DataRow r = drv.Row;
Теперь нам нужно просмотреть структуру таблицы данных и выявить, какие столбцы строковые, а какие нет. Для перебора отлично подходит коллекция Columns таблицы набора данных t. Значения нестроковых столбцов, согласно условию, трогать не будем. Значения строковых столбцов приводем к верхнему регистру. //---переменная под строковое представление типа столбца string sType = ""; //---перебираем все столбцы таблицы for (int i = 0; i < t.Columns.Count; i++) { //---определяем тип i-го столбца sType = drv.Row[i].GetType().ToString(); //---если столбец – строковый if (sType == "System.String") { //---изменяем его значение в строке просмотра drv[i] = drv[i].ToString().ToUpper(); } }
Впрочем, изменять можно и саму строку, ассоциированную с текущей строкой dataGrid2: r[i] = r[i].ToString().ToUpper();
Можно также воспользоваться и просмотром, связанным с dataGrid2. Индексом записи просмотра в этом случае служит индекс текущей строки в dataGrid2: v[dataGrid2.CurrentRowIndex][i] = v[dataGrid2.CurrentRowIndex][i].ToString().ToUpper();
И в первом, и во втором, и в третьем случае результат будет один и тот же (рис. 4.17).
244
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
рис. 4.17
4.1.7. Cвойство Position и текущий элемент в ListBox, CheckedListBox, ComboBox Элементы управления ListBox, CheckedListBox, ComboBox используются для связывания со столбцом данных (см. рис. 4.1). При смене текущего элемента списка для объектов ListBox, CheckedListBox, ComboBox наступает событие SelectedIndexChanged. В обработчике этого события свойства SelectedItem, SelectedValue перечисленных выше объектов указывает на строку (объект DataRowView) просмотра, ассоциированную с текущей в источнике данных. Пример. Пусть элементы управления dataGrid1 и listBox1 связаны с таблицей Kaf набора данных ds1. При смене текущего элемента в списке listBox1 происходит смена позиции в источнике данных. Используя обработчик события SelectedIndexChanged объекта listBox1, будем выдавать в элементы управления textBox1 и textBox2 сведения о текущей записи источника данных. private void listBox1_SelectedIndexChanged(object sender, System.EventArgs e) { System.Data.DataRowView selItem = (System.Data.DataRowView)listBox1.SelectedItem; textBox1.Text = "Выбрана запись с KK = " + selItem["KK"].ToString(); System.Data.DataRowView selValue = (System.Data.DataRowView)listBox1.SelectedValue; textBox2.Text = "Выбрана запись с KK = " + selValue["KK"].ToString(); }
Результат показан на рис. 4.18
4. Связывание в приложении. объектов-источников данных и элементов управления
245
рис. 4.18
4.1.8. Особенности связывания обычных массивов Обычный массив (т. е. массив значений – типа int, string и пр.) можно без труда связывать с элементами типа ListBox, ComboBox и пр. Связывание массива и элемента управления DataGrid требует некоторых усилий. Пример. Связать обычный строковый массив aFIO и элемент управления ListBox. string[] aFIO = {"Иванов", "Петров", "Сидоров"}; … listBox1.DataSource = aFIO;
Результат показан на рис. 4.19.
рис. 4.19
Элемент управления DataGrid может предоставлять доступ лишь к общедоступным (public) свойствам объектов. Каждый элемент массива – размерного типа (value type). А у объектов размерных типов общедоступных свойств нет. Поэтому "связывание в лоб" элемента управления DataGrid и массива, состоящего из размерных значений, обречено на неудачу. Пример. Попробуем связать массив значений с элементом управления DataGrid так же, как это делалось в предыдущем примере: string[] aFIO = {"Иванов", "Петров", "Сидоров"};
246
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
… dataGrid1.DataSource = aFIO;
Результат показан на рис. 4.20. Вряд ли он нас устроит.
рис. 4.20
Чтобы массив можно было связать с элементом управления DataGrid, от значений размерных типов в массиве нужно перейти к ссылочным типам и снабдить их общедоступным свойством, выдающим во внешний мир значение объектов. Пример. Зададим класс Stroka (ссылочный тип): public class Stroka { protected string sZnachenie; public Stroka (string val){ sZnachenie = val; } public string Znachenie { get {return sZnachenie.ToUpper();} }
}
и массив объектов типа Stroka: Stroka[] aFIO_modified = {new Stroka("Иванов"), new Stroka("Петров"), new Stroka("Сидоров")};
Связывание такого массива с элементом управления DataGrid не составит труда: dataGrid1.DataSource = aFIO_modified;
Результат показан на рис. 4.21. Ниже показывается связывание других элементов управления с подобным массивом: listBox1.DataSource = aFIO_modified; listBox1.DisplayMember = "Znachenie"; textBox1.DataBindings.Add(new Binding ("Text", aFIO_modified, "Znachenie"));
рис. 4.21
Результат показан на рис. 4.22.
рис. 4.22
4. Связывание в приложении. объектов-источников данных и элементов управления
247
4.1.9. Особенности связывания коллекций Рассмотрим коллекцию типа ArrayList. Если такая коллекция содержит элементы-объекты, обладающие общедоступным свойством, то: • DataGrid связывают со всей коллекцией. Каждому общедоступному свойству объектов-элементов коллекции в DataGrid будет соответствовать столбец, каждому объекту-элементу коллекции – строка. • ListBox, ComboBox, CheckedListBox и др., предоставляющие доступ к списку значений, связываются лишь с одним из общедоступных свойств объектовэлементов коллекции. • TextBox, CheckBox, Label и др., предоставляющие доступ к единичному (скалярному) значению, связываются лишь с одним из общедоступных свойств объектов-элементов коллекции. Пример. Пусть задан класс public class Nekto { protected string sName; protected string sStatus; //---конструктор public Nekto (string Namevalue, string Statusvalue){ this.sName = Namevalue; this.sStatus = Statusvalue; } //---свойство "Имя" public string Name { get {return(sName);} } //---свойство "Статус" public string Status { get {return sStatus;} }
}
и создана коллекция объектов этого класса: ArrayList A = new ArrayList();
A.Add(new Nekto("Иванов", "директор")); A.Add(new Nekto("Петрович", "инженер"));
A.Add(new Nekto("Сидоровский", "бухгалтер")); Свяжем коллекцию с элементами управления DataGrid, ListBox, TextBox: //---связываем DataGrid со всеми свойствами dataGrid1.DataSource = A; //---связываем ListBox с одним свойством listBox1.DataSource = A; listBox1.DisplayMember = "Name"; //---связываем TextBox с одним свойством
textBox1.DataBindings.Add(new Binding ("Text", A, "Status")); Результат показан на рис. 4.23. 248
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
рис. 4.23
4.1.10. Особенности связывания отдельной строки данных Доступ к значениям столбцов отдельной строки – объекта DataRow (возможно, не связанного с какой-либо таблицей данных) без особых проблем может осуществляться в элементах управления ListBox, CombBox и пр. Для этого достаточно получить массив значений столбцов строки в свойстве ItemArray строки) и поместить ссылку на этот массив в свойство DataSource элемента управления. Пример. Поместить в элемент управления ListBox значения столбцов первой по порядку записи таблицы Kaf набора данных ds1. DataRow MyRow; … MyRow = ds1.Kaf.Rows[0]; … listBox1.DataSource = MyRow.ItemArray; Результат показан на рис. 4.24.
рис. 4.24
4.1.11. Особенности связывания массива строк данных Результатом выполнения ряда методов в ADO.NET служит массив строк DataRow[]. Просто так указать этот массив как источник данных для элемента управления DataGrid или, например, ListBox, нельзя. В DataGrid выводятся общедоступные свойства каждого объекта DataRow массива. То, что выводится в ListBox, лучше и вовсе не комментировать. 4. Связывание в приложении. объектов-источников данных и элементов управления
249
Пример. На рис. 4.25 сверху показано содержимое таблицы Kaf . Из этой таблиы получим массив записей, в которых значения столбца Sostav превышают 10: //---получаем все записи таблицы Kaf //---со значением столбца Sostav, превышающим 10 DataRow[] drsel = ds1.Kaf.Select("Sostav > 10");
Свяжем массив с элементами управления DataGrid и ListBox: dataGrid2.DataSource = drsel; listBox2.DataSource = drsel;
Как видно из рис. 4.25, ни к чему хорошему такое связывание не привело.
рис. 4.25
Решение напрашивается такое: если элемент управления DataGrid напрямую не желает показывать содержимое массива строк данных, то нужно DataGrid обмануть: поскольку этот элемент управления можно связывать с таблицей данных, то стоит создать новую таблицу данных и в коллекцию Rows таблицы поместить строки, входящие в исходный массив строк. Несомненно, что в новой таблице нужно повторить структуру исходной таблицы – той, откуда записи взяты в массив. Для этого в новой таблице нужно создать столько же столбцов и с такими же типами, как и в исходной таблице. Идея верная, но в лоб не решается: строка (объект DataRow), в какие бы массивы ее ни помещали, может принадлежать только к одной таблице данных. Чтобы обойти это ограничение, нужно во временную таблицу добавлять не сами строки из массива, а их копии. Пример. Получить массив, состоящий из тех записей таблицы Kaf набора данных nd1, у которых значение столбца Sostav превышает 10. Отобразить содержимое столбцов записей, входящих в массив, в элементе управления dataGrid2. Значение столбца Nazv записей массива показывать в элементе управления listBox2. 250
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
//---получаем все записи таблицы Kaf //---со значением столбца Sostav, превышающим 10 DataRow[] drsel = ds1.Kaf.Select("Sostav > 10"); //---создаем временную таблицу DataTable dt = new DataTable("HiHi"); //---копируем структуру таблицы Kaf foreach (DataColumn dac in ds1.Kaf.Columns) { DataColumn tmp = new DataColumn(dac.ColumnName, dac.DataType); dt.Columns.Add(tmp); } //---добавляем копии записей из массива в новую таблицу foreach (DataRow darw in drsel) { DataRow tmp = dt.NewRow(); tmp.ItemArray = darw.ItemArray; dt.Rows.Add(tmp); } //---связываем dataGrid и временную таблицу dataGrid2.DataSource = dt; //---в listBox показывается только значение одного //---столбца каждой из записей массива listBox2.DataSource = dt;
listBox2.DisplayMember = "Nazv"; Результат показан на рис. 4.26.
рис. 4.26
4.2. Использование элемента управления DataGrid Элемент управления System.Windows.Forms.DataGrid обеспечивает доступ ко множеству записей из таблицы данных, просмотра или других источников данных. В силу этого DataGrid – наиболее используемый элемент при работе с данными в клиентских Windows-приложениях. 4. Связывание в приложении. объектов-источников данных и элементов управления
251
4.2.1. Строки, столбцы и ячейки DataGrid Данные в элементе управления DataGrid представляются в виде матрицы ("сетки данных"), где строки соответствуют записям данных, а столбцы – столбцам этих записей (рис. 4.27).
рис. 4.27
4.2.1.1. Строки DataGrid Когда для объекта DataGrid источником данных служит таблица данных, то число записей в сетке данных совпадает с числом записей в таблице. В этом случае число строк в DataGrid можно определить следующим образом: label1.Text = "Строк : " + nd1.Kaf.Rows.Count.ToString();
В более общем случае, когда объектом-источником данных для DataGrid является просмотр, либо менеджер просмотров, либо отношение, либо таблица данных, число строк в сетке данных определяют по-другому. Известно, что независимо от реального источника данных данные в сетке DataGrid визуализируются из некоторого неявно создаваемого просмотра (объект DataView). И число строк в сетке DataGrid равно текущему числу записей в этом просмотре: //---получаем CurrencyManager, связывающий источник данных и DataGrid CurrencyManager cm = (CurrencyManager)this.BindingContext[dataGrid1.DataSource, dataGrid1.DataMember]; //---переходим к неявному просмотру DataView dv = ((DataRowView)cm.Current).DataView; //---число строк в неявном просмотре равно числу строк в DataGrid int nSkolkoStrok = dv.Count; label1.Text = "Строк : " + nSkolkoStrok.ToString();
Когда элемент управления DataGrid служит для доступа к записям дочерней таблицы отношения, никто заранее не решится сказать, сколько записей будет в текущий момент визуализировано в сетке данных. То же можно сказать о просмотре, у которого условие фильтрации меняется во времени. Пример. Пусть между таблицами Kaf и Prep в схеме набора данных задано отношение "один ко многим" и источником данных dataGrid2 служит именно это отношение со стороны дочерней таблицы Prep. Тогда в dataGrid2 визуализиру252
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
ются только записи Prep, подчиненные текущей записи в таблице Kaf, и число записей в dataGrid2 непостоянно (рис. 4.28 и 4.29).
рис. 4.28
рис. 4.29
4.2.1.2. Столбцы в DataGrid В сетке данных DataGrid могут отображаться не все столбцы из источника данных. Такую выборочную настройку можно задать в свойстве TableStyles элемента управления DataGrid. Оно подробно рассматривается в 4.2.3). Здесь лишь отметим, что свойство TableStyles позволяет задать коллекцию стилей, по одному для каждой таблицы в составе того источника данных, с которым связан DataGrid. Каждый такой стиль, в свою очередь, – это коллекция GridColumnStyles, состоящая из отдельных объектов – стилей столбцов. Поэтому, чтобы узнать, какие столбцы визуализируются в DataGrid и каковы их параметры, следует обратиться к коллекции GridColumnStyles. 4. Связывание в приложении. объектов-источников данных и элементов управления
253
Однако пользователь может игнорировать применение коллекции TableStyles как для всего источника данных, так и для конкретного раздела данных. В последнем случае для определения числа столбцов в сетке данных, нужно получить строку неявно задаваемого просмотра, ассоциированного с DataGrid (объект DataRowView), перейти к связанной с нею строке данных (объект DataRow) и далее к таблице данных (DataTable), где в коллекции Columns свойство Count возвращает число столбцов таблицы: //---число столбцов в Datagrid int nSkolkoStolbzov = 0; //---если для раздела источника задан стиль //---в коллекции стилей DataGrid if (dataGrid3.TableStyles.Contains(dataGrid3.DataMember)) { //---перебираем стили всех столбцов и вычисляем число столбцов foreach (DataGridColumnStyle dgcs in dataGrid3.TableStyles[dataGrid3.DataMember].GridColumnStyles) { nSkolkoStolbzov ++; } } else { //---получаем CurrencyManager, связывающий источник данных //---и DataGrid CurrencyManager cm = (CurrencyManager)this.BindingContext[ dataGrid3.DataSource, dataGrid3.DataMember]; //---переходим к строке неявного просмотра, //---ассоциированного с DataGrid DataRowView dv = (DataRowView)cm.Current; //---а от нее – к строке данных и таблице набора данных, //---а в ней – к коллекции Columns nSkolkoStolbzov = dv.Row.Table.Columns.Count; } label1.Text = "Столбцов : " + nSkolkoStolbzov.ToString();
4.2.1.3. Ячейки DataGrid Ячейка DataGrid – это объект типа System.Windows.Forms.DataGridCell. Обращение к этому объекту в принципе ничего не дает, кроме номеров столбца и строки, на пересечении которых находится ячейки. Эти значения возвращаются соответственно свойствами ячейки public int ColumnNumber {get; set;} public int RowNumber {get; set;}
Данные, т. е. значение, содержащееся в ячейке, возвращается и задается одним из следующих индексаторов объекта DataGrid: public object this[ DataGridCell cell ] {get; set;} public object this[ int rowIndex, int colIndex ] {get; set;}
254
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
В первом случае индексом служит ссылка на ячейку, во втором – ее координаты (номер строки rowIndex и столбца colIndex). Пример. Считать значения ячеек [0,0] и [1,1], а также задать новое значение ячейки [2,1]. short nKK = (short)dataGrid1[0,0]; string sNazv = dataGrid1[1,1].ToString(); dataGrid1[2,1] = "ХИМИИ";
Текущая ячейка в сетке DataGrid обычно выделяется инверсным цветом (рис. 4.30).
рис. 4.30
Свойство CurrentCell элемента управления DataGrid возвращает ссылку на текущую ячейку: public DataGridCell CurrentCell {get; set;}
Свойство CurrentRowIndex элемента управления DataGrid возвращает номер строки, в которой находится текущая ячейка: public int CurrentRowIndex {get; set;}
Смена текущей ячейки в DataGrid сопровождается наступлением события public event EventHandler CurrentCellChanged;
Пример. Выдавать в элемент управления label1 содержимое текущей ячейки DataGrid: private void dataGrid1_CurrentCellChanged(object sender, System.EventArgs e) { label1.Text = dataGrid1[dataGrid1.CurrentCell].ToString(); }
Если верхняя или нижняя строка DataGrid уходит за пределы видимости в сетке данных, имеет место так называемая "прокрутка" данных. Она сопровождается наступлением события public event EventHandler Scroll;
4. Связывание в приложении. объектов-источников данных и элементов управления
255
4.2.2. Связывание DataGrid с источником данных Элемент управления DataGrid можно связывать со следующими источниками данных: • набором данных (объект DataSet); • таблицей набора данных (объект DataTable); • просмотром (объект DataView); • менеджером просмотров (объект DataViewManager). Когда в качестве источника данных (свойство DataSource элемента управления DataGrid) указывается таблица или просмотр, то раздел данных (т. е. значение свойства DataMember) не указывается. Например, в элементе управления dataGrid3 предоставить доступ к записям умолчательного просмотра таблицы Kaf набора данных nd1: nd1.Kaf.DefaultView.RowFilter = "SOSTAV > 10"; dataGrid3.DataSource = nd1.Kaf.DefaultView;
Когда в качестве источника данных выступает набор данных, то разделом данных может выступать таблица набора данных или отношение (объект DataRelation). Если источник данных – менеджер просмотров, то разделом данных может служить таблица. Пример. Менеджер просмотров DVM задает настройки фильтрации для таблиц Kaf и Prep набора данных ds1. Затем производится связывание фильтрованных данных из таблиц с элементами управления dataGrid1 и dataGrid2. //---создадим менеджер просмотров для набора ds1 DataViewManager DVM = new DataViewManager(ds1); //---зададим настройки для таблицы Kaf DVM.DataViewSettings["Kaf"].RowFilter = "SOSTAV > 10"; DVM.DataViewSettings["Kaf"].Sort = "SOSTAV"; //---зададим настройки для таблицы Prep DVM.DataViewSettings["Prep"].RowFilter = "RANK = 'Доцент'"; //---свяжем dataGrid1 с менеджером и настройками для Kaf dataGrid1.DataSource = DVM; dataGrid1.DataMember = "Kaf"; //---свяжем dataGrid1 с менеджером и настройками для Prep dataGrid2.DataSource = DVM; dataGrid2.DataMember = "Prep";
Если источник данных – набор данных или менеджер просмотров и притом не указано значение свойства DataMember (раздел данных), то элемент управления DataGrid предоставляет пользователю возможность интерактивного выбора раздела данных. Изначально такой DataGrid содержит единственный узел (рис. 4.31).
256
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
рис. 4.31
Раскрыв этот узел, получим список таблиц набора данных (рис. 4.32).
рис. 4.32
Выберем одну из таблиц (например, Kaf), и DataGrid приобретет вид, показанный на рис. 4.33.
рис. 4.33
В заголовке DataGrid, по правому краю, появится значок стрелки . Нажатие значка приведет к возврату элемента управления DataGrid в состояние, показанное выше на рис. 4.32. Теперь можно выбрать другую таблицу набора (рис. 4.34) и т. д.
рис. 4.34
4. Связывание в приложении. объектов-источников данных и элементов управления
257
Рассмотренный пример иллюстрирует, как один элемент управления DataGrid может использоваться для доступа к содержимому всех таблиц набора данных или менеджера просмотров.
4.2.3. Коллекция стилей таблиц – разделов источника данных Для каждой таблицы, которую можно задать для DataGrid в качестве раздела источника данных, можно определить стиль. Этот стиль будет использоваться всякий раз, когда таблица будет указываться в качестве раздела данных: • либо путем указания имени этой таблицы в качестве значения свойства Data Member элемента управления DataGrid; • либо, если для DataGrid в свойстве DataMember не указан раздел данных, путем интерактивного выбора пользователем таблицы из списка имен таблиц набора данных, как это показано выше на рис. 4.32. • •
Таблицу стилей элемента управления DataGrid можно задавать: во время разработки приложения, используя возможности, предоставляемые средой Visual Studio; во время выполнения приложения, используя коллекции GridTableStylesCollection и GridColumnStylesCollection.
4.2.3.1. Использование среды Visual Studio Для задания таблицы стилей: 1. Выберите мышью в форме элемент управления DataGrid и в окне свойств перейдите к свойству TableStyles. 2. В поле значений нажмите кнопку эллипса (…). 3. В появившемся окне DataGridTableStyle Collection Editor добавьте новый элемент в список Members, используя для этого кнопку Add. 4. Перейдите в список Properties. 5. Если нужно, измените имя элемента коллекции стиля в свойстве Name. 6. Задайте имя таблицы в свойстве Mapping Name, выбрав имя таблицы из выпадающего списка ( рис. 4.35). рис. 4.35
258
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
7. Выберите свойство GridColumnStyles и нажмите в поле данных кнопку эллипса (…). 8. В появившемся окне DataGridTableStyle Collection Editor для каждого столбца, доступ к которому необходимо предоставить в DataGrid, добавьте новый элемент в список Members, используя для этого кнопку Add. 9. Чтобы задать свойства каждого столбца стиля, выберите нужный элемент в списке Members и перейдите к списку Properties. 10. Если нужно изменить имя элемента, измените значение свойства Name. 11. Выберите нужный столбец данных из выпадающего списка свойства Mapping Name. 12. Если значение столбца не должно изменяться пользователем, задайте для свойста ReadOnly значение true. 13. Установите значения свойств: NullText (символ, показываемый для тех ячеек столбца, чье значение это null); Width (ширина столбца); Header Text (текст заголовка столбца Allignment (выравнивание значений в столбце). На рис. 4.36 показан пример коллекции из трех столбцов таблицы Kaf в окне DataGridTable Style Collection Editor. На рис. 4.37 показано, как выглядит элемент управления Data Grid, когда в качестве раздела данных для него указывается таблица Kaf.
рис. 4.36
рис. 4.37 4. Связывание в приложении. объектов-источников данных и элементов управления
259
ПРИМЕЧАНИЕ. Если стиль для таблицы не задан, в DataGrid предоставляется доступ ко всем столбцам таблицы. Ширина, выравнивание и символ NullText берутся по умолчанию; заголовок столбца совпадает с именем столбца.
4.2.3.2. Объект GridTableStylesCollection – коллекция стилей DataGrid Итак, каждый элемент управления DataGrid обладает свойством TableStyles public GridTableStylesCollection TableStyles {get;}
возвращающим ссылку на коллекцию System.Windows.Forms.GridTableStylesCollection, каждый элемент которой задает стиль для представления в DataGrid конкретной таблицы набора данных или менеджера просмотров. Обращаться к элементам коллекции GridTableStylesCollection можно при помощи индексаторов: public DataGridTableStyle this[ int index ] {get;} public DataGridTableStyle this[ string tableName ] {get;}
где конкретный стиль идентифицируется индексом в коллекции (index) или именем таблицы (tableName). Обращение к индексатору возвращает стиль таблицы как объект типа System.Windows.Forms.DataGridTableStyle.
4.2.3.3. Объект DataGridTableStyle – стиль таблицы данных в DataGrid Ниже рассматривается ряд свойств, методов и событий объекта DataGridTableStyle. Имя таблицы, для которой задается стиль, определяется свойством public string MappingName {get; set;}
Свойство AllowSorting задает возможность динамической пересортировки в DataGrid, когда столбец, по которому производится сортировка, выбирается пользователем путем щелчка по его заголовку: public bool AllowSorting {get; set;}
Свойство ColumnHeadersVisible в значениии true определяет видимость заголовков столбцов: public bool ColumnHeadersVisible {get; set;}
Свойство HeaderFont задает шрифт заголовка: public Font HeaderFont {get; set;}
Свойства PreferredColumnWidth и PreferredRowHeight задают умолчательные значения соответственно ширины и высоты столбцов данных. Такие значения назначаются вновь создаваемому столбцу. Для конкретного столбца они могут изменяться. public int PreferredColumnWidth {get; set;} public int PreferredRowHeight {get; set;}
Если свойство имеет значение true, то все столбцы таблицы, задаваемой данным стилем, будут доступны в DataGrid только на чтение. public virtual bool ReadOnly {get; set;}
260
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Свойство RowHeadersVisible задает видимость заголовков строк записей в DataGrid, а свойство RowHeaderWidth задает ширину такого заголовка. На рис. 4.38, а такой столбец отсутствует, на рис. 4.38, б он в наличии.
а
б рис. 4.38
public bool RowHeadersVisible {get; set;} public int RowHeaderWidth {get; set;}
Свойства SelectionBackColor и SelectionForeColor задают цвет содержимого и фона выделенных ячеек DataGrid (рис. 4.39). public Color SelectionBackColor {get; set;} public Color SelectionForeColor {get; set;}
рис. 4.39
Объект стиля таблицы обладает группой методов, позволяющих изменять характеристики представления данных в сетке DataGrid (цвета, линии разметки, шрифты), и соответствующей группой методов ResetXXX, восстанавливающих исходное (имевшее быть по умолчанию) значение цветов, линий и пр. У объекта стиля таблицы есть группа событий XXXChnanged, наступающих при изменении того или иного цвета, стиля линии, шрифта и т. д. Подробнее с этими методами и событиями можно при необходимости ознакомиться в технической документации. Главным, по существу, свойством объекта стиля таблицы данных является свойство GridColumnStyles: public virtual GridColumnStylesCollection GridColumnStyles {get;}
Оно возвращает ссылку на коллекцию System.Windows.Forms. GridColumnStylesCollection. Это коллекция стилей отдельных столбцов таблицы данных. 4. Связывание в приложении. объектов-источников данных и элементов управления
261
4.2.3.4. Объект GridColumnStylesCollection – коллекция стилей отдельных столбцов таблицы Коллекция System.Windows.Forms.GridColumnStylesCollection содержит группу объектов типа System.Windows.Forms.DataGridColumnStyle, каждый из которых реализует стиль конкретного столбца таблицы. Обратиться к тому или иному стилю столбца можно при помощи индексатора коллекции GridColumnStylesCollection: public DataGridColumnStyle this[ int index ] {get;} public DataGridColumnStyle this[ string columnName ] {get;}
В качестве параметра индексатора могут указываться либо индекс объекта в коллекции, либо его имя. Число стилей столбцов в коллекции GridColumnStylesCollection возвращается ее свойством public virtual int Count {get;}
Для добавления стиля столбца в коллекцию GridColumnStylesCollection используется метод Add этой коллекции public virtual int Add( DataGridColumnStyle column );
Массив стилей столбцов можно добавить в коллекцию методом public void AddRange( DataGridColumnStyle[] columns );
Удалить стиль столбца из коллекции можно при помощи методов Remove и RemoveAt коллекции GridColumnStylesCollection: public void Remove( DataGridColumnStyle column ); public void RemoveAt( int index );
причем первый удаляет конкретный объект стиля столбца по ссылке на него, второй – по индексу, т. е. порядковому номеру, объекта стиля столбца в коллекции. Определить, присутствует ли в коллекции GridColumnStylesCollection конкретный объект DataGridColumnStyle, можно при помощи метода коллекции: public bool Contains( DataGridColumnStyle column); public bool Contains( string columnName );
где объект стиля столбца задается ссылкой на него (column) либо своим именем (columnName).
4.2.3.5. Объект DataGridColumnStyle – стиль отдельного столбца таблицы данных Объект типа System.Windows.Forms.DataGridColumnStyle реализует стиль отдельного столбца в рамках стиля таблицы данных. Для создания объекта DataGridColumnStyle применяют конструктор public DataGridColumnStyle();
При создании нового объекта DataGridColumnStyle его свойства инициализируются следующими значениями: 262
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Свойство Alignment DataGridTableStyle FontHeight Invalidate NullText ReadOnly
Значение по умолчанию HorizontalAlignment.Left null –1 false Строка "(null)". false
Объект System.Windows.Forms.DataGridColumnStyle – базовый для двух объектов стилей столбцов: System.Windows.Forms.DataGridBoolColumn System.Windows.Forms.DataGridTextBoxColumn
Разница между ними видна из рис. 4.40. DataGridBool Column служит для представления булевых значений (столбец Основная каф.), в то время как DataGridTextBox Column используется для всех прочих значений (столбцы Код, Название), представляя их в текстовом виде и как бы накладывая элемент TextBox на ячейку DataGrid.
рис. 4.40
Рассмотрим свойства объекта стиля столбца. Свойство MappingName связывает объект DataGridColumnStyle с конкретным столбцом таблицы данных (объект DataColumn): public string MappingName {get; set;}
Свойство HorizontalAlignment задает выравнивание значений в столбце DataGrid: public virtual HorizontalAlignment Alignment {get; set;}
Свойство HeaderText задает текст заголовка столбца: public virtual string HeaderText {get; set;}
Свойство NullText задает значение, которое будет показываться в тех ячейках столбца, которые содержат "пустое" значение: public virtual string NullText {get; set;}
Свойство ReadOnly в значениеи true блокирует изменения значения в столбце: public virtual bool ReadOnly {get; set;} 4. Связывание в приложении. объектов-источников данных и элементов управления
263
Свойство Width задает ширину столбца: public virtual int Width {get; set;}
Группа событий наступает для объекта DataGridColumnStyle в случае изменения одного из перечисленных выше свойств: public public public public public public
event event event event event event
EventHandler EventHandler EventHandler EventHandler EventHandler EventHandler
AlignmentChanged; HeaderTextChanged; MappingNameChanged; NullTextChanged; ReadOnlyChanged; WidthChanged;
4.2.3.6. Пример динамического добавления и удаления столбцов в DataGrid Перечисленных выше коллекций и объектов достаточно для того, чтобы во время выполнения приложения динамически управлять параметрами стиля таблицы, а именно изменять состав столбцов для стилей таблиц данных и(или) значения свойств стилей столбцов. Пример. Добавим новый столбец в стиль таблицы Kaf. Пусть изначально данный стиль определен так, как это показано на рис. 4.41.
Выполним следующий код: рис. 4.41 //---создадим новый стиль столбца DataGridTextBoxColumn MyColStyle = new DataGridTextBoxColumn(); //---назначим параметры стиля MyColStyle.MappingName = "Fakultet"; MyColStyle.Width = 150; MyColStyle.HeaderText = "Факультет"; //---добавим стиль столбца в стиль таблицы Kaf dataGrid1.TableStyles["Kaf"].GridColumnStyles.Add(MyColStyle); Результат выполнения показан на рис. 4.42.
рис. 4.42
264
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Выполним далее следующий код: //---получим ссылку на стиль столбца "Osnovnaya" DataGridColumnStyle RemovedColStyle = dataGrid1.TableStyles["Kaf"].GridColumnStyles["Osnovnaya"]; //---удалим стиль столбца из стиля таблицы Kaf dataGrid1.TableStyles["Kaf"].GridColumnStyles.Remove(RemovedColStyle);
Результат выполнения показан на рис. 4.43.
рис. 4.43
4.2.4. DataGrid и представление связанных таблиц Рассмотрим простейший пример представления в DataGrid двух связанных таблиц. Пусть набор данных ds содержит две такие таблицы: связанные отношением "один-ко-могим" родительскую Kaf и дочернюю Prep (рис. 4.44).
рис. 4.44
Для представления такой связи в форме, в том числе для показа записей каждой из таблиц, можно использовать один элемент управления DataGrid. При этом в качестве источника данных (свойство DataSource) элемента управления DataGrid указывается имя набора данных, а в качестве раздела данных (свойство DataMember) – имя родительской таблицы, в данном случае Kaf. На этапе выполнения приложения элемент управления DataGrid с такими свойствами будет иметь вид, показанный на рис. 4.45.
4. Связывание в приложении. объектов-источников данных и элементов управления
265
рис. 4.45
Необходимо обратить внимание, что в столбце заголовков строк (крайне левом столбце сетки данных) для каждой записи родительской таблицы Kaf появился значок – квадратик с крестиком внутри. Так обычно помечаются узлы деревьев. Если раскрыть такой узел для любой записи, то получим список имен подчиненных таблиц. В данном случае у Kaf есть только одна дочерняя таблица. Ее имя мы и видим в списке (рис. 4.46).
рис. 4.46
ПРИМЕЧАНИЕ. Для записи с индексом rowIndex свойство public bool IsExpanded(int rowIndex);
возвращает true, если узел (список дочерних таблиц) для этой записи раскрыт, и false в противном случае. Свернуть список для записи с номером позволяет метод public void Collapse(int rowIndex);
Если rowIndex = -1, сворачиваются списки всех записей. Выбор подчиненной таблицы из списка приводит к тому, что в сетке данных DataGrid теперь показывается содержимое выбранной дочерней таблицы (рис. 4.47).
рис. 4.47
266
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Заметим, что в сетке появляется новая строка, содержащая значения столбцов текущей записи родительской таблицы Kaf (рис. 4.48), где стрелки тывать данные, не уместившиеся в строке.
и
позволяют пролис-
рис. 4.48
Рассмотрим параметры представления этой строки. Ее видимость задается значением true (по умолчанию) свойства ParentRowsVisible элемента управления DataGrid: public bool ParentRowsVisible {get; set;}
При смене значения этого свойства наступает событие public event EventHandler ParentRowsVisibleChanged;
Цвет символов и фон данных, представляемых в строке родительской записи, задаются свойствами ParentRowsForeColor и ParentRowsBackColor элемента управления DataGrid: public Color ParentRowsForeColor {get; set;} public Color ParentRowsBackColor {get; set;}
Режим визуализации строки, содержащей сведения о родительской записи, определяется свойством элемента управления DataGrid: public DataGridParentRowsLabelStyle ParentRowsLabelStyle {get; set;}
Перечисление System.Windows.Forms.DataGridParentRowsLabelStyle содержит следующие значения: Значение Both
ColumnName
None
TableName
Описание Показываются: имя родительской таблицы, имена и значения столбцов текущей записи родительской таблицы
Показываются названия столбцов и их значения для текущей записи родительской таблицы
Показываются только значения столбцов для текущей записи родительской таблицы
Показываются только значения столбцов для текущей записи родительской таблицы, а также ее имя
Смена значения свойства сопровождается наступлением события public event EventHandler ParentRowsLabelStyleChanged; 4. Связывание в приложении. объектов-источников данных и элементов управления
267
Интересны в этом случае свойства и самого заголовка грида (рис. 4.49).
рис. 4.49
Нажатие символа приводит к исчезновению информационной строки родительской таблицы, показанной выше на рис. 4.48. Повторное нажатие означенного символа возвращает информационную строку на ее первоначальное место. При нажатии символа всякий раз наступает событие public event EventHandler ShowParentDetailsButtonClick;
Символ используется для возврата к родительской таблице (см. выше рис. 4.46). При нажатии символа всякий раз наступает событие public event EventHandler BackButtonClick; Резюмируем вышесказанное. Если в наборе данных присутствует множество таблиц, возможно связанных между собой, то один и тот же элемент управления DataGrid можно применять для доступа к содержимому всех этих таблиц. Подобная функциональность позволяет, экономя на пространстве формы и приложения, связывать один DataGrid, связываемый с максимальным числом таблиц. Впрочем, столь ценную для разработчика особенность DataGrid на практике следует применять осторожно, ориентируясь на потенциального пользователя создаваемого приложения. Ряд пользователей, натренированных на одновременное восприятие взаимосвязей между несколькими сущностями и средне- и долговременное удерживание их в буферных зонах сознания, несомненно, сочтут такую возможность удобной и остроумной либо просто примут ее как данность. У другой категории пользователей, не обладающих подобными навыками, все это может вызвать существенный дискомфорт. Здесь, вероятно, уместнее прибегнуть к "традиционному" представлению взаимосвязанных сущностей, поместив в форме родительскую и дочернюю таблицы рядом или друг под другом (рис. 4.50).
рис. 4.50
268
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
4.2.5. Программная навигация в DataGrid по таблицам Для навигации по связанным таблицам в DataGrid применяются методы NavigateTo и NavigateBack. Метод NavigateTo переходит к дочерней таблице, сответствующей отношению relationName, притом на запись этого отношения, определяемую параметром rowNumber. public void NavigateTo( int rowNumber, string relationName ); При выполнения метода имеет место пикантный момент: в дочерней таблице текущей будет запись с индексом rowNumber, независимо от того, какая запись была текущей в дочерней таблице на момент выполнения метода NavigateTo. Пример. Пусть текущая запись в родительской таблице – это кафедра № 11 (рис. 4.51).
рис. 4.51
Однако выполнение кода dataGrid1.NavigateTo(0, "KafPrep");
приведет к загрузке в dataGrid1 отношения KafPrep, но притом указатель встанет на самую первую запись отношения с индексом 0, никак не связанную с кафедрой № 11 (рис. 4.52).
рис. 4.52
Методу NavigateBack подобная пикантность не присуща. Он попросу возвращается на один уровень выше в родительскую таблицу и притом текущей становится запись, которой были подчинены записи в дочерней таблице, выгруженной из DataGrid. public void NavigateBack();
Например, для подчиненной таблицы Prep (рис. 4.53) 4. Связывание в приложении. объектов-источников данных и элементов управления
269
рис. 4.53
выполнение метода dataGrid1.NavigateBack();
приведет к загрузке в DataGrid родительской таблицы Kaf и установке в качестве текущей записи кафедры № 11 (рис. 4.54), т. е. той записи, которой было подчинено содержимое дочерней таблицы на момент отработки метода NavigateBack.
рис. 4.54
Событие смены таблицы данных в DataGrid (неважно, ручное либо программное) сопровождается сменой значения свойства DataMember названного элемента управления, а также наступлением события public event NavigateEventHandler Navigate;
Обработчик этого события имеет вид public delegate void NavigateEventHandler( object sender, NavigateEventArgs ne );
Польза от параметра ne, впрочем, небольшая. Он содержит, помимо общих, свойство Forward, возвращающее true, если мы движемся в направлении от родителя к потомку. public bool Forward {get;} Пример. Обработчик события смены таблицы в DataGrid должен выводить название текущего раздела данных в заголовке DataGrid ( рис. 4.55 и 4.56). private void dataGrid1_Navigate(object sender, System.Windows.Forms.NavigateEventArgs ne) { dataGrid1.CaptionText = dataGrid1.DataMember; }
270
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
рис. 4.55
рис. 4.56
Навигация по связанным и несвязанным таблицам в DataGrid возможна, если свойство public bool AllowNavigation {get; set;} содержит значение true. Если отключить возможности навигации, список ссылок на таблицы (рис. 4.57, а) пропадает (рис. 4.57, б).
а
б рис. 4.57
При включении возможности навигации список появляется вновь. Нечто похожее происходит, когда, находясь в дочерней таблице (рис. 4.58), мы отключим навигацию. Вернуться в родительскую таблицу теперь нельзя (обратите внимание, как в заголовке DataGrid на рис. 4.59 исчез символ при повторном включении навигации.
). Впрочем, возврат разрешается немедленно
4. Связывание в приложении. объектов-источников данных и элементов управления
271
рис. 4.58
рис. 4.59
При изменении значения свойства AllowNavigation наступает событие public event EventHandler AllowNavigationChanged;
4.2.6. Куда "кликнул" пользователь? В ряде случаев полезно знать, по какой части элемента управления DataGrid пользователь щелкнул мышью. Анализ действий пользователя производят в обработчике события MouseDown элемента управления DataGrid. Этот обработчик принимает параметр e типа System.Windows.Forms.MouseEventArgs. Названный параметр обладает, помимо прочих, свойствами public int X {get;} public int Y {get;} Они содержат значения текущих координат указателя мыши. Эти значения используются при вызове метода HitTest элемента управления DataGrid: public HitTestInfo HitTest( int X, int Y ); Метод HitTest возвращает объект типа System.Windows.Forms.Data Grid.HitTestInfo. Свойство Type названного объекта public DataGrid.HitTestType Type {get;} возвращает одно из следующих значений перечисления System.Windows.Forms. DataGrid.HitTestType, на основании которых и можно судить о том, по какой части DataGrid щелкнул мышью пользователь. Значение Caption Cell
272
По какой части DataGrid щелкнул мышью пользователь По заголовку По ячейке ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
ColumnHeader ColumnResize None ParentRows RowHeader RowResize
По заголовку ячейки По линии, разделяющей заголовки двух соседних столбцов. В дальнейшем возможно изменить ширину столбца. По обрамлению DataGrid По строке заголовка, содержащей данные о текущей записи родительской таблицы – для DataGrid, позволяющего загружать дочерние записи, как, например, показано на рис. 4.56 По заголовку строки – в столбце заголовков строк По линии, разделяющей две соседние строки в столбце заголовков строк. В дальнейшем возможно изменить ширину строки
Если пользователь щелкнул мышью по ячейке либо по заголовку строки, номер строки можно получить из свойства объекта HitTestInfo: public int Row {get;} Если пользователь щелкнул мышью по ячейке либо по заголовку столбца, номер столбца можно получить из свойства объекта HitTestInfo: public int Row {get;} Пример. Выясним, по какой части элемента управления DataGrid щелкнул мышью пользователь, и выведем свое заключение в виде информационного сообщения на экран. Если пользователь шелкнул в ячейке, в информационном окне выведем также содержимое ячейки. private void dataGrid1_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e) { System.Windows.Forms.DataGrid.HitTestInfo hti = dataGrid1.HitTest(e.X,e.Y); string sMsgTxt = ""; switch (hti.Type) { case (System.Windows.Forms.DataGrid.HitTestType.Caption): { sMsgTxt = "Заголовок"; break; } case (System.Windows.Forms.DataGrid.HitTestType.Cell): { string sCurrentCellValue = dataGrid1[hti.Row,hti.Column].ToString(); sMsgTxt = "Ячейка, значение = " + sCurrentCellValue; break; } case (System.Windows.Forms.DataGrid.HitTestType.ColumnHeader): { sMsgTxt = "Заголовок столбца"; break; } case (System.Windows.Forms.DataGrid.HitTestType.ColumnResize): { 4. Связывание в приложении. объектов-источников данных и элементов управления
273
sMsgTxt = "Изменение ширины столбца"; break;
} case (System.Windows.Forms.DataGrid.HitTestType.None): { sMsgTxt = "Ничего"; break; } case (System.Windows.Forms.DataGrid.HitTestType.ParentRows): { sMsgTxt = "Родительская запись"; break; } case (System.Windows.Forms.DataGrid.HitTestType.RowHeader): { sMsgTxt = "Заголовок строки"; break; } case (System.Windows.Forms.DataGrid.HitTestType.RowResize): { sMsgTxt = "Изменение ширины строки"; break; }
} MessageBox.Show(sMsgTxt); }
Случай, когда пользователь щелкнул в ячейке DataGrid, показан на рис. 4.60.
рис. 4.60
4.2.7. Режим "только-на-чтение" Внесение изменений в элемент управления DataGrid невозможно в том случае, когда его свойство public bool ReadOnly {get; set;}
содержит значение false. Значение true для свойства ReadOnly разрешает внесение изменений в ячейки сетки данных DataGrid (и, соответственно, ассоциированный с ним источник данных), если не задан ни один стиль таблиц данных, т. е. свойство TableStyles 274
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
элемента управления DataGrid ссылается на пустую коллекциею GridTableStylesCollection. Если в DataGrid заданы стили таблиц, то внесение изменений в данные запрещается или разрешается для каждого стиля таблицы (объект DataGridTableStyle). Если свойство ReadOnly стиля таблицы содержит значение false, то нельзя вносить изменения в столбцы и записи таблицы набора данных, ассоциированной с этим стилем. Если же свойство ReadOnly объекта стиля таблицы содержит значение true, то возможность редактирования отдельного столбца внутри стиля таблицы задается свойством ReadOnly стиля этого столбца (объекта DataGridColumnStyle, ассоциированного с конкретным столбцом). При изменении значения свойства ReadOnly во время выполнения для того объекта, где это свойство изменилось (DataGrid, DataGridTableStyle или Data GridColumnStyle), наступает событие public event EventHandler ReadOnlyChanged;
4.2.8. Особенности изменения значений в ячейках DataGrid Любое изменение значения ячейки DataGrid приводит к немедленному внесению изменений в соответствующий столбец строки данных (объект DataRow). Сказанное верно как для "ручного", так и для программного способа внесения изменений. Пример. Рассмотрим таблицу Kaf набора данных nd1 (рис. 4.61).
рис. 4.61
Для того чтобы привести к верхнему регистру значение столбца Nazv во 2-й записи таблицы Kaf, достаточно либо изменить значение, находящееся в соответствующей ячейке DataGrid: dataGrid1[1, 1] = dataGrid1[1, 1].ToString().ToUpper();
либо внести изменения непосредственно в столбец Nazv 2-й записи таблицы Kaf: nd1.Kaf.Rows[1]["Nazv"]= nd1.Kaf.Rows[1]["Nazv"].ToString().ToUpper();
или nd1.Tables[0].Rows[1][1] = nd1.Tables[0].Rows[1][1].ToString().ToUpper();
Результат представлен на рис. 4.62. 4. Связывание в приложении. объектов-источников данных и элементов управления
275
рис. 4.62
4.2.9. Особенности добавления записей данных в DataGrid Пусть в данные, с которыми связан элемент управления DataGrid, разрешено вносить изменения. В сетке данных DataGrid в таком случае последняя запись всегда пуста (рис. 4.63).
рис. 4.63
Если поместить курсор на эту запись и ввести в нее значения (рис. 4.64), то мы создадим новую запись, пока еще отсоединенную от источника данных, т. е. не состоящую в коллекции Rows соответствующей таблицы данных и находящуюся в состоянии System.Data.DataRowState.Detached.
рис. 4.64
В этом легко убедиться. Напишем обработчик события смены текущей ячейки в DataGrid. Получим ссылку на запись данных (объект DataRow), связанную с текущей строкой DataGrid, узнаем состояние этой строки и запишем его в элемент управления типа Label: private void dataGrid1_CurrentCellChanged(object sender, System.EventArgs e) {
276
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
//---перейдем к CurrencyManager System.Windows.Forms.CurrencyManager cm = (System.Windows.Forms.CurrencyManager)this.BindingContext[nd1, dataGrid1.DataMember]; //---запись dataGrid связана со строкой (тип DataRowView) некоего //--просмотра DataRowView drv = (DataRowView)cm.Current; //---от строки просмотра перейдем к связанной с ней строке //---источника данных DataRow dr = drv.Row; //---покажем текущее состояние строки источника данных label1.Text = dr.RowState.ToString(); }
Как видно из рис. 4.65, в момент заполнения значений последней строки DataGrid состояние заполняемой строки будет именно Detached.
рис. 4.65
Однако как только мы перейдем в DataGrid на другую строку (вверх на предыдущую или вниз, снова на последнюю пустую строку), то инициируем окончание редактирования. Вследствие этого ранее отсоединенная строка будет добавлена в коллекцию строк источника данных и получит статус System.Data.DataRowState.Added. Вернемся в примере на добавленную строку, чтобы удостовериться в этом (рис. 4.66).
рис. 4.66
Теперь строка "полноправный член" коллекции строк источника данных. Дальнейшая судьба добавленной строки зависит от того, отменим мы изменения (методы RejectChanges набора данных, таблицы или строки) или запомним их в источнике данных методом Update адаптера данных. 4. Связывание в приложении. объектов-источников данных и элементов управления
277
4.2.10. Назначение умолчательных значений при добавлении записей данных в DataGrid Довольно часто в момент добавления новой записи требуется присвоить некоторым столбцам записи стартовые значения. Вообще говоря, это может быть все, что угодно, – значения ключевых столбцов; автоматически наращиваемый программой номер; шаблон ввода значения для телефонного номера ###-##-## и т. д. Событие RowChanging, наступающее при изменении значений столбцов записи данных, для назначения стартовых значений столбцов использовать нельзя: добавляемая строка находится в состоянии Detached (отсоединенная). Пока строка не будет присоединена к таблице данных, названное событие для добавляемой записи не наступит. Поэтому используем другой способ. Всякий раз при смене текущей ячейки в DataGrid в обработчике события CurrentCellChanged будем проверять статус строки данных, связанной с добавляемой строкой в DataGrid. Когда эта строка отсоединенная и пусты те ячейки, в которых должны быть стартовые значения, констатируем: добавляется новая строка и в ней нужно задать стартовые значения столбцов. Что далее и делаем. Пример. Пусть элемент управления dataGrid1 связан с таблицей Kaf набора данных nd1 (рис. 4.67).
рис. 4.67
Зададим следующий обработчик события смены текущей ячейки в dataGrid1: private void dataGrid1_CurrentCellChanged(object sender, System.EventArgs e) { //---перейдем к CurrencyManager System.Windows.Forms.CurrencyManager cm = (System.Windows.Forms.CurrencyManager)this.BindingContext[nd1, dataGrid1.DataMember]; //---запись dataGrid связана со строкой (тип DataRowView) некоего //--просмотра DataRowView drv = (DataRowView)cm.Current; //---от строки просмотра перейдем к связанной с ней строке //---источника данных DataRow dr = drv.Row; //---индекс текущей (добавляемой) строки в dataGrid int ndx = dataGrid1.CurrentRowIndex;
278
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
//---глядим: а не пусты ли 0-й и 3-й столбцы текущей записи dataGrid? bool Pusto = (dataGrid1[ndx, 3].ToString() == ""); //---если строка отсоединенная и нужные столбцы пусты, заполняем их //---стартовыми значениями if ((dr.RowState == System.Data.DataRowState.Detached) && Pusto) { dataGrid1[ndx, 0] = 999; dataGrid1[ndx, 3] = "Общий"; } }
Позиционируем указатель dataGrid1 на последнюю, пустую запись. Увидим, что стартовые значения появились в нужных ячейках добавляемой строки dataGrid1 (рис. 4.68).
рис. 4.68
Cтартовые значения столбцов можно задавать не в ячейках DataGrid, а в записи (объект DataRowView) просмотра, ассоциированно связанного с DataGrid: if ((dr.RowState == System.Data.DataRowState.Detached) && Pusto) { drv["KK"] = 999; drv["Fakultet"] = "Общий"; }
или в самой добавляемой записи (объект DataRow): if ((dr.RowState == System.Data.DataRowState.Detached) && Pusto) { dr["KK"] = 999; dr["Fakultet"] = "Общий"; }
4.2.11. Выбор вариантов значений в ячейке DataGrid Некоторые столбцы таблицы базы данных могут cодержать значение из ограниченного подмножества. Например: пол – {мужской, женский}; гражданство – {гражданин РФ, негражданин, двойное гражданство} и пр. Чтобы помочь пользователю выбрать одно из разрешенных для данного столбца значений, в DataGrid, как правило, используют элемент управления ComboBox, совмещая его с границами редактируемой ячейки (рис. 4.69). 4. Связывание в приложении. объектов-источников данных и элементов управления
279
рис. 4.69
Для этой цели в форме при создании приложения размещают два элемента управления – DataGrid и ComboBox. Свойство DropDownStyle элемента управления ComboBox устанавливают в значение DropDownList. Список значений элемента управления ComboBox (свойство Items) формируют либо на этапе создания приложения, либо программно на этапе выполнения. При перемещении по ячейкам DataGrid в обработчике события CurrentCell Changed последнего отслеживают, не принадлежит ли текущая ячейка сетки данных к тому столбцу, значения которого нужно выбирать из списка. Если не принадлежит, ComboBox со списком значений девизуализируют, а если принадлежит, наоборот, визуализируют: private void dataGrid1_CurrentCellChanged(object sender, System.EventArgs e) { int nCl = dataGrid1.CurrentCell.ColumnNumber; if (nCl == 3) { … MycomboBox.Visible = true; } else { MycomboBox.Visible = false; } }
Однако, если столбец – тот самый, значения которого должны выбираться из списка, то ComboBox мало визуализировать, нужно еще совместить его границы с границами текущей ячейки DataGrid. Для этого следует обратиться к свойству Rectangle элемента управления DataGrid: public Rectangle GetCurrentCellBounds();
Это свойство возвращает объект типа System.Drawing.Rectangle (прямоугольник), соответствующий текущей ячейке. Обратившись к свойствам public public public public
280
int int int int
Left {get;} Top {get;} Height {get; set;} Width {get; set;} ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
полученного прямоугольника, мы узнаем значения координат X и Y левого угла ячейки и ее высоту и ширину. Этого достаточно для наложения объекта ComboBox на текущую ячейку DataGrid. MycomboBox.Left = dataGrid1.GetCurrentCellBounds().Left; MycomboBox.Top = dataGrid1.GetCurrentCellBounds().Top; MycomboBox.Height = dataGrid1.GetCurrentCellBounds().Height; MycomboBox.Width = dataGrid1.GetCurrentCellBounds().Width;
Одновременно в качестве текущего значения в списке ComboBox устанавливают содержимое текущей ячейки DataGrid: if (dataGrid1[dataGrid1.CurrentCell].ToString() != "") { MycomboBox.Text = dataGrid1[dataGrid1.CurrentCell].ToString(); }
Наконец, при выборе какого-либо значения в списке ComboBox его нужно поместить в текущую ячейку DataGrid – ту самую, которую ComboBox накрывает собою: private void MycomboBox_SelectionChangeCommitted(object sender, System.EventArgs e) { dataGrid1[dataGrid1.CurrentCell] = MycomboBox.Items[MycomboBox.SelectedIndex]; }
4.2.12. Кнопка эллипса в ячейке DataGrid Иногда при выполнении приложения, когда мы редактируем значение в ячейке DataGrid, нужно выполнить ряд действий в других формах приложения, а затем вернуться в ячейку. Типичным примером может служить выбор значения из элемента управления DataGrid, размещенного в другой форме. Подобную задачу принято решать, помещая в ячейку DataGrid экранную кнопку (объект типа TextButton) с текстовым обозначением "…". Ее называют также кнопкой эллипса. Эта кнопка визуализируется, только когда мы щелкаем мышью в ячейке, переводя ее в состояние редактирования (рис. 4.70).
рис. 4.70
Нажав кнопку эллипса, мы выполняем какие-либо действия, как правило, перемещаясь в другие формы приложения, и по выполнении таких действий снова возвращаемся в исходную ячейку DataGrid. 4. Связывание в приложении. объектов-источников данных и элементов управления
281
Пример. Пусть в приложении имеется форма fSelFakultet (рис. 4.71), представляющая доступ к записям таблицы Fakultety (факультеты вуза) набора данных nd1.
рис. 4.71
Пусть у формы fSelFakultet есть свойство SelectedFakulted, выдающее во внешний мир строковое значение приватной переменной SelectedFakulted: public class fSelFakultet : System.Windows.Forms.Form { private string sSelectedFakulted = ""; … //---выбранный факультет (свойство) public string SelectedFakulted { get {return sSelectedFakulted;} } }
Обработчик нажатия экранной кнопки Выбрать помещает в приватную переменную sSelectedFakulted формы значение Fakultet столбца текущей записи таблицы Fakultety набора данных nd1: //---нажата кнопка "Выбрать" private void btnSelect_Click(object sender, System.EventArgs e) { sSelectedFakulted = nd1.Fakultety.Rows[this.BindingContext[nd1, "Fakultety"].Position]["Fakultet"].ToString(); }
Рассмотрим другую форму приложения, Form1, где задан элемент управления dataGrid1, связанный с таблицей Kaf набора данных nd1. Когда пользователь попадает в столбец Факультеты, должна появляться кнопка эллипса, по нажатии которой происходит вызов рассмотренной выше формы fSelFakultet, выбор факультета и перенос его в текущую ячейку – ту самую, в которой находится кнопка эллипса. Создадим в форме Form1 кнопку эллипса с именем btnEllips, заголовком "…" и произвольной длиной и шириной. Зададим следующий обработчик события смены текущей ячейки в объекте dataGrid1 формы Form1: 282
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
private void dataGrid1_CurrentCellChanged(object sender, System.EventArgs e) { //---номер столбца выбранной ячейки грида int nCl = dataGrid1.CurrentCell.ColumnNumber; //---выбрана ячейка в столбце "Факультет" if (nCl == 2) { //---помещаем кнопку в левый край текущей ячейки, //---приводя в соответствие с размером ячейки, //---и затем визуализируем btnEllips.Left = dataGrid1.GetCurrentCellBounds().Right – btnEllips.Width; btnEllips.Top = dataGrid1.GetCurrentCellBounds().Top; btnEllips.Height = dataGrid1.GetCurrentCellBounds().Height; btnEllips.Visible = true; //---если значения в ячейке нет сразу вызываем форму //---выбора факультета if (dataGrid1[dataGrid1.CurrentCell].ToString().Trim() == "") { eSelFakultet.ShowDialog(); //---если пользователь выбрал факультет, то заносим его //---в текущую ячейку if (eSelFakultet.SelectedFakulted != "") { dataGrid1[dataGrid1.CurrentCell] = eSelFakultet.SelectedFakulted; } } } else //---выбрана ячейка в другом столбце { btnEllips.Visible = false; } }
Зададим следующий обработчик нажатия кнопки эллипса: private void btnEllips_Click(object sender, System.EventArgs e) { eSelFakultet.ShowDialog(); if (eSelFakultet.DialogResult == DialogResult.OK) { dataGrid1[dataGrid1.CurrentCell] = eSelFakultet.SelectedFakulted; } }
Запустим приложение и посмотрим, каков приведенный выше код в действии. Перейдем в dataGrid1 (форма Form1) на ячейку в столбце Факультет (рис. 4.72).
4. Связывание в приложении. объектов-источников данных и элементов управления
283
рис. 4.72
Нажмем кнопку эллипса, что вызовет форму fSelFakultet (рис. 4.73). Выберем Гуманитарный факультет и нажмем кнопку Выбрать. Управление будет возвращено в Form1 и выбранный факультет будет помещен в текущую ячейку dataGrid1 (рис. 4.74).
рис. 4.73
рис. 4.74
4.2. Выделение записей в DataGrid В сетке данных DataGrid можно выделить одну или несколько записей. Выделенные записи представляются цветом и(или) фоном, отличным от цвета и(или) фона остальных записей (рис. 4.75). 284
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
рис. 4.75 • •
Цель выделения: если она выполняется программно, значит, приложению нужно привлечь внимание пользователя к отдельным записям; если она выполняется пользователем вручную, значит, пользователю нужно очертить круг записей, к которым затем применить однотипные действия.
Для "ручной" отметки записей в DataGrid необходимо щелкнуть указателем мыши в соответствующей ячейке столбца заголовков строк. При этом, если необходимо отметить несколько записей подряд, следует, не отпуская левой кнопки мыши, вести указатель мыши вниз, пока не будет выделено желаемое число строк. Если же нужно выделить строки вразбивку (т. е. не непрерывным диапазоном), следует отметить первую строку из выделяемых и, удерживая нажатой кнопку Ctrl на клавиатуре компьютера, выделить остальные строки. Для программного выделения в сетке данных строки с индексом rowIndex используется метод Select элемента управления DataGrid: public void Select( int rowIndex );
Для снятия выделения строки с индексом rowIndex используется метод Select элемента управления DataGrid: public void UnSelect( int rowIndex );
Цвет символов в выделенных записях задается свойством SelectionForeColor элемента управления DataGrid: public Color SelectionForeColor {get; set;}
а цвет фона выделенных записей – свойством SelectionBackColor элемента управления DataGrid: public Color SelectionBackColor {get; set;}
Отметим, что значения свойств SelectionForeColor и SelectionBack Color действуют для всего DataGrid, только если не задан ни один стиль таблиц данных, т. е. свойство TableStyles объекта DataGrid ссылается на пустую коллекцию GridTableStylesCollection. В противном случае свойства SelectionForeColor и SelectionBackColor задаются для каждого стиля таблицы (т. е. для каждого объекта DataGridTableStyle). И наконец, судить о том, выделена ли запись грида с индексом rowIndex, позволяет свойство IsSelected элемента управления DataGrid: 4. Связывание в приложении. объектов-источников данных и элементов управления
285
public bool IsSelected( int rowIndex );
Оно возвращает true, если запись выделена, и false в противном случае. Пример программного выделения записей DataGrid. Пусть объект dataGrid1 связан с таблицей Kaf набора данных nd1. Выделим строки грида, у которых столбец Fakultet содержит значение Главный. CurrencyManager cm = (CurrencyManager)this.BindingContext[dataGrid1.DataSource, dataGrid1.DataMember]; //---переходим к неявному просмотру, ассоциированному с DataGrid DataView dv = ((DataRowView)cm.Current).DataView; //---число строк в неявном просмотре равно числу строк в DataGrid int nSkolkoStrok = dv.Count; //---перебираем все строки DataGrid for(int i = 0; i < nSkolkoStrok; i++){ //---если корпус – "Главный", выделить запись if (dataGrid1[i, 3].ToString() == "Главный") { dataGrid1.Select(i); } }
Результат выполнения кода показан на рис. 4.76.
рис. 4.76
Пример анализа того, какие строки в сетке данных вручную выделил пользователь. В отмеченных строках приведем к верхнему регистру значение в столбце Nazv. CurrencyManager cm = (CurrencyManager)this.BindingContext[dataGrid1.DataSource, dataGrid1.DataMember]; //---переходим к неявному просмотру, ассоциированному с DataGrid DataView dv = ((DataRowView)cm.Current).DataView; //---число строк в неявном просмотре равно числу строк в DataGrid int nSkolkoStrok = dv.Count; //---перебираем все строки DataGrid for(int i = 0; i < nSkolkoStrok; i++) { //---если строка выделена пользователем, привести //---значение столбца Nazv к верхнему регистру
286
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
}
if (dataGrid1.IsSelected(i)) { dataGrid1[i, 1] = dataGrid1[i, 1].ToString().ToUpper(); }
Результат выполнения кода показан на рис. 4.77.
рис. 4.77
4.2.13. Параметры визуального представления DataGrid В этом разделе рассматриваются параметры, определяющие, как наш объект DataGrid будет выглядеть на экране компьютера.
4.2.13.1. Заголовок DataGrid В приводимой ниже таблице рассматриваются свойства элемента управления DataGrid, влияющие на представление заголовка (рис. 4.78) этого элемента управления.
рис. 4.78 Свойство public bool CaptionVisible {get; set;} При смене значения свойства наступает событие public event EventHandler CaptionVisibleChanged; public string CaptionText {get; set;} public Font CaptionFont {get; set;} public Color CaptionForeColor {get; set;} public Color CaptionBackColor {get; set;}
Описание Видимость заголовка (значение true)
Текст заголовка Шрифт, которым выводится заголовок Цвет символов заголовка Цвет фона заголовка
4. Связывание в приложении. объектов-источников данных и элементов управления
287
4.2.13.2. Цвета составных частей DataGrid1 Ниже рассматриваются свойства, определяющие цвета символов и фона, а также стили в основных конструктивных частях элемента управления DataGrid. Их применение имеет следующую особенность: эти свойства • относятся в целом к элементу управления DataGrid, если таблица стилей DataGrid не задана, т. е. свойство TableStyles грида ссылается на пустую коллекцию GridTableStylesCollection; • устанавливаются для каждого стиля таблицы (объект DataGridTableStyle), если указать свойство TableStyles грида ссылается на коллекцию GridTable StylesCollection, содержащую хотя бы один элемент. Свойство public override Color BackColor {get; set;} public override Color ForeColor {get; set;} public Color BackgroundColor {get; set;} При смене значения свойства наступает событие public event EventHandler BackgroundColorChanged; public Color GridLineColor {get; set;} public DataGridLineStyle GridLineStyle {get; set;} Значения перечисления System.Windows. Forms. DataGridLineStyle: None – линии отсутствуют; Solid – линии присутствуют public Color LinkColor {get; set;} public Color AlternatingBackColor {get; set;}
Описание Цвет фона записей DataGrid в сетке данных Цвет символов записей DataGrid в сетке данных Цвет фона в области данных, не занятой записями
Цвет линий – разделитетей строк и столбцов в сетке грида Стиль линий – разделителей строк и столбцов
Цвет ссылок на имена таблиц (см., например, рис. 4.79) Если это свойство отлично от значения Window, то сетка данных выводится в стиле "зебры" – одна строка текущим цветом фона, вторая – фоном AlternatingBackColor и т. д. (рис. 4.80)
1
Для восстановления значений свойств цвета и фона, принятых по умолчанию, используется группа методов ResetXXXColor.
288
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
рис. 4.79
рис. 4.80
4.2.13.3. Параметры представления заголовков столбцов и строк Свойства и задают соответственно цвет фона, цвет символов и шрифт заголовков столбцов и строк в сетке данных (рис. 4.81): public Color HeaderBackColor {get; set;} public Color HeaderForeColor {get; set;} public Font HeaderFont {get; set;}
Видимость заголовков столбцов задается свойством public bool ColumnHeadersVisible {get; set;}
Видимость заголовков строк задается свойством public bool RowHeadersVisible {get; set;}
рис. 4.81
Свойства PreferredColumnWidth и PreferredRowHeight задают значения по умолчанию (в пикселах) соответственно ширины столбцов и высоты строк: public int PreferredColumnWidth {get; set;} public int PreferredRowHeight {get; set;}
4.2.13.4. Объемность изображения Свойство BorderStyle задает объемность обрамляющей линии (бордюра) элемента управления DataGrid: public BorderStyle BorderStyle {get; set;}
Перечисление System.Windows.Forms.BorderStyle содержит следующие значения: Значение Fixed3D FixedSingle None
Описание Трехмерный бордюр Одномерный бордюр Вообще бордюра никакого нет
4. Связывание в приложении. объектов-источников данных и элементов управления
289
Изменение значения свойства BorderStyle сопровождается наступлением события public event EventHandler BorderStyleChanged;
Если свойство FlatMode установлено в значение true, то DataGrid изображается полностью в плоском, двумерном виде. По умолчанию принято значение false (трехмерное изображение): public bool FlatMode {get; set;}
4.2.14. Текущие параметры сетки данных Если в сетке данных DataGrid много столбцов, все они могут не поместиться в рабочем пространстве сетки на экране. Переход к невидимым столбцам производится при помощи горизонтальной прокрутки в сетке данных. В связи с этим во время выполнения приложения неизвестно, какие столбцы в текущий момент видимы в сетке, а какие нет. Свойство FirstVisibleColumn возвращает индекс первого столбца, видимого в данный момент в сетке данных DataGrid: public int FirstVisibleColumn {get;}
Количество столбцов, видимых в данный момент в сетке данных DataGrid, возвращается свойством public int VisibleColumnCount {get;}
Большое число строк, что очевидно, не может уместиться в сетке данных DataGrid. Количество строк, видимых в данный момент в сетке данных DataGrid, возвращается свойством public int VisibleRowCount {get;}
Пример. Зададим в форме кнопку btn_Info, выводящую в элементы управления типа Label сведения об имени первого видимого в текущий момент столбца таблицы, числа видимых строк и столбцов. Пусть обработчик события нажатия этой кнопки содержит следующий код: private void btn_Info_Click(object sender, System.EventArgs e) { lbl_ImaPervogoStolbza.Text = ds1.Customers.Columns[dataGrid1.FirstVisibleColumn].ColumnName; lbl_VidonoStrok.Text = dataGrid1.VisibleRowCount.ToString(); lbl_VidnoStolbzov.Text = dataGrid1.VisibleColumnCount.ToString(); }
Запустим приложение и нажмем кнопку (рис. 4.82). Затем изменим размеры сетки грида, прокрутим столбцы и снова нажмем кнопку Info. Результат представлен на рис. 4.83.
290
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
рис. 4.82
рис. 4.83
4.2.15. Управление сортировкой строк DataGrid Данные в источнике, с которым связан элемент DataGrid, могут иметь предустановленный порядок сортировки. Если источником данных элемента управления DataGrid служит просмотр (объект DataView), то перечень столбцов, по которым должна производиться сортировка, задается в свойстве Sort этого просмотра. Если источником данных для DataGrid служит менеджер просмотров и в качестве раздела источника данных указывается имя таблицы данных, то порядок сортировки задается в свойстве Sort объекта DataViewSetting, хранящего установки этой таблицы. Если источником данных элемента управления DataGrid служит таблица данных (либо источник данных – набор и раздел источника – таблица), то порядок сортировки определяется в свойстве Sort неявного просмотра, ассоциированного с этим объектом DataGrid. Изначально сортировка записей таблицы данных не задается и записи показываются в DataGrid в том порядке, в котором они залиты адаптером в таблицу данных. Чтобы порядок сортировки записей в DataGrid можно было изменить во время выполнения приложения, нужно установить в true (принято по умолчанию) свойства AllowSorting этого элемента управления DataGrid: public bool AllowSorting {get; set;} • •
Порядок сортировки строк DataGrid можно изменять двумя способами: в сетке данных DataGrid щелкнуть мышью по заголовку того столбца, по значениям которого нужно отсортировать строки; программно изменить свойство Sort соответствующего объекта-источника данных (см. выше).
4. Связывание в приложении. объектов-источников данных и элементов управления
291
Пример. Пусть элемент управления dataGrid1 связан с таблицей Kaf набора данных nd1. Зададим в форме кнопку btnInfo, выводящую в textBox1 значение свойства Sort неявного просмотра, ассоциированного с dataGrid1: private void btnInfo_Click(object sender, System.EventArgs e) { CurrencyManager cm = (CurrencyManager)this.BindingContext[nd1, "Kaf"]; DataView dv = ((DataRowView)cm.Current).DataView; textBox1.Text = dv.Sort; }
Изначально, после заливки данных в таблицу, записи таблицы специальным образом не сортировались: просто так уж случилось, что данные в таблице базы данных создавались "по порядку", т. е. по возрастанию значений столбца KK (рис. 4.84).
рис. 4.84
Щелкнем мышью по левой части заголовка столбца Sostav. Увидим, что строки в DataGrid будут динамически пересортированы по убыванию значений этого столбца (рис. 4.85).
рис. 4.85
292
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
ПРИМЕЧАНИЕ. Такого же эффекта можно было бы добиться, выполнив код CurrencyManager cm = (CurrencyManager)this.BindingContext[nd1, "Kaf"]; DataView dv = ((DataRowView)cm.Current).DataView; dv.Sort = "[SOSTAV] DESC";
Щелкнем мышью по правой части заголовка столбца Sostav и увидим, что строки в DataGrid будут динамически пересортированы по возрастанию значений этого столбца (рис. 4.86).
рис. 4.86
ПРИМЕЧАНИЕ. Такого же эффекта можно было бы добиться, выполнив код CurrencyManager cm = (CurrencyManager)this.BindingContext[nd1, "Kaf"]; DataView dv = ((DataRowView)cm.Current).DataView; dv.Sort = "[SOSTAV]";
Пример. Пусть элемент управления dataGrid1 связан с умолчательным просмотром таблицы Kaf набора данных nd1, т. е. объектом nd1.Kaf.Default View. Зададим в форме кнопку btn_Info, выводящую в textBox1 значение свойства Sort этого просмотра: private void btnInfo_Click(object sender, System.EventArgs e) { textBox1.Text = nd1.Kaf.DefaultView.Sort; }
Пусть изначально просмотр отсортирован по значениям столбцов Fakultet и Nazv (рис. 4.87). nd1.Kaf.DefaultView.Sort = "FAKULTET, NAZV"; dataGrid1.DataSource = nd1.Kaf.DefaultView;
4. Связывание в приложении. объектов-источников данных и элементов управления
293
рис. 4.87
Щелкнем мышью по правой части заголовка столбца Sostav. Увидим, что строки DataGrid будут динамически пересортированы по возрастанию значений этого столбца (рис. 4.88).
рис. 4.88
ПРИМЕЧАНИЕ. Такого же эффекта можно было бы добиться, выполнив код nd1.Kaf.DefaultView.Sort = "[SOSTAV]";
294
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
5 ЧАСТЬ 5 КОМАНДЫ ДАННЫХ И РИДЕРЫ 5.1. Команды данных 5.1.1. Что такое команды данных и когда нужно их применять Команда данных выполняет какую-либо операцию непосредственно в базе данных. Когда пользователь выполняет команду данных, то он работает напрямую с провайдером данных (например, SQL Server), избегая при этом услуг посредников, какими в ADO.NET являются наборы данных и адаптеры данных. Адаптеры данных, собственно говоря, также пользуются услугами команд данных. В этом легко убедиться, обратившись к таким свойствам адаптеров, как SelectCommand, InsertCommand, UpdateCommand и DeleteCommand. Каждое из них содержит ссылку на команду данных, выполняющую соответственно выборку данных, вставку, модификацию и удаление данных для той таблицы в источнике данных, с которой связан этот адаптер. Итак, без команд данных невозможно обойтись при чтении данных из источника записи и занесении в него изменений, внесенных в данные при работе с ними в приложении. Второе, менее частое, но не менее важное применение команд – это управление структурой источника данных (например, выполнение оператора создания таблицы CREATE TABLE). Третье применение команд данных – это "экономная" выборка данных. Записи, считанные из источника данных таким образом, можно использовать только по принципу "forward-only", т. е. только на чтение и только в одном направлении – от первой считанной записи к последней. Такой режим выборки данных применяют, когда нужно выдать какой-либо отчет, для чего перебрать все записи, удовлетворяющие некоторому условию, и, возможно, вычислить по ним группу показателей. Режим forward-only часто применяют и когда нужно отыскать в таблице какую-либо запись по сложному условию. Как только таблица найдена, перебор записей прекращают. В общем случае команды данных используются для считывания данных весьма ограниченно. Для интерактивной обработки данных пользователем, например в элементе управления DataGrid в Windows Forms, такой способ не подходит совершенно и нужно прибегнуть к помощи наборов данных. В формах Web, наоборот, использование для чтения записей команд данных и связанных с ними ридеров данных (см. ниже) предпочтительнее, учитывая тот факт, что Web-формы пересоздаются по каждому ответу на запрос пользователя. Если мы применяем команды данных для чтения записей в режиме forwardonly, мы должны понимать физику процесса. Когда мы считываем (при помощи ридера данных) запись из провайдера, следующая запись не считывается до следующего обращения к методу Read ридера. Поэтому чем быстрее обрабатываются считанные 295
записи в приложении, тем меньшее время мы будем держать соединение открытым и недоступным другим процессам и пользователям. Если обработка каждой считанной записи занимает длительное время, от использования команд данных лучше отказаться и использовать наборы данных. В них "заливают" из источника данных все, что нужно, и сразу же отключают соединение. А затем сколько душе угодно обрабатывают данные в физически отсоединенном от источника данных наборе.
5.1.2. Объекты команд данных • •
Известно два класса команд данных: System.Data.SqlClient.SqlCommand – команда, "заточенная" для вазимодействия с SQL Server версии 7.0 и выше; System.Data.OleDb.OleDbCommand – команда данных для взаимодействия с произвольным OLD DB-источником данных.
Приводимые ниже примеры мы будем рассматривать для объектов класса SqlCommand.
5.1.3. Создание команды данных 5.1.3.1. Создание объектов команд данных на шаге разработки приложения •
•
Для создания команды данных во время разработки приложения нужно: На панели Toolbox выбрать закладку Data, отметить компонент SqlCommand или OleDBCommand и перетащить его в форму. В форме будет создан объект команды соответствующего типа. Отметить объект команды в форме и задать свойства Connection (ссылка на соединение), CommandType (тип команды), CommandText (текст запроса к источнику данных), Parameters (параметры – для параметрических запросов).
Текст запроса, выполняемый командой, можно задать в свойстве CommandText в окне свойств вручную. Можно также вызвать Query Bilder, который построит текст запроса команды данных за вас. Для вызова Query Bilder следует сначала установить значение свойства Connection, поместив в него ссылку на нужный объект соединения, а затем нажать кнопку эллипса (…) в поле свойства CommandText в окне свойств. Query Bilder считает имена всех таблиц, просмотров, функций и пр. из того источника данных, с которым установлено соединение (рис. 5.1). 296
рис. 5.1
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Выберем нужную таблицу (просмотр, функцию) и нажмем кнопку Add. Можно выбрать также две и более таблицы с тем, чтобы впоследствии выполнить их соединение. Это, впрочем, не совсем согласуется с идеологией наборов данных ADO.NET, согласно которой лучше заливать данные из таблиц источника данных как есть в отдельные таблицы набора данных и затем уже в приложении связывать данные при помощи отношений (объектов DataRelation). После выбора таблиц(ы) нажмем кнопку Close. В окне формирования запроса (рис. 5.2) отметим нужные столбцы и проверим на всякий случай текст запроса, автоматически сгенерированный Query Builder.
рис. 5.2
Когда текст нас не устраивает, нажмем кнопку OK. Сформированный запрос будет помещен в свойство CommandText команды данных. Отдельно стоит сказать о параметрических запросах. Если при формировании запроса вы поместите в его текст параметры (используя синтаксис соответствующего провайдера), то все они автоматически будут добавлены в коллекцию Parameters этой команды данных. При этом каждому параметру будут автоматически поставлены в соответствие характеристики того столбца, на который параметр ссылается в запросе. Например, если в окне формирования запроса (рис. 5.3) мы укажем параметрический запрос вида SELECT FROM WHERE
KK, KP, FIO, Rank Prep (KK = @KK) and (KP = @KP)
5. Команды данных и ридеры
297
рис. 5.3
то оба параметра автоматически добавятся в коллекцию параметров (рис. 5.4).
рис. 5.4
По умолчанию Query Builder формирует запрос на извлечение данных (SELECT), однако его можно вручную изменить и на любой другой, например UPDATE Prep
298
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
SET KK = @KK, KP = @KP WHERE (KK = @Original_KK) AND (KP = @Original_KP)
Выполнение параметрических запросов вообще и запросов изменения данных в частностив рассматривается ниже (см. 5.1.5). Здесь же остается с удовлетворением констатировать, что Query Builder умеет распознавать введенные пользователем параметры и весьма благосклонно к ним относиться. Когда команду данных планируется использовать для вызова хранимой процедуры SQL Server, необходимо: • установить свойство CommandType команды данных в значение CommandType. StoredProcedure; • указать имя хранимой процедуры (без значений параметров) в свойстве Command Text команды. Параметры будут автоматически считаны из процедуры и добавлены в коллекцию Parameters объекта команды данных. Если команда данных будет считывать данные не из таблицы, а из функции SQL Server, то в Query Builder следует выбрать закладку Functions, из списка – имя нужной функции и нажать кнопку Add (рис. 5.5).
рис. 5.5
В остальном все действия разработчика аналогичны описанным выше для случая чтения из таблицы. К сожалению, если у функции есть параметры, то автоматически в объект команды данных они не добавляются и это нужно делать вручную (см. 5.1.9) .
5.1.3.2. Программное создание объектов команд данных Для программного создания объекта SqlCommand или OleDbCommand команды данных нужно воспользоваться его конструктором. Если используется конструктор без параметров, то затем следует программно установить значения свойств Connection (ссылка на соединение), CommandType (тип команды), Command 5. Команды данных и ридеры
299
Text (текст запроса к источнику данных), Parameters (параметры – для параметрических запросов). Ниже рассматриваются конструкторы для обоих объектов команд данных. Если какие-либо параметры конструктора не указаны, то при создании команды соответствующим ее свойствам назначаются следующие умолчательные значения: Properties CommandText CommandTimeout CommandType Connection
Initial Value Пустая строка ("") 30 Text null
Для объекта SqlCommand доступны следующие варианты конструктора: public SqlCommand(); public SqlCommand( string CmdText ); public SqlCommand( string CmdText, SqlConnection connection ); public SqlCommand( string CmdText, SqlConnection connection, SqlTransaction transaction );
Для объекта OleDBCommand доступны следующие варианты конструктора: public OleDbCommand(); public OleDbCommand( string CmdText ); public OleDbCommand(string CmdText, OleDbConnection connection); public OleDbCommand( string CmdText, OleDbConnection connection, OleDbTransaction transaction );
Назначение параметров: CmdText – текст команды; connection – ссылка на объект соединения соответствующего типа; transaction – ссылка на объект транзакции соответствующего типа. Пример создания объекта команды данных. System.Data.SqlClient.SqlCommand cmdUpdate; cmdUpdate = new System.Data.SqlClient.SqlCommand( "UPDATE Prep SET KP = KP + 1", sqlConnection1);
5.1.4. Выполнение команд, возвращающих множество записей Для того чтобы при помощи команды данных считать данные из источника данных (например, выполнить напрямую команду SELECT для SQL Server), нужно: • создать команду данных; • в свойстве Connection команды задать ссылку на объект соединения; • в свойстве CommandType команды задать значение Text; • выполнить команду, вызвав ее метод ExecuteReader; • прочитать данные из ридера данных, возвращенного методом ExecuteReader. Для объекта SqlCommand метод ExecuteReader имеет следующие разновидности: 300
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
public SqlDataReader ExecuteReader(); public SqlDataReader ExecuteReader( CommandBehavior behavior );
Для объекта OleDBCommand метод ExecuteReader имеет следующие разновидности: public OleDbDataReader ExecuteReader(); public OleDbDataReader ExecuteReader( CommandBehavior behavior );
Параметр behavior задает поведения объекта соединения после выполнения команды данных и может задавать одно из значений перечисления System.Data. CommandBehavior: Значение
Описание После выполнения команды то соединение, ссылку на которое содержит Close команда, закрывается. Закрывается и ассоциированный с командой объект Connection ридера Запрос может возвращать множественный результирующий набор. ВыполDefault нение ExecuteReader(CommandBehavior.Default) эквивалентно выполнению ExecuteReader() Запрос возвращает информауцию о столбцах первичного ключа. Запрос выполняется без какой-либо блокировки выбранных записей. Замечание При использовании KeyInfo[cref, System.Data.Command KeyInfo Behavior], .NET-провайдер SQL Server добавляет к тексту исполняемого оператора предложение FOR BROWSE. (См. также оператор SET FMTONLY ON в руководстве по SQL Server) SchemaOnly Запрос возвращает только информацию о столбцах записи Задает способ доступа ридера к строкам, содержащим столбцы с двоичными данными большого размера. Вместо загрузки целой строки Sequential Sequential Access Access позволяет ридеру трактовать данные как поток, что позволяет использовать методы GetBytes или GetChars SingleResult Запрос возвращает единичный результирующий набор SingleRow Ожидается, что запрос вернет единичную запись
Объект ридера 1 – это объект типа System.Data.SqlClient.SqlDataReader или System.Data.OleDb.OleDbDataReader. Метод ридера public bool Read();
считывает следующую запись результата и в случае успеха возвращает true. Если записей больше нет, возвращается false. Имена или индексы столбцов записи служат для ридера индексатором: public object this[string] {get;} public object this[int] {get;}
Таким образом, правомерно обращение к столбцу вида dr["ColName"] и dr[5]. 1
Подробно ридеры рассматриваются в следующей главе книги.
5. Команды данных и ридеры
301
Пример. Пусть в приложении на шаге выполнения создан объект типа sqlCommand с именем sqlMulti. Пусть он ссылается на соединение sqlConnection1 (элемент управления типа SqlConnection) и значение свойства sqlMulti.CommandText = "[vz_SelPrep]". Иными словами, команда вызывает на выполнение процедуру vz_SelPrep, полный текст которой приводится ниже: CREATE PROCEDURE vz_SelPrep AS select * from prep
Как видно из текста процедуры, в качестве результата без особых мудрствований возвращаются все записи таблицы prep. Выполним данную процедуру с использованием команд данных. Для каждой полученной в результате выполнения запроса записи поместим в listBox1 значения полей FIO и Rank. … //---если нужно, откроем соединение if (sqlConnection1.State == ConnectionState.Closed) { sqlConnection1.Open(); } //---опишем объект ридера System.Data.SqlClient.SqlDataReader dr; //---создадим объект ридера, выполнив запрос dr = sqlMulti.ExecuteReader(); //---поместим в listBox значения столбцов записей, которые возвращены //---в результате выполнения запроса listBox1.Items.Clear(); while (dr.Read()) { listBox1.Items.Add(dr["FIO"].ToString() + (char)9 + dr["Rank"].ToString()); } //---закроем соединение sqlConnection1.Close();
5.1.5. Выполнение параметрических запросов В тексте параметрических запросов присутствуют параметры, замещающие заранее неизвестные либо многократно изменяющиеся значения. Для OLE DB-источников параметр помечается знаком вопроса, например: SELECT FROM WHERE
Prep.* Prep KK = ?
Для запросов, направляемых на выполнение SQL Server, применяются именованные параметры; они состоят из символа @ и следующего за ним имени параметра, например: SELECT Prep.* FROM Prep
WHERE 302
KK = @KK ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Параметры команды содержатся в коллекции ParameterCollection соответствующего типа: public SqlParameterCollection Parameters {get;} public OleDbParameterCollection Parameters {get;}
ЗАМЕЧАНИЕ. Когда мы включаем в текст запроса параметр вида @Имя или ? на этапе создания приложения, то объект параметра автоматически помещается в коллекцию Parameters этой команды. Если же мы формируем текст запроса для команды динамически (программно, во время выполнения приложения), то параметры в названную коллекцию придется добавлять вручную. Пример. Пусть в приложении на этапе проектирования создан объект sqlCommand с именем sqlMultiPar. Пусть он ссылается на соединение sqlConnection1 (элемент управления типа SqlConnection) и значение свойства sqlMultiPar.CommandText = "SELECT Prep.* FROM Prep WHERE (KK = @KK)". Параметр @KK автоматически добавлен в коллекцию Parameters команды данных, так что нам самим нужно заботиться лишь о том, чтобы перед выполнением команды данных задать значение параметра. //---открытие соединения if (sqlConnection1.State == ConnectionState.Closed) { sqlConnection1.Open(); } //---ридер данных – описание System.Data.SqlClient.SqlDataReader dr; //---установка значения параметра sqlMultiPar.Parameters["@KK"].Value = 3; //---выполнение запроса dr = sqlMultiPar.ExecuteReader(); //---поместим в listBox значения полей //---полученных в результате выполнения запроса записей listBox1.Items.Clear(); while (dr.Read()) { listBox1.Items.Add(dr["FIO"].ToString() + (char)9 + dr["Rank"].ToString()); } //---закрытие соединения sqlConnection1.Close();
Пример. Программно создадим команду данных sqlCmd и назначим значения ее свойств. Пусть имеем дело с параметрическим запросом sqlCmd.CommandText = "SELECT Prep.* FROM Prep WHERE (KK = @parKK)". Поскольку команда данных создается на этапе выполнения программно, необходимо программно же создать объект параметра, добавить его в коллекцию параметров команды, затем присвоить ему значение и лишь затем выполнить команду данных. //---ридер данных – описание 5. Команды данных и ридеры
303
System.Data.SqlClient.SqlDataReader dr; //---параметр – описание System.Data.SqlClient.SqlParameter ppp; //---команда данных – создание и установка значений свойств System.Data.SqlClient.SqlCommand sqlCmd = new System.Data.SqlClient.SqlCommand(); sqlCmd.Connection = sqlConnection1; sqlCmd.CommandType = CommandType.Text; sqlCmd.CommandText = "SELECT Prep.* FROM Prep WHERE (KK = @parKK)"; //---открытие соединения if (sqlConnection1.State == ConnectionState.Closed) { sqlConnection1.Open(); } //---создание параметра @parK и установка его свойств ppp = new System.Data.SqlClient.SqlParameter(); ppp.ParameterName = "@parKK"; //имя ppp.DbType = DbType.Int32; //тип данных ppp.Direction = ParameterDirection.Input; //тип входной //---установка значения параметра ppp.Value = 1; //---добавление параметра в коллекцию параметров команды данных sqlCmd.Parameters.Add(ppp); //---выполнение запроса dr = sqlCmd.ExecuteReader(); //---поместим в listBox значения полей //---полученных в результате выполнения запроса записей listBox1.Items.Clear(); while (dr.Read()) { listBox1.Items.Add(dr["FIO"].ToString() + (char)9 + dr["Rank"].ToString()); } //---закрытие соединения sqlConnection1.Close();
5.1.6. Выполнение команд, возвращающих скалярное значение Команда данных может возвращать скалярное (т. е. единичное значение) как результат выполнения запроса, вычисляющего агрегат: SELECT count(*) FROM SomeTable
или SELECT SUM(RUROST) FROM SomeTable
Для получения скаляра команду выполняют методом public object ExecuteScalar();
Возвращаемый методом результат приводят к нужному типу и в дальнейшем используют по назначению. 304
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Пример. Пусть имеется процедура, возвращающая скалярное значение – число записей: CREATE PROCEDURE vz_CntPrep AS select count(*) from Prep
Для доступа к названной процедуре создадим в приложении объект – команду данных типа sqlCommand с именем sqlСntPrep. Пусть эта команда ссылается на соединение sqlConnection1 (объект типа SqlConnection) и значение свойства sqlСntPrep.CommandText = "[vz_CntPrep]". Выполним команду и получим результат – число записей в таблице Prep. Выведем его в информационном окне. int nRes; //сюда поместим результат выполнения команды //---открытие соединения if (sqlConnection1.State == ConnectionState.Closed) { sqlConnection1.Open(); } //---выполним команду и получим скаляр nRes = (int)sqlСntPrep.ExecuteScalar(); //---покажем значение скаляра широкой публике MessageBox.Show("Результат = " + nRes.ToString()); //---закроем соединение
sqlConnection1.Close();
5.1.7. Выполнение команд, возвращающих результаты в выходных параметрах Ряд процедур SQL Server может возвращать результат выполнения в виде выходных параметров. Для выполнения команд, вызывающих такие процедуры, используют метод public int ExecuteNonQuery();
Коллекция параметров команды данных должна содержать столько же выходных параметров, сколько имеется в процедуре. Свойство Direction каждого выходного параметра public ParameterDirection Direction {get; set;}
должно содержать значение System.Data.ParameterDirection.Output. Если команда вызывает на выполнение процедуру SQL Server, то в свойстве CommandType этой команды нужно поместить значение System.Data.CommandType.StoredProcedure, а в свойстве CommandText задать имя процедуры. Если объект команды данных создается в форме во время разработки приложения, то коллекция Parameters такой команды формируется автоматически. Если команда создается программно, то коллекцию Parameters придется пополнять объектами также программно. 5. Команды данных и ридеры
305
Команду выполняют методом ExecuteNonQuery, извлекают из коллекции Parameters выходные параметры, приводят каждый к нужному типу и далее используют полученные значения в приложении. Пример. Пусть процедура vz_NullCounters возвращает результат выполнения в двух парарметрах. Это – число записей таблицы Prep, у которых столбец rank содержит соответственно значения null и значения, отличные от null: CREATE PROCEDURE vz_NullCounters @nNotNull int OUTPUT, @nNull int OUTPUT AS select @nNotNull =count(*) from Prep where rank is not null select @nNull =count(*) from Prep where rank is null
Пусть в приложении создан объект команды данных типа sqlCommand с именем sqlTwoParams. Пусть эта команда ссылается на соединение sqlConnection1 (элемент управления типа SqlConnection) и ее свойство sqlTwoParams. CommandText содержит запрос "[vz_NullCounters]". Автоматически сформированная коллекция параметров команды включает оба параметра процедуры (рис. 5.6).
рис. 5.6
Выполним команду и выведем в информационном окне значения выходных параметров процедуры NullCounters: int nNotNull; //значение первого параметра
306
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
int nNull; //значение второго параметра //---открываем соединение if (sqlConnection1.State == ConnectionState.Closed) { sqlConnection1.Open(); } //---выполняем команду sqlTwoParams.ExecuteNonQuery(); //---извлекаем значения выходных параметров процедуры nNotNull = (int)sqlTwoParams.Parameters["@nNotNull"].Value; nNull = (int)sqlTwoParams.Parameters["@nNull"].Value; //---выводим значения параметров MessageBox.Show(nNotNull.ToString() + ", " + nNull.ToString()); //---закрываем соединение sqlConnection1.Close();
5.1.8. Выполнение команд, не возвращающих результата Ряд запросов выполняют какие-либо действия над записями источника данных и не возвращают никакого результата. Таковы, например, запросы, использующие операторы INSERT, UPDATE, DELETE, CREATE TABLE и пр. Команды, содержащие такие запросы, выполняются методом public int ExecuteNonQuery();
который возвращает число записей, затронутых запросом. Пример. Пусть процедура vz_IncKode изменяет значение столбца KP таблицы Prep: CREATE PROCEDURE vz_IncKode AS UPDATE Prep SET KP = KP + 1
Пусть в приложении создан объект типа SqlCommand с именем sqlIncKode. Пусть эта команда ссылается на соединение sqlConnection1 (объект типа SqlConnection) и свойство sqlIncKode.CommandText содержит вызов процедуры: "[vz_IncKode]". Выполним команду и выведем в информационное окно записей, измененных в результате ее выполнения: int nAffectedRows; //число затронутых записей //---откроем соединение if (sqlConnection1.State == ConnectionState.Closed) { sqlConnection1.Open(); } //---выполним команду и получим число затр. записей nAffectedRows = (int)sqlIncKode.ExecuteNonQuery(); //---выведем число затр. записей MessageBox.Show("Изменено " + nAffectedRows.ToString() + " записей"); //---закроем соединение sqlConnection1.Close(); 5. Команды данных и ридеры
307
В ряде случаев, например при изменении структуры таблиц базы данных, получение числа затронутых записей бессмысленно. Тогда результат, возвращаемый методом ExecuteNonQuery, попросту не используют. Пример. Пусть команда sqlCrTable ссылается на соединение sqlConnection1 (объект типа SqlConnection) и содержит в свойстве CommandText запрос на создание таблицы вида CREATE TABLE XXX (A int, B nvarchar(10),PRIMARY KEY(A)).
Выполнение команды происходит следующим образом: //---откроем соединение if (sqlConnection1.State == ConnectionState.Closed) { sqlConnection1.Open(); } //---выполним команду sqlCrTable.ExecuteNonQuery(); //---сообщим о выполнении MessageBox.Show("Готово "); //---закроем соединение sqlConnection1.Close();
5.1.9. Вызов функций Команды данных могут использоваться для вызова функций SQL Server. Вызов функций ничем особым не отличается от вызова процедур, за исключением того, что параметры функции приходится задавать вручную. Впрочем, этого можно избежать, если при задании функции в диалоге (он появляется при нажатии кнопки эллипса в тексте свойства CommandText команды) явно задать имена параметров. В этом случае будет сгенерирована коллекция Params, содержащая нужное число параметров. Однако их типы входящих придется уточнить. Пример. Пусть в базе данных задана функция CREATE FUNCTION vz_FPrepPar (@nKK int) RETURNS TABLE AS RETURN SELECT * FROM PREP WHERE KK = @nKK
Создадим в приложении команду данных – объект типа SqlCommand, перейдем в окно свойств и в поле значения свойства CommandText нажмем кнопку эллипса. В появившемся окне Add Table перейдем на закладку Functions, где из списка выберем имя функции – FPrepPar, после чего нажмем кнопки Add и Close. В окне диалога Query Builder появится выбранная функция (рис. 5.7). 308
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
рис. 5.7
Изменим сгенерированный текст запроса SELECT FROM
vz_FPrepPar.* vz_FPrepPar() vz_FprepPar
на SELECT FROM
vz_FPrepPar.* vz_FPrepPar(@KK)
и нажмем кнопку OK. После этого автоматически будет сгенерирована коллекция Params команды данных. В данном случае коллекция будет состоять из одного элемента. Желательно уточнить тип параметра в свойстве SqlDbType(рис. 5.8).
5. Команды данных и ридеры
309
рис. 5.8
Если же функция не имеет параметров, то в свойстве CommandText команды должно указываться только имя команды. Пример. Пусть в базе данных задана функция vz_FPrep. CREATE FUNCTION vz_FPrep () RETURNS TABLE AS RETURN SELECT * FROM PREP
В свойстве CommandText команды, вызывающей эту функцию на выполнение, должно содержаться следующее значение: SELECT vz_FPrep.* FROM vz_FPrep()
Выполнение команды данных, вызывающей функцию, ничем не отличается от вызова процедур или непосредственно заданных запросов. Вид метода, вызывающего команду данных, зависит от типа возвращаемого результата. Пример. Выполним функцию CREATE FUNCTION vz_FPrepPar (@nKK int) RETURNS TABLE
310
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
AS RETURN SELECT * FROM PREP WHERE KK = @nKK
Для выполнения функции в приложении создадим объект типа SqlCommand с именем sqlFPar. Установим значение свойства CommandText этого объекта равным "SELECT vz_FPrepPar_1.* FROM vz_FPrepPar(@KK)". Коллекция Params команды будет содержать один элемент с именем @nKK. Ниже приводится код для вызова функции и помещения значений столбцов результирующих записей в элемент управления listBox1. if (sqlConnection1.State == ConnectionState.Closed) { sqlConnection1.Open(); } System.Data.SqlClient.SqlDataReader dr; sqlFPar.Parameters["@nKK"].Value = 3; dr = sqlFPar.ExecuteReader(); listBox1.Items.Clear(); while (dr.Read()) { listBox1.Items.Add(dr["FIO"].ToString() + (char)9 + dr["Rank"].ToString()); } sqlConnection1.Close();
5.1.10. Команды данных и транзакции 1
• •
Транзакции реализуются классами: System.Data.SqlClient.SqlTransaction – при работе с SQL Server; System.Data.OleDb.OleDbTransaction – при работе с источниками данных OLE DB.
Объект транзакции возвращается методом BeginTransaction объекта соединения. Ссылку на полученный таким образом объект транзакции нужно занести в свойство Transaction команды данных: public SqlTransaction Transaction {get; set;}
Теперь команда данных будет выполняться в контексте этой транзакции. Заметим, что в контекст транзакции может входит и одна команда, но, как правило, их несколько. После успешного выполнения всех команд, заданных в ее контексте, транзакция подтверждается методом Commit; если при выполнении хотя бы одной из команд произошел сбой, транзакция откатывается методом Rollback.
1
Более подробно о транзакциях в частности и конкурентном изменении данных вообще см. 6.2. 5. Команды данных и ридеры
311
Пример. Пусть команда данных (объект типа SqlCommand) с именем cmdIns) содержит запрос вида INSERT INTO lsh (nbs1, nbs2, nls, rdat, ccy, ccyost, rurost, client) VALUES (@nbs1, @nbs2, @nls, @rdat, @ccy, @ccyost, @rurost, @client)
и выполняет добавление записи в таблицу lsh. Эта таблица содержит сведения об остатках на лицевых счетах баланса банка. Ниже показывается выполнение команды в контексте транзакции. //---объект транзакции System.Data.SqlClient.SqlTransaction t; //---открытие соединения if (sqlConnection1.State == System.Data.ConnectionState.Closed) { sqlConnection1.Open(); } //---получаем объект транзакции t = sqlConnection1.BeginTransaction(); //---включаем команду данных в контекст транзакции cmdIns.Transaction = t; //---присваиваем значения параметрам команды cmdIns.Parameters["@nbs1"].Value = 407; cmdIns.Parameters["@nbs2"].Value = 40702; cmdIns.Parameters["@nls"].Value = "40702810300000000019"; cmdIns.Parameters["@rdat"].Value = "12/05/2002"; cmdIns.Parameters["@ccy"].Value = "RUR"; cmdIns.Parameters["@ccyost"].Value = 200; cmdIns.Parameters["@rurost"].Value = 200; cmdIns.Parameters["@client"].Value = 19; try { //---выполняем команду cmdIns.ExecuteNonQuery(); //---если попали сюда, выполнение было успешным; //---подтверждаем транзакцию t.Commit(); } catch (Exception xins) { //---сбой при выполнении команды MessageBox.Show("Ошибка при добавлении: " + xins.Message); //---откатываем транзакцию t.Rollback(); } finally { //---сюда попадаем всегда; закрываем соединение if (sqlConnection1.State == System.Data.ConnectionState.Open) { sqlConnection1.Close(); } }
312
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
5.2. Ридеры 5.2.1. Зачем нужны ридеры Ряд команд данных возвращает поток записей в результате выполнения оператора SELECT, процедур или функций, выполняющих выборку данных. Сами команды данных такой поток читать не умеют. Для этого применяются объекты ридеров. Объекты ридеров (датаридеров, от data reader) используются для чтения данных только в одном направлении (от первой записи в потоке к последней) и только в режиме read-only. Режим read-only (только-на-чтение) следует трактовать так, что изменения в полученные данные нельзя внести непосредственно в объекте ридера. Однако никто не мешает нам: • считать запись с использованием ридера; • переписать значения столбцов записи куда-либо (например, в элементы управления TextBox, поля формы); • изменить эти значения там, как нужно; • записать в базу данных измененные записи, используя для этого объект команды данных. Иными словами, данные, полученные с использованием ридера, изменить можно, однако объект ридера в этом деле нам не помощник. В ADO.NET три основных ридера представлены объектами классов System.Data.SqlClient.SqlDataReader, System.Data.OleDb.OleDbDataReader и System.Xml.XmlTextReader. Они предназначены для чтения потоков данных: • из баз данных SQL Server (SqlDataReader); • баз данных SQL Server в XML- виде (XmlTextReader); • источников OLE-DB (OleDbDataReader).
5.2.2. Общий алгоритм использования ридера 1. Задается команда данных (объекты SqlCommand или OleDbCommand), выполнение которой должно возвратить поток записей. 2. Открывается соединение, с которым связана команда. 3. Команда данных выполняется методом ExecuteReader. Этот метод возвращает ссылку на поток записей. 4. Чтение записи из потока производится методом Read ридера. Каждое выполнение этого метода забирает запись из потока. Как только метод Read возвращает false, данные можно далее не считывать – их в потоке больше нет. 5. После считывания из потока нужного количества записей ридер больше не нужен и его нужно закрыть. Сделать это нужно обязательно: ридер использует объект соединения в монопольном режиме. Пока ридер не закрыт, другой ридер соединение использовать не сможет и ему придется открывать новое соединение. 6. Закрывется соединение, связанное с командой данных. 5. Команды данных и ридеры
313
Пример. Считаем записи, сколько есть, из таблицы Kaf базы данных VUZ (SQL Server) и поместим значения столбцов каждой записи в элемент управления ListBox. //---описываем ридер SqlDataReader r; //---новое соединение с базой VUZ SqlConnection cn = new SqlConnection( "data source=PAVLIK-50R6ZD;initial catalog=VUZ;" + Эintegrated security=SSPI;persist security info=False;" + "workstation id=PAVLIK-50R6ZD;packet size=4096"); //---команда для чтения из таблицы Kaf базы данных VUZ SqlCommand cmd = new SqlCommand("SELECT KK, Nazv, Fakultet, Sostav FROM Kaf", cn); try { //---открытие соединения cn.Open(); //---чтение данных из базы в ридер r = cmd.ExecuteReader(); //---будем помещать данные из ридера в listBox1 listBox1.Items.Clear(); //---читаем запись из ридера while (r.Read()) { //---и помещаем значения столбцов в listBox1 listBox1.Items.Add(r["KK"].ToString() + (char)9 + r["Nazv"].ToString() + (char)9 + r["Fakultet"].ToString() + (char)9 + r["Sostav"].ToString()); } //---закрываем ридер r.Close(); } catch (Exception exp) { MessageBox.Show("Ошибка : " + exp.Message); } finally { //---закрываем соединение cn.Close(); }
Результат работы приложения показан на рис. 5.9.
рис. 5.9
314
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
5.2.3. Ридеры SqlDataReader, OleDbDataReader: основные свойства и методы Рассмотрим основные свойства и методы объектов SqlDataReader и OleDbDataReader. Свойство RecordsAffected возвращает число записей, затронутых командой данных: public int RecordsAffected {get;}
Для оператора SELECT, когда все записи считаны в ридер, значение равно –1; для операций добавления, изменения, удаления записей свойство содержит число фактически затронутых записей. Свойство IsClosed возвращает true, когда ридер открыт (доступен для чтения) и false в противном случае: public bool IsClosed {get;}
Метод Close закрывает ридер: public void Close();
Метод NextResult возвращает true, если при чтении результата пакета операторов Transact-SQL имеет место еще хотя бы один результирующий набор. В противном случае возвращается false. public bool NextResult();
Чтение записи данных и одновременно удаление ее из потока производится методом Read объекта ридера. В случае успешного чтения строки метод возвращает true. Если записей в потоке данных более нет (либо нет вообще), возвращается false. public bool Read();
Считанная запись трактуется как коллекция значений типа object, причем каждый элемент коллекции соответствует значению столбца. Число и последовательность значений столбцов в записи определяются запросом команды данных, результат выполнения которой и считывается ридером. Свойство FieldCount возвращает число столбцов в текущей записи ридера: public int FieldCount {get;}
Доступ к отдельному столбцу возможен при помощи индексатора public object this[ string ColumnName ] {get;} public object this[ int ColumnIndex ] {get;}
Для идентификации столбца при этом используется его строковое имя: SqlDataReader r; … listBox1.Items.Add(r["KK"].ToString());
либо индекс столбца в строке: listBox1.Items.Add(r[0].ToString()); 5. Команды данных и ридеры
315
Лучше, конечно, использовать строковое имя столбца, поскольку в этом случае мы не привязаны к физическому порядку следования столбцов в строке. Это порядок запросто может поменяться, стоит нам внести коррективы в текст оператора SELECT, процедуры или функции, т. е. в тот запрос, результат выполнения которого и считывает ридер. Обращение к индексатору ридера возвращает объект типа object, который еще нужно приводить к типу соответствующего столбца. Это не всегда удобно. Поэтому ридер снабдили группой методов вида public XXX GetXXX(int ColumnIndex);
Каждый такой метод читает в ридере значение столбца, указанного по его индексу, приводя это значение к типу XXX: public public public public public public public public public public public public
bool GetBoolean( int ColumnIndex ); byte GetByte( int ColumnIndex ); char GetChar( int ColumnIndex ); DateTime GetDateTime( int ColumnIndex ); decimal GetDecimal( int ColumnIndex ); double GetDouble( int ColumnIndex ); float GetFloat( int ColumnIndex ); Guid GetGuid( int ColumnIndex ); short GetInt16( int ColumnIndex ); int GetInt32( int ColumnIndex ); long GetInt64( int ColumnIndex ); string GetString( int ColumnIndex );
public SqlBinary GetSqlBinary( int ColumnIndex ); public SqlBoolean GetSqlBoolean( int ColumnIndex ); public: SqlByte GetSqlByte( int ColumnIndex ); public: SqlDateTime GetSqlDateTime( int ColumnIndex ); public SqlDecimal GetSqlDecimal( int ColumnIndex ); public: SqlDouble GetSqlDouble( int ColumnIndex ); public SqlGuid GetSqlGuid( int ColumnIndex ); public SqlInt16 GetSqlInt16( int ColumnIndex ); public SqlInt32 GetSqlInt32( int ColumnIndex ); public SqlInt64 GetSqlInt64( int ColumnIndex ); public SqlMoney GetSqlMoney( int ColumnIndex ); public SqlSingle GetSqlSingle( int ColumnIndex ); public SqlString GetSqlString( int ColumnIndex );
Пример. При добавлении в список элемента управления ListBox значений столбцов из ридера первое значение берется как object, второе как string, третье также как string, четвертое как целочисленное. SqlDataReader r; … listBox1.Items.Add(r["KK"].ToString() + (char)9 + r.GetString(1) + (char)9 + r.GetString(2) + (char)9 + r.GetInt32(3));
316
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Есть также два метода, берущие значение столбца как есть, в том формате, в котором они присутствуют в строке. В этом случае пользователь несет ответственность за то, чтобы применить это значение в приложении соответственно их типу. public object GetValue( int ColumnIndex ); public object GetSqlValue( int ColumnIndex );
Например, пусть первый столбец ридера числового типа, поэтому при помещении в список элемента управления ListBox его нужно приводить к строковому виду. Пусть второй столбец ридера строкового типа, поэтому в приведении нет нужды, так как элемент коллекции Items объекта ListBox также строкового типа: SqlDataReader r; … listBox1.Items.Add(r.GetValue(0).ToString() + (char)9 + r.GetValue(1));
В том случае, когда тип столбца нам неизвестен, определить его помогут методы ридера GetDataTypeName и GetFieldType: public string GetDataTypeName( int ColumnIndex ); public Type GetFieldType( int ColumnIndex );
Первый возвращает строковое название типа столбца, второй возвращает соответствующий объект Type. Например, поместить в элемент управления ListBox наименование типа первого столбца ридера можно двумя способами: SqlDataReader r; … listBox1.Items.Add(r.GetDataTypeName(0));
или System.Type t = r.GetFieldType(0); listBox1.Items.Add(t.FullName);
Существует также группа методов, помещающая значения столбцов строки ридера в разного рода массивы. Метод GetBytes считывает строку ридера как поток байтов в байтовый массив, GetChars – как поток символов в символьный массив. Наконец, два метода считывают значения столбцов строки ридера в массив объектов: public int GetSqlValues( object[] values ); public int GetValues( object[] values );
Например, прочитаем строку ридера в массив объектов, после чего поместим значения столбцов в строку списка элемента управления ListBox: SqlDataReader r; … object[] massiv = new object[r.FieldCount]; r.GetValues(massiv); 5. Команды данных и ридеры
317
string stroka = ""; for (int i = 0; i < massiv.Length; i++) { stroka = stroka + massiv[i].ToString() + (char)9; } listBox1.Items.Add(stroka);
Протестировать столбец ридера на наличие в нем пустого или пропущенного значения (эквивалент DBNull) можно методом IsDBNull: public bool IsDBNull( int ColumnIndex );
Если в столбце DBNull, метод возвратит true. В противном случае возвращается false.
5.2.4. Метод ExecuteReader команды данных Чтобы использовать ридер для чтения потока данных, порождающая этот поток команда данных должны выполняться методом public SqlDataReader ExecuteReader(); public SqlDataReader ExecuteReader( CommandBehavior behavior );
где параметр задает одно из следующих значений перечисления System.Data.CommandBehavior: Значение
Описание После выполнения команды ассоциированное с ним соединение автоCloseConnection матически будет закрыто, но только после закрытия объекта ридера, считывающего результат выполнения команды По умолчанию то же, что вызов ExecuteReader(). Запрос может Default возвращать множественный результирующий набор Возвращает данные и схему данных (которая отдельно может считыKeyInfo ваться методом GetSchemaTable ридера) SchemaOnly Возвращает только схему данных Используется для чтения двоичных данных больших размеров. Ридер загружает данные как поток байтов или символов. Для интерпретации SequentialAccess потока затем используются специфические методы ридера GetBytes и(или) GetChars Если команда выполняет пакет операторов Transact-SQL, которые могут возвратить множественный запрос, возвращается только результат SingleResult первого такого оператора. Результаты выполнения остальных операторов в составе пакета игнорируются SingleRow Возвращается только первая строка результирующего набора данных
5.2.5. Особенности чтения потока записей Чтобы команда данных в результате выполнения возвратила поток записей, она должна выполняться методом ExecuteReader без параметра или с параметрами CloseConnection, KeyInfo или SingleResult. 318
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Алгоритм вызова команды данных на выполнение и ассоциирование ридера с результатом выполнения команды приводится в целом по алгоритму, рассмотренному в 5.2.2. Однако интерпретация результата (в частности, представление его пользователю) может иметь характерные для конкретного случая особенности. Различие в интерпретации записей из потока можно проследить, сопоставив два приводимых ниже примера. В первом результат выполнения команды – поток записей – помещается в элемент управления ListBox, во втором – в элемент управления ListView. Пример. Поместим поток записей в список Items элемента управления ListBox. Команда данных читает записи из таблицы Kaf базы данных VUZ.mdb (Access). //---соединение OleDbConnection cn = new OleDbConnection( "Provider=Microsoft.Jet.OLEDB.4.0;Password=\"\";" + "User ID=Admin;Data Source=D:\\VUZ\\VUZ.mdb;" + "Mode=Share Deny None;Extended Properties=\"\";" + "Jet OLEDB:System database=\"\";" + "Jet OLEDB:Registry Path=\"\";Jet OLEDB:Database Password=\"\";" + "Jet OLEDB:Engine Type=4;Jet OLEDB:Database Locking Mode=0;" + "Jet OLEDB:Global Partial Bulk Ops=2;" + "Jet OLEDB:Global Bulk Transactions=1;" + "Jet OLEDB:New Database Password=\"\";" + " "Jet OLEDB:Create System Database=False;"+ "Jet OLEDB:Encrypt Database=False;" + "Jet OLEDB:Don't Copy Locale on Compact=False;" + "Jet OLEDB:Compact Without Replica Repair=False;Jet OLEDB:SFP=False" ); //---команда OleDbCommand cmd = new OleDbCommand( "SELECT KK, Nazv, Fakultet, Sostav FROM Kaf", cn); //---ридер OleDbDataReader r; try { //---открываем соединение cn.Open(); //---выполняем команду r = cmd.ExecuteReader(); listBox1.Items.Clear(); //---строки из ридера пишем в listBox while (r.Read()) { listBox1.Items.Add(r["KK"].ToString() + (char)9 + r.GetString(1) + (char)9 + r.GetString(2) + (char)9 + r.GetInt32(3)); } //---закрываем ридер r.Close(); 5. Команды данных и ридеры
319
} catch (Exception xpt) { MessageBox.Show("Ошибка : " + xpt.Message); } finally { //---закрываем соединение cn.Close(); }
Результат выполнения приведенного выше кода показан на рис. 5.10
рис. 5.10
Пример. Поместим поток записей в список элемента управления ListView. Для этого поместим в форму пустой объект этого типа с именем listView1, не содержащий ни столбцов, ни строк. Команда данных читает записи из таблицы Kaf базы данных VUZ (SQL Server). //---создадим в listView1 4 столбца listView1.Columns.Add("KK", 40, HorizontalAlignment.Left); listView1.Columns.Add("Nazv", 150, HorizontalAlignment.Left); listView1.Columns.Add("Fakultet", 150, HorizontalAlignment.Left); listView1.Columns.Add("Sostav", 75, HorizontalAlignment.Left); //---ридер SqlDataReader r; //---соединение SqlConnection cn = new SqlConnection( "data source=PAVLIK-50R6ZD;initial catalog=VUZ;" + "integrated security=SSPI;" + "persist security info=False;" + "workstation id=PAVLIK-50R6ZD;packet size=4096"); //---команда SqlCommand cmd = new SqlCommand( "SELECT KK, Nazv, Fakultet, Sostav FROM Kaf", cn); try { //---открываем соединение и выполняем команду cn.Open(); r = cmd.ExecuteReader(); //---чтение из ридера while (r.Read()) { //---столбцы строки ридера считываются в массив объектов object[] mas = new object[r.FieldCount];
320
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
r.GetValues(mas); //---создание нового элемента списка ListView //---со значением 1-го столбца строки ридера ListViewItem element = new ListViewItem(((object)mas[0]).ToString()); //---заполнение оставшихся столбцов //---элемента списка ListView //---значениями прочих столбцов строки ридера for(int i = 1; i < r.FieldCount; i++) { element.SubItems.Add(((object)mas[i]).ToString()); } //---добавление элемента в список ListView listView1.Items.Add(element);
} //---закрываем ридер r.Close();
} catch (Exception expt) { MessageBox.Show("Ошибка : " + expt.Message); } finally { //---закрываем соединение cn.Close(); }
Результат выполнения кода показан на рис. 5.11.
рис. 5.11
5.2.6. Чтение схемы данных Схему данных можно получить, если команда данных выполняется методом ExecuteReader с параметром SchemaOnly или KeyInfo. В первом случае ридер считывает только схему, во втором – и схему и данные. После выполнения ассоциированной с ридером команды данных схему данных можно извлечь методом GetSchemaTable ридера: public DataTable GetSchemaTable(); • •
Метод возвращает ссылку на таблицу данных, в которой: столбцами служат параметры столбцов схемы; каждому столбцу схемы соответствует одна строка таблицы.
5. Команды данных и ридеры
321
Извлеченную схему удобно представлять пользователю при помощи элемента управления DataGrid, поскольку его легко связать с таблицей данных (объект DataTable). Пример. Считаем схему данных как результат выполнения запроса на считывание данных из таблицы Kaf базы данных VUZ (SQL Server). Извлечем схему и поместим ее в объект dataGrid1. //---ридер SqlDataReader r; //---соединение SqlConnection cn = new SqlConnection( "data source=PAVLIK-50R6ZD;initial catalog=VUZ;" + "integrated security=SSPI;" + "persist security info=False;" + "workstation id= PAVLIK -50R6ZD;packet size=4096"); //---команда SqlCommand cmd = new SqlCommand( "SELECT KK, Nazv, Fakultet, Sostav FROM Kaf", cn); try { //---откроем соединение cn.Open(); //---выполним команду r = cmd.ExecuteReader(CommandBehavior.SchemaOnly); //---извлечем схему и свяжем ее с гридом dataGrid1.SetDataBinding(r.GetSchemaTable(), ""); //---закроем ридер r.Close(); } catch (Exception exptn) { MessageBox.Show("Ошибка : " + exptn.Message); } finally { //---закроем соединение cn.Close(); }
Результат выполнения кода представлен на рис. 5.12.
рис. 5.12
322
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
5.2.7. XmlTextReader: чтение из потока XML-данных Объект System.Xml.XmlTextReader может использоваться для чтения потока XML-данных, возвращаемых, например, оператором SELECT …FOR XML (SQL Server 2000). Рассмотрим использование ридера на примере. Пример. Пусть в базе данных VUZ (SQL Server) имеется хранимая процедура SELECTXML: CREATE PROCEDURE SELECTXML AS select KK, Nazv, Fakultet from Kaf for XML AUTO GO
Если выполнить эту процедуру в среде утилиты Query Analyzer (SQL Server 2000): EXEC SELECTXML
получим следующий результат в XML-виде:
Выполним в приложении Windows следующий код для вызова процедуры и считывания возвращаемого результата в элемент управления listBox1: listBox1.Items.Clear(); //---соединение SqlConnection cn = new SqlConnection( "data source=PAVLIK-50R6ZD;initial catalog=VUZ;" + "integrated security=SSPI;" + "persist security info=False;" + "workstation id= PAVLIK -50R6ZD;packet size=4096"); //---команда SqlCommand cmd = new SqlCommand("SELECTXML", cn); cmd.CommandType = CommandType.StoredProcedure; //---ридер XmlTextReader x; try { //---открываем соединение cn.Open(); //---выполняем команду x = (XmlTextReader) cmd.ExecuteXmlReader(); //---читаем ридер while (x.Read()) { //---если текущий узел соответствует таблице Kaf, //---поместить значения полей в listBox1 if (x.Name == "Kaf") { listBox1.Items.Add(x.GetAttribute(0) + (char)9 + x.GetAttribute(1) + (char)9 + 5. Команды данных и ридеры
323
}
x.GetAttribute("Sostav"));
} //---закроем ридер x.Close();
} catch (Exception ex) { MessageBox.Show("Ошибка : " + ex.Message); } finally { //---закроем соединение cn.Close(); }
Результат выполнения показан на рис. 5.13. рис. 5.13
324
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
6 ЧАСТЬ 6 ЗАПОМИНАНИЕ ОТЛОЖЕННЫХ ИЗМЕНЕНИЙ В ИСТОЧНИКЕ ДАННЫХ 6.1. Подтверждение и откат внесенных в данные изменений После того как данные из источника данных были залиты в набор данных приложения, приложение разрывает соединение с источником данных и работает с данными в автономном режиме. В процессе автономного использования данных в приложении в них могут вноситься изменения. А именно записи могут добавляться, удаляться и значения столбцов записи могут корректироваться. Все эти изменения не известны в источнике данных до тех пор, пока мы их там не актуализируем. При актуализации изменений в приложении выбираются все измененные записи. Потом они как бы перебираются по одной. Для каждой измененной записи к источнику отсылается соответствующая инструкция на изменение. Например, пусть источник – базы данных SQL Server. Для каждой удаленной в приложении записи к серверу отсылается оператор DELETE; для каждой добавленной к приложению записи – оператор INSERT; для каждой откорректированной – оператор UPDATE. Все эти операторы снабжаются соответствующими параметрами. После актуализации данные в источнике и в приложении приходят в согласованное состояние, т. е. значения записей, присутствующие и в таблицах источника и в таблицах набора в приложении, не отличаются. Такое "равновесие" будет нарушено либо когда в приложении автономные данные снова будут изменены нами в приложении, либо когда другой пользователь изменит данные в самом источнике. На основании сказанного можно сформулировать ряд узловых моментов, существенных при актуализации в источнике данных тех изменений, которые были внесенны в данные приложении. Мы должны уметь: • выделить в приложении из всего множества записей только те, которые реально изменялись; • переносить изменения из приложения в источник; • отказываться от изменений – полностью или частично; • должным образом обрабатывать возникающие ошибки. В настойщей главе рассматривается, как сделать все это.
6.1.1. Изменялись ли записи в наборе Если хоть одна запись в наборе данных была добавлена, откорректирована или удалена, то метод HasChanges набора данных возвратит значение true. Если ни одна запись набора не изменялась, метод возвратит значения false. 325
Метод имеет две разновидности. Первая исследует наличие любых изменений: public bool HasChanges();
в то время как вторая – изменений только определенного вида. Вид изменений задается параметром rowState: public bool HasChanges( DataRowState rowState );
Перечисление System.Data.DataRowState задает следующие значения (выборочно): Значение Added Deleted Modified Unchanged
Описание Строка добавлена Строка удалена Строка изменена Строка не изменялась
Отметим, что метод HasChanges возвращает значение true до тех пор, пока все измененные записи не будут запомнены в источнике (базе) данных, либо пока изменения во всех записях не будут отменены. Примеры типичного применения метода: if (dsVUZ1.HasChanges()) { //---записи изменялись } else { //---никаких изменений } if (dsVUZ1.HasChanges(DataRowState.Modified)) { //---есть хотя бы одна откорректированная запись } else { //---значения столбцов ни одной записи не изменялись }
6.1.2. Изменялась ли строка данных Для того чтобы определить, изменялась ли конкретная строка, можно использовать свойство объекта DataRow: public DataRowState RowState {get;} Свойство возвращает значения перечисления System.Data.DataRowState. Они рассматривались в предыдущем разделе. Пример. В информационном окне вывести сведения о том, какие изменения произведены во второй строке таблицы Kaf набора данных dsVUZ1. string sRowState = ""; switch (dsVUZ1.Kaf[1].RowState) {
326
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
case DataRowState.Added : sRowState = "Запись добавлена"; break; case DataRowState.Deleted : sRowState = "Запись удалена"; break; case DataRowState.Modified : sRowState = "Запись изменена"; break; case DataRowState.Unchanged : sRowState = "Запись не изменялась"; break;
} MessageBox.Show(sRowState);
Отметим, что строка, если строка в приложении добавлена, удалена или изменена, находится в состоянии, отличном от Unchanged, до тех пор, пока ее изменение либо не актуализируют (не запомнят) в источнике данных, либо не отменят методом RejectChanges объектов DataRow, DataTable или DataSet.
6.1.3. Получение измененных записей таблицы данных Метод GetChanges таблицы данных (объект DataTable) public DataTable GetChanges(); public DataTable GetChanges( DataRowState rowState );
возвращает другой объект таблицы данных, коллекция Rows которой включает только измененные записи из исходной таблицы. Вид изменения задает параметр rowState метода GetChanges. Если параметр не указан, подразумеваются все виды изменений. Пример. Получить в таблице dtChangedRecords все записи, добавленные в таблицу Kaf набора данных dsVUZ1. System.Data.DataTable dtChangedRecords; dtChangedRecords = dsVUZ1.Kaf.GetChanges(DataRowState.Added);
6.1.4. Запоминание внесенных изменений в базе данных Как известно, адаптер данных (объект типа SqlDataAdapter или OleDbData Adapter) связывает таблицу 1 в источнике данных и таблицу в наборе данных. Метод Fill адаптера переносит (заливает) данные из таблицы источника в таблицу набора, т. е. действует в направлении от источника к набору данных. Метод же Update адаптера данных актуализирует (запоминает) в таблице источника данных изменения, внесенные в записи соответствующей таблицы данных в приложении. То есть метод Update действует в направлении от набора данных к источнику данных. 1
Реже таблицы.
6. Запоминание отложенных изменений в источнике данных
327
Из сказанного следует, что изменения, внесенные в каждую таблицу набора данных, переносятся в источник данных методом Update соответствующего адаптера данных. Поэтому в скольких таблицах изменялись данные, столько раз и нужно выполнить метод Update от разных адаптеров. Рассмотрим разновидности метода Update. Первая переносит в таблицу источника данных все изменения, внесенные те в строки данных, которые содержатся в массиве dataRows: public int Update( DataRow[] dataRows );
Вторая разновидность метода Update переносит в источник изменения, внесенные во все строки набора данных ds: public override int Update( DataSet ds);
Третья разновидность метода Update переносит в источник изменения, внесенные во все строки таблицы данных dt: public int Update( DataTable dt );
Четвертая разновидность метода Update переносит в источник изменения, внесенные во все строки таблицы данных dt набора данных ds: public int Update( DataSet ds, string dt);
Последняя разновидность метода Update переносит в таблицу источника данных все изменения, внесенные в те строки данных, которые содержатся в массиве dataRows и отображаются в базе данных посредством маппинга tableMapping 1. protected virtual int Update( DataRow[] dataRows, DataTableMapping tableMapping );
Пример. Запомнить в базе данных изменения, внесенные в записи таблицы Kaf набора данных dsVUZ1. sqlDataAdapter1.Update(dsVUZ1, "Kaf");
Алгоритм действия метода Update следующий. Сначала просматриваются все записи в массиве или таблице данных, которые заданы как параметры метода. Отыскиваются все записи, у которых свойство RowState содержит значение, отличное от Unchanged (т. е. значения Added, Deleted или Modified). Неизмененные строки, у которых RowState = Unchanged, игнорируются. Понятно, что значение свойства RowState для каждой измененной записи однозначно указывает на вид изменения, внесенного в запись, – добавление, редактирование или удаление. В соответствии с этим для записи и вызывается одна из команд данных, заданных в адаптере – именно в свойствах InsertCommand, UpdateCommand, DeleteCommand адаптера. Выбранная команда выполняет для источника данных один из операторов INSERT, 1 Подробнее о связывании столбцов таблицы источника и набора данных см. в 2.2, подраздел "Установка однозначного соответствия между таблицами источника и таблицей данных ".
328
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
UPDATE, DELETE SQL-сервера (адаптер SqlDataAdapter) либо реализует аналогичную операцию для адаптеров OLE DB (объект OleDbDataAdapter). После выполнения метода Update измененная запись помечается как неизменявшаяся, т. е. в ее свойство RowState заносится значение Unchanged: Во всех столбцах записи величины исходной версии значения столбца (Original) приравниваются к значению текущей версии значения столбца (Current), Original = Current. Пример. Исследуем поведение записи данных в процессе внесения в нее изменений. Пусть запись принадлежит таблице данных Kaf из набора dsVUZ1. В элементах управления типа TextBox будем выводить текущее состояние записи и версии значений столбца Код кафедры – исходную и текущую. Изменим код кафедры с 44 на 22 (рис. 6.1).
рис. 6.1
Запомним изменения, внесенные в запись, в таблице Kaf базы данных: sqlDataAdapter1.Update(dsVUZ1, "Kaf");
Рассмотрим состояние записи после запоминания в базе данных (рис. 6.2).
рис. 6.2 6. Запоминание отложенных изменений в источнике данных
329
6.1.5. Откат (отказ от) изменений Отказаться от изменений, внесенных в запись (записи) данных, можно методом RejectChanges public void RejectChanges();
записи данных (объект типа DataRow), таблицы данных (объект DataTable) или набора данных (объект DataSet). ПРИМЕЧАНИЕ. Когда вызывается метод RejectChanges набора данных, происходит отказ от изменений, внесенных во все записи набора. Если вызвать метод RejectChanges таблицы, то будут отменены изменения, внесенные в записи таблицы данных. Метод RejectChanges отдельной записи отменяет изменения, внесенные только в эту запись. При откате изменений во всех записях, для которых производится этот откат, во всех столбцах исходные версии значений (Original) переписываются в текущие версии значений (Current). Напомним, что версия Original содержит то значение столбца, которое он имел либо сразу после заливки данных в таблицу методом Fill адаптера, либо, если после заливки данные изменялись и перезаписывались в источник данных, сразу после последнего выполнения метода Update адаптера. Пример. Покажем, как откат изменений приводит запись к ее первоначальному состоянию. Пусть после заливки данными таблица Kaf набора данных ds1 находится в состоянии, показанном на рис. 6.3. В нижней части формы для последней записи таблицы будем показывать текущее состояние записи и версии значения столбца Nazv. Изменим значение столбца на Алгебры (рис. 6.4), а затем Арифметики (рис. 6.5).
рис. 6.3
рис. 6.4
рис. 6.5
330
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Затем выполним откат изменений для последней записи таблицы Kaf: ds1.Kaf[2].RejectChanges();
После отката будут восстановлены те значения столбцов записи, какие имели место сразу после заливки таблицы Kaf данными из источника данных (рис. 6.6). рис. 6.6
Откат изменений широко применяется, когда попытка переноса изменений из приложения в источник данных приводит к ошибке. Пример. Чтобы обрабатывать ошибки, возникающие при переносе изменений из приложения в источник данных, зададим следующий обработчик события RowUpdated адаптера данных: private void sqlDataAdapter1_RowUpdated(object sender, System.Data.SqlClient.SqlRowUpdatedEventArgs e) { if (e.Status == UpdateStatus.ErrorsOccurred) { //---сообщение об ошибке в строке e.Row.RowError = e.Errors.Message; //---сообщение об ошибке в одном из столбцов e.Row.SetColumnError("KK", "Значение уже изменено другим приложением"); MessageBox.Show("Ошибка при обновлении записи: " + e.Errors.Message); } }
Все измененные записи приложения, которым отказано в переносе в источник данных, будут помечены как ошибочные (их свойство HasErrors будет установлено в true). Рассмотренный выше обработчик события RowUpdated заполнит для таких записей сообщение об ошибке (запишет значение в свойство RowError записи). Все эти записи останутся помеченными как модифицированные (свойство RowState строки будет продолжать хранить значение, отличное от Unchanged). В текущей и исходной версиях изменявшихся столбцов будут по-прежнему содержаться различные значения. При констатации факта, что перенос изменений из приложения в источник данных сопровождался ошибками, нужно откатить изменения, послужившие источником ошибки, и снять в записях отметки об ошибках. Для этого следует: • Очистить свойства HasErrors и RowError в ошибочных строках. Это достигается выполнением метода ClearErrors каждой ошибочной строки либо всего набора данных в целом. 6. Запоминание отложенных изменений в источнике данных
331
•
Для измененных столбцов записать исходную версию их значения в текущую версию, а также пометить их как неизменявшиеся, добившись того, чтобы свойство RowState таких строк установилось в значение Unchanged. Для этого подойдет метод RejectChanges каждой ошибочной строки либо всего набора данных.
В приводимом ниже фрагменте кода снятие признака ошибки и откат изменений производится для каждой из ошибочных строк набора данных: //---получим все ошибочные записи DataRow[] ar_dr = dsVUZ1.Kaf.GetErrors(); foreach (DataRow dr in ar_dr) { if (dr.HasErrors) { dr.RejectChanges(); //отменим изменения dr.ClearErrors(); //очистим признак ошибки } }
6.1.6. Подтверждение изменений методом AcceptChanges Метод AcceptChanges подтверждает изменения в записи, таблице или наборе данных. При этом: • исходная версия измененных столбцов записи (Original) приравнивается к текущей версии (Current); • запись помечается как неизменявшаяся (свойство RowState = Unchanged), в силу чего последующие выполнения методов Update эту строку не затрагивают, а ранее внесенные в такую строку изменения не будут запомнены в источнике данных, т. е. попросту говоря, потеряются. Для строки (объект DataRow), таблицы (объект DataTable) и набора данных (объект DataSet) вид метода одинаков: public void AcceptChanges();
Выполнение метода затрагивает все строки объекта, для которого он вызван, – только текущую строку (вызван метод объекта DataRow), все записи таблицы (метод объекта DataTable) и все записи набора данных (метод объекта DataSet). Пример. Пусть запись принадлежит таблице данных Kaf из набора dsVUZ1. В элементах управления типа TextBox будем выводить текущее состояние записи и версии значений столбца Код кафедры – исходную и текущую. Изменим код кафедры с 2 на 55 (рис. 6.7). рис. 6.7
332
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Выполнение метода AcceptChanges для текущей строки dsVUZ1.Kaf[this.BindingContext[dsVUZ1, "Kaf"].Position].AcceptChanges();
приравнивает исходную версию (Original) измененного столбца к текущей (Current) и помечает запись как неизменявшуюся (Unchanged), как это показано на рис. 6.8. Если к такой, по существу, измененной строке применить метод Update для запоминания изменений в базе данных, то внесенные в строку изменения будут проигнорированы, поскольку метод Update занимается только измененными записями, чье свойство RowState содержит значение, отличное от Unchanged.
рис. 6.8
Наоборот, если к измененной записи, показанной на рис. 6.7, применить метод RejectChanges: dsVUZ1.Kaf[this.BindingContext[dsVUZ1, "Kaf"].Position].RejectChanges();
то текущая версия значения измененного столбца будет приравнена к исходной версии и строка пометится как неизменявшаяся (рис. 6.9), т. е. попросту произойдет откат изменений.
рис. 6.9 6. Запоминание отложенных изменений в источнике данных
333
Для метода AcceptChanges характерно использование при слиянии данных метода Merge набора данных. Если произвести слияние с параметром Preserve Changes = true, то те записи результирующего набора данных, которые имеют парные (т. е. с таким же значением первичного ключа) записи в исходном наборе, останутся как есть и не будут перезаписаны. Однако если в них будут добавлены дополнительные столбцы, такие записи пометятся как модифицированные. Это может создать массу неудобств при следующем выполнении метода Update. Поскольку подобная пометка не есть результат корректировки, напрямую инициированной пользователем, а является следствием самой операции слияния, то к результирующему набору применяют метод AcceptChanges. В результате после слияния все записи результирующего набора помечаются как неизменявшиеся. Пример. Пусть имеем таблицу Kaf в исходном и результирующем наборах данных dsSource1 и dsTarget1 (рис. 6.10). Выполним слияние наборов методом dsTarget1.Merge(dsSource1, true);
и получим результат, показанный на рис. 6.11. Запись таблицы Kaf результирующего набора (со значением столбца KK, равным 3) будет помечена как измененная (RowState = Modified) в силу добавления в нее нового столбца и, следовательно, нового значения этого столбца. Поэтому требуется выполнить метод dsTarget1.AcceptChanges();
для перевода этой записи в состояние Unchanged.
рис. 6.10
рис. 6.11
Наконец, есть и довольно экзотическое применение метода AcceptChanges – для отката изменений в записях, помеченных как ошибочные в процессе выполнения метода Update. 334
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Пример. Отмена изменений в записях таблицы Kaf набора данных dsVUZ1, помеченных как ошибочные в процессе выполнения метода Update. Чтобы откатить изменения в таких записях, в каждом из их столбцов текущая версия значения столбца принудительно приравнивается к исходной (Original) версии. Затем вызывается метод AcceptChanges, подтверждающий это изменение: DataRow[] ar_dr = dsVUZ1.Kaf.GetErrors(); foreach (DataRow dr in ar_dr) { if (dr.HasErrors) { //отменим изменения dr["KK"] = dr["KK",DataRowVersion.Original]; dr.AcceptChanges();
}
}
dr.ClearErrors();
//очистим признак ошибки
Впрочем, хотя такой способ и работоспособен, все же удобнее выполнять откат методом RejectChanges.
6.1.7. Запоминание изменений в связанных таблицах Когда в приложении изменяются записи связанных (дочерних и родительской) таблиц, при запоминании подобных изменений в источнике данных методом Update адаптера следует придерживаться следующего порядка: 1. Сначала в источнике актуализируются удаления (RowState = Deleted) в дочерних таблицах. 2. Затем актуализируются все измененные записи из родительской таблицы, независимо от вида изменений. 3. В последнюю очередь актуализируются откорректированные (RowState = Modified) и вновь добавленные (RowState = Added) записи из дочерних таблиц. Для извлечения удаленных, а затем модифицированных и добавленных записей из дочерней таблицы применяется метод GetChanges таблицы данных. При этом следует помнить об одном важном обстоятельстве. В отличие от одноименного метода набора данных метод GetChanges таблицы данных, даже после актуализации измененных записей в источнике данных, продолжает возвращать измененные записи. Чтобы этого не происходило, нужно подтверждать изменения методом AcceptChanges. Пример. Пусть набор данных dsVUZ1 содержит родительскую таблицу Kaf (Кафедры) и подчиненную таблицу Prep (Преподаватели), связанные по столбцу KK (рис. 6.12). Пусть с таблицей Kaf связан адаптер aKaf, а с таблицей Prep – адаптер aPrep.
6. Запоминание отложенных изменений в источнике данных
335
Пусть априори неизвестно, как в конкретном случае пользователь изменит дочернюю и родительскую таблицы, и изменит ли вообще. Зададим в приложении код, выполняющий актуализацию в источнике данных измененных записей из родительской и дочерней таблиц:
рис. 6.12
//---получаем удаленные записи в дочерней таблице и записываем в базу DataTable dtDelChild = dsVUZ1.Prep.GetChanges(DataRowState.Deleted); if (dtDelChild != null) { aPrep.Update(dtDelChild); dtDelChild.Dispose(); } //---записываем в базу изменения в родительской таблице //---любых типов DataTable dtAnyChangesInParent = dsVUZ1.Kaf.GetChanges(); if (dtAnyChangesInParent != null) { aKaf.Update(dtAnyChangesInParent); dtAnyChangesInParent.Dispose(); } //---получаем добавленные записи в дочерней таблице DataTable dtAddChild = dsVUZ1.Prep.GetChanges(DataRowState.Added); if (dtAddChild != null) { aPrep.Update(dtAddChild); dtAddChild.Dispose(); } //---получаем измененные записи в дочерней таблице DataTable dtEditChild = sVUZ1.Prep.GetChanges(DataRowState.Modified); if (dtEditChild != null) { aPrep.Update(dtEditChild); dtEditChild.Dispose(); }
//---подтверждаем изменения dsVUZ1.AcceptChanges();
336
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Другой способ актуализации изменений, внесенных в связанные таблицы набора данных, использует временное отключение ограничений набора данных, связанных с подчинением таблиц. Для этого свойству EnforceConstraints набора данных public bool EnforceConstraints {get; set;}
придают значение true. На время прекращения действия ограничений целостность данных в наборе данных автоматически не отслеживается. Поэтому, если отступить от изложенного порядка запоминания изменений, внесенных в связанные таблицы, никаких исключений сгенерировано не будет. После актуализации изменений, ограничения снова активизируют, записав значение false в свойство EnforceConstraints набора данных. Пример. То же самое, что в предыдущем примере, но с временным отключением ограничений. //---отключаем ограничения dsVUZ1.EnforceConstraints = false; //---запоминаем изменения в произвольном порядке aPrep.Update(dsVUZ1); aKaf.Update(dsVUZ1); //---включаем ограничения dsVUZ1.EnforceConstraints = true;
6.1.8. Особенности отката изменений в связанных таблицах Пусть в наборе данных ds1 имеем связанные таблицы – дочернюю Prep и родительскую Kaf. В процессе работы с такими таблицами всегда есть искушение откатить изменения в дочерней таблице и оставить в силе изменения, внесенные в родительскую таблицу. Такой откат будет срабатывать до тех пор, пока не изменится значение первичного ключа в родительской таблице. Тогда, откатив изменения в дочерней таблице, мы моментально получим исключение из-за нарушения ссылочной целостности. Пример. Рассмотрим форму для работы со связанными таблицами Kaf и Prep (рис. 6.13). Кнопки, размещенные рядом с элементом управления DataGrid, позволяют подтверждать или откатывать изменения в таблице данных, с которой связан этот DataGrid. Особо нас интересует кнопка Отменить рядом с нижним объектом DataGrid, который связан с дочерней таблицей Prep. Обработчик события нажатия этой кнопки следующий: private void btnRejectPrep_Click(object sender, System.EventArgs e) { //---отменяем изменения в дочерней таблице try { ds1.Prep.RejectChanges(); } catch (Exception x) { 6. Запоминание отложенных изменений в источнике данных
337
}
}
MessageBox.Show("Ошибка обновления данных : " + (char)13 + (char)10 + x.Message);
рис. 6.13
Изменим в первой записи таблицы Kaf значение первичного ключа – столбца KK – с 1 на 111. Как видно из рис. 6.14, значение этого столбца автоматически изменилось и в подчиненных записях таблицы Prep.
рис. 6.14
Если теперь откатить изменения в таблице Prep, нажав рассматривавшуюся выше кнопку Отменить, немедленно получим исключение вследствие нарушения целостности данных в наборе данных ds1 (рис. 6.15).
рис. 6.15
338
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Действительно, значение столбца KK в родительской записи таблицы Kaf останется прежним – 111, в то время как в соответствующих дочерних записях это значение вновь станет 1. То же, впрочем, может произойти, если запоминать изменения, внесенные только в родительскую таблицу: стоит не запомнить или откатить изменения, внесенные в дочернюю таблицу, как ссылочная целостность в наборе данных нарушится. Произойдет это – и тогда сохраняются изменения только в дочерней таблице: ведь изменения в родительской таблице могут быть впоследствии отменены. Из всего сказанного следует очевидный вывод. Если изменялись значения столбцов первичного ключа родительской таблицы, то все последующие действия по запоминанию или откату изменений должны касаться обеих таблиц: и дочерней и родительской. Это правило может не соблюдаться, если заведомо известно, что значения столбцов первичного ключа ни в одной из записей родительской таблицы не изменялись. Чтобы быть в этом полностью уверенным, следует либо защитить эти столбцы от внесения изменений, либо отслеживать факты такого изменения программно, применяя для этой цели более или менее хитроумную программную логику.
6.1.9. Обработка ошибок, возникающих в процессе запоминания изменений в источнике данных Когда внесенные в записи изменения перезаписываются из приложения в источник данных, некоторые такие записи могут быть отторгнуты источником – например, когда значения этих записей нарушают какие-либо существующие в источнике данных ограничения, когда перезаписываемые данные уже успел изменить другой пользователь и пр. Каждый отказ источника принять запись из приложения диагностируется как ошибка. Рассмотрим, каким образом можно реагировать на такие ошибки. Свойство адаптера данных public bool ContinueUpdateOnError {get; set;}
если оно установлено в true, предписывает оставить повторные попытки перенести в источник ошибочную запись и продолжить перенос всех оставшихся записей. При значении false перенос изменений в источник немедленно прекращается. Для каждой измененной в приложении записи, когда метод Update адаптера пытается перезаписать ее в источник, происходит событие RowUpdated адаптера данных. Как параметр в обработчик этого события передается объект типа System.Data.SqlClient.SqlRowUpdatedEventArgs или System.Data.OleDB. OleDbRowUpdatedEventArgs. Если свойство Status этого параметра public UpdateStatus Status {get; set;}
содержит значение UpdateStatus.ErrorsOccurred, то запись ошибочна и источник данных ее не принял. Причину этого можно выявить из свойства Errors параметра: 6. Запоминание отложенных изменений в источнике данных
339
public Exception Errors {get; set;}
возвращающего ссылку на соответствующий объект исключения. Значение свойства Message этого исключения public virtual string Message {get;}
принято копировать в свойство RowError ошибочной строки данных: public string RowError {get; set;}
ссылку на которую содержит свойство Row рассматриваемого параметра обработчика события RowUpdated: public DataRow Row {get;} Пример. Пусть таблица данных Kaf набора данных dsVUZ1 после заливки в нее данных методом Fill адаптера имеет вид, показаный на рис. 6.16.
рис. 6.16
Пусть свойство ContinueUpdateOnError адаптера данных установлено в true (при ошибке продолжать перенос в источник прочих измененных в приложении записей) и реализован такой обработчик события обновления записи для адаптера: private void sqlDataAdapter1_RowUpdated(object sender, System.Data.SqlClient.SqlRowUpdatedEventArgs e) { //---если запись – ошибочна и отторгнута источником данных if (e.Status == System.Data.UpdateStatus.ErrorsOccurred) { //---копировать текст сообщения об ошибке в //---объект ошибочной строки e.Row.RowError = e.Errors.Message; //---выдать сообщение об ошибке MessageBox.Show("Ошибка при обновлении записи: " + e.Errors.Message); } }
Пусть наше приложение изменило код кафедры (исходное значение 2) во второй записи таблицы: dsVUZ1.Kaf.Rows[1]["KK"] = 200;
340
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Пусть в то время, как набор данных dsVUZ1 отсоединен от базы данных, другое приложение изменило код той же записи с 2 на 500 и запомнило изменение в базе данных. И уже после этого наше приложение пытается запомнить изменения в базе данных: sqlDataAdapter1.Update(dsVUZ1, "Kaf");
в результате чего происходит ошибка: записи с кодом кафедры = 2 в таблице Kaf, находящейся в источнике данных, уже нет. Как и предписано в обработчике sqlDataAdapter1_RowUpdated, выдается уведомление об ошибке и продолжается запоминание изменений других записей. В элементе управления DataGrid ошибочная запись, изменения в которой не удалось запомнить в источнике данных, маркируется особым образом, как это показано на рис. 6.17.
рис. 6.17
Вне зависимости от того, пытались ли мы в приложении отлавливать ошибки в обработчике события RowUpdated адаптера данных, все отторгнутые источником записи данные маркируются как ошибочные. Их свойство public bool HasErrors {get;}
автоматически устанавливается в значение true. Ошибочные строки исправляют (по возможности локализуя источник ошибки), после чего строку очищают от ошибок. Очистка выполняется методом ClearErrors объекта строки public void ClearErrors();
и состоит в сбросе значения свойства HasErrors ошибочной строки в значение false, очистке значения свойства RowError и, если они установлены, сообщений об ошибках для отдельных столбцов1 записи. ПРИМЕЧАНИЕ. Локализация источника ошибки состоит либо в полном отказе от внесенных в запись изменений методом RejectChanges объекта строки, либо в исправлении неверных значений каких-либо столбцов записи, после чего может быть снова предпринята попытка выполнить метод Update адаптера.
1
См. ниже метод SetColumnError.
6. Запоминание отложенных изменений в источнике данных
341
Пример. Для всех ошибочных строк в таблице Kaf набора данных dsVUZ1 производится откат изменений и очистка статуса ошибочной строки: foreach (DataRow dr in dsVUZ1.Kaf.Rows) { if (dr.HasErrors) { dr.RejectChanges(); dr.ClearErrors(); } }
Выше показан первый способ поиска ошибочных строк в таблице данных. Он основан на переборе всех строк таблицы и анализе значения свойства HasErrors каждой строки. Другой способ состоит в получении массива ошибочных строк методом таблицы данных: public DataRow[] GetErrors();
Пример. Сделаем то же, что и в предыдущем примере: для всех ошибочных строк в таблице Kaf набора данных dsVUZ1 откатим изменения и очистим статус ошибочной строки: DataRow[] ar_dr = dsVUZ1.Kaf.GetErrors(); foreach (DataRow dr in ar_dr) { if (dr.HasErrors) { dr.RejectChanges(); dr.ClearErrors(); } }
Сообщение об ошибке можно назначить не только строке, но и отдельному столбцу. Метод SetColumnError объекта DataRow : public void SetColumnError( DataColumn column, string error ); public void SetColumnError( string column, string error );
задает сообщение об ошибке для столбца данных. Ошибки столбцов данных можно обрабатывать методом GetColumnsInError объекта DataRow: public DataColumn[] GetColumnsInError();
Извлечь описание ошибки для конкретного столбца можно методом объекта DataRow, указав либо ссылку на объект столбца, либо его индекс в коллекции DataColumns строки, либо имя столбца: public string GetColumnError( DataColumn column ); public string GetColumnError( int index ); public string GetColumnError( string columnName );
Пример. Пусть обработчик события RowUpdated адаптера данных присваивает сообщение об ошибке не только строке, но и ошибочному столбцу. Несомненно, что в строке источником ошибки могут служить значения нескольких столбцов. Чтобы впоследствии определить, какой именно столбец послужил источником ошибки, нужно написать дополнительный код, анализирующий, например, текст сообщения 342 ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
об ошибке и т.д. В настоящем примере этого для простоты не предпринимается; считается, что ошибка всегда проистекает из неверного значения столбца KK. private void sqlDataAdapter1_RowUpdated(object sender, System.Data.SqlClient.SqlRowUpdatedEventArgs e) { if (e.Status == UpdateStatus.ErrorsOccurred) { //---сообщение об ошибке в строке e.Row.RowError = e.Errors.Message; //---сообщение об ошибке в одном из столбцов e.Row.SetColumnError("KK", "Значение уже изменено " + " другим приложением"); }
}
На рис. 6.18 представлен пример того, как представляет элемент управления DataGrid ошибочные строки, в которых маркированы также и ошибочные столбцы.
рис. 6.18
Выдадим в список элемента управления listBox1 сообщения для всех ошибочных столбцов ошибочных записей. DataColumn[] adc; //массив столбцов listBox1.Items.Clear(); //---перебираем все строки в таблице НД foreach (DataRow dr in dsVUZ1.Kaf.Rows) { //---если строка ошибочна if (dr.HasErrors) { //---получаем массив ошибочных столбцов adc = dr.GetColumnsInError(); //---и для каждого выводим его сообщение об ошибке foreach (DataColumn dc in adc) { listBox1.Items.Add( "Строка: " + dr["KK"].ToString() + "столбец: " + dc.ColumnName + "ошибка: " + dr.GetColumnError(dc) ); } }
6. Запоминание отложенных изменений в источнике данных
343
6.1.10. Отложенные изменения и их подтверждение Режим отложенных изменений для строки инициируется методом BeginEdit объекта DataRow: public void BeginEdit();
В этом режиме, когда изменяется значение столбца записи, это новое значение не сразу переписывается в текущую (Current) версию столбца, а сначала записывается в "предполагаемую" версию (Proposed). Версия Proposed доступна только в режиме отложенных изменений, а также в обработчике события ColumnChanged, наступающего при изменении значения столбца данных. Попытка обращения к версии Proposed в прочих местах программного кода повлечет за собой ошибку. Отказ от внесенных в отложенном режиме изменений производится CancelEdit методом объекта DataRow: public void CancelEdit(); Подтверждение изменений в отложенном режиме производится методом EndEdit объекта DataRow: public void EndEdit(); После выхода из режима отложенных изменений версия Current будет содержать последнее присвоенное столбцу значение. Пример. Пусть таблица Kaf набора данных dsVUZ1 после заливки ее данными имеет вид, показанный на рис. 6.19.
рис. 6.19
Применим ко второй строке таблицы Kaf (Код каф. = 2) отложенное изменение названия кафедры и просмотрим значения различных версий этого столбца. int nInd = 1; DataRow drCurRow = dsVUZ1.Kaf[nInd]; //---перейдем в режим отложенных изменений drCurRow.BeginEdit(); //---выдадим значения различных версий измененного столбца Nazv drCurRow["Nazv"] = "ОВЦЕСТРИЖЕНИЯ"; textBox1.Text = dsVUZ1.Kaf[nInd]["Nazv", DataRowVersion.Original].ToString();
344
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
textBox2.Text = dsVUZ1.Kaf[nInd]["Nazv", DataRowVersion.Current].ToString(); textBox3.Text = dsVUZ1.Kaf[nInd]["Nazv", DataRowVersion.Proposed].ToString(); //---завершим режим отложенных изменений drCurRow.EndEdit();
Результат представлен на рис. 6.20.
рис. 6.20
После завершения режима отложенных изменений методом EndEdit версия Current рассмотренной записи установится в значение ОВЦЕСТРИЖЕНИЯ. Вообще говоря, режим отложенных изменений чаще всего практикуют при пакетном изменении данных, когда сразу изменяют множество записей, притом, как правило, однотипным способом. Каждая запись изменяется в режиме отложенных изменений. Анализ правильности обновлений производится в обработчике события изменения столбца данных. В случае, если новое значение столбца не удовлетворяет какимлибо требованиям, производится откат изменений для текущей строки. Пример. Изменим коды кафедры для всех записей в таблице Kaf набора данных dsVUZ1, умножив существующие коды в 100 раз. Действует ограничение: коды не могут превышать 200. Пусть таблица Kaf набора данных dsVUZ1 после заливки в нее данных имеет вид, показанный на рис. 6.21. Подключим обработчик события изменения значения столбца в таблице Kaf: dsVUZ1.Kaf.ColumnChanged += new DataColumnChangeEventHandler(Kaf_ColumnChanged); … protected static void Kaf_ColumnChanged(Object sender, DataColumnChangeEventArgs args) { if (args.Column.ColumnName == "KK") if ((int)args.ProposedValue > 200) { 6. Запоминание отложенных изменений в источнике данных
345
} }
}
MessageBox.Show("Код не может быть больше 200! " + "Изменение отменено."); args.Row.CancelEdit();
рис. 6.21
Выполним изменение всех записей в таблице dsVUZ1.Kaf: foreach (DataRow dr in dsVUZ1.Kaf.Rows){ dr.BeginEdit(); dr["KK"] = ((int)dr["KK"]) * 100; dr.EndEdit(); }
Результат обновления показан на рис. 6.22.
рис. 6.22
6.1.11. Освежение таблицы данных, имеющей измененнные записи Пусть таблица данных имеет измененные записи, свойство RowState которых отлично от значения Unchanged. Если освежить1 такую таблицу данными из источника данных, выполнив метод Fill соответствующего адаптера данных, то:
1
346
Подробнее об освежении таблиц данных см. в 2.2.7. ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
•
• •
все записи в таблице, для которых в источнике есть "парные" записи с такими же значениями первичного ключа, замещаются в таблице этими "парными" записями из источника данных; все записи, добавленные в таблицу данных, для которых в источнике данных нет "парных" записей, после освежения остаются в таблице данных; все записи, удаленные из таблицы данных, но все еще существующие в источнике, восстанавливаются в таблице данных. Пример. Пусть таблица данных после заливки в нее данных из источника (т. е. первого выполнения метода Fill) имеет вид, показанный на рис. 6.23.
рис. 6.23
Изменим таблицу, удалив запись с кодом 2, добавив запись с кодом 4 и изменив запись с кодом 1 (рис. 6.24).
рис. 6.24
Затем освежим данные в таблице, повторно выполнив метод Fill адаптера данных. Пусть за промежуток времени между двумя выполнениями метода Fill данные в источнике никак не менялись. Результат освежения таблицы данных показан на рис. 6.25.
6. Запоминание отложенных изменений в источнике данных
347
рис. 6.25 • • •
Как видно из рис. 6.25, после освежения: измененная запись с кодом 1 была восстановлена в том виде, как она хранится в источнике на момент обновления; запись с кодом 2, удаленная в таблице данных, но существующая в источнике данных, была в таблице данных восстановлена; запись с кодом 4, добавленная в таблицу и еще не существующая в базе данных, после обновления оставлена в таблице данных без изменений. Пример. Пусть таблица данных после заполнения ее данными (т. е. первого выполнения метода Fill) имеет вид, показанный на рис. 6.26:
Пусть после того, как наше приложение отсоединилось от источника данных, другое приложение изменяет запись с кодом 2 (название кафедры с Информатика на Информактика и выч. техники). Когда мы вновь соединимся с источником данных и выполним освежение таблицы текущими данными из источника, внесенные другим приложением изменения станут видны и у нас (рис. 6.27).
348
рис. 6.26
рис. 6.27
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
6.2. Конкуренция пользователей при изменении данных Когда несколько пользователей одновременно изменяют одни и те же данные, между ними неизбежно возникает конкуренция. Имеет место следующая ситуация. Пусть некая запись хранится в базе данных и пребывает в состоянии Снач. Пусть пользователи А и Б считывают эту запись и как-то с нею работают. Затем пользователь А вносит в запись изменения и запоминает их в базе данных. Запись в базе данных переходит в состояние СА. Потом пользователь Б, работающий у себя в приложении с копией записи, находящейся в состоянии Снач, изменяет эту запись и также пытается записать изменения в базу данных. Однако в базе данных записи в состоянии Снач нет; та же запись, как уже сказано, находится в базе в состоянии СА. Тут возникают вопросы: • позволить ли перевести запись в состояние СБ, возможно, потеряв при этом изменения, внесенные пользователем А? • как сообщить пользователю Б, что запись уже изменена в базе данных (быть может, если он узнает об этом, то сочтет дальнейшую работу с записью бессмысленной)? Эти вопросы решаются при помощи различных стратегий, сущность которых состоит в том, что, когда один пользователь изменяет (или даже просто читает) данные, права других пользователей по применению этих данных ограничиваются. Известны три общие стратегии конкурентного обновления данных: • пессимистическая; • оптимистическая; • по принципу "последний победивший".
6.2.1. Пессимистическая стратегия При пессимистической стратегии, если один из параллельно работающих пользователей считал из базы данных, эти данные становятся недоступны другим пользователям до тех пор, пока первый пользователь не запомнит в базе данных внесенные в эти записи изменения. В рамках пессимистической стратегии исходят из того, что, когда мы записываем данные в источник данных, нам обязательно кто-нибудь будет мешать. ADO.NET, как известно, использует данные в отсоединенном от источника данных режиме и не поддерживает постоянного соединения с источником. В таких условиях пессимистическая стратегия как общий подход к изменению данных практически невозможна. Ведь данные, с которыми работает пользователь, на все это время блокируются для оставшихся пользователей. Если учесть, что заранее неизвестно, сколько времени монопольный пользователь будет работать с копиями данных в своем приложении и когда ему придет в голову записать внесенные изменения обратно в источник, то в рамках пессимистической стратегии все остальные пользователи потеряют доступ к данным на неопределенное время. А это немыслимо. Пессимистическую стратегию применяют лишь тогда, когда действия другого, конкурирующего с нами пользователя, могут нарушить смысловую целостность дан6. Запоминание отложенных изменений в источнике данных
349
ных. Тогда действия сторонних пользователей действительно стоит блокировать. Классическим примером может служить обновление двух счетов во время одной бухгалтерской проводки, когда одна и та же сумма проводится одновременно по дебету одного счета и по кредиту другого, причем либо должны пройти оба изменения, либо не должно пройти ни одного. Если этого не обеспечить, данные перейдут в несогласованное состояние. Поэтому в ADO.NET возможно точечное применение пессимистической стратегии с использованием транзакций и с учетом следующих факторов: • пессимистическую стратегию следует применять только тогда, когда иными средствами невозможно гарантировать смысловую целостность данных или когда цена блокировки данных меньше цены отката к первоначальному состоянию данных; • провайдер данных должен поддерживать транзакции (например, SQL Server); • выполнение транзакции (и следовательно, блокировка других пользователей) должно занимать как можно меньше времени.
6.2.2. Оптимистическая стратегия В рамках оптимистической стратегии все действия строят из предположения, что, когда мы записываем изменения в источник данных, нам никто из других пользователей мешать не собирается. Когда один пользователь считал данные из базы, действия других пользователей по отношению к этим данным не ограничиваются, но только до тех пор, пока кто-либо не начнет перезаписывать эти данные в источник. На время записи изменений доступ к записываемым данным со стороны других пользователей блокируется. Если пессимистическая стратегия гарантирует, что никто не вторгнется в наши данные и, пока мы с ними работаем, не внесет туда никаких изменений, то при оптимистической стратегии проверяется только факт адекватности данных в источнике, причем лишь в тот момент, когда эти данные в источнике перезаписываются. Итак, пусть пользователь считал запись из источника в состоянии Снач, изменил в приложении эту запись и перезаписал ее обратно в источник. Перед запоминанием автоматически проверяется, находится ли нужная запись в базе именно в состоянии Снач, а не в каком-либо другом. При этом возможны два варианта: 1. Запись хранится в базе в состоянии Снач, т. е. за это время никто из других пользователей не успел их изменить и перезаписать. Тогда наши действия не ограничиваются и мы без проблем перезаписываем запись в источнике. 2. Запись хранится в источнике в состоянии, отличном от Снач: за это время ее умудрился изменить кто-то другой. Тогда перезаписывать несуществующую версию записи не имеет смысла. Чтобы предотвратить перезапись, генерируется исключение класса DBConcurrencyException.
6.2.3. Стратегия "последний победивший" В рамках стратегии "последний победивший" нам все равно, кто и когда изменял данные, которые нам нужно перезаписать в источнике: мы считаем, что наша версия данных самая правильная и в силу этого храниться в источнике должна именно она. 350
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Запись блокируется, т. е. становится недоступна другим пользователям только во время физического запоминания в источнике. Никто не анализирует, пребывает ли перезаписываемая запись в источнике данных в том самом состоянии, в котором мы ее оттуда считали некоторое время назад. То есть пусть мы считали в приложение из источника данных запись в состоянии Снач, изменили ее в приложении и пытаемся перезаписать ее в источнике. Если кто-то уже успел изменить ее и перезаписать до нас (т. е. запись в источнике находится в состояниии, отличном от Снач), то наши изменения записываются поверх чужих, причем никакое исключение не генерируется.
6.2.4. Реализация оптимистической стратегии в ADO.NET ADO.NET по умолчанию использует оптимистическую стратегию. А она, как говорилось, сравнивает в источнике данных перезаписываемую запись и ту ее версию, которая некоторое время назад была считана из источника в приложение. Есть два способа помнить исходное состояние запоминаемой записи: • в момент считывания записи из базы данных запоминать исходные значения всех полей; • хранить и обновлять в записи время ее изменения в базе данных. ADO.NET использует первый подход, храня старую (исходную) версию значений всех столбцов записи. Для каждого столбца записи хранятся версии: исходная Original и текущая Current. Пример. Пусть для взаимодействия с таблицей Disciplina, размещенной в базе данных, в приложении создан адаптер данных. Для адаптера сгенерированы 4 команды данных. Ссылки на них возвращают свойства SelectCommand, InsertCommand, UpdateCommand, DeleteCommand адаптера. Рассмотрим текст запроса команды UpdateCommand, выполняющей запоминание измененных записей из приложения в таблицу Disciplina в базе данных. UPDATE SET WHERE
Disciplina KD = @KD, Nazv = @Nazv, Chasov = @Chasov (KD = @Original_KD) AND (Chasov = @Original_Chasov OR @Original_Chasov IS NULL AND Chasov IS NULL) AND (Nazv = @Original_Nazv OR @Original_Nazv IS NULL AND Nazv IS NULL); SELECT KD, Nazv, Chasov FROM Disciplina WHERE (KD = @KD)
Как видно из текста запроса, в нем используются две версии значения каждого столбца таблицы. Параметры, соответствующие исходным версиям полей, предваряются префиксом Original. Они используются в предложении WHERE запроса. Параметры, соответствующие текущим (Current) версиям столбцов записи, совпадают с именем столбца. Они используются в предложении SET запроса. 6. Запоминание отложенных изменений в источнике данных
351
Ниже приводится пример реализации пессимистической стратегии в приложении ADO.NET. Пример. Для того чтобы спровоцировать конкурентное обновление данных, нам вовсе не обязательно иметь два различных приложения, работающих с одними и теми же данными. Достаточно и одного. Создадим приложение (рис. 6.28) и в нем два отдельных набора данных, A и X, работающих с одной таблицей данных – Disciplina (учебная дисциплина в вузе). Пусть данные из этой таблицы заливаются в оба набора одновременно.
рис. 6.28
Изменим название дисциплины История на Физика в наборе X и запишем изменения в базу данных (рис. 6.29).
рис. 6.29
После этого изменим название дисциплины История на Электроника в наборе A и попытаемся записать это изменение в базу данных. Получим сообщение об ошибке 352
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
конкурентного доступа к данным и о том, что команда обновления адаптера данных не затронула ни одной записи набора (рис. 6.30).
рис. 6.30
Необновленная запись набора A получит статус ошибочной. Это, в частности, хорошо видно из рис. 6.31, поскольку элемент управления DdataGrid автоматически помечает ошибочные записи красным кружком с восклицательным знаком.
рис. 6.31
6.2.5. Реализация стратегии "последний победивший" в ADO.NET Для того чтобы применить стратегию "последний победивший", нужно вручную исправить текст запроса на обновление данных в команде UpdateCommand адаптера. В запросе следует удалить ссылки на исходные (Original) версии значений столбцов в предложении WHERE оператора UPDATE. Пример. Ниже показан запрос на обновление записи в таблице Disciplina, сгенерированный ADO.NET по умолчанию в рамках оптимистической стратегии. UPDATE SET WHERE
Disciplina KD = @KD, Nazv = @Nazv, Chasov = @Chasov (KD = @Original_KD) AND (Chasov = @Original_Chasov OR @Original_Chasov IS NULL AND Chasov IS NULL) AND (Nazv = @Original_Nazv OR @Original_Nazv IS NULL AND Nazv IS NULL); SELECT KD, Nazv, Chasov
6. Запоминание отложенных изменений в источнике данных
353
FROM WHERE
Disciplina (KD = @KD)
Удалим из предложения WHERE ссылки на исходные версии столбцов записи: UPDATE SET WHERE
Disciplina KD = @KD, Nazv = @Nazv, Chasov = @Chasov (KD = @Original_KD); SELECT KD, Nazv, Chasov FROM Disciplina WHERE (KD = @KD)
При выполнении такого оператора UPDATE ошибка конкурентного обновления данных будет генерироваться только только если другой пользователь успел перезаписать в этой записи значение первичного ключа (столбец KD). Значения всех прочих столбцов записи, перезаписываемой в источнике данных, нам неинтересны. Пример стратегии "последний победивший". Создадим приложение (рис. 6.32) и в нем два отдельных набора данных, A и X, работающих с одной таблицей данных – Disciplina (учебная дисциплина вуза). Пусть оба набора заливаются данными из названной таблицы одновременно. рис. 6.32
Изменим название дисциплины История на Физика в наборе X и запишем изменения в базу данных (рис. 6.33).
рис. 6.33
354
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
После этого изменим название дисциплины История на Электроника в наборе A и попытаемся записать это изменение в базу данных. Запись изменений в базу выполнится успешно (рис. 6.34).
рис. 6.34
Повторно считаем содержимое таблицы из базы данных в набор X для того, чтобы узнать, сработал ли принцип "последний победивший". Оказывается, сработал (рис. 6.35).
рис. 6.35
6.2.6. Управление ошибками конкурентного изменения данных При возникновении ошибки конкурентного изменения данных автоматически генерируется исключение DBConcurrencyException. Поскольку оно наследует от Exception, то, помимо прочих, обладает свойством public virtual string Message {get;}
возвращающим текст сообщения об ошибке, а также свойством public virtual string Source {get; set;}
возвращающим имя источника ошибки. В классе DBConcurrencyException Exception свойство
есть
и
отсутствующее
в
классе
public DataRow Row {get; set;}
Оно возвращает ссылку на ту строку данных, при попытке записи которой в базу данных и возникла ошибка. 6. Запоминание отложенных изменений в источнике данных
355
Пример. Если при запоминании измененных записей из приложения в таблицу Disciplina базы данных возникла ошибка конкурентного изменения данных, в информационном окне выдать сообщение с указанием значения столбца KD ошибочной записи; источника ошибки и текста сообщения об ошибке (рис. 6.36). try { aDisciplina.Update(dsDisciplina1); } catch (DBConcurrencyException ex ) { MessageBox.Show( "Строка, KD = " + ex.Row["KD"].ToString() + ", " + "Источник: " + ex.Source + ", " + "Ошибка : " + ex.Message); }
рис. 6.36
6.3. Транзакции в ADO.NET
1
Транзакция переводит базу данных при ее изменении из одного согласованного состояния в другое согласованное состояние. Изменение базы может включать в себя множество действий, минимум одно. Транзакция запоминает изменения, если все действия в контексте транзакции выполнились успешно. В этом состоит подтверждение транзакции. Если хотя бы одно из действий, выполняемых в контексте транзакции, закончилось аварийно, транзакция отказывается ото всех изменений, внесенных в процессе ее выполнения в базу данных, и возвращает базу в состояние, которое та имела до выполнения транзакции. В этом состоит откат транзакции.
6.3.1. Проблемы взаимодействия транзакций Когда с одними и теми же данными параллельно работает более одной транзакции, возможны следующие проблемные ситуации. 1 Известно два подхода к организации транзакций – транзакции вручную и автоматические (COM+) транзакции. В этой книге рассматриваются только транзакции вручную, использующие транзакции ADO.NET и Transact-SQL и реализуемые путем написания программного кода в приложениях .NET или хранимых процедурах серверных баз данных. Механизм автоматических транзакций, основанный на использовании в классах .NET декларативных атрибутов, более применим для реализации распределенных транзакций. Обсуждение таких транзакций – достаточно емкая тема, изначально не входившая в замысел этой книги.
356
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Проблема последнего измененения (потерянных изменений). Транзакция A вносит изменения в данные, а транзакция B вносит изменения в те же данные, до подтверждения транзакции A. Проблема "грязного чтения". Транзакция A вносит изменения в данные, а транзакция B считывает те же данные. Очень велика вероятность того, что транзакция A считает искаженные данные – например, 40 % уже измененных данных и 60 % данных, еще не измененных (на момент чтения), но затем изменившихся. Проблема неповторяемого чтения. Транзакция A считывает одни и те же данные несколько раз. В это время транзакция B изменяет данные. Когда транзакция A вновь считывает данные, она получает уже не то, что в прошлый раз. Проблема чтения фантомов. Транзакция A считывает данные, а транзакция B в то же время вставляет новые записи или удаляет существующие. Транзакция A при считывании видит записи, вставленные транзакцией B, т. е. видит данные уже не в том состоянии, в котором они пребывали на момент запуска этой транзакции А. В принципе проблема похожа на неповторяемое чтение, за исключение того, что фантомы – это вставленные сторонней транзакцией записи. Отметим, что при обсуждении неповторяемого чтения упор делался на то, что уже существующие записи изменяются другой транзакцией. С другой стороны, если транзакция B удалит запись и подтвердит удаление, то транзакция A будет "видеть" фактически несуществующую запись и работать с ней. Так будет происходить до тех пор, пока A не повторит чтение.
6.3.2. Уровни изоляции транзакций Чтобы разрешить перечисленные выше проблемы, параллельно выполняющиеся транзакции изолируют друг от друга. Изоляция состоит в частичной или полной блокировке данных, с которыми работает одна транзакция, для оставшихся транзакций. Строгость такой блокировки определяется уровнем изоляции транзакций. Перечислим их в порядке возрастания уровня (от менее строгого к нииболее строгому): Read Uncommitted, Read Committed, Repeateable Read, Serializable.
6.3.2.1. Уровень изоляции READ UNCOMMITTED Read Uncommitted – самый низший уровень изоляции. Невозможны потерянные изменения (т. е. внесение изменений в данные, в которые другой транзакцией уже внесены неподтвержденные изменения). Возможно "грязное" чтение, неповторяемое чтение, чтение фантомов. Рассмотрим взаимодействие транзакций, изолированных друг от друга на уровне Read Uncommitted, на примере двух транзакций, одновременно работающих с таблицей Kaf базы данных VUZ (SQL Server).
6. Запоминание отложенных изменений в источнике данных
357
Потерянные изменения (попытка перезаписи неподтвержденных изменений). Пусть транзакции в приложениях A и B запускаются одновременно и считывают одни и те же данные (рис. 6.37). Пусть приложение А вносит изменения в последнюю строку и запоминает изменения в базе данных, но транзакцию не подтверждает (рис. 6.38). Пусть приложение B вносит изменения в ту же строку и пытается запомнить эти изменения в базе (рис. 6.39). рис. 6.37
рис. 6.38
рис. 6.39
Эта попытка будет блокирована до тех пор, пока транзакция A не подтвердит или не откатит внесенные изменения. Грязное чтение (попытка чтения неподтвержденных изменений). Пусть приложение A запускает транзакцию и считывает данные (рис. 6.40). Пусть затем приложение A изменяет в последней записи значение столбца Sostav с 1 на 77 и запоминает изменение в базе, но транзакцию пока не подтверждает (рис. 6.41).
рис. 6.40
рис. 6.41
Пусть затем приложение B запускает транзакцию и считывает данные. Как видно из рис. 6.42, из таблицы Kaf считаны "грязные" данные – измененные приложением A, но пока им не подтвержденные. 358 ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Пусть затем приложение A откатывает транзакцию, вследствие чего значение столцба Sostav в последней записи снова становится равным 11. Тем не менее, приложение B продолжает работать с грязными данными (рис. 6.43).
рис. 6.42
рис. 6.43
Неповторяемое чтение. Пусть транзакция в приложении A запускается и считывает данные. Затем запускается транзакция в приложении B (рис. 6.44).
Пусть затем приложение B изменяет данные (значение столбца Nazv) в последней записи и подтверждает транзакцию (рис. 6.45). Когда транзакция A снова считывает данные, она получает их уже не в том виде, как при предыдущем чтении (сравните рис. 6.44 и 6.46).
рис. 6.44
рис. 6.45
рис. 6.46
Фантомные записи. Пусть транзакция в приложении A запускается и считывает данные (рис. 6.47). 6. Запоминание отложенных изменений в источнике данных
359
Пусть затем в приложении B стартует транзакция, удаляет последнюю запись и подтверждает транзакцию (рис. 6.48).
рис. 6.47
рис. 6.48
Рассмотрим рис. 6.49. Приложение B содержит актуальную копию данных из источника данных, в то время как приложение A продолжает работать с фактически удаленной (фантомной) записью.
рис. 6.49
6.3.2.2. Уровень изоляции READ COMMITTED Потерянные изменения невозможны. Для всех прочих транзакций блокируется внесение изменений в данные, в которые другой транзакцией X уже внесены неподтвержденные изменения). Блокировка снимается после завершения транзакции X. Грязное чтение невозможно. Если транзакция X внесла изменения в данные и не подтвердила и не откатила изменений, то никакие другие транзакции не смогут считывать эти данные до тех пор, пока X не завершится. Проблема неповторяемого чтения остается. Проблема чтения фантомов остается.
6.3.2.3. Уровень изоляции REPEATEABLE READ Потерянные изменения невозможны. Генерируется исключение при попытке внесения транзакцией Y изменений в данные, в которые другой транзакцией X уже внесены неподтвержденные изменения. 360
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Грязное чтение невозможно. Если транзакция X внесла изменения в данные и не подтвердила и не откатила изменений, то никакие другие транзакции не смогут считывать эти данные до тех пор, пока X не завершится. Неповторяемое чтение невозможно. Если транзакция X считала данные, то никакие другие транзакции не смогут изменить эти данные до тех пор, пока транзакция X не завершится. Проблема чтения фантомов остается.
6.3.2.4. SERIALIZABLE Потерянные изменения невозможны. Генерируется исключение при попытке внесения транзакцией Y изменений в данные, в которые другой транзакцией X уже внесены неподтвержденные изменения. Грязное чтение невозможно. Если транзакция X внесла изменения в данные и не подтвердила и не откатила изменений, то никакие другие транзакции не смогут считывать эти данные до тех пор, пока X не будет подтверждена или откачена. Неповторяемое чтение невозможно. Если транзакция X считала данные, то никакие другие транзакции не смогут изменить эти данные до тех пор, пока транзакция X не завершится. Чтение фантомов невозможно. Если транзакция X считала данные, то никакие другие транзакции в этом диапазоне данных не могут добавлять или удалять записи до тех пор, пока X не завершится.
6.3.3. Выбор подходящего уровня изоляции транзакций Выбор уровня изоляции транзакций – это, собственно, компромисс между безопасностью данных и производительностью. Чем ниже уровень изоляции, тем выше производительность. С увеличением уровня изоляции производительность приложения падает: параллельно работающие пользователи должны ожидать, пока разблокируются данные, заблокированные другими транзакциями. Если, например, транзакции уровня Read Committed блокируют только фактически измененные ею данные, то транзакции уровня Serializable полностью блокируют даже считанные данные и к ним уже никто до завершении такой транзакции обратиться не может. По умолчанию в ADO.NET принят уровень Read Committed. Следует помнить, что транзакция – дорогостоящий ресурс и что применение транзакций снижает степень масштабируемости приложения. В силу этого транзакции нужно применять только в тех случаях, когда иными способами невозможно обеспечить перевод данных (как правило, в нескольких таблицах и(или) базах) из одного согласованного состояния в другое согласованное состояние. Время выполнения транзакций стоит минимизировать, чтобы минимизировать то время блокировки данных, в течение которого эти данные недоступны другим пользователям. Выполнение транзакции и ее завершение (откат или подтверждение) должны санкционироваться самим приложением и ни в коем случае не пользователем. И наконец, транзакции не стоит употреблять для выполнения SQL-операторов, обновляющих один источник данных: SQL Server и так автоматически выполняет каждый такой оператор в контексте отдельной транзакции. Так что если происходит обновление 1000 записей оператором UPDATE и на обновлении 801-й записи произойдет 6. Запоминание отложенных изменений в источнике данных
361
сбой, сервер сам, без нашего вмешательства, обязан отменить изменение предыдущих 800 записей.
6.3.4. Общая схема использования транзакций в приложении Если провайдер данных (такой, как SQL Server) поддерживает транзакции, то транзакция начинается методом BeginTransaction объекта соединения (OleDb Connection или sqlConnection). Разновидности этого метода для обоих классов соединения приводятся ниже в 6.3.4.1. Метод BeginTransaction возвращает объект транзакции (класса SqlTransaction или OleDbTransaction). Операции изменения данных должны включаться в контекст транзакции. Для этого команда данных в свойство Transaction должна установить ссылку на ранее созданный объект транзакции. Шаги изменения данных заключаются в блок try … catch, и, если произошла ошибка, происходит отмена всех изменений (откат транзакции) методом Rollback объекта транзакции. Если ни одной ошибки не произошло, изменения запоминаются в базе данных методом Commit объекта транзакции. Отметим, что соединение должно быть открыто на все время действия транзакции. По умолчанию в ADO.NET соединение "живет" 15 секунд. Чтобы продлить время жизни соединения, добавьте в строку соединения соответствующего объекта OleDbConnection или sqlConnection параметр Connection Lifetime = время, например: sqlConnection1.ConnectionString = "data source=PAVLIK-50R6ZD;" + "initial catalog=VUZ;integrated security=SSPI;" + "persist security info=False;workstation id= PAVLIK -50R6ZD;" + "packet size=4096; Connection Lifetime=0";
Приравнивание этого параметра к нулю означает неограниченное время соединения, вплоть до его принудительного разрыва методом Close. Пример. Пусть в базе данных SCHETA хранятся остатки на счетах клиентовграждан (таблица AccountPerson) и остатки на счетах организаций (таблица AccountOrg). Пусть некий гражданин (код гражданина 111) получает от организации (код организации 2) плату в размере 222 рублей за оказанные услуги. • •
В рамках транзакции мы должны на означенную сумму: увеличить счет гражданина; уменьшить счет организации. Рассмотрим реализацию подобной транзакции средствами ADO.NET.
ПРИМЕЧАНИЕ. Понятно, что в реальной жизни коды организации и гражданина вряд ли будут помещаться "вживую в текст SQL-запроса" и наверняка станут сообщаться объекту команды данных посредством параметров. В данном случае, чтобы не загромождать пример, мы вставили коды прямо в текст SQL-запросов. 362
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
SqlTransaction t; //объект транзакции SqlCommand Zapros = new SqlCommand(); //команда данных //---если текущее соединение закрыто, открыть if (sqlConnection1.State == ConnectionState.Closed) { sqlConnection1.Open(); } //---запускаем транзакцию, получаем объект транзакции t = sqlConnection1.BeginTransaction(); //---команда использует открытое существующее соединение Zapros.Connection = sqlConnection1; //---команда использует транзакцию Zapros.Transaction = t; //шаги изменения данных в контексте транзакции try { //---увеличение остатка на счете гражданина Zapros.CommandText = "UPDATE SCHETA..AccountPerson SET OSTATOK = OSTATOK + 222 " + "WHERE KODPERSON = '111'"; Zapros.ExecuteNonQuery(); //---уменьшение остатка на счете организации Zapros.CommandText = "UPDATE SCHETA..AccountOrg SET OSTATOK = OSTATOK – 222 " + "WHERE KODORG = '2'"; Zapros.ExecuteNonQuery(); //---раз попали сюда, ошибок не было MessageBox.Show("Транзакция завершена успешно"); //---можно и подтвердить транзакцию t.Commit(); } catch (Exception x) { //---сюда попадаем, когда были ошибки; откатываем транзакцию t.Rollback(); MessageBox.Show("Ошибка выполнения транзакции: " + x.Message); } finally { //---сюда попадаем всегда и закрываем соединение if (sqlConnection1.State == ConnectionState.Open) { sqlConnection1.Close(); } }
6.3.4.1. Запуск и завершение транзакции Транзакция начинается методом BeginTransaction объекта соединения. Класс OleDbConnection имеет две сигнатуры метода BeginTransaction: public OleDbTransaction BeginTransaction(); public OleDbTransaction BeginTransaction( IsolationLevel isolationLevel ); 6. Запоминание отложенных изменений в источнике данных
363
Класс sqlConnection имеет 4 сигнатуры метода BeginTransaction: public OleDbTransaction BeginTransaction(); public OleDbTransaction BeginTransaction( IsolationLevel isolationLevel ); public SqlTransaction BeginTransaction( string transactionName ); public SqlTransaction BeginTransaction( IsolationLevel isolationLevel, string transactionName );
Параметр isolationLevel задает уровень изоляции транзакции как одно из значений перечисления System.Data.IsolationLevel: Значение Chaos ReadCommitted
Описание Незавершенные изменения из транзакций с более высоким уровнем изоляции не могут перезаписываться См. выше уровень изоляции ReadCommitted
ReadUncommitted См. выше уровень изоляции ReadUncommitted RepeatableRead См. выше уровень изоляции ReadUncommitted Serializable Unspecified •
•
См. выше уровень изоляции Serializable Уровень изоляции, отличный от приведенных выше и который нельзя определить
Завершение транзакции может состоять в: В подтверждении действий, выполненных в контексте транзакции. В этом случае данные переводятся из одного согласованного состояния в другое согласованное состояние. В откате транзакции, т. е. отмене всех действий, выполненных в контексте транзакии. В этом случае данные остаются в том согласованном состоянии, в котором они пребывали на момент старта транзакции.
Подтверждают или откачивают транзакцию методы объекта транзакции (типов SqlTransaction или OleDbTransaction). Метод Commit подтверждает транзакцию: public void Commit(); Метод Rollback используется для отката транзакции: public void Rollback();
6.3.4.2. Объект транзакции SqlTransaction Объект транзакции класса System.Data.SqlClient.SqlTransaction возвращается методом BeginTransaction объекта соединения SqlConnection. Ниже рассматриваются специфические свойства и методы класса SqlTransaction. Ссылка на объект соединения возвращается свойством public SqlConnection Connection {get;}
Уровень изоляции транзакции возвращается свойством public IsolationLevel IsolationLevel {get;}
364
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Транзакция подтверждается методом public void Commit();
Откат транзакции производится методом public void Rollback(); public void Rollback( string transactionName );
Для создания контрольной точки транзакции (к которой впоследствии можно откатиться) используют метод public void Save( string savePointName );
Более подробно см. аналогичный этому методу оператор Transact-SQL SAVE TRANSACTION в руководствах по SQL Server.
6.3.4.3. Объект транзакции OleDbTransaction Объект транзакции класса System.Data.OleDb.OleDbTransaction возвращается методом BeginTransaction объекта соединения oleDbConnection. Состав его специфических методов и свойств аналогичен заданному в классе SqlTransaction, но метод Save отсутствует.
6. Запоминание отложенных изменений в источнике данных
365
7 ЧАСТЬ 7 ФИЛЬТРАЦИЯ И СОРТИРОВКА ЗАПИСЕЙ ДАННЫХ 7.1. Просмотры данных 7.1.1. Для чего нужны просмотры Часто в приложениях нужно работать не со всеми записями таблицы данных, а с их подмножеством. При этом говорят, что записи отфильтровываются по определенному критерию. Рассмотрим простой пример. На рис. 7.1 и 7.2 показана форма, где пользователь из выпадающего списка выбирает название факультета. Затем в таблице кафедр производится фильтрация и в элементе управления DataGrid пользователю представляются только записи с выбранным им факультетом.
рис. 7.1
рис. 7.2
В прошлые времена, когда мы работали в идеологии "клиент-сервер", после выбора пользователем условия фильтрации (в нашем случае – названия факультета) приложение действовало следующим образом. Провайдеру данных (например, SQL Server) отсылался соответствующий запрос, содержащий оператор SELECT … WHERE …, где в предложении WHERE задавалось условие (критерий) фильтрации, например 366
SELECT KK, Nazv, Sostav, Fakultet FROM Kaf WHERE Fakultet = @Falultet
и параметр @Falultet содержал название выбранного пользователем факультета. Такой запрос выполнялся провайдером и возвращал записи, соответствующие запросу, в приложение, которое и представляло эти записи пользователю. В ADO.NET, как известно, наборы данных в приложении отсоединены от источника. В таблицы данных из источника накачивается необходимое число записей, после чего соединение разрывается и приложение должно тревожить источник данных как можно реже, в основном для запоминания в базе данных изменений, внесенных в записи данных в приложении. Отсюда следует, что задача извлечения подмножества записей, удовлетворяющих какому-либо условию, должна решаться приложением самостоятельно, без помощи провайдера данных. Именно такую функцию выполняют в ADO.NET просмотры данных, которые реализуются объектами типа (System.Data.DataView). Просмотр данных обеспечивает работу с подмножеством записей какой-либо таблицы набора данных. Просмотр содержит ссылки на те записи из таблицы данных, которые удовлетворяют задаваемому условию фильтрации. Физически записи данных никуда не копируются. Поэтому все изменения, вносимые нами в записи просмотра, непосредственно затрагивают таблицу данных. Пример. На рис. 7.3 показано содержимое таблицы Kaf (кафедры университета) и просмотр, содержащий только кафедры гуманитарного факультета).
рис. 7.3 7. Фильтрация и сортировка записей данных
367
Удалим из просмотра кафедру Истории и увидим, как эта запись сразу же исчезнет и из полной таблицы Kaf (рис. 7.4).
рис. 7.4
Число просмотров, которые можно создать для таблицы набора данных, физически не ограничено, и мы можем иметь в приложении сколь угодно много таких "взглядов" на записи одной и той же таблицы. Пример. На рис. 7.5 показана таблица кафедр и три различных просмотра: • • •
все кафедры с составом больше 20 человек (слева внизу); все кафедры, в состав которых входит Ав (справа вверху); все кафедры технического факультета (справа внизу).
рис. 7.5
368
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Просмотр может возвращать все записи исходной таблицы набора данных (когда условие фильтрации не задано), но притом отсортировывать эти записи в каком-либо особом порядке. Пример. На рис. 7.6 показана таблица кафедр и три различных просмотра, отличные от таблицы только сортировкой записей: • • •
по факультету, названию кафедры (слева внизу); по названию кафедры, факультету (справа вверху); по составу (справа внизу).
рис. 7.6
ПРИМЕЧАНИЕ. Фильтрацию записей и сортировку таблицы можно также выполнить с помощью метода Select таблицы данных (объект DataTable): public DataRow[] public DataRow[] public DataRow[] DataViewRowState
Select( string filterExpression ); Select( string filterExpression, string sortOrder ); Select( string filterExpression, string sortOrder, recordState);
Параметры: filterExpression – выражение фильтрации; sortOrder – список столбцов, определяющий порядок сортировки в результирующем массиве записей; recordState – состояние записи. К недостаткам этого метода следует отнести в первую очередь то, что он возвращает массив записей и напрямую визуализировать в элементе DataGrid такой массив не представляется возможным. Поэтому метод Select лучше применять для тех слу-
7. Фильтрация и сортировка записей данных
369
чаев, когда не требуется визуализация результата фильтрации, а сам результат используется для каких-либо расчетов, построения отчетов и пр.
7.1.2. Создание просмотров Чтобы создать просмотр, нужно создать объект типа System.Data.DataView – или программно на этапе выполнения, или в дизайнере Visual Studio .NET на этапе разработки приложения. Для программного создания просмотра используются соответствующие разновидности конструктора объекта DataView. Пример программного создания просмотра с заданием исходной таблицы, условия фильтрации записей, порядка сортировки и версии записей в качестве параметров конструктора: System.Data.DataView dv; dv = new System.Data.DataView(dsVUZ1.Kaf, "SOSTAV 20". При этом в первом будем показывать текущие версии записей таблицы Kaf, во втором – исходные версии (рис. 7.8): System.Data.DataView dvc; System.Data.DataView dvp; dvc = new System.Data.DataView(dsVUZ1.Kaf, "SOSTAV > 20", "KK", DataViewRowState.CurrentRows); dvp = new System.Data.DataView(dsVUZ1.Kaf, "SOSTAV > 20", "KK", DataViewRowState.OriginalRows);
Однако если мы после описанного выше изменения захотели увидеть в просмотрах текущие и исходные версии только измененных строк (рис. 7.9), мы должны были создать просмотры иным образом: dvc = new System.Data.DataView(dsVUZ1.Kaf, "SOSTAV > 20", "KK", DataViewRowState.ModifiedCurrent); dvp = new System.Data.DataView(dsVUZ1.Kaf, "SOSTAV > 20", "KK", DataViewRowState.ModifiedOriginal);
рис. 7.8
7. Фильтрация и сортировка записей данных
рис. 7.9
373
7.1.7. Сортировка записей в просмотре Порядок сортировки записей просмотра задается свойством Sort просмотра public string Sort {get; set;}
или аналогичным параметром конструктора просмотра. Строка, помещаемая в свойство Sort, должна включать перечисленные через запятую имена тех столбцов данных, по которым выполняется сортировка. После имени столбца может стоять слово ASC, задающее сортировку по возрастанию значений, или DESC – по убыванию значений. Например: dvKafSostav.Sort = "FAKULTET DESC, NAZV ASC";
Пример сортировок. Пусть имеем просмотр dvKafSostav. Зададим порядок сортировки по возрастанию значений столбца NAZV (рис. 7.10). dvKafSostav.Sort = "NAZV ASC";
рис. 7.10
Изменим порядок сортировки по возрастанию значений столбца NAZV (рис. 7.11). dvKafSostav.Sort = "NAZV DESC";
рис. 7.11
Отметим, что порядок сортировки можно изменить и вручную, используя элемент управления DataGrid, связанный с просмотром. Для этого всего лишь нужно щелкнуть мышью на заголовке соответствующего столбца. При этом автоматически изменится и значение свойства Sort просмотра.
374
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
7.1.8. Изменение записей просмотра Разрешение на изменение записей просмотра задают его свойства public bool AllowDelete {get; set;} public bool AllowEdit {get; set;} public bool AllowNew {get; set;}
Когда такое свойство содержит true, изменение соответствующего вида разрешено; когда false – запрещено. Поскольку просмотр содержит лишь ссылки на записи таблицы данных, то любая операция добавления, модификации либо удаления записи физически затрагивает объекты DataRow в коллекции Rows той таблицы данных, с которой связан просмотр. Добавление записей в просмотр (и соответственно, в исходную таблицу данных) производится методом DataRowView просмотра: public virtual DataRowView AddNew();
Запись можно добавить в просмотр и с использованием элементов управления, связанных с просмотром. В первую очередь это DataGrid. Если добавляемая запись не удовлетворяет условию фильтрации, заданному в свойстве RowFilter просмотра, то эта запись пропадает из просмотра, оставаясь доступной в таблице данных. Для изменения записей просмотра используются обычные операторы присваивания. Изменить значения столбцов записей просмотра можно и вручную, из элементов управления, связанных с просмотром. В первую очередь это элемент управления DataGrid. Пример программного изменения значения поля Nazv в записи с индексом ndx просмотра dvKafSostav. int ndx = …; … dvKafSostav[ndx]["Nazv"] = "Логики";
Изменение записей в просмотре может затрагивать столбцы, участвующие в условии фильтрации. В частности, пользователь может назначить полю фильтрации такое значение, когда запись просмотра перестает удовлетворять фильтру просмотра. В таком случае запись немедленно пропадает из просмотра. Удаление записи просмотра можно выполнить вручную, из элементов управления, связанных с просмотром (например, DataGrid). Программно удаление выполняется методом Delete просмотра, с указанием индекса удаляемой записи: public void Delete(int index);
или одноименным методом строки просмотра (объект DataRowView) public void Delete();
7. Фильтрация и сортировка записей данных
375
Пример. Удалим вторую запись просмотра: dvKafSostav.Delete(1);
//удаление методом просмотра
или dvKafSostav[1].Delete(); //удаление методом строки просмотра
При любом изменении записей просмотра генерируется событие ListChanged изменения списка записей просмотра: public event ListChangedEventHandler ListChanged;
Обработчик этого события имеет вид public delegate void ListChangedEventHandler( object sender, ListChangedEventArgs e );
Параметр e типа System.ComponentModel.ListChangedEventArgs включает следующие свойства: Название свойства Описание ListChangedType Возвращает вид изменения записи NewIndex Возвращает новый индекс измененной записи в просмотре OldIndex Возвращает старый индекс измененной записи в просмотре
Свойство ListChangedType возвращает одно из значений перечисления System.ComponentModel. ListChangedType: Значение ItemAdded
ItemChanged
ItemDeleted
ItemMoved
Описание Запись добавлена в просмотр. Значение ListChangedEvent Args.NewIndex содержит индекс добавленной записи Запись в просмотре изменена без изменения ее местоположения (т. е. значения столбцов, по которым производится сортировка, сортировки не затронуты либо затронуты так, что это не повлекло изменения местоположения изменяемой записи относительно других записей просмотра). ListChangedEventArgs.NewIndex содержит индекс измененной записи Запись удалена из просмотра. Значение ListChangedEvent Args.NewIndex содержит индекс удаленной записи Запись изменена таким образом, что ее местоположение относительно других записей просмотра изменилось. Значение ListChangedEventArgs.OldIndex содержит старый индекс измененной записи, а значение ListChangedEventArgs. NewIndex – новый ее индекс
PropertyDescriptor Добавлен PropertyDescriptor, что изменило схему просмотра Added PropertyDescriptor Изменен PropertyDescriptor, что изменило схему просмотра Changed PropertyDescriptor Удален PropertyDescriptor, что изменило схему просмотра Deleted
376
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Reset
Список записей просмотра сильно поменялся, в силу чего все связанные с просмотром элементы управления должны обновить показываемые в них данные
Пример использования обработчика события ListChanged. Создадим класс Modif Counter, свойства которого хранят количество изменений в просмотре данных: class ModifCounter { private static int nAddCnt = 0; private static int nUpdCnt = 0; private static int nDelCnt = 0; //---число добавленных записей public static int AddCnt { get {return nAddCnt;} set {nAddCnt = value;} } //---число модифицированных записей public static int UpdCnt { get {return nUpdCnt;} set {nUpdCnt = value;} } //---число удаленных записей public static int DelCnt { get {return nDelCnt;} set {nDelCnt = value;} } }
Увеличим свойства названного класса в обработчике событий ListChanged: private void dvKafSostav_ListChanged(object sender, System.ComponentModel.ListChangedEventArgs e) { switch (e.ListChangedType) { //---запись добавлена case ListChangedType.ItemAdded : { ModifCounter.AddCnt ++; break; } //---запись изменена case ListChangedType.ItemChanged: case ListChangedType.ItemMoved : { ModifCounter.UpdCnt ++; break; } //---запись удалена case ListChangedType.ItemDeleted : { ModifCounter.DelCnt ++; break; } } } 7. Фильтрация и сортировка записей данных
377
Следует помнить, что если мы добавим в просмотр запись, не удовлетворяющую условию фильтрации просмотра, то для нее приведенный выше обработчик сработает дважды: сначала со значением e.ListChangedType = ListChangedType.Item Added, а затем, когда запись автоматически удалится из просмотра, со значением e.ListChangedType = ListChangedType.ItemDeleted.
7.1.9. Поиск записей в просмотре Поиск записи в просмотре ведется по столбцам, указанным в свойстве Sort просмотра (по столбцам, по которым отсортированы записи в просмотре). Выполняется просмотр методами, возвращающими либо индекс строки, удовлетворяющей условию поиска, либо массив таких записей. Метод Find возвращает целочисленный индекс первой записи, удовлетворяющей заданному значению столбца (столбцов) сортировки, и –1 в случае, когда ни одной такой записи не найдено: public int Find( object key); public int Find( object[] keys );
Параметр key используется для задания поиска по одному полю: int ndx = dvKafSostav.Find("Технический");
Параметр keys употребляют при поиске по нескольким полям.
Пример. Пусть просмотр отсортирован по полям FAKULTET, NAZV. Показать значение поля KK записи, у которых поля сортировки содержат соответственно значения "Технический" и "Газовой сварки". int ndx = dvKafSostav.Find(new object[]{"Технический", "Газовой сварки"}); if (ndx == -1) { MessageBox.Show("Запись не найдена"); } else { MessageBox.Show("Найдена запись KK = " + dvKafSostav[ndx]["KK"]); }
Метод FindRows возвращает массив записей, удовлетворяющих условию поиска, задаваемого параметром key или keys. Условия поиска задается аналогично тому, как это описано выше для метода Find. public DataRowView[] FindRows( object key); public DataRowView[] FindRows( object[]keys);
Пример. Пусть просмотр отсортирован по полям FAKULTET, NAZV. Записать в элемент управления listBox1 значение поля Nazv всех записей, у которых столбцы сортировки содержат соответственно значения "Технический" и "Газовой сварки". 378
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
DataRowView[] drv = dvKafSostav.FindRows( new object[]{"Технический", "Газовой сварки"}); listBox1.Items.Clear(); foreach(DataRowView d in drv) { listBox1.Items.Add(d["Nazv"].ToString()); }
7.1.10. Просмотр как подмножество записей, удовлетворяющих произвольному условию Условие фильтрации, задаваемое для просмотра, не всегда задается в явном виде. Ниже приводится пример с заранее неизвестным условием фильтрации, интерактивно задаваемым пользователем во время выполнения приложения. Пример. В просмотре dvx показывать те записи таблицы Kaf набора данных dsVUZ1, которые в поле NAZV содержат вхождение символов, вводимых пользователем в текстовое поле элемента управления textBox1. System.Data.DataView dvx; dvx = new DataView(dsVUZ1.Kaf); … //---обработчик нажатия экранной кнопки "Найти" private void btnPoisk_Click(object sender, System.EventArgs e) { //---поисковый контекст string sFindContext = textBox1.Text.Trim(); //---фильтруем записи в просмотре dvx.RowFilter = "NAZV LIKE '%" + sFindContext + "%'"; //---связываем dataGrid и просмотр dataGrid4.DataSource = dvx; dataGrid4.CaptionText = "Кафедры, в названия которых входит '%" + sFindContext + "%'"; }
Пусть пользователь задал в textBox1 значение авт. Результат поиска показан на рис. 7.12.
рис. 7.12 7. Фильтрация и сортировка записей данных
379
7.1.11. Работа с записями просмотра Запись просмотра представляется объектом типа DataRowView. Конкретная запись просмотра возвращается индексатором public DataRowView this[ int index] {get;}
где параметр index задает индекс записи в просмотре. Пример обращения к записи просмотра: System.Data.DataView SomeView; … DataRowView drv = SomeView[ndx];
Ниже рассматриваются свойства и методы записи просмотра – объекта DataRowView. Свойство DataView возвращает ссылку на тот просмотр, к которому принадлежит данная запись: public DataView DataView {get;}
Свойство IsEdit возвращает true, если запись редактируется, и false в противном случае: public bool IsEdit {get;}
Свойство IsNew возвращает true, если запись вновь добавлена в просмотр, и false в противном случае: public bool IsNew {get;}
Обращение к столбцу записи просмотра производится через индексатор объекта DataRowView: public object this[ string property ] {get; set;}
где параметр property задает имя столбца. Пример обращения к столбцу записи просмотра: System.Data.DataView SomeView; … SomeView[ndx]["SomeColumn"] = "Значение";
Свойство Row возвращает ссылку на ту строку данных, с которой связана эта строка просмотра: public DataRow Row {get;}
Свойство RowVersion возвращает версию значений столбцов строки данных, представленных в данной записи просмотра: public DataRowVersion RowVersion {get;}
380
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Для изменения значений столбцов записи просмотра могут использоваться методы public void BeginEdit(); public void CancelEdit(); public void Delete();
7.1.12. Получение дочерних просмотров Если таблица A является родительской для таблицы B, то просмотр, построенный по таблице A, может извлечь дочерние записи из таблицы B. При этом используется объект DataRelation, реализующий в наборе данных отношение между таблицами A и B. Извлеченные дочерние записи представляются в виде просмотра. Для извлечения записей используется метод CreateChildView строки просмотра (объект DataRowView). public DataView CreateChildView( DataRelation dr ); public DataView CreateChildView( string relationName );
Пример. Пусть в наборе данных dsVUZ таблицы Kaf и Prep связаны отошением KafPrep (объект DataRelation), причем Kaf является родительской, а Prep – дочерней. Содержимое обеих таблиц показано на рис. 7.13. Пусть просмотр dwKafTehno создан для таблицы с условием фильтрации "FAKULTET = 'Технический'". Создадим для текущей строки dwKafTehno просмотра новый просмотр, состоящий из дочерних записей таблицы Prep (рис. 7.14). System.Data.DataView dv; int ndx = this.BindingContext[dwKafTehno].Position; dv = dwKafTehno[ndx].CreateChildView("KafPrep");
рис. 7.13
7. Фильтрация и сортировка записей данных
рис. 7.14
381
7.1.13. Умолчательный просмотр таблицы набора данных У таблицы данных – объекта DataTable есть свойство DefaultView: public DataView DefaultView {get;}
Оно возвращает ссылку на умолчательный просмотр таблицы. Для каждой таблицы по умолчанию создается такой просмотр, причем условие фильтрации для него не задается, поэтому после заливки данных в таблицы такой просмотр содержит все записи таблицы. Начальная сортировка просмотра аналогична сортировке, принятой для таблицы. С умолчательным просмотром можно работать так же, как и с любым другим просмотром, за исключением того, что его не нужно создавать. Например, после занесения записей в таблицу Kaf набора данных dsVUZ1 задать сортировку для умолчательного просмотра этой таблицы dsVUZ1.Kaf.DefaultView.Sort = "NAZV, FAKULTET";
7.2. Менеджер просмотров DataViewManager 7.2.1. Назначение менеджеров просмотров Для набора данных можно создать менеджер просмотров. Это объект типа System.Data.DataViewManager. Он содержит коллекцию DataViewSettings. При создании менеджера просмотров в этой коллекции для каждой таблицы набора данных автоматически добавляется объект типа System.Data.DataViewSetting. Объект DataViewSetting содержит ряд свойств, задающих условие фильтрации и(или) порядок сортировки в таблице данных. Изначально для всех таблиц эти свойства содержат пустые значения. Таким образом, менеджер просмотров DataViewManager содержит коллекцию настроек, по одной для каждой таблицы набора данных. Менеджер просмотров можно использовать как источник данных при связывании данных и элементов управления в форме Windows. При этом в качестве раздела источника указывается имя таблицы. В результате мы получим доступ к просмотру данных, как если бы мы явно создали для этой таблицы просмотр с такими же параметрами, какие указаны в объекте DataViewSetting. Пример. Пусть набор данных ds1 состоит из двух таблиц – Kaf и Prep. Нужно создать в форме два элемента управления DataGrid. В первом нужно представить записи таблицы Kaf, у которых значение столбца Sostav не меньше 10. При этом записи нужно отсортировать по возрастанию значений в столбце Sostav. Во втором DataGrid нужно представить записи таблицы Prep, у которых столбец Rank содержит значение 'Доцент'. Для решения поставленной задачи используем менеджеры просмотров. //---создадим менеджер просмотров для набора ds1 DataViewManager DVM = new DataViewManager(ds1);
382
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
//---зададим настройки для таблицы Kaf DVM.DataViewSettings["Kaf"].RowFilter = "SOSTAV > 10"; DVM.DataViewSettings["Kaf"].Sort = "SOSTAV"; //---зададим настройки для таблицы Prep DVM.DataViewSettings["Prep"].RowFilter = "RANK = 'Доцент'"; //---свяжем dataGrid1 с менеджером и настройками для Kaf dataGrid1.DataSource = DVM; dataGrid1.DataMember = "Kaf"; //---свяжем dataGrid1 с менеджером и настройками для Prep dataGrid2.DataSource = DVM; dataGrid2.DataMember = "Prep";
Результат выполнения кода показан на рис. 7.15.
рис. 7.15
7.2.2. Создание менеджеров просмотров Для создания менеджера просмотров используются следующие разновидности конструктора: public DataViewManager(); public DataViewManager( DataSet dataset );
Если имя набора данных не указано как параметр при вызове конструктора, это затем необходимо сделать в свойстве DataSet менеджера просмотров: public DataSet DataSet {get; set;}
Например, оба приводимых ниже варианта создания менеджера просмотров и связывания его с набором данных ds1 равнозначны: DataViewManager DVM = new DataViewManager(ds1);
или DataViewManager DVM = new DataViewManager(); DVM.DataSet = ds1;
7. Фильтрация и сортировка записей данных
383
7.2.3. Задание индивидуальных настроек таблиц набора данных Свойство DataViewSettings менеджера просмотров содержит коллекцию объектов System.Data.DataViewSetting. Каждый из них задает настройки условий фильтрации и(или) порядка сортировки для одной из таблиц набора данных: public DataViewSettingCollection DataViewSettings {get;}
Для обращения к конкретному элементу DataViewSetting в коллекции DataViewSettings используется один из следующих индексаторов: public virtual DataViewSetting this[ DataTable table ] {get; set;} public virtual DataViewSetting this[ string tableName ] {get;} public virtual DataViewSetting this[ int index ] {get; set;} • • •
При этом в качестве индексов могут выступать: ссылка на таблицу (table); строковое имя таблицы (tableName); индекс таблицы (index) в коллекции Tables того набора данных, с которым ассоциирован менеджер просмотров. Примеры обращения к настройкам таблицы:
DVM.DataViewSettings["Kaf"].Sort = "SOSTAV"; DVM.DataViewSettings[ds1.Kaf].Sort = "SOSTAV"; DVM.DataViewSettings[0].Sort = "SOSTAV";
Объект DataViewSetting содержит следующие свойства. Свойство ApplyDefaultSort возвращает значение true, если для таблицы используется принятый по умолчанию порядок сортировки, и false в противном случае. public bool ApplyDefaultSort {get; set;}
Например, если в настройках для таблицы Kaf ранее не был задан порядок сортировки по значениям столбца Sostav, то установить такой порядок сортировки: DataViewManager DVM;
…
if (DVM.DataViewSettings["Kaf"].ApplyDefaultSort) { DVM.DataViewSettings["Kaf"].Sort = "SOSTAV"; }
Свойство RowFilter задает условие фильтрации записей таблицы: public string RowFilter {get; set;}
Например, задать фильтрацию только по тем записям таблицы Prep, у которых содержится вхождение в столбец FIO строки ов: DataViewManager DVM; … DVM.DataViewSettings["Prep"].RowFilter = "FIO LIKE '%ов%'";
Свойство RowStateFilter задает фильтрацию по версии записей таблицы: public DataViewRowState RowStateFilter {get; set;}
384
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Например, задать фильтрацию только по неизменявшимся строкам таблицы Prep: DataViewManager DVM; … DVM.DataViewSettings["Prep"].RowStateFilter = DataViewRowState.Unchanged;
Свойство Sort задает порядок сортировки: public string Sort {get; set;}
Например, задать для таблицы Kaf сортировку по столбцу Sostav: DataViewManager DVM;
…
DVM.DataViewSettings["Kaf"].Sort = "SOSTAV";
Свойство Table позволяет определить, для какой таблицы задаются настройки фильтрации и(или) сортировки: public DataTable Table {get;}
Свойство DataViewManager возвращает ссылку на тот менеджер просмотров, к коллекции DataViewSettings которого принадлежит данный объект DataView Setting: public DataViewManager DataViewManager {get;}
7. Фильтрация и сортировка записей данных
385
8 ЧАСТЬ 8 ОТНОШЕНИЯ МЕЖДУ ТАБЛИЦАМИ НАБОРА ДАННЫХ 8.1. Отношения между таблицами набора данных Таблицы в базе данных могут быть связаны так называемыми реляционными отношениями (связями), т. е. отношениями подчинения. Такие отношения бывают трех типов: "один-к-одному", "один-ко-многим" (либо "многие-к-одному") и "многие-ко-многим". Особенности реализации отношений между таблицами рассматриваются ниже. Когда таблицы данных связаны с другими таблицами, то всегда возникает проблема внесения изменений в одну из связанных таблиц. Изменения должны вноситься так, чтобы не нарушилась ссылочная целостность данных. Особенности ее поддержания также обсуждаются ниже. Поскольку набор данных – это как бы база данных в миниатюре, то внутри набора таблицы тоже могут связываться отношениями. Последние реализуются в ADO.NET при помощи объектов типа DataRelation. Все такие отношения хранятся в коллекции Relations набора данных.
8.1.1. Типы связей (отношений) таблиц базы данных 8.1.1.1. Отношение "один-ко-многим" Таблица "Товары" Товар Сахар Макароны Куры Фанта
Ед. изм. кг кг кг бут.1 л
Таблица "Отпуск товаров"
Цена ед. 5000 7000 10000 6000
Товар Сахар Сахар Сахар Макароны Макароны Фанта Фанта
Дата 10.01.02 12.01.02 14.01.02 10.01.02 11.01.02 10.01.02 12.01.02
Кол-во, ед. 100 200 50 1000 500 2000 3000
рис. 8.1
Как видно из рис. 8.1, одной записи из родительской таблицы Товары может соответствовать несколько записей в дочерней таблице Отпуск товаров. Обратите внимание на глагол может: он означает, что такая возможность потенциальная и что в родительской таблице могут быть записи, для которых в данный момент нет записей в дочерней таблице (например, товар Куры). Различают две разновидности связи "один-ко-многим": в первом случае выдвигается жесткое требование, согласно которому всякой записи в родительской таблице должны соответствовать записи в дочерней таблице; во втором случае подобное требование не носит жесткого характера и подразумевается (как в описанном выше слу386
чае), что некоторые записи в родительской таблице могут не иметь связанных с ними записей в дочерней таблице. Связь "один-ко-многим" является самой распространенной для реляционных баз данных. Как можно заметить, она позволяет моделировать иерархические структуры данных.
8.1.1.2. Отношение "один-к-одному" Отношение "один-к-одному" имеет место, когда одной записи в родительской таблице соответствует одна запись в дочерней таблице (рис. 8.2). Таблица "Сотрудники" № п/п 1 2 3 ...
Ф.И.О Иванов И.И. Петров П.П. Васин В.В. ...
Должность Инженер Бухгалтер Прораб ...
Таблица "Информация о сотрудниках" Отдел
10 20 10 ...
№ п/п 1 2 3 ...
Год рождения 1940 1942 1960 ...
Количество детей 3 1 2 ...
... ... ... ... ...
рис. 8.2
Данное отношение встречается много реже, чем отношение "один-ко-многим". Его используют, если не хотят, чтобы таблица базы данных "распухала" от второстепенной информации. Связь "один-к-одному" приводит к тому, что для чтения связанной информации в нескольких таблицах приходится производить несколько операций чтения, что замедляет получение нужной справки. Кроме того, базы данных, в состав которых входят таблицы со связью "один-к-одному", не могут считаться полностью нормализованными (о нормализации см. ниже). Подобно связи "один-ко-многим", связь "один-к-одному" может быть жесткой и нежесткой.
8.1.1.3. Отношение "многие-ко-многим" На рис. 8.3 показаны таблицы, состоящие в отношении "многие-ко-многим". Каждой учебной группе соответствует несколько преподавателей. Каждый преподаватель может вести, во-первых, несколько разных предметов и, во-вторых, преподавать в разных группах. Таблица "Учебные группы и дисциплины"
Группа ПС-1 ТИ-1 ПС-1 РТ-2 ПС-1 ...
Предмет Программирование Программирование Теория систем Философия Социология ...
Таблица "Преподаватели" № преподавателя 10 12 62 78 85 ...
№ преподавателя 10 12 10 62 62 ...
Ф.И.О Краснов Ю.Б. Володин В.Н. Булгаков В.М. Гноенский Л.С. Подушкин М.А. ...
Кафедра ТИ-1 ТИ-1 РИО ТИ-1 ЭИ-1 ...
рис. 8.3 8. Отношения между таблицами набора данных
387
Многие СУБД не поддерживают связи "многие-ко-многим" на уровне индексов и ссылочной целостности (см. следующий подраздел), хотя и позволяют реализовывать ее в таблицах неявным образом. Считается, что базу данных можно перестроить так, чтобы любая связь "многие-ко-многим" была заменена на одну или более связей "один-ко-многим".
8.1.1.4. Связи между записями одной таблицы Между записями одной таблицы также могут существовать связи, т. е. одни записи могут ссылаться на другие. Пусть в реляционной базе данных необходимо хранить древовидную структуру произвольного уровня, например структуру организации (рис. 8.4). Департамент автоматизации Техническое управление Отдел сетевого оборудования Ремонтный отдел АТС Управление программными системами Отдел эксплуатации Информационная группа Административная группа Диспетчерское бюро Отдел разработки рис. 8.4
В этом случае можно создать таблицу (рис. 8.5), в которой каждому подразделению организации соответствует одна запись. Эта запись ссылается на запись, соответствующую подразделению более высокого уровня, в которое входит данное подразделение. И только в записи о подразделении самого высокого уровня нет подобной ссылки. № подразНазвание подразделения деления 1 Департамент автоматизации 2 Техническое управление 3 Управление разработки и эксплуатации программных систем 4 Отдел сетевого оборудования 5 Ремонтный отдел 6 АТС 7 Отдел эксплуатации 8 Отдел разработки 9 Информационная группа 10 Административная группа 11 Диспетчерское бюро
№ подразделения предыдущего уровня 1 1 2 2 2 3 3 7 7 10
рис. 8.5
388
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
8.1.2. Ссылочная целостность Рассмотрим наиболее часто встречающуюся в базах данных связь "один-комногим". Соответствующие таблицы показаны на рис. 8.6. Как можно заметить, дочерняя и родительская таблицы связаны между собой по общему полю Товар. Назовем это поле полем связи. Таблица "Товары" Товар Сахар Макароны Куры Фанта
Ед. изм. кг кг кг бут.1 л
Таблица "Отпуск товаров"
Цена ед. 5000 7000 10000 6000
Товар Сахар Сахар Сахар Макароны
Дата 10.01.02 12.01.02 14.01.02 10.01.02
Кол-во, ед. 100 200 50 1000
Макароны Фанта Фанта
11.01.02 10.01.02 12.01.02
500 2000 3000
рис. 8.6
Возможны два вида изменений, которые приведут к утере связей между записями в родительской и дочерней таблицах: • изменение значения поля связи в записи родительской таблицы без изменения значений полей связи в соответствующих записях дочерней таблицы; • изменение значения поля связи в одной из записей дочерней таблицы без соответствующего изменения значения полей связи в родительской и дочерней таблицах. Рассмотрим первый случай. На рис. 8.7 показано изменение значения поля Товар с Сахар на Рафинад в таблице Товары. В таблице Отпуск товаров значение поля связи Сахар осталось прежним. В результате: • в дочерней таблице Отпуск товаров для товара Рафинад (таблица Товары) нет сведений о его отпуске со склада; • некоторые записи таблицы Отпуск товаров содержат сведения об отпуске товара Сахар, о котором нет информации в таблице Товары. Таблица "Товары" Товар Рафинад Макароны Куры Фанта
Ед. изм. кг кг кг бут.1 л
Таблица "Отпуск товаров" Цена ед. 5000 7000 10000 6000
Товар Сахар Сахар Сахар Макароны
Дата 10.01.02 12.01.02 14.01.02 10.01.02
Кол-во, ед. 100 200 50 1000
Макароны Фанта Фанта
11.01.02 10.01.02 12.01.02
500 2000 3000
рис. 8.7 8. Отношения между таблицами набора данных
389
Рассмотрим второй случай. Пусть в одной из записей таблицы Отпуск товаров значение поля связи Сахар изменилось на Рафинад (рис. 8.8). В результате: • в дочерней таблице Отпуск товаров недостоверны сведения об отпуске со склада товара Сахар (таблица Товары); • одна из записей таблицы Отпуск товаров содержит данные об отпуске товара Рафинад, сведения о котором отсутствуют в таблице Товары. Таблица "Товары"
Таблица "Отпуск товаров"
Товар Сахар
Ед. изм. кг
Цена ед. 5000
Товар Рафинад
Дата 10.01.02
Кол-во, ед. 100
Макароны
кг
7000
Сахар
12.01.02
200
Куры
кг
10000
Сахар
14.01.02
50
Фанта
бут.1 л
6000
Макароны
10.01.02
1000
Макароны
11.01.02
500
Фанта
10.01.02
2000
Фанта
12.01.02
3000
рис. 8.8
И в первом и во втором случае мы наблюдаем нарушение целостности базы данных, что означает, что хранящаяся в ней информация становится недостоверной. СУБД обычно блокирует действия, которые нарушают целостность связей между таблицами, т. е. нарушают ссылочную целостность. Когда говорят о ссылочной целостности, имеют в виду совокупность связей между отдельными таблицами во всей базе данных. Нарушение хотя бы одной такой связи делает информацию в базе данных недостоверной. Чтобы предотвратить потерю ссылочной целостности, используется механизм каскадных изменений. Он состоит в обеспечении следующих действий: • при изменении поля связи в записи родительской таблице следует синхронно изменить значения полей связи в соответствующих записях дочерней таблицы; • при удалении записи в родительской таблице следует удалить соответствующие записи в дочерней таблице. Изменения или удаления в записях дочерней таблицы при одновременном изменении (удалении) записи родительской таблицы называются каскадными изменениями и каскадными удалениями. Существует другая разновидность каскадного удаления: при удалении родительской записи в записях дочерних таблиц значения полей связи обнуляются. Эта разновидность применяется редко, так как дочерние таблицы будут содержать избыточные данные, например сведения о товаре, которого нет на складе. Обычно для реализации ссылочной целостности в дочерней таблице создают внешний ключ, в который входят поля связи дочерней таблицы. Этот ключ для дочерней таблицы является первичным и поэтому по составу полей должен совпадать с первичным ключом родительской таблицы или – реже – с частью первичного ключа. 390
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
8.1.3. Способы задания отношений между таблицами набора данных 8.1.3.1. Создание отношения "вручную" Рассмотрим задание отношения "один-ко-многим" для таблиц publishers и titles учебно-демонстрационной базы данных pubs, входящей в комплект поставки SQL Server. Создадим приложение и установим в нем соединение с базой данных pubs. Создадим два адаптера данных, sqlDataAdapter1 и sqlDataAdapter2, для связи с таблицами publishers и titles. Сгенерируем для этих адаптеров набор данных dsPublishersTitles1, включающий обе названные таблицы. Отыщем в окне Solution Explorer схему этого набора данных (dsPublishersTitles.xsd) и щелкнем по нему мышью, после чего попадем в окно редактора схем XML. Как показано на рис. 8.9, первоначально схема набора данных содержит две не связанные друг с другом таблицы данных.
рис. 8.9
Чтобы установить отношение между таблицами publishers (родительская) и titles (дочерняя) по ключу pub_id, нужно мышью выделить обе таблицы и затем нажать правую кнопку мыши. В появившемся контекстном меню нужно выбрать элемент New Relation (рис. 8.10). В появившемся на экране диалоге Edit Relation (рис. 8.11) в принципе ничего менять не нужно. Рассмотрим назначение полей диалога. Поле Name задает имя отношения. Поля Parent element и Child element задают соответственно родительскую и дочернюю таблицы. Список Fields задает столбцы (в данном случае один столбец), по которому связываются записи родительской и дочерней таблиц. Столбец Key Fields задает столбцы родительской таблицы, Foreign Key Fields – дочерней. 8. Отношения между таблицами набора данных
391
рис. 8.10
рис. 8.11
392
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Поля Update Rule, Delete Rule и Accept / Reject Rule задают действия каскадного изменения дочерних записей при изменении, удалении родительской записи (Update Rule, Delete Rule), а также при подтверждении или откате изменений (Accept / Reject Rule) методами AcceptChanges и RejectChanges. После нажатия кнопки OK между таблицами установится отношение подчинения. Представление этого факта в дизайнере схем XML показано на рис. 8.12. В коллекцию Relations будет добавлен новый элемент (объект типа System.Data.Data Relation) с именем "publisherstitles".
рис. 8.12
Создадим в форме приложения два элемента управления типа DataGrid. Первый будет показывать записи из родительской таблицы publishers. В его свойстве DataSource укажем ссылку на набор данных dsPublishersTitles1, а в свойстве DataMember – имя таблицы publishers. Во втором элементе управления DataGrid мы хотим показывать дочерние записи из таблицы titles, подчиненные текущей записи в таблице publishers. В свойстве DataSource этого DataGrid укажем ссылку на набор данных dsPublishers Titles1, а в свойстве DataMember – имя отношения publisherstitles (рис. 8.13). рис. 8.13 Запустим приложение на выполнение и убедимся, что во втором DataGrid присутствуют только те записи дочерней таблицы titles, значение столбца pub_id у которых совпадает со значением аналогичного столбца текущей записи родительской таблицы publishers (рис. 8.14).
8. Отношения между таблицами набора данных
393
рис. 8.14
Отметим: верхний DataGrid может представлять данные обеих связанных таблиц. Выберем в этом элементе управления какую-либо запись родительской таблицы (например, с кодом 1389) и нажмем крестик в столбце заголовков строк. Появится список всех отношений, для которых таблица publishers выступает в качестве родительской таблицы (рис. 8.15). В нашем наборе только одно такое отношение.
рис. 8.15
Выберем мышью имя отношения (publisherstitles). В ответ на это DataGrid станет показывать записи дочерней таблицы titles, значение столбца pub_id в которых совпадает со значением аналогичного столбца текущей записи родительской таблицы publishers (рис. 8.16). 394
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
рис. 8.16
Информация о текущей записи родительской таблицы publishers показывается при этом в заголовке DataGrid. Вернуться к показу записей таблицы publishers можно, нажав стрелку в правом верхнем углу DataGrid. Возникает вопрос: а что если бы для нижнего элемента управления DataGrid (показывающего записи таблицы titles) в свойстве DataMember указать не имя объекта DataRelation (publishers.publisherstitles), а просто таблицы titles (рис. 8.17)?
рис. 8.17
В этом случае в DataGrid будут показываться все записи таблицы titles, вне зависимости от значения столбца pub_id текущей записи родительской таблицы publishers (рис. 8.18).
8. Отношения между таблицами набора данных
рис. 8.18
395
8.1.4. Программное создание отношения Для программной установки связи между двумя таблицами нужно создать соответствующий объект DataRelation и добавить его в коллекцию Relations набора данных. Для создания объекта DataRelation, реализующего связь между таблицами, можно использовать следующие разновидности конструктора: public DataRelation( string relationName, DataColumn ParentColumn, DataColumn ChildColumn ); public DataRelation( string relationName, DataColumn ParentColumn, DataColumn ChildColumn, bool createConstraints ); public DataRelation( string relationName, DataColumn[] ParentColumns, DataColumn[] ChildColumns ); public DataRelation( string relationName, DataColumn[] ParentColumns, DataColumn[] ChildColumns, bool createConstraints );
Назначение параметров: relationName – имя отношения; ParentColumn – столбец первичного ключа родительской таблицы (если ключ состоит из одного столбца); ParentColumns – массив столбцов первичного ключа родительской таблицы (если ключ состоит более чем из одного столбца); ChildColumn – столбец внешнего ключа дочерней таблицы (если ключ состоит из одного столбца); ChildColumns – массив столбцов внешнего ключа дочерней таблицы (если ключ состоит более чем из одного столбца); createConstraints – если true, то отношение создается; если false – не создается. Пример. Создать связь между таблицами Kaf (родительская) и Prep (дочерняя) набора данных nd1. В элементе управления dataGrid2 показывать записи таблицы Prep, подчиненные текущей записи таблицы Kaf. Пусть в схеме набора данных nd1 связи между названными таблицами нет (рис. 8.19).
рис. 8.19
396
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Создадим в приложении форму и поместим в нее два элемента управления DataGrid. Первый предназначен для показа записей таблицы Kaf (DataSource = nd1, DataMember = "Kaf"). Второй будет показывать все записи таблицы Kaf (DataSource = nd1, DataMember = "Prep"). Пример первоначального использования формы показан на рис. 8.20. Зададим следующий обработчик для события нажатия кнопки Добавим связь:
рис. 8.20
//---создание объекта связи System.Data.DataRelation dr = new System.Data.DataRelation("Svyaz", nd1.Kaf.Columns["KK"], nd1.Prep.Columns["KK"], true); //---добавление его в набор данных nd1.Relations.Add(dr); //---dataGrid2 будет показывать только подчиненные записи из Prep dataGrid2.DataMember = "Kaf.Svyaz";
После нажатия этой кнопки в набор данных будет добавлено отношение между таблицами Kaf и Prep. Элемент управления dataGrid2 отныне станет показывать только те записи из Prep, которые подчинены текущей записи в таблице Kaf (рис. 8.21).
рис. 8.21 8. Отношения между таблицами набора данных
397
8.1.5. Коллекция Relations набора данных Все отношения между таблицами набора данных содержатся в его коллекции Relations – объекте типа System.Data.DataRelationCollection. Для обращения к элементу коллекции используется индексатор коллекции, где элемент идентифицируется своим именем или индексом: public abstract DataRelation this[ string name ] {get;} public abstract DataRelation this[ int index ] {get;}
Например, обратиться ко второму по счету элементу с именем "MyRelation" можно так: nd1.Relations[1]
или так: nd1.Relations["MyRelation "]
Число элементов в коллекции Relations возвращается ее свойством Count. Для добавления элементов в коллекцию используются методы Add и AddRange. Свойство CanRemove возвращает true, если элементы коллекции можно удалять. Для удаления используют методы Remove и RemoveAt, для очистки – метод Clear. Содержится ли элемент в коллекции, поможет установить метод Contains. Метод IndexOf возвращает индекс элемента в коллекции.
8.1.6. Объект DataRelation Объект System.Data.DataRelation задает отношение между двумя таблицами набора данных. Рассмотрим свойства этого объекта. Имя отношения содержится в свойстве public virtual string RelationName {get; set;}
Ссылка на набор данных, к которому принадлежит отношение, возвращается свойством public virtual DataSet DataSet {get;}
8.1.6.1. Сведения о дочерней таблице отношения Ссылка на дочернюю таблицу отношения возвращается свойством public virtual DataTable ChildTable {get;}
Набор столбцов внешнего ключа дочерней таблицы отношения возвращается свойством public virtual DataColumn[] ChildColumns {get;}
Ссылка на ограничение внешнего ключа дочерней таблицы возвращается свойством public virtual ForeignKeyConstraint ChildKeyConstraint {get;}
8.1.6.2. Сведения о родительской таблице отношения Ссылка на родительскую таблицу отношения возвращается свойством 398
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
public virtual DataTable ParentTable {get;}
Набор столбцов первичного ключа родительской таблицы отношения возвращается свойством public virtual DataColumn[] ParentColumns {get;}
Ссылка на ограничение уникального ключа родительской таблицы возвращается свойством public virtual UniqueConstraint ParentKeyConstraint {get;}
8.1.7. Ограничение внешнего ключа ForeignKeyConstraint и правила каскадных изменений 8.1.7.1. Сведения о таблицах и ключах отношения, к которому принадлежит ограничение Свойство ChildKeyConstraint объекта DataRelation возвращает ссылку на ограничение внешнего ключа дочерней таблицы. Названное ограничение представляется объектом System.Data.ForeignKeyConstraint. Рассмотрим свойства ограничения. Имя ограничения задается в свойстве public virtual string ConstraintName {get; set;}
Свойство RelatedTable возвращает ссылку на родительскую таблицу отношения, которому соответствует данное ограничение: public virtual DataTable RelatedTable {get;}
Свойство Table возвращает ссылку на дочернюю таблицу отношения, т. е. таблицу, для которой построен этот внешний ключ: public override DataTable Table {get;}
Свойство RelatedColumns возвращает набор столбцов уникального ключа родительской таблицы ограничения: public virtual DataColumn[] RelatedColumns {get;}
Свойство Columns возвращает набор столбцов данного внешнего ключа: public virtual DataColumn[] Columns {get;}
8.1.7.2. Каскадные изменения Свойство AcceptRejectRule объекта ограничения внешнего ключа определяет, что нужно делать, когда для таблицы, к которой принадлежит ограничение ForeignKeyConstraint (т. е. дочерней таблице отношения), выполнены ее методы AcceptChanges или RejectChanges: public virtual AcceptRejectRule AcceptRejectRule {get; set;}
Значением свойства может быть одно из значений перечисления System.Data. AcceptRejectRule: 8. Отношения между таблицами набора данных
399
Значение Cascade None
Описание Каскадные изменения в связанных записях Никаких действий
Свойство DeleteRule задает действие каскадного изменения в дочерней таблице при удалении связанной записи в родительской таблице: public virtual Rule DeleteRule {get; set;}
Свойство UpdateRule задает действие каскадного изменения для дочерней таблицы в случае модификации значений столбцов родительской таблицы, по которым она связана с дочерней: public virtual Rule UpdateRule {get; set;}
Оба свойства должны содержать одно из следующих значений перечисления System.Data.Rule: Значение Cascade None SetDefault SetNull
Описание По умолчанию удаление или обновление дочерних записей Никаких действий Установка значения связанных записей на значение, заданное в их свойстве DefaultValue Установка значений связанных записей, равных DBNull
8.1.8. Ограничение уникального ключа родительской таблицы UniqueConstraint Свойство ParentKeyConstraint объекта DataRelation возвращает ссылку на ограничение уникального ключа родительской таблицы. Названное ограничение реализуется объектом System.Data.UniqueConstraint. Рассмотрим свойства ограничения. Имя ограничения задается в свойстве public virtual string ConstraintName {get; set;}
Ссылка на родительску таблицу ограничения возвращается свойством public override DataTable Table {get;}
Свойство Columns возвращает набор столбцов уникального ключа: public virtual DataColumn[] Columns {get;}
Свойство IsPrimaryKey возвращает true, если тот уникальный ключ, по которому построено данное ограничение, – это первичный ключ родительской таблицы. public bool IsPrimaryKey {get;}
8.1.9. Пример доступа к основным объектам отношения Пусть имеется набор данных nd1. Известно, что в нем задано единственное отношение. Стоит задача – вывести в элемент управления textBox1 сведения о родительской и дочерней таблицах отношения и о составе уникального и внешнего ключа соединения. 400 ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
textBox1.Text = ""; //---отношение System.Data.DataRelation dr = nd1.Relations[0]; //---переменная под список столбцов string sColList = ""; //---список столбцов внешнего ключа foreach (DataColumn dc in dr.ChildColumns) { sColList = sColList + dc.ColumnName + " "; } textBox1.Text = textBox1.Text + "Внешний ключ: " + sColList + (char)13 + (char)10; //---действие каскадного изменения при удалении //---родительской записи System.Data.ForeignKeyConstraint fc = dr.ChildKeyConstraint; System.Data.Rule r = fc.DeleteRule; textBox1.Text = textBox1.Text + "Каскадное изменение при удалении: " + r.ToString() + (char)13 + (char)10; //---имя дочерней таблицы DataTable t = dr.ChildTable; textBox1.Text = textBox1.Text + "Дочерняя таблица: " + t.TableName + (char)13 + (char)10; //---перечень столбцов в уникальном ключе родительской таблицы sColList = ""; foreach (DataColumn dc in dr.ParentColumns) { sColList = sColList + dc.ColumnName + " "; } textBox1.Text = textBox1.Text + "Уникальный ключ: " + sColList + (char)13 + (char)10; //---является ли уникальный ключ родительской таблицы //---ее первичным ключом System.Data.UniqueConstraint uc = dr.ParentKeyConstraint; textBox1.Text = textBox1.Text + "Первичный ключ: " + uc.IsPrimaryKey.ToString() + (char)13 + (char)10; //---имя родительской таблицы DataTable pt = dr.ParentTable; textBox1.Text = textBox1.Text + "Родительская таблица: " + pt.TableName + (char)13 + (char)10;
Пример отработки приведенного выше кода представлен на рис. 8.22.
рис. 8.22 8. Отношения между таблицами набора данных
401
8.2. Программный доступ к дочерним записям подчиненных таблиц В этой главе рассматривается, как из родительской таблицы программно получить и обработать подчиненные записи дочерней таблицы. Когда между двумя таблицами задано отношение в наборе данных, для доступа к записям дочерней таблицы можно использовать: • массив дочерних записей, возвращаемый методом GetChildRows записи родительской таблицы (объект DataRow); • просмотр (объект DataView), создаваемый для представления дочерних записей; • ячейки элемента управления DataGrid.
8.2.1. Метод GetChildRows В родительской таблице выбирают одну из записей и вызывают ее метод GetChildRows, чтобы получить подчиненные ей записи дочерней таблицы. Результат возвращается в виде массива записей. Метод имеет следующие разновидности: public DataRow[] GetChildRows( DataRelation relation); public DataRow[] GetChildRows( DataRelation relation, DataRowVersion version ); public DataRow[] GetChildRows( string relationName ); public DataRow[] GetChildRows( string relationName, DataRowVersion version );
Параметры: relation – ссылка на объект DataRelation, устанавливающий связь между таблицами; relationName – имя объекта DataRelation, устанавливающего связь между таблицами; version – версия записей. Число дочерних записей можно узнать из свойства Length полученного массива записей. Пример. Пусть в наборе данных nd1 имеем родительскую таблицу Kaf, подчиненную таблицу Prep и объект DataRelation с именем "Преподаватели_Кафедры", задающий связь между названными таблицами. Необходимо получить дочерние записи в таблице Prep для текущей записи таблицы Kaf и перезаписать в дочерних записях содержимое столбца FIO, приведя старое значение этого столбца к верхнему регистру. Для решения поставленной задачи можно использовать следующий код: //---текущая родительская запись (таблица Kaf) DataRow CurrentKafRow = nd1.Kaf.Rows[this.BindingContext[nd1, "Kaf"].Position]; //---массив дочерних записей для текущей записи в Kaf
402
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
DataRow[] ChildPrepRows = CurrentKafRow.GetChildRows("Преподаватели_Кафедры"); //---изменяем записи массива for (int i = 0; i < ChildPrepRows.Length; i++){ ChildPrepRows[i]["FIO"] = ChildPrepRows[i]["FIO"].ToString().ToUpper(); }
8.2.2. Просмотры и записи дочерних таблиц Чтобы представить записи дочерней таблицы в элементе управления DataGrid, неявно создается просмотр данных (объект DataView). Чтобы получить ссылку на этот просмотр, нужно сначала получить ссылку на текущую запись этого просмотра (объект DataRowView): DataRowView drw = (DataRowView)this.BindingContext[ИмяНД, ИмяДочернегоDataGrid.DataMember].Current;
и затем от нее перейти к самому просмотру: DataView dw = drw.DataView;
Свойство Count полученного просмотра возвращает число содержащихся в просмотре записей. Индексатор просмотра позволяет обратиться к различным записям просмотра. Если нужно напрямую обратиться к той записи данных (объект DataRow), с которой ассоциирована конкретная строка просмотра (объект DataRowView), можно использовать свойство Row последней. Пример. Пусть в наборе данных nd1 имеем две таблицы – родительскую (Kaf) и дочернюю (Prep) и между ними задана связь с именем "Преподаватели_Кафедры" (рис. 8.23).
рис. 8.23
Представим в форме содержимое таблиц таким образом, чтобы размещенный в нижней части формы элемент управления dataGridChild предоставлял доступ только к дочерним записям, подчиненным текущей записи в родительской таблице Kaf (рис. 8.24). 8. Отношения между таблицами набора данных
403
рис. 8.24
Во всех записях таблицы Prep, доступных в элементе управления dataGrid Child, изменим содержимое столбца FIO, приведя старое значение этого столбца к верхнему регистру. Используем для этого неявный просмотр, ассоциированный с dataGridChild: //---текущая запись просмотра, содержащего дочерние записи DataRowView drw = (DataRowView)this.BindingContext[nd1, dataGridChild.DataMember].Current; //---просмотр, содержащий дочерние записи DataView dw = drw.DataView; //---изменяем записи просмотра for (int i = 0; i < drw.DataView.Count; i++) { dw[i]["FIO"] = dw[i]["FIO"].ToString().ToUpper(); }
Результат показан на рис. 8.25.
рис. 8.25
404
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
8.2.3. DataGrid и записи дочерних таблиц Когда записи дочерней таблицы представляются в элементе управления DataGrid, то доступ к записям возможен не только в "ручном" режиме, но и в программном. Доступ к данным и их изменение можно выполнять не ссылаясь при этом ни на какие таблицы, записи и просмотры данных. Вполне достаточно использовать ячейки DataGrid, т. е. объекты DataGridCell. Все изменения, вносимые в значения, хранящиеся в ячейках DataGridCell, автоматически отражаются в связанных с ними столбцах данных и немедленно переводят соответствующие строки данных в состояние, отличное от Unchanged. Для обращения к ячейкам элемента управления DataGrid используют его индексатор: public object this[ DataGridCell cell ] {get; set;} public object this[ int rowIndex, int colIndex ] {get; set;}
указывая в качестве индексов либо номер строки и столбца, либо ссылку на какуюлибо ячейку DataGrid. В качестве последней может, например, выступать текущая ячейку DataGrid, ссылка на которую возвращается свойством CurrentCell названного объекта. Пример. Стартовые условия такие же, как в предыдущем примере. Пусть в наборе данных nd1 имеем две таблицы – родительскую (Kaf) и дочернюю (Prep) и между ними задана связь с именем "Преподаватели_Кафедры". В форме содержимое таблиц представлено таким образом, чтобы размещенный в нижней части формы элемент управления dataGridChild содержал только записи таблицы Prep, подчиненные текущей записи в таблице Kaf. Во всех записях таблицы Prep доступным в элементе управления dataGrid Child изменим содержимое столбца FIO, переведя старое значение этого столбца к верхнему регистру. Используем для этого неявный просмотр, ассоциированный с dataGridChild: //---текущая запись просмотра, содержащего дочерние записи DataRowView drw = (DataRowView)this.BindingContext[nd1, dataGridChild.DataMember].Current; //---просмотр, содержащий дочерние записи DataView dw = drw.DataView; //---изменяем ячейки грида for (int i = 0; i < drw.DataView.Count; i++) { dataGridChild[i, 2] = dataGridChild[i, 2].ToString().ToUpper(); }
8. Отношения между таблицами набора данных
405
9 ЧАСТЬ 9 ИСПОЛЬЗОВАНИЕ XML 9.1. Введение в XML В этой части книги речь пойдет о схемах XML и о том, как с ними работать в ADO.NET и Visual Studio.NET. Тема эта для практического использования крайне важная, поскольку именно схема описывает структуру набора данных, т. е.: • все таблицы, входящие в набор; • связи между таблицами; • ограничения первичного, внешнего ключа и просто уникального столбца; • все столбцы, входящие в каждую таблицу набора данных, и характеристики этих столбцов. Такие схемы можно создавать на основе уже имеющихся данных, задавать вручную и импортировать в набор данных извне. При чтении XML-документа, содержащего схему и данные, можно указать на необходимость следующих выполняемых автоматически действий: • дополнения столбцов, недостающих в таблице набора данных; • дополнения в набор схемы и, возможно, данных, отсутствующих в таблице набора; • дополнения данных, если они имеют такую же структуру, которую имеет и уже существующая таблица в наборе данных. Иными словами, используя XML-документы и методы набора данных для чтения и записи данных и структуры в XML-виде, мы можем очень быстро и эффективно: • закачать данные из внешнего документа в набор данных; • изменить структуру существующих таблиц набора данных; • создать новые таблицы в наборе данных. Для того чтобы понимать схемы XML, мы должны иметь общее представление о языке XML. Практика показывает, что прикладному разработчику не очень часто приходится писать XML-тексты вручную, поскольку ADO.NET прекрасно умеет делать это само. Однако ясное понимание того, что написано в том или ином XMLпредставлении схемы или данных, не повредит нам никогда. В этой части книги мы попытаемся дать именно такое представление об этом языке. За деталями обращайтесь к специализированным руководствам.
9.1.1. Что такое язык XML и для чего он нужен В реальном мире приходится очень часто обмениваться данными из приложений, работающих на разных платформах. Проблемы при этом неизбежны. Для обмена данными нужно, чтобы принимающее приложение могло читать форматы файлов базы данных источника. Поэтому возникла идея заключать данные в текстовые файлы и обмениваться именно ими. Ведь текстовые файлы умеют читать все ОС. Договори406
лись и о формате представления данных в текстовом файле, снабдив данные разметкой. Итак для того, чтобы приложение-приемник могло прочитать передаваемые данные, оно должно уметь читать текстовые файлы и интерпретировать символы разметки. В настоящее время XML (Extencible Markup Languague, расширяемый язык разметки) – это Интернет-стандарт, утвержденный World Wide Web Consortium (W3C). Подробная спецификация языка находится на http://www.w3.org.
9.1.2. Представление данных в XML Пусть в базе данных есть таблица с именем Kaf (рис. 9.1).
рис. 9.1
Одно из возможных представлений этих данных в XML-формате приводится ниже: 1 Автомобилей 10 Технический 3 Автоматики 20 Технический 11 Математики 22 Гуманитарный
В принципе все интуитивно понятно. Немного поговорим о разметке. В тексте мы видим начальный символ разметки и завершающий со слэшем . Это теги. Тег указывает на начало элемента и его окончание. В приведенном выше XML-фрагменте таких элементов три. Первый соответствует таблице – и ; второй соответствует записи таблицы – и ; третий – столбцу записи таблицы, например и . Внутри элемента в данном случае задается значение столбца. Если столбец содержит пустое значение, то вместо элемента 9. Использование XML
407
Математики
можно задавать просто , например:
555 Стали и сплавов
9.1.3. Корректные XML-документы Любой XML-документ должен быть корректен и правилен. Правильность документа относится к его содержимому: документ должен содержать все данные, приличествующие этому классу документов. Корректность документа состоит в возможности адекватно интерпретироваться XML-анализатором. Рассмотрим правила, которым должен удовлетворять корректный XML-документ. ПРАВИЛО 1. Документ должен включать один корневной элемент, который, в свою очередь, включает все прочие элементы документа. Например, в документе …
в качестве корневого выступает элемент . ПРАВИЛО 2. Каждому открывающему тегу должен соответствовать закрывающий тег. Так, следующий фрагмент ошибочен: … …
поскольку это правило нарушается. А вот следующий фрагмент верен, ибо правилу удовлетворяет: … …
ПРАВИЛО 3. В XML-тегах строчные и заглавные литеры различаются. Поэтому и – не одно и то же и следующий фрагмент будет ошибочен: …
ПРАВИЛО 4. Элементы не должны перекрываться. Именно такое перекрытие мы имеем в следующем фрагменте:
408
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
1 Автомобилей ...
Следующий фрагмент, где элементы и не пересекаются, будет верным: 1 Автомобилей
Корректный XML-документ может содержать инструкции по обработке и комментарии. Инструкция по обработке заключаются в теги . Они содержат уточняющие сведения для XML-анализатора. Обязательной является инструкции с указанием версии XML-спецификации:
Комментарии заключается в теги . Пример комментария:
1 Автомобилей 10 Технический
9.1.4. Пространства имен Пространство имен в XML имеет то же значение, что и в .NET; оно задает некую именованную область, внутри которой можно задавать какие-либо структуры данных или данные. Тогда одноименные элементы, заданные в разных пространствах имен, будут различаться XML-анализатором, если перед именем этих элементов указать имя соответствующего пространства. Рассмотрим пример. Ниже задаются два элемента Person, вложенные соответственно в элементы FAKULTET и DEKAN. В первом случае Person задает количественный состав факультета и содержит целочисленное значение, во стором – фамилию декана факультета (строковое значение). 1 9. Использование XML
409
200 Ивченко
Необходимо разделить по смыслу и впредь не путать два одноименных, но разнотипных элемента с различной смысловой нагрузкой и к тому же входящих в состав неодинаковых родительских элементов. Для задания пространства имен используется атрибут xmlns: 1 200 Ивченко
В приводимом выше примере каждому из элементов FAKULTET и DEKAN задается собственное пространство имен. Они связывают содержимое названных элементов с неким универсальным указателем ресурса (URI, Universal Resource Identifier). URI не обязательно может содержать интернетовский URL. Он может соответствовать и любому иному универсальному идентификатору. URL здесь подходит в силу своей уникальности, ведь в Интернете не найдется другого сайта с таким же URL. Чаще для краткости используют префиксы пространств имен. Для этого устанавливают соответствие между таким префиксом и URI и затем перед именами элементов через двоеточие указывают такой префикс, например: 1 200 Ивченко
9.1.5. Упражнения по получению XML-документов в SQL Server Известно несколько способов представления данных в XML-виде. Ознакомимся с ними, получив их экспериментальным путем. Самый доступный способ поупражняться в получении XML-описаний данных – это выполнить для баз данных SQL Server оператор SELECT…FOR XML. У этого оператора есть группа режимов: RAW, AUTO и EXPLICIT. Кратко рассмотрим эти режи410
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
мы, не забыв отметить, что некоторые из них, например EXPLICIT, предоставляют более широкие возможности по сравнению с рассмотренными в этой главе. Для получения лополнительных сведений обратитесь к руководству по SQL Server. Пусть имеем в базе данных две таблицы – Kaf (рис. 9.2) и подчиненную ей по столбцу KK таблицу Prep (рис. 9.3).
рис. 9.2
рис. 9.3
9.1.5.1. Режим RAW Результатом выполнения запроса вида SELECT * FROM KAF FOR XML RAW
будет следующий XML-текст:
Каждая строка таблицы базы данных представляется отдельным элементом с общим именем row. Каждому столбцу соответствует атрибут с именем, совпадающим с именем столбца.
9.1.5.2. Режим AUTO Выполним запрос SELECT * FROM KAF FOR XML AUTO
и получим следующий результат:
Каждой строке соответствует элемент с именем таблицы. Каждому столбцу соответствует атрибут с именем, совпадающим с именем столбца.
9.1.6. Режим AUTO с параметром ELEMENTS Когда данные выдаются в режиме AUTO с параметром ELEMENTS, например SELECT * FROM KAF 9. Использование XML
411
FOR XML AUTO, ELEMENTS
то в результирующем XML-тексте таблице соответствуют элементы, а столбцам таблицы – вложенные в них элементы: 1 Автомобилей 10 Технический 3 Автоматики 20 Технический 11 Математики 22 Гуманитарный
9.1.7. Режим EXPLICIT Запросы в режиме EXCPLICIT предоставляют широкие возможности по формированию XML-документов. Рассмотрим некоторые из них. Запрос SELECT … FOR XML EXCPLICIT задает XML-текст с точки зрения универсальной таблицы, где элементу данных соответствует столбец, плюс два дополнительных столбца используются для хранения метаданных XML-фрагмента. Столбец с именем TAG идентифицирует тег, а столбец PARENT задает вложенность элементов. Имя столбца универсальной таблицы задается в формате ИмяЭлемента!НомерТега! ИмяАтрибута!Директива
Выполним следующий запрос: SELECT 1 AS TAG, NULL AS PARENT, KK AS [Item!1!] FROM KAF FOR XML EXPLICIT
Результат выполнения запроса представлен ниже: 1 3 11
Результатом выполнения сходного запроса, где задано имя атрибута: SELECT 1 AS TAG, NULL AS PARENT, KK AS [Item!1!KodKafedry] FROM KAF FOR XML EXPLICIT
412
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
будет следующий XML-фрагмент:
Выполним аналогичный запрос для всей таблицы Kaf: SELECT 1 AS TAG, NULL AS PARENT, K.KK AS [Item!1!KK], K.NAZV AS [Item!1!NAZV] FROM KAF K FOR XML EXPLICIT
и получим следующий результат:
9.1.7.1. Вложенные данные в режиме EXPLICIT Вспомним о таблице Prep, дочерней для таблицы Kaf (см. рис. 9.3). Выполним соединение обеих таблиц и получим данные из таблицы Prep как вложенные элементы по отношению к элементу, представляющему записи таблицы Kaf. Для этого при описании столбца FIO в универсальной таблице зададим директиву ELEMENT: SELECT K.KK, K.NAZV, P.FIO, P.RANK FROM KAF K, PREP P
WHERE
K.KK = P.KK
SELECT 1 AS TAG, NULL AS PARENT, K.KK AS [Item!1!KK], K.NAZV AS [Item!1!NAZV], P.FIO AS [Item!1!PREP!ELEMENT] FROM KAF K, PREP P WHERE K.KK = P.KK FOR XML EXPLICIT
Результирующий XML-фрагмент будет выглядеть следующим образом: Високовский В.В. Вуюйпсеэ В.Э. Мешков В.В. Шальнов О.Е. Форзатцдерайвер Ф.Т. 9. Использование XML
413
9.1.8. Итоги В настоящей главе нами рассмотрены основные моменты, связанные с XML – языком разметки данных, хранящихся в таблицах баз данных. Эти сведения будут нами востребованы далее, поскольку в ADO.NET можно записывать и считывать данные в XML-представлении. Кроме того, именно на XML базируется язык для описания схем наборов данных (XDR). Его основные элементы рассматриваются в следующей главе.
9.2. Введение в схемы XML 9.2.1. Назначение схем XML Схемы XML задают структуру типов документов XML и также представляют собой XML-документы. Каждая схема XML описывает типы данных XML на языке описания схем XML (XML Schema definition language, XSD). В Visual Studio .NET схемы XML используются для задания структуры наборов данных. "Строительными кирпичиками" схем XML служат элементы, атрибуты, типы и группы. Рассмотрим структуру схемы XML, которая описывает набор данных nd, содержащий одну таблицу Kaf.
414
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
Первая строка задает используемую версию XML, . Второй оператор состоит из нескольких частей. Описание xs:schema указывает, что следующий далее текст – схема и что далее перед элементами схемы будет использован префикс xs:. Описание xmlns:xs="http://www.w3.org/2001/XMLSchema" указывает, что все теги в схеме следует интерпретировать в соответствии с принимаемым по умолчанию пространством имен, созданным World Wide Web Consortium (W3C) в 2001 г. Описание targetNamespace задает имя схемы (в данном случае это XMLSchema1.xsd) и ее местоположение, совпадающее по умолчанию с заданным по умолчанию URI (universal resource identifier) на вашем сервере разработки. Последний именуется как tempuri.org. Далее следует элемент (element) сложного типа (complexType) с именем nd. Он описывает набор данных. Этот элемент включает в себя остальные элементы. Следующий из них, также сложного типа, описывают структуру таблицы Kaf. Входящие в нее простые элементы описывают столбцы этих таблиц (например, элементы KK, Nazv и пр.). Далее следует ограничение (unique с параметром PrimaryKey="true"), описывающие первичный ключ для названной таблицы.
9.2.2. Когда создаются схемы XML Схема генерируется Visual Studio .NET автоматически при создании типизированного набора данных. Напомним, что такой набор создается после того, как вы создадите группу адаптеров, каждый из которых связан с уже существующей таблицей баз данных, и после этого сгенерируете набор данных. Для каждой схемы автоматически генерируется ее текстовое описание на языке схем XSD. Можно создать схему и вручную, используя для этого XML-дизайнер Visual Studio. Наконец, схему можно импортировать из внешнего файла схемы XML.
9.2.3. Схемы и наборы данных Как известно, в ADO.NET типизированный набор данных – это класс, связанный со схемой XML, а нетипизированный набор ни с какой схемой XML не связан. Типизированный набор данных генерируется во время разработки приложения. При этом создаются схема и класс набора данных. Схема XML описывает и проверяет 9. Использование XML 415
данные, импортируемые в типизированный набор данных. Схему XML можно просматривать и модифицировать в XML-дизайнере Visual Studio. Структура нетипизированного набора во время создания приложения не определена, в силу чего схема XML для такого набора на шаге разработки создана быть не может. При выполнении приложения, когда такой набор содержит данные, есть возможность создать для него схему программно.
9.2.4. Дизайнер схем Visual Studio .NET В состав Visual Studio .NET входит дизайнер схем XML. Он избавляет разработчика от неприятностей, связанных с необходимостью описания схем вручную на языке XDR. Ниже на рис. 9.4 показан экран дизайнера. Как с ним работать, рассказывается в следующих главах.
рис. 9.4
9.2.5. Элементы и атрибуты схем XML Элементы схем XML описывают структуры данных. Известно два типа элементов: простые и сложные. Элементы сложного типа включают другие элементы или атрибуты. Для элементов простого типа это невозможно. Атрибуты используются для представления характеристик данных и для описания простейших структур данных. К именам атрибутов и элементов предъявляются следующие требования: • XML регистрочувствителен. Поэтому "Elname" и "ELNAME" – имена разных элементов. 416
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
• •
Значения атрибутов нужно указывать внутри кавычек. Имена элементов не могут содержать пробелов и должны начинаться только с литеры, за исключением литер "XML.
Порядок следования элементов в схеме может быть существен, в то время как атрибуты можно задавать в произвольном порядке. Атрибуты также следует использовать для задания данных простых типов, когда: • нужно задать значение данных (в том числе по умолчанию); • нужно задать метаданные для существующего элемента; • файл XML имеет критический размер (атрибуты занимают меньше места, чем элементы).
9.2.6. Типы данных Типы данных схем XML могут быть простыми и сложными, а также именованными и безымянными.
9.2.6.1. Простые типы данных • •
•
Известны две разновидности простых типов данных XML: Встроенные типы. Определены в спецификации схем XML, выпущенных World Wide Web Consortium; это такие типы, как string, boolean, float. Встроенные типы могут содержать примитивы и производные типы. Примитивы – это типы данных, не унаследованные (непроизводные) от других типов данных, например float. Производные типы данных задаются в терминологии существующих типов данных. Пользовательские простые типы производны от встроенных типов с применением ограничений (facets).
9.2.6.2. Ограничения (Facets) Ограничения задают допустимые значения простых типов. Пользовательские простые типы могут использовать такие ограничения. В приводимом ниже примере ограничение с именем maxInclusive применяется простым типом qtyLimiter для ограничения возможных значений типа positiveInteger диапазоном 1…100:
9.2.6.3. Сложные типы Сложные типы могут включать описания других элементов, атрибутов и групп. Этим они отличаются от простых типов, которые могут включать только ограничения (facets).
9. Использование XML
417
9.3. Схемы XML и отображение в них таблиц, столбцов, реляционных отношений, первичных и внешних ключей 9.3.1. Таблицы набора данных Таблица наборов данных представляется в схеме элементом сложного типа (ComplexType). Имя такого элемента – это имя таблицы набора данных. Столбцы данных представлены элементами и атрибутами, вложенными в рассматриваемый элемент сложного типа. На рис. 9.5 показан сложный элемент Kaf, соответствующий структуре таблицы Kaf набора данных. Под рисунком приводится XMLописание этого элемента. рис. 9.5 …
ЗАМЕЧАНИЕ. В качестве первичного ключа задан столбец KK, что на рисунке обозначено ключиком рядом с этим столбцом. После определения таблиц в XMLописании схемы задается следующее описание ограничения первичного ключа:
9.3.2. Столбцы таблиц набора данных Столбцы набора данных могут представляться либо элементами (element), либо атрибутами (atribute). Выбор между атрибутом и элементом диктуется потребностями интеграции с другими приложениями. При создании схем в Visual Studio .NET предпочтение отдается элементам. Однако может случиться, что вы интегрируетесь с приложением, у которого в качестве столбцов используются атрибуты. В этом случае придется применять атрибуты и вам. В предыдущем разделе приведен пример схемы и XML-кода для таблицы Kaf. В этой схеме 4 столбца, притом значения тех из них, описания которых снабжены атрибутом minOccurs, можно не указывать. Столбец без названного атрибута (в дан418
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
ном случае это столбец KK) имеет ограничение unique, поскольку по названному столбцу построен первичный ключ и, следовательно, значение столбца для любой добавляемой в таблицу записи указывать обязательно.
9.3.3. Ограничение первичного и уникального ключей Изображение ключика рядом с именем элемента столбца в XML-схеме свидетельствует о том, что данный столбец входит в состав первичного ключа. Первичному ключу в XML-описании схемы соответствует ограничение unique с параметром PrimaryKey. При задании ограничения столбцы первичного ключа повторно не описываются; вместо этого на них уазывается ссылка. Например, в сложном элементе Kaf описывается элемент, соответствующий столбцу KK: … …
а ниже в схеме задается описание ограничения первичного ключа, содержащего ссылку на указанный столбец:
Изображение ключа в элементе, соответствующем столбцу, может также указывать на то, что на данный столбец наложено ограничение уникальности, хотя такой столбец может и не входить в состав первичного ключа1. Такие ограничения unique не содержат параметр PrimaryKey. Ниже показан код, добавляемый в схему, если выдвигается требование уникальности значений столбца Nazv в таблице Kaf:
9.3.4. Отношения между таблицами и ограничение внешнего ключа Таблицы набора данных могут быть связаны отношениями подчинения. Установка таких отношений отражается в схеме набора данных. На рис. 9.6 показана связь "одинко-многим" между таблицами Kaf (родительской) и Prep (дочерней) в схеме набора данных nd.
1
Наличие в схеме таблицы уникальных столбцов, не входящих в состав первичного ключа, свидетельствует о недостаточной нормализованности таблицы. Впрочем, в ряде случаев на это идут сознательно, чтобы минимизировать число соединений между таблицами в момент выполнения запросов. 9. Использование XML
419
рис. 9.6
Отношение между таблицами задается ограничением внешнего ключа keyref, где указываются столбцы связи обеих таблиц:
9.3.5. Все вместе – это схема Соединенные в определенной последовательности вместе все описанные выше "строительные кирпичики" и образуют схему набора данных. Выше на на рис. 9.6 показана схема набора данных nd, состоящего из двух таблиц Kaf и Prep, связанных отношением "один-ко-многим". Ниже приводится XML-описание этой схемы. В ней вы найдете все описанные выше элементы. Комментарии автора заключены в тег и выделены жирным шрифтом.
420
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
9.4. Создание элементов в схемах XML 9.4.1. Добавление атрибутов в схему • • • •
Элементы можно добавлять: в схему; прочие элементы; сложные типы; группы. Для создания элемента в среде XML Designer проделайте следующее: 1. Добавьте в проект схему XML или набор данных. 2. Перетащите из окна Toolbox (закладка XML Schema, см. рис. 9.7) элемент (element) и далее:
9. Использование XML
421
•
•
Если нужно создать новый элемент с глобальной областью видимости, то опустите элемент на поверхность в области дизайнера. Появится "сетка" для создания схемы (рис. 9.8 и 9.9). Если нужно добавить элемент в уже существующий элемент, сложный тип либо группу, то поместите перетаскиваемый элемент в "сетку". Тогда элемент появится в "сетке" с учетом имеющих место иерархических взаимоотношений между элементами схемы (рис. 9.10).
3. В заголовке "сетки" схемы выберите элемент element1 и задайте ему подходящее имя. 4. В ячейке правее имени элемента из выпадающего списка выберите нужный тип элемента (рис. 9.11). 5. В окне свойств задайте для элемента значения свойств, таких, как minOccurs, maxOccurs и пр.
рис. 9.7
рис. 9.8
422
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
рис. 9.10
рис. 9.9
рис. 9.11
9.4.2. Создание групп элементов Группы применяются для управления последовательностью элементов. Назначение групп можно уяснить из следующей таблицы: Группа sequence (последовательность) choice (выбор) all (все)
Для чего применяется Элементы группы перечисляются в строгом порядке и только в таком порядке могут использоваться в дальнейшем Может использоваться только один из перечисленных элементов Все элементы могут использоваться совместно или не используется ни один
Группы могут быть как именованные, так и неименованные.
9.4.2.1. Группа sequence Данные, схема которых включает группу sequence, проверяются не только на соответствие заданным в составе группы спецификациям (тип, ограничения и пр.), но и на последовательность следования элементов группы друг за другом в заданном порядке. Пример группы sequence показан на рис. 9.12. рис. 9.12
9. Использование XML
423
9.4.2.2. Группа choice Группа choice задает варианты элементов, но только один такой вариант из перечисленных в составе группы может присутствовать в единице данных. Пример группы choice показан на рис. 9.13. рис. 9.13
9.4.2.3. Группа all Когда элементы собираются в группу all, это значит, что в составе данных или указываются одновременно все элементы группы (притом в произвольном по отношению друг к другу порядке), или не указывается ни одного. Ниже на рис. 9.14 показан пример группы all с именем Provodka. Она включает элементы, которые могут содержать данные о бухгалтерской проводке. Вполне очевидно, что когда для проводки не указаны либо счет по дебету, либо по кредиту, либо сумма проводки, то такие сведения неполны и оттого рис. 9.14 бессмысленны.
9.4.3. Добавление групп элементов в схемы XML Для создания именованной группы в схеме XML: 1. Добавьте в проект схему XML или набор данных.
424
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
2. В окне Toolbox (закладка XML Schema) выберите группу (group) и перетащите ее мышью на поверхность создаваемой схемы. В ней появится "сетка" группы (рис. 9.15). 3. В заголовке группы измените присвоенное по умолчанию имя группы с group1 на ваше собственное. рис. 9.15 4. В правой ячейке заголовка группы выберите тип группы из выпадающего списка (рис. 9.16). 5. Для задания содержимого группы выполните следующие действия: а) в первой пустой строке "сетки" группы выберите слева самую маленькую ячейку рядом со звездочкой и из выпадающего списка выберите тип элемента группы (рис. 9.17);
рис. 9.16
рис. 9.17
б) в средней ячейке укажите имя элемента группы; в) в правой ячейке укажите тип элемента группы, используя выпадающий список (рис. 9.18). Выполните действия, перечисленные в п. 5, для каждого элемента группы. рис. 9.18
9.4.4. Синтаксис элемента схемы XML Элементы в схемах XML представляются в следующем формате: Содержимое: (annotation?, ((simpleType | complexType)?, (unique | key | keyref)*))
Назначение параметров показано ниже. abstract
Необязательный. Если true, элемент не может использоваться в качестве экземпляра документа. По умолчанию false. default
Необязательный. Задает значение по умолчанию для элемента (только простого типа или, если содержимое элемента, только текстового типа). Несовместим с параметром fixed. substitutionGroup
Необязательный. Имя элемента, для которого этот элемент может быть замещен. fixed
Необязательный. Предопределенное, неизменяемое значение элемента (только простого типа или, если его содержимое, только текстового типа). Несовместим с параметром default. form
Необязательный. Форма именования элемента. Возможные значения: qualified и unqualified. Когда задано unqualified, элемент можно именовать без префикса и пространства имен. Если задано qualified, элемент нужно именовать с указанием пространства имен. id
Необязательный. Задает ID этого элемента. Значение id должно иметь тип и быть уникальным внутри документа, содержащего этот элемент. maxOccurs
Необязательный. Максимальное число упоминаний элемента внутри объемлющего элемента. Целочисленное значение не меньше нуля. Для установки безлимитного использования элемента употребляется строка unbounded. Запрещено к использованию, когда объемлющим элементом является схема. minOccurs
Необязательный. Минимальное число упоминаний элемента внутри объемлющего элемента. Целочисленное значение не меньше нуля. Запрещено к использованию, когда объемлющим элементом является схема. name
Необязательный. Имя элемента. Name нельзя указывать, если указан параметр ref. Обязателен к указанию, когда объемлющим элементом является схема. 426 ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
nillable
Необязательный. Когда true, то параметру nil можно присваивать значение true. Например, задать единичный элемент с параметром nillable = true.
Ниже показывается фрагмент, использующий этот элемент и явно назначающий элементу значение nil (путем установки параметра nil в true). ref
Необязательный. Имя элемента, заданного в этой или другой схеме (в последнем случае указывается также пространство имен). Если указан ref, то нельзя указывать элементы complexType, simpleType, key, keyref, unique и параметры nillable, default, fixed, form, block, type. type
Необязательный. Или имя встроенного типа данных, или имя элемента простого или сложного типа. Нельзя указывать, если указан параметр ref.
9.5. Создание атрибутов в схемах XML Атрибуты – это, пользуясь привычной нам объектной терминологией, как бы свойства элементов. Атрибуты можно добавлять: • в схемы; • элементы; • группы атрибутов; • сложные типы.
9.5.1. Добавление атрибутов в схему Для создания атрибутов в среде XML Designer: 1. Добавьте в проект схему XML или набор данных. 2. В окне Toolbox (закладка XML Schema) выберите атрибут (attribute). Далее: • Если нужно создать атрибут с глобальной областью видимости, то опустите этот атрибут на поверхность в области дизайнера. Появится "сетка" (рис. 9.19).
рис. 9.19 •
Если нужно добавить атрибут в уже существующий элемент, группу атрибутов или сложный тип, поместите атрибут на этом элементе. Тогда атрибут появится в сетке (рис. 9.20).
9. Использование XML
427
рис. 9.20
3. В сетке выберите attribute1 и укажите его имя. 4. В ячейке справа от имени атрибута выберите тип атрибута из выпадающего списка (рис. 9.21).
рис. 9.21
9.5.2. Добавление групп атрибутов в схему Атрибуты могут объединяться в группы. В таком случае они именуются одним именем, что облегчает их использование. На рис. 9.22 показана группа атрибутов Avtomobil, в составе которой объединены атрибуты Marka, DvigatelNom и KuzovNom.
рис. 9.22
Для задания группы атрибутов в схеме XML: 1. Добавьте в проект схему XML или набор данных. 2. В окне Toolbox (закладка XML Schema) выберите группу атрибутов (attributeGroup) и поместите ее на поверхность схемы XML. Появится "сетка" группы атрибутов (рис. 9.23).
428
рис. 9.23
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
3. В заголовке атрибута замените заданное по умолчанию имя группы атрибутов attributeGroup1 на свое. 4. В первой пустой строке в "сетке" группы выберите слева самую маленькую ячейку рядом со звездочкой (рис. 9.24) и из выпадающего списка выберите тип группы следующим образом: • если вам нужно включить в группу все атрибуты из некоторого пространства имен, выберите anyAttribute; • если вам нужно включить в группу стандартные атрибуты, выберите attribute; • если вам нужно включить в группу другие, вложенные группы атрибутов, выберите attributeGroup. рис. 9.24 5. Содержимое прочих столбцов строки атрибута группы зависит от выбранного типа группы. Это средний столбец (имя атрибута) и самый правый столбец (тип атрибута). Сведения по заполнению названных полей приводятся ниже в таблице. Тип группы anyAttribute
Имя атрибута Не указывается ничего
attribute
Указывается имя атрибута
attributeGroup
Указывается 'ref=x', гда x – имя группы атрибутов
Тип атрибута Не указывается ничего Выбирается тип атрибута из выпадающего списка Не указывается ничего
6. Повторите действия, описанные в пп. 4 и 5, для каждого включаемого в группу атрибута. На рис. 9.25 показана группа Provodka. В составе этой группы три стандартных атрибута – Debet, Kredit и Summa, а также группа атрибутов типа ранее объявленной группы атрибутов Dogovor.
рис. 9.25 9. Использование XML
429
Ниже приводится текст XML, сгенерированный для групп атрибутов Dogovor и Provodka.
9.5.3. Синтаксис атрибута схемы XML Атрибуты в схемах XML представляются в следующем формате: Содержимое: (annotation?, (simpleType?))
Ниже рассматриваются параметры атрибута. default
Задает значение по умолчанию для атрибута. Необязательный.
fixed
Задает фиксированное значение атрибута. Необязательный.
form
Необязательный. Форма указания атрибута. По умолчанию принимается значение атрибута attributeFormDefault элемента, содержащего данный атрибут. Возможные значения: qualified или unqualified. Если задано unqualified, то атрибут не нужно предварять префиксом. Если задано qualified, атрибут нужно предварять: значением targetNamespace схемы и NCName атрибута.
id
Необязательный. Задает ID этого атрибута. Значение id должно иметь тип и быть уникальным внутри документа, содержащего этот атрибут. name
Необязательный. Имя атрибута. Name нельзя указывать, если указан параметр ref. ref
Необязательный. Позволяет ссылаться на другие атрибуты, в том числе заданные в других схемах. Значение ref должно содержать полное имя, включая название про430
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
странства имен. Если задано значение ref, нельзя указывать значения параметров form, type, а также элемента простого типа. Для описания атрибута, использующего описание атрибута, заданного в сложном типе, применяйте ref:
type Необязательный. Имя встроенного типа данных или простого типа, заданного в этой же схеме (либо других схемах; тогда нужно указывать их пространства имен). Название типа может включать название пространства имен. Параметр type может указываться, если только содержимое не содержит элемент простого типа. use
optional
Необязательный. Указывает способ использования атрибута. Возможны следующие значения: Атрибут необязателен и может иметь любое значение. Это значение принимается по умолчанию, так что оба нижеследующих описания эквивалентны:
required
prohibited
Атрибут использовать нельзя. Он применяется для ограничения другого сложного типа.
Параметр может появляться только единожды. Атрибут должен содержать значение.
9. Использование XML
431
Описание атрибута обычно ассоциируется с каким-либо встроенным или простым типом данных и может присутствовать как дочерний элемент в схеме, сложном типе данных, группе атрибутов. Примеры A. Атрибут используется внутри сложного типа и употребляет ссылку на другой атрибут со значением по умолчанию " KapitanNemo".
B. Атрибут с обязательным (required) значением задан внутри элемента сложного типа.
C. Атрибут задан как производный от встроенного целочисленного типа с ограничением возможных значений атрибута от 100 до 200.
D. Атрибут задается списком десятичных значений.
432
ADO.NET и создание приложений баз данных в среде Microsoft Visual Studio .NET
9.6. Создание типов в схемах XML 9.6.1. Пользовательские простые типы в схемах XML Задаваемые пользователем простые пользовательские типы накладывают ограничения на содержимое элементов и атрибутов. При этом указывается разрешенное подмножество значений того простого типа, от которого производен этот простой пользовательский тип. Простой тип, от которого пользователь производит свой тип, должен либо быть встроенным типом (например, как string), либо представлять собой ранее объявленный в схеме XML пользовательский простой тип. Пример. На рис. 9.26 показан простой пользовательский тип Skolko, заданный в схеме XML. Ниже дается описание этого типа на языке XML.
рис. 9.26
Тип производен Skolko от базового типа int. Экземпляры элементов этого типа не могут содержать значений, превосходящих 1000. Созданный впоследствии элемент Magazin может содержать элементы типа Skolko (рис. 9.27).
рис. 9.27
9.6.2. Создание простых типов в схемах XML Для создания нового простого типа проделайте следующее 1. Добавьте в проект схему XML или набор данных. 2. В окне Toolbox (закладка XML Schema) выберите simpleType и поместите его на поверхность схемы XML. Появится "сетка" простого типа (рис. 9.28).
9. Использование XML
433
3. В заголовке "сетки" выберите левую ячейку и замените simpleType1 на имя простого типа. 4. В заголовке типа, в правой ячейке, выберите из выпадающего списка тип элемента (рис. 9.29).
рис. 9.28 •
•
рис. 9.29
5. Если нужно наложить ограничения (facets) на значения типа: в первой пустой строке "сетки" щелкните мышью в первом столбце и выберите facet; затем во втором столбце выберите из выпадающего списка тип ограничения (рис. 9.30). в третьем столбце той же строки задайте соответствующее значение (рис. 9.31).
рис. 9.30
рис. 9.31
Для показанного на рис. 9.31 элемента будет сгенерирован следующий код XML: