Для определения свойств InsertCommand, UpdateCommand (Команда обновления) и DeleteCommand можно использовать класс SqlCommandBuilder. Но из-за того, что для динамического определения этих свойств класс SqlCommandBuilder должен получить нужную информацию, его использование повлечет за собой необходимость произвести несколько дополнительных обращений к базе данных и уменьшение производительности. Поэтому, если структура базы данных, используемой приложением, известна при его разработке, лучше определять свойства InsertCommand, UpdateCommand (Команда обновления) и DeleteCommand явно. Это поможет избежать снижения производительности. Если же структура базы данных точно не известна, но пользователь определил запрос, то SqlCommandBuilder можно использовать для обновления результатов "при последующих запросах.
Этот метод действенен для экземпляров DataSet (Набор данных), соответствующих единичной таблице. Если же данные в наборе данных — результат запроса, использовавшего соединение данных, или между таблицами в наборе данных есть связи, то механизм автоматической генерации не сможет корректно определить команду, обновляющую данные в обеих таблицах. Так как SqlCommandBuilder использует при генерации команд свойство SelectCommand, оно должно быть определено.
Для того чтобы описанный метод работал корректно, в таблице, содержащейся в наборе данных, должен быть главный или единственный столбец. Этот столбец будет результатом выполнения SQL-запроса, установленного в свойстве SelectCommand. Главный столбец используется при обновлении или удалении данных как where-фраза.
Имена столбцов не должны содержать специальных символов, таких, как пробелы, запятые, точки, кавычки или другие символы, отличные от буквенно-цифровых. Это обязательно даже тогда, когда имя взято в скобки. Имя таблицы можно задавать полностью, как, например, SchemaName . OwnerName . TableName.
Простейший способ использования класса SqlCommandBuilder— передача экземпляра класса SqlDataAdapter конструктору SqlCommandBuilder в качестве аргумента. После этого объект SqlCommandBuilder зарегистрирует себя в качестве обработчика события RowUpdating. А дальше он сможет генерировать необходимые команды InsertCommand, UpdateCommand (Команда обновления) и DeleteCoramand до обновления строки.
Для рассмотрения доступа к данным XML мы будем использовать базу данных Air-linesBrokers. Указанную базу данных можно создать и инициализировать с помощью SqlServer Query Manager и SQL-скрипта, прилагаемого к примерам для этой главы. База данных AirlinesBrokers имеет функциональные возможности, используемые системой бронирования Acme. С ее помощью клиенты Acme могут бронировать авиабилеты. Такая база данных содержит следующие таблицы:
Airlines (Авиалинии): информация об авиалиниях; PlaneType (Тип самолета): типы самолетов, используемые на авиалиниях; Flights (Рейсы): информация о рейсах на различных авиалиниях: Customers (Клиенты): информация о клиентах; Reservations (Резервирование): информация о забронированных клиентами местах.Хотя в реальной жизни списки клиентов систем AirlinesBroker и HotelBroker (Посредник, бронирующий места в гостинице) вряд ли могут совпасть, в нашем примере мы для простоты будем использовать те же таблицы Customers (Клиенты) и те же компоненты для доступа к данным, ч го и ранее.
При его создании SqlDataReader не указывает ни на какую запись возвращенного набора данных. Поэтому для получения доступа к данным следует вызвать метод Read (Читать). Как показано в примере Connected, для получения доступа к отдельным полям или столбцам текущей строки можно использовать свойство Item (Элемент). Получить все поля строки можно также с помощью метода GetValues.
Object * fields [] = new Object *[NumberFields]; // новый Объект
int NumberFields = reader->GetValues(fields); // читать поля
GetValue возвращает значение столбца в его исходном формате Для доступа к данным определенных форматов можно использовать методы GetBoolean (Прочитать Логическое значение), GetDecimal (Прочитать Десятичное число) и GetString (Прочитать Строку). Метод GetName возвращает имя определенного столбца.
Еще раз повторим, что при использовании DataReader в каждый момент времени доступна только одна запись. Убедитесь в том, что по завершении работы с DataReader вы его закрыли.
Объект DataSet (Набор данных) содержит методы WriteXml и WriteXmlSchema, которые выдают данные и схему данных, хранящихся в наборе данных. Схема XML, возвращаемая объектом DataSet (Набор данных), определяется из самих данных. Пока вы не добавите явным образом в объект DataSet (Набор данных) ограничения, такие, как первичные или внешние ключи, они не будут частью схемы.
Объект DataSet (Набор данных) содержит также и методы, предназначенные для чтения XML: ReadXml и ReadXmlSchema. С помощью ReadXml можно считывать данные и схему в объект DataSet (Набор данных). Когда схема отсутствует, метод попытается извлечь ее изданных. Если же это не удастся, возникнет исключение. ReadXmlSchema считывает схему документа.
При отсутствии в документе XML схемы, DataSet (Набор данных) будет извлекать данные, как если бы они были таблицами, руководствуясь при этом набором правил. Оставшиеся элементы будут считаться столбцами таблиц
Для определения того, будет столбец записываться в документ XML как элемент или как атрибут, используется свойство Col-rr.Kapping объекта DataColamn. Запись столбца как элемента предпочтительней. Элемент, содержащий нескалярные данные, считается таблицей. Атрибуты и скалярные значения являются столбцами. Подробнее эти правила описаны в документации к .NET.
Для иллюстрации связи между реляционной моделью объекта DataSet (Набор данных) и моделью ХМ L прежде всего извлечем некоторую информацию из базы данных. В примере DataSetXml для этого используются те же команды и способы, что и ранее в этой главе.
Первым делом создадим соединение, набор данных и преобразователь данных для различных таблиц.
SqlConnect]on *conn =
new SqlConnect K>n ( connectStr ing) ;
DatdSet *d - new Pdtr.Set ( "Ад r±ineBroker") ; // новый Набор данных SqlDjtaAd.-pter "air ] i -n^s Adaptcr =
new Sq]DataAoar te L ; SqiDctaAaapter * f 1 ^qn4- sAdapter -
new SqlLataAdapter; SqlDataAdapter ^plar.etypeAdapter =
new SqlDdtdAdapter; SqlDataAdapter *custon,ersAdaptei =
new SqlDataAaapter; SqlDataAdapter ^reservationsAaapter -
new SqlDataAaapter;
Затем создадим несколько команд select (выбрать) для получения данных, и, используя эти команды, заполним набор данных данными из таблиц.
airlinesAdapter->SelectCommand =
new SqlCommand("select * from Airlines", conn);
// "выбрать * из Авиалиний " airlinesAdapter->Fill(d, "Airlines"); // Заполнить (d, "Авиалинии");
flightsAdapter->SelectCommand =
new SqlCommand("select * from Flights", conn);
// "выбрать * из Рейсов" flightsAdapter->Fill(d, "Flights"); // Заполнить (d, "Рейсы");
planetypeAdapter->SelectCommand =
new SqlCommand("select * from PlaneType", conn); // выбрать planetypeAdapter->Fill(d, "PlaneType"); // Заполнить
customersAdapter->SelectCommand =
new SqlCommand("select * from Customers", conn);
// "выбрать * из Клиентов " customersAdapter->Fill(d, "Customers"); // Заполнить (d, "Клиенты");
reservationsAdapter->SelectCommand =
new SqlCommand("select * from Reservations", conn);
// "выбрать * из Резервирования " reservationsAdapter->Fill(d, "Reservations"); // Заполнить (d, "Резервирование");
Теперь в объекте DataSet (Набор данных) есть данные таблиц Airlines (Авиалинии), PlaneType (Тип Самолета), Flights (Рейсы), Customers (Клиенты) и Reservations (Резервирование). Далее извлечем из данных объекта DataSet (Набор данных) схему XML. Затем извлечем сами данные и запишем их в формате XML.
d->WriteXmlSchema("Airlines.xsd");
d->WriteXml("Airlines.xml");
Приведенные операторы создают два файла: Airlines . xsd и Airlines . xml. Ниже вы видите некоторые данные, записанные в Airlines. xml. Главным элементом является AirlineBroker; именно так назывался объект DataSet (Набор данных). На один уровень ниже находятся элементы, соответствующие разным таблицам объекта DataSet (Набор данных): Airlines (Авиалинии), PlaneType (Тип самолета), Flights (Рейсы) и Customers (Клиенты). О забронированных местах информации в базе данных не было. В получившемся документе каждой строке исходных таблиц соответствует одна запись. Элементы этих записей соответствуют полям исходных таблиц.
<?xml version="l.О" standalone="yes"?> <AirlineBroker> <Airlines>
<Name>America West</Name>
<Abbreviation>AW</Abbreviation>
<WebSite>www.americawest.com</WebSite>
<ReservationNumber>555-555-1212</ReservationNumber> </Airlines> <Airlines>
<Name>Delta</Name>
<Abbreviation>DL</Abbreviation>
<WebSite>www.delta.com</WebSite>
<ReservationNumber>800-456-7890</ReservationNumber>
</Airlines>
<Flights>
<Airline>DIX/Airlj.n£>
<FlightNurrber>98^</FlightNumber>
<StartCity>Atlanta</StartCity>
<EndCity>New Orleans</EndCity>
<Departure>2001-10-05T20:15:00.0000000-04:00 </Departure>
<Amval>2001-10-05T22 : 30: 00. 0000000-04 : 00</Amval>
<PlaneType>737</?ianeType>
<FirstCost>1300</FirstCost>
<BusinessCost>0</BusinessCost>
<EconoiryCost>450</EconomyCost> </Flights>
<PlaneType>
<PlaneType> 737 </PlaneType>
<FirstClass>10</FirstClass>
<BusinessClass>0</BusinessClass>
<EconomyClass>200</EconomyClass> </PlaneType->
<Customers>
<LastName>Adams</LastName> <FirstName>John</FirstName>
<EmailAddress>adams@presidents.org</EmailAddress> <CustomerId>1</CustomerId> </Custorrers> </AirlineBroker>
Вот более русифицированная версия этого XML-документа:
<? xml версия = "1.0" автономный = "да"?> <AirlineBroker> <Авиалинии>
<Название> Американский Запад </Название>
<Сокращение> AW </Сокращение>
<УзелИеЬ> www.americawest.com </УзелМеЬ>
<ReservationNumber> 555-5Ь5-1212 </ReservationNumber> </Авиалинии> <Авиалинии>
<Название> Дельта </Название>
<Сокращение> DL </Сокращение>
<Узeлweb> www.delta.com </УзeлWeb>
<ReservationNumber> 800-456-7890 </ReservationNumber> </Авиалинии> <Рейсы>
<Авиалиния> DL </Авиалиния> <FlightNumber> 987 </FlightNumber> <StartCity> Атланта </StartCity> <EndCity> Новый Орлеан </EndCity> <Вьшет> 2001-10-05Т20:15:00.0000000-04:00
</Вылет> <Прибытие> 2001-10-05Т22:30:00.0000000-04:00 </Прибытие>
<Тип самолета> 737 </Тип самолета> <F.L-(Cust> 1300 </FirstCost> <Bus _ncssCost> 0 </BusinessCost> <EconcmyCost> 450 </EconomyCost> </Рейсь:>
<Тип самолета>
<Тип самолета>737</Тип самолета>
<FirstClass> 10 </FirstClass>
<BusinessClass> 0 </BusinessClass>
<EconomyClass> 200 </EconomyClass> </Тип самолета>
<Клиенты>
<LastName> Адаме </LastName> <FirstName> Джон </FirstName>
<EmailAddress> adams@presidents.org </EmailAddress> <CustomerId> 1 </CustomerId> </Клиенты> </AirlineBroker>
Исходя из этих данных, объект DataSet (Набор данных) создал схему и сохранил ее в файле Airlines.xsd. Дальше мы обсудим некоторые отрывки из этого файла. В нем нет информации о связях или первичных ключах какой бы то ни было таблицы, такой, как Airlines (Авиалинии) или Flights (Рейсы), по той простой причине, что они не были определены в исходной базе данных. Если вы просмотрите созданный файл, вы увидите, что в нем записана и информация о схеме данных таблицы Reservations (Резервирование), несмотря на то, что в этой таблице нет никаких данных.
В первой строке заголовка схемы определено название схемы (AirlineBroker). Кроме того, в нем определены два пространства имен, используемых в этой схеме документа. Одно пространство имен, названное xsd, содержит описание стандарта схемы XML. Второе, названное msdata, содержит описание от Microsoft.
<xsd:schema id="AirlineBroker" targetNamespace="" xmlns="" xmlns:xsd=http://www.w3.org/2001/XMLSchema xmlns:rnsdata="urn:schemas-microsoft-com:xml-msdata">
В следующей строке описывается элемент под названием AirlineBroker, имеющий атрибут, указывающий, что эта схема получена из объекта DataSet (Набор данных). Это атрибут в определениях Microsoft, а не в пространстве имен W3C Schema. Элемент AirlineBroker относится к составному (не скалярному) типу, т.е. является структурой, состоящей из элементов других типов. Такая структура может содержать произвольное количество элементов (или не содержать ни одного) любого типа, определенного в остальной части схемы.
<xsd:element name="AirlineBroker" msdata:IsDataSet="true"> <xsd:complexType> <xsd:choice maxOccurs="unbounded">
Далее описывается элемент, определяющий тип данных. Этот тип — тоже структура и потому относится к составному (не скалярному) типу, очередность элементов в котором совпадает с очередностью их определения в объекте DataSet (Набор данных). Так уж получилось, что все элементы, соответствующие столбцам таблицы базы данных, определены здесь как имеющие строковый тип string, причем их напичие не считается обязательным. В исходной таблице первичные ключи не определялись, а так как все эти строки в записях базы данных обязательны, объект DataSet (Набор данных) при преобразовании данных вывел и\ из набора таблиц, ограничений и связей, определенных в объекте DataSet (Набор данных) в момент преобразования.
<xsd : element r.ame = "Airlines"> <xsd:conplexType> <xsd:sequence>
<xsd: element narre = "Kame" type="xsd:string"
minOccurs="0" /> <xsd: element narr:e="Abbreviation"
type="xsd:string" minOccurs="0" /> <xsd:element name="KebSite" rype="xsd:string"
minOccurs="0" /> <xsd:element name="ReservationNumber"
type="xsd:string" minOccurs="0" /> </xsd:sequence> </xsd:complexType> </xsd:element>
Таблица Flights (Рейсы) определена аналогичным образом. Кроме того, что в ней не определен первичный ключ, в ней нет и внешних ключей для столбцов Airline (Авиалиния) и Plane Type (Тип самолета).
<xsd: element na.me = "Fliqhts"> <xsd:complexType> <xsd:sequence>
<xsd:element name="Airline" type="xsd:string"
minOccurs="0" /> <xsd:element name="FlightNumber" type="xsd:int"
rainOccurs="0" /> <xsd:element name="StartCity" type="xsd:string"
minOccurs="0" /> <xsd:element name="EndCity" type="xsd:string"
minOccurs="C" /> <xsd:element name="Derarture" type="xsd:dateTime"
minOccurs="0" />
<xsd:element name="Arrival" type="xsd:dateTime"
minOccurs="0" />
<xsd:element name="PlaneType" type="xsd:string"
minOccurs="0" />
<xsd:element name="FirstCost" type="xsd:decimal"
minOccurs="0" /> <xsd:element name="BusinessCost"
type="xsd:decimal" minOccurs="0" /> <xsd:element name="EconomyCost"
type="xsa:decimal" minOccurs="0" /> </xsd:sequence> </xsd: ccm,plexType> </xsd:element>
</xsd:choice>
</xsd:complexType> </xsd:element> </xsd:schema>
К этому определению схемы данных мы еще вернемся позже, а сейчас продолжим рассмотрение примера.
Как будет показано в главе 11 "Web-службы", XML имеет много преимуществ при описании данных, которые нужно перемещать между разнородными системами и источниками данных. Поскольку вы можете обеспечить данные XML описанием схемы данных XML, во многих случаях имеет смысл передавать именно такие данные, а не DataSet (Набор данных). Так как данные ХМL являются текстом, они могут проходить через порты брандмауэров, которые обычно открыты, в отличие от протокола распределенной модели компонентных объектов DCOM (Distributed COM — Distributed Component Object Model) или протокола RMI, используемого в JAVA, которые требуют открытия особых портов.
Мы не ставим себе цель обсудить в следующих разделах все детали языка XML. Мы хотим только продемонстрировать, как можно использовать концепции данных, принятые в ХМL и в DataSet (Набор данных).
На Рисунок 9.5 представлена иерархия объектов, содержащихся в DataSet (Набор данных). Прежде, чем приступить к материалу, излагаемому ниже, полезно ознакомиться с этой диаграммой.
Рисунок 9.5. Иерархия класса DataSet (Набор данных)
Префикс имен классов и методов указывает на источник данных. Например, префикс OleDb (OLE для баз данных) указывает на использование источника данных OleDb (OLE для баз данных). Префикс Sql указывает на использование источника данных SqlServer.
Источник данных SQL Server использует родной протокол SQL Server. Источник данных OleDb (OLE для баз данных) через промежуточный уровень модели компонентных объектов Microsoft (COM) обращается к различным драйверам доступа OleDb (OLE для баз данных). Например, можно взаимодействовать с SqlServer через источник данных OleDb (OLE для баз данных) с целью обращения к драйверу доступа OLEDB (OLE для баз данных) для SQL Server. Но быстродействие при таком способе будет, конечно, меньше, чем при использовании источника данных SqlServer. Преимуществами источников данных OleDb (OLE для баз данных) и ODBC является то, что, используя их при работе в ADO.NET, можно работать с большинством из доступных сегодня источников данных.
Хотя в ADO.NET есть несколько интерфейсов, определяющих общие возможности, и несколько базовых классов, которые можно использовать для обеспечения этих возможностей, к источникам данных не предъявляется требование удовлетворять спецификациям, не соответствующим принятым способам работы с используемыми источниками данных.
Например, классы SqlDataAdapter и OleDbDataAdapter в качестве базовых используют базовые абстрактные классы DbDataAdapter и DataAdapter, находящиеся в пространстве имен System: :Data: : Common (Система::Данные::Общие). С другой стороны, классы SqlTransaction и OleDbTransaction не наследуют реализации какого-либо класса, предназначенного для работы с базами данных. Классы OleDbError и SqlError вообще не похожи друг на друга. Указатель, реализуемый сервером, не поддерживается в ADO.NET из-за того, что некоторые базы данных (например, Oracle и DB2) не имеют встроенной поддержки этой возможности. Поэтому поддержка такой возможности для источника данных SQL Server будет расширением.
В табл. 9.1 приведены классы источников данных О1е и Sql, предназначенные для соединения, задания команд, чтения данных, преобразования данных, хранения параметров данных. Как видно из этой таблицы, приведенные там классы источников данных Ole и Sql имеют общие черты, определенные в интерфейсах IDbConnection, IDbCom-mand, IDataReader, IDbDataAdapter и IDataParameter. Ничто, конечно же, не препятствует реализовать в любом из этих классов методы, не определенные соответствующим интерфейсом.
Таблица 9.1. Сравнение соответствующих друг другу классов источников данных OleDb (OLE для баз данных) и SqlServer
Интерфейс | OleDb | SQL Server |
IDbConnection | OleDbConnection | SqlConnection |
IDbCommand | OleDbCommand | SqlCommand |
IDataReader | OleDbDataReader | SqlDataReader |
IDbDataAdapter | OleDbDataAdapter | SqlDataAdapter |
IDataParameter | OleDbDataParameter | SqlDataParameter |
Классы, не зависящие от какого-либо источника данных, например, DataSet (Набор данных) или DataTable (Таблица данных), не имеют префиксов.
Если важна масштабируемость (расширяемость) базы данных, желательно запретить завершение (fmalization) объектов, не нуждающихся в нем. Тем самым повышается производительность приложения, так как уменьшается время работы потока завершителей (fmalizer).
При необходимости внести значительные изменения в объект DataSet (Набор данных) и отложить при этом проверку ограничений и событий, можно использовать режим редактирования набора данных.
BeginEdit. EndEdit, CancelEdit
Переход в режим редактирования осуществляется вызовом метода BeginEdit объекта строки, которую необходимо изменить. Для выхода из этого режима используются методы EndEdit и CancelEdit.
В примере DataEditing мы нарушаем ограничение, заданное внешним ключом, добавляя в таблицу Books (Книги) строку, описывающую книгу автора, идентификатор которого отсутствует в базе данных. Исключение, являющееся результатом этого нарушения, возникнет только после вызова метода EndEdit. Так, при выполнении следующего фрагмента файла DataEditing. h примера DataEditing исключение не возникает.
DataRow *rowToEdit = books->Rows->get_Item(0); // книги-> Строки
rowToEdit->BeginEdit();
try
{
rowToEdit->set_Item("Author!d", _box(21));
}
catch(Exception *e) // Исключение
{
Console::WriteLine(
"\n {0] while editing a row.", e->Message); // при редактировании строки, Сообщение); Console::WriteLine(); }
Однако исключение возникает, как только в программе вызывается метод EndEdit.
try {
rowToEdit->EndEdit(); }
catch(Exception *e) // Исключение {
Console::WriteLine();
Console::WriteLine(
"\n{0} on EndEdit", e->Message); // Сообщение
Console::WriteLine(); }
В результате будет напечатано следующее сообщение, указывающее на то, что по окончании сеанса изменения содержимого строки обнаружено нарушение ограничения.
ForeignKeyConstraint Authors->Books requires the child key values (21) to exist in the parent table, on EndEdit
Версии объекта DataRow
До того, как будут подтверждены внесенные в строку изменения, доступны и исходные, и измененные значения полей строки. С помощью свойства элемента строки28 Da-taRowVersion можно определить, какое именно значение вы хотите использовать. Это свойство может иметь значения Original (Первоначальное), Default (Заданное по умолчанию), Current (Текущее) и Proposed (Предложенное).
В следующем фрагменте примера DataEditing приведен код, выполняющийся до вызова метода EndEdit:
DataRow *rowToEdit = books->Rows->get_Item(0);
// книги-> Строки rowToEdit->BeginEdit() ; try {
rowToEdit->set_Item("AuthorId", _box(21)); Console::WriteLine(
"Book Author Id Field Current Value {0}",
// Текущее значение поля идентификатора автора книги
rowToEdit->get_Item(
"Authorld", DataRowVersion::Current)); Console::WriteLine(
"Book Author Id Field Proposed Value {0}",
// Предложенное значение поля идентификатора автора книги
rowToEdit->get_Item(
"Authorld", DataRowVersion::Proposed)); // Предложенное Console::WriteLine(
"Book Author Id Field Default Value {0}",
// Значение по умолчанию поля идентификатора автора книги
rowToEdit->get_Item(
"Authorld", DataRowVersion::Default));
// Значение по умолчанию }
В результате программа напечатает:
Book Author Id Field Current Value I Book Author Id Field Proposed Value 21 Book Author Id Field Default Value 21
Вот перевод:
Текущее значение поля идентификатора автора книги 1
Предложенное значение поля идентификатора автора книги 21
Значение по умолчанию поля идентификатора автора книги 21
При выполнении транзакционного редактирования доступны значения Current (Текущее) и Proposed (Предложенное). После вызова метода CancelEdit значение Proposed (Предложенное) становится недоступным. После вызова метода EndEdit значение, имевшее атрибут Proposed (Предложенное) меняет атрибут на Current (Текущее) а значение, имевшее атрибут Proposed (Предложенное), становится недоступным.
Свойство RowState объекта DataRow
Кроме того, что в режиме редактирования доступны значения поля Current (Текущее) и Proposed (Предложенное), сам объект DataRow имеет свойство, описывающее состояние соответствующей строки. Это свойство может принимать значения Added (Добавлено), Deleted (Удалено), Detached (Отсоединено), Modified (Изменено) или Unchanged (He изменено).
Строка находится в состоянии Detached (Отсоединено) в случаях, когда она создана, но либо еще не добавлена ни в одну коллекцию объектов DataRow, либо удалена из какой-нибудь коллекции.
Какое из значений будет возвращено при использовании значения Default (Заданное по умолчанию) свойства DataRowVersion определяется значением свойства RowState.
Принятие или отмена изменений
Вызов метода EndEdit объекта DataRow не приводит к фиксации изменений, сделанных в строке. Вызов методов AcceptChanges или RejectChanges объектов DataSet (Набор данных), DataTable (Таблица данных) или DataRow приводит к выходу из режима транзакционного редактирования для всех строк соответствующего объекта. Если до этого не были вызваны EndEdit или CancelEdit, AcceptChanges или Rejec-tChanges вызывают эти методы для всех строк соответствующего объекта.
После вызова метода AcceptChanges значения, имевшие атрибут Proposed (Предложенное) становятся основными (т.е. имеющими атрибут Original (Первоначальное)). Если при этом свойство RowState имело значение Added (Добавлено), Modified (Изменено) или Deleted (Удалено), ему присваивается значение Unchanged (He изменено), а сами изменения вступают в силу (т.е. строки добавляются, изменяются или удаляются).
После выполнения метода RejectChanges значение, имевшее атрибут Proposed (Предложенное), удаляется. Если при этом свойство RowState имело значение Deleted (Удалено) или Modified (Изменено), значение поля становится прежним и свойству RowState присваивается значение Unchanged (He изменено). Если же RowState имело значение Added (Добавлено), строка удаляется из коллекции Rows (Строки).
Так как после вызова метода AcceptChanges свойство RowState имеет значение Unchanged (He изменено), вызов метода Update (Обновить) объекта DataAdapter не приведет к каким-либо изменениям базы данных. Поэтому, при необходимости внести изменения в базу данных, метод Update (Обновить) следует вызывать до вызова метода AcceptChanges строки, таблицы или объекта DataSet (Набор данных).
Ниже приведен фрагмент реализации метода CancelReservation класса HotelBroker (Посредник, бронирующий места в гостинице) из примера CaseStudy. Фрагмент взят из файла HotelBookings . h, находящегося в папке CaseStudy\HotelBrokerAdmin\Hotel. Обратите внимание, что метод AcceptChanges объекта DataSet (Набор данных) вызывается при успешном завершении работы метода SqlDataAdapter: : Update (Обновить). В случае же возникновения исключения вызывается метод RejectChanges.
void CancelReservation(int id) // идентификатор
{
DataTable *t = 0;
try
{
t = dataset->Tables->get_Item("Reservations");
// набор данных-> Таблицы-> get_Item ("Резервирование");
DataRow *rc [] = t->Select ( // Выбор
String::Format("Reservationld = {0} ", // Строка:: Формат
id.ToString())); // идентификатор for (int i=0; i<rc->Length; i++) re[i]->Delete(); // Удалить
int NumberRows = adapter->Update( // Обновление dataset, "Reservations"); // набор данных, "Резервирование"); if (NumberRows > 0) // если (NumberRows> 0)
t->AcceptChanges () ; else
t->RejectChanges () ; }
catch(Exception *e) // Исключение {
t->RejectChanges();
throw e; }
return;
}
Если вы не будете отменять внесенные изменения в случае возникновения ошибки, измененные строки останутся в объекте DataSet (Набор данных). Тогда, при попытке произвести следующее обновление, оно также будет отменено из-за того, что строки все еще не обновлены и наличие их приводит к возникновению исключения. Поскольку объект DataSet (Набор данных) независим от других баз данных, тот факт, что данные в базе данных были обновлены, не имеет никакого отношения к принятию или отмене произведенных изменений данных в самом объекте DataSet (Набор данных).
Ошибки объекта DataRow
Если при изменении данных строки произошла ошибка, свойство HasError объекта DataSet (Набор данных), DataTable (Таблица данных) или DataRow примет значение true (истина). Для получения информации об ошибке используются методы GetCol-umnError или GetCoiunmsInError.
Класс DataSet (Набор данных) представляет собой резидентную упрощенную реляционную базу данных, не соединенную прямо ни с какой другой базой данных. Некоторые из его свойств описывают таблицы (Tables) и отношения (Relations) между ними в наборе данных. Управлять проверкой ограничений можно с помощью свойства Enf ог-ceConstraint. Имя набора данных можно установить с помощью свойства DataSet-Name, а кроме того, его можно определить и в конструкторе DataSet (Набор данных).
И Класс SqlDataAdapter используется для передачи данных от базы данных объекту DataSet (Наборданных). В конструкторе класса HotelBroker (Посредник, бронирующий места в гостинице) продемонстрировано, как использовать SqlDataAdapter для заполнения набора данных. Пример CaseStudy для данной главы содержит приведенный ниже исходный код15. Этот фрагмент находится в файле HotelBroker. h из папки CaseStudy\HotelBrokerAdmin\Hotel.
conn = new SqlConnection(connString) ; citiesAdapter = new SqlDataAdapter(); citiesAdapter->SelectCommand = new SqlCommand( "select distinct City from Hotels", conn); citiesDataset = new DataSet; // новый Набор данных citiesAdapter->Fill(citiesDataset, "Cities"); // Города
Среди свойств класса SqlDataAdapter есть такие, которые связывают его с операциями выборки, вставки, обновления или удаления данных источника данных. В нашем примере экземпляр класса SqlCommand не вызывается непосредственно одним из его методов, а связывается со свойством SelectCommand класса SqlDataAdapter.
Затем для выполнения указанной команды используется метод Fill (Заполнить) класса SqlDataAdapter. При этом объект DataSet (Набор данных) заполняется информацией из таблицы, имя которой указано как аргумент метода Fill (Заполнить). После завершения работы этого метода соединение остается в том же состоянии, в котором оно было при вызове метода.
Теперь соединение с базой данных можно закрыть. Но при этом, вне зависимости от наличия соединения с базой данных, можно продолжить работу с объектом DataSet (Набор данных), содержащим данные.
Класс SqlDataAdapter реализован на основе класса SqlDataReader, поэтому при использовании последнего можно ожидать большей производительности. SqlDataReader может также эффективнее использовать память. Это зависит от структуры.
Примером CaseStudy для этой главы является решение AcmeGui, состоящее из трех проектов AcmeGui, Customer и Hotel. Проекты Customer и Hotel реализованы на управляемом C++, проект Customer — на С». Так сделано потому, что AcmeGui реализует аспекты программы, связанные с графическим интерфейсом пользователя (GUI), а это значительно удобнее делать на С», нежели на управляемом C++. Тем не менее, поскольку в данной главе рассматриваются вопросы, связанные с доступом к базам данных, весь исходный код для работы с базами данных реализован на управляемом C++.
приложения. Так что, если у вас нет необходимости использовать преимущества класса DataSet (Набор данных), нет смысла увеличивать накладные расходы в приложении.
При помещении данных в объект DataSet (Набор данных) считываются также и связанные с этими данными таблицы и столбцы. Каждый набор данных имеет коллекции, представляющие все таблицы, столбцы и строки, связанные с данными, содержащимися в этом наборе.
Класс HotelBroker (Посредник, бронирующий места в гостинице) из используемого нами в качестве примера приложения содержит метод ListHotelsToFile, в котором продемонстрировано, как получить такую информацию и записать ее в файл Hotels. txt. Этот метод вызывается при нажатии кнопки на форме, описанной в файле MainAdmin-Form.cs. Вывод данных осуществляется перенаправлением вывода на консоль. hotelDA-taset — набор данных, содержащий данные из базы данных HotelBroker (Посредник, бронирующий места в гостинице). Приведем фрагмент файла HotelBroker.h.
TextWriter *tw = new StreamWriter("Hotels.txt");
Console::SetOut(tw); // печатающее устройство -
// переадресовать вывод
try
{
Console::WriteLine("Hotels"); // Гостиницы
DataTable *t =
hotelsDataset->Tables->get_Item( // Таблицы
"Hotels"); // Гостиницы
if (t == 0) // если (t == 0)
return;
lEnumerator *pEnum = t->Columns->GetEnumerator(); // Столбцы
while (pEnum->MoveNext())
{
DataColumn *c =
dynamic_cast<DataColumn *>(pEnum->Current);
Console::Write("{0, -20}", c->ColumnName);
}
Console::WriteLine("");
pEnum = t->Rows->GetEnumerator();
while (pEnum->MoveNext())
{
DataRow *r =
dynamic_cast<DataRow *> (pEnum->Current) ;
for (int i=0; i<t->Colunms->Count; i++) // Столбцы-> Счет
{
Type *type = r->get_Item(i)->GetType() ;
if (type->FullName->Equals("System::Int32"))
// если равняется ("Система:: Int32"))
Console::Write("{О, -20}", r->get_Item(i) ) ;
else
{
String *s = r->get_Item(i)->ToString(); // Строка
s = s->Trim(); // Вырезка
Console::Write("{0, -20}", s);
}
}
Console::WriteLine("");
}
Console::WriteLine("");
}
catch(Exception *e) // Исключение
{
throw e;
}
_finally // наконец
{
tw->Close ();
}
Коллекция Tables (Таблицы) — это коллекция всех экземпляров DataTable (Таблица данных), содержащихся в объекте DataSet (Набор данных). В нашем примере такой экземпляр один, так что нет необходимости перемещаться по коллекции. Поэтому программа просто проходит по столбцам таблицы, воспринимая их содержимое как заголовки для данных, которые будут распечатаны далее. После считывания заголовков просматривается содержимое каждой строки таблицы Для каждого из значений в строке программа выясняет тип значения и печатает его в соответствующем формате В нашем случае программа проверяет, какой тип имеет рассматриваемое поле базы данных Hotel (Гостиница) Проверка типа значения поля, а не просто вывод на печать как Object (Объект), позволяет вывести данные в соответствующем формате.
Как мы увидим позже, заполнять набор данных можно с помощью этих коллекций, без получения данных от их источника Для этого просто следует добавить таблицы, столбцы или строки в соответствующие коллекции.
Иногда необходимо параметризировать SQL-запрос. Кроме того, бывает желательно связать входные и выходные аргументы хранимой процедуры с переменными программы.
Для того чтобы сделать это, следует определить свойство Parameters (Параметры) класса SqlCommand, которое является коллекцией экземпляров класса SqlParameter. Процедура инсталляции, имеющаяся на Web-узле данной книги, добавляет в базу данных Northwind хранимую процедуру get_customers. To же самое можно выполнить и вручную с помощью Server Explorer в Visual Studio.NET или SQL Query Analyzer (Анализатор запросов SQL). Еще один способ — запустить макрос SQL, поставляемый вместе с примерами к данной книге. Хранимая процедура get_customers иллюстрирует, как можно использовать простую хранимую процедуру, имеющую один аргумент, а именно — название компании, и возвращающую идентификатор (ID) клиента, т.е. указанной компании.
CREATE PROCEDURE get_customers
(dcompanyname nvarchar ( 40), Scustomerid nchar(5) OUTPUT)
AS
select @customerid = CustomerlD from Customers where
CompanyName = @companyname
RETURN
GO
Пример StoredProcedure (Хранимая процедура) демонстрирует, как это можно сделать.
command = // команда
new SqlCommand("get_customers", conn);
command->CommandType = // команда
CommandType::StoredProcedure;
SqlParameter *p = 0; p = new SqlParameter(
"@companyname",
SqlDbType::NVarChar, 40);
p->Direction = ParameterDirection::Input;
// Направление = Ввод p->set_Value(S"Ernst Handel");
// Эрнст Хандель command->Parameters->Add(p);
// команда-> Параметры-> Добавить
p = new SqlParameter(
"@customerid", SqlDbType::NChar, 5);
p->Direction = ParameterDirection::Output;// Направление = Вывод
command->Parameters->Add(p); // команда-> Параметры-> Добавить command->ExecuteNonQuery(); // команда
Console::WriteLine(
"{0} Customerld = {!}",
command->get_Parameters()-> // команда
get_Item("gcompanyname")->Value, // Значение
command->get_Parameters()-> // команда
get_Item("Scustomerid")->Value) ; // Значение
Каждый отдельный член коллекции SqlParameterCollection, являющийся объектом SqlParameter, соответствует одному параметру SQL-запроса или хранимой процедуры. Как показано в примере, параметру не обязательно иметь какую-либо взаимосвязь с определенной таблицей или столбцом базы данных.
Тем минимумом, который необходимо определить в конструкторе или установкой свойств, являются имя и тип параметра. Если параметр имеет непостоянную длину, необходимо также определить его размер.
В приведенном примере к коллекции параметров добавляются два параметра. Первый соответствует аргументу хранимой процедуры. Второй соответствует возвращаемому хранимой процедурой значению.
Имя параметра соответствует имени аргумента хранимой процедуры get_customers. Другие параметры конструктора SqlParameter определяют тип параметра. В первом случае это строка Unicode переменного размера, длиной до 40 символов. Во втором — строка Unicode постоянного размера (5 символов). Обозначение SqlDbType : :NVarChar означает постоянный подлине поток символов Unicode.
Свойство Value (Значение) используется для установки или получения значения параметра. В нашем примере оно используется для инициализации входного параметра @companyname, соответствующего аргументу хранимой процедуры. Оно используется также для получения значения параметра @customerid, соответствующего возвращаемому хранимой процедурой значению.
Выходной параметр должен быть определен как таковой с помощью свойства Direction (Направление). В нашем примере параметр @companyname устанавливается как входной присвоением этому свойству значения ParameterDirection: : Input (Входной параметр). Аналогично, параметр @customerid устанавливается как выходной присвоением этому свойству значения ParameterDirection: :Output (Выходной параметр). Данная операция для выходного параметра должна быть проведена обязательно, так как по умолчанию свойство Direction (Направление) имеет значение, соответствующее входному параметру. Для того чтобы связать параметр с возвращаемым хранимой процедурой значением, используется значение ParameterDirection: :ReturnValue. Для параметров, используемых в обоих направлениях, берется значение ParameterDirection: : InputOutput (Входной и выходной параметр).
Имена параметров можно использовать для доступа к каждому из параметров коллекции параметров SqlCommand. Параметризованные команды могут работать как с классом SqlDataReader, так и с классом DataSet (Набор данных). Позже, при рассмотрении класса DataSet (Набор данных), мы расскажем, как определить свойство параметра Source (Источник), которое указывает, какому именно столбцу объекта DataSet (Набор данных) соответствует параметр.
Класс SqlDataReader может хранить несколько результирующих множеств, что продемонстрировано в примере DataReader. Два запроса, разделенные точкой с запятой, являются двумя SQL-запросами, которые приводят к возврату двух результирующих множеств, по одному на каждый запрос.
String *ConnString = // Строка
"server=localhost;uid=sa;pwd=;
database=Northwind";
String *cmd = // Строка
"select Customerld,
CompanyName from Customers where
// выбрать Customerld,
CompanyName из Клиентов где
Customerld like 'T%'/select Customerld, CompanyName ..."
// Customerld подобно "I % '; выбрать Customerld, CompanyName ...
int ResultSetCounter = -1; int NumberFields = 0;
reader = command->ExecuteReader(); // команда
if (reader != 0)
{
NumberFields = reader->FieldCount;
Object *fields[] = new Object*[NumberFields]; // новый
// Объект Console::WriteLine (
"Result Set\tCustomerId\tCompanyName");
// "Результат Set\tCustomerId\tCompanyName");
do
{
ResultSetCounter++;
while(reader->Read() == true) // пока Чтение ()
// == истина {
NumberFields =
reader->GetValues(fields); // поля Console::Write( " { 0 } " ,
ResultSetCounter.ToStringt)); for (int i = 0; i<NumberFields; i++)
{
Console::Write(
"\t\t{0}", fields[i]); // поля
Console::Write("\n"); // Запись
};
}
while(reader->NextResult() == true); // пока NextResult ()
// == истина
}
Метод FieldCount возвращает количество столбцов в результирующем множестве. Поскольку метод GetValues возвращает данные в их исходном формате, в качестве аргументов ему передается массив объектов. Метод NextResult обеспечивает перемещение к следующему результирующему множеству.
Результатом работы программы DataReader будет вывод на экран следующих строк:
Result Set Customerld 'CompanyName
0 THEBI The Big Cheese
0 THECR The Cracker Box
0 TOMSP Toms Spezialitaten
0 TORTU Tortuga Restaurante
0 TRADH Tradigao Hipermercados
0 TRAIH Trail's Head Gourmet
Provisioners
1 WANDK Die Wandernde Kuh
1 WARTH Wartian Herkku
1 WELLI Wellington Importadora
1 WHITC White Clover Markets
1 WILMK Wilman Kala
1 WOLZA Wolski Zajazd
Каждый объект DataSet (Набор данных) содержит коллекцию из одного или более объектов1,DataTable (Таблица данных). Каждый объект DataTable (Таблица данных) соответствует одной таблице. С помощью свойства SelectCommand, в котором содержится операция соединения, можно производить выборку из нескольких таблиц базы данных в один объект DataTable (Таблица данных). При необходимости обновить содержимое множественных таблиц достаточно определить лишь команду обновления, так как информация о связях между таблицами базы данных уже известна. В файле Hotel-Bookings, h нашего примера свойство SelectCommand объекта SqlDataAdapter, содержащегося в объекте HotelBroker (Посредник, бронирующий места в гостинице), определено следующим образом:
String *cmd = // Строка
"select Customerld, HotelName, City, ArrivalDate, // выбрать
DepartureDate, Reservationld from Reservations, Hotels
where Reservations.Hotelld = Hotels.Hotelld";
// где Резервирование.Hotelld = Гостиницы.Hotelld"; adapter->SelectCommand = new SqlCommand(cmd, conn); dataset = new DataSet; // новый Набор данных adapter->Fill(dataset, "Reservations");
// Заполнить (набор данных, "Резервирование")
В этом случае DataSet (Набор данных) содержит один объект DataTable (Таблица данных), представляющий таблицу, называющуюся Reservations (Резервирование). Информация о том, что некоторые данные получены из таблицы Hotels, не сохраняется.
В один набор данных можно загрузить данные нескольких таблиц. Это продемонстрировано в примере DataSchema, в котором используется база данных Northwind.
adapter->SelectCommand = new SqlCommand(
"select * from [Order Details] where Productld = 1",
// "выбрать * из [Подробности заказа] где Productld = 1 ",
conn);
adapter->FillSchema (
dataset, SchemaType::Source, "Order Details");
// набор данных, SchemaType:: Источник, " Подробности заказа");
adapter->Fill(dataset, "Order Details");
// Заполнить (набор данных, " Подробности заказа");
adapter->SelectCommand =
new SqlCommand("select * from Shippers", conn);
// выбрать * из Грузоотправителей adapter->FillSchema(
dataset, SchemaType::Source, "Shippers");
// набор данных, SchemaType:: Источник, "Грузоотправители"); adapter->Fill(dataset, "Shippers"); // Заполнить (набор данных, "Грузоотправители");
В этом случае объект DataSet (Набор данных) содержит две таблицы, OrderDetails и Shippers. Метод SqlDataAdapter: :FillSchema заполняет DataSet (Набор данных) данными из таблиц, а также информацией о первичных ключах, связанных с таблицами. Затем программа просматривает содержимое таблиц и выводит на печать данные и первичные ключи таблиц. Доступ к содержащимся в DataTable (Таблица данных) объектам DataColumn осуществляется с помощью коллекции Columns (Столбцы), также являющейся частью объекта DataTable (Таблица данных).
lEnumerator *pEnum = dataset->Tables->GetEnumerator();
// набор данных-> Таблицы while (pEnum->MoveNext ()) {
DataTable *t =
dynamic_cast<DataTable *>(pEnum->Current); Console::WriteLine(t->TableName); DataColumn *dc [] = t->PrimaryKey; for (int i=0; i<dc->Length; i++)
{
Console::WriteLine(
"\tPrimary Key Field {0} = {!}", // " \t Поле первичного ключа {0} = {1} ", _box(i),
dc[i]->ColumnName); }
Console::Write("\t"); // Запись
lEnumerator *pEnum = t->Columns->GetEnumerator(); // Столбцы while (pEnum->MoveNext()) {
DataColumn *c =
dynamic_cast<DataColumn *>(pEnum->Current); Console::Write ("{0, -20}", c->ColumnName); // Запись }
Console::WriteLine("");
pEnum = t->Rows->GetEnumerator() ; // Строки while (pEnum->MoveNext()) {
DataRow *r =
dynamic_cast<DataRow *>(pEnum->Current) ; Console::Write("\t"); // Запись for (int i=0; i<r->ItemArray.Length; i++) Console::Write ( // Запись "{0, -20}",
r->get_Item(i)->ToString()->Trim()); // Вырезка Console::WriteLine(""); } }
Программа выводит на экран название таблицы, первичные ключи, названия столбцов и данные таблиц:
Order Details
Primary Key Field 0 = OrderlD
Primary Key Field 1 = ProductID
OrderlD ProductID UnitPrice Quantity Discount
10285 1 14.4 45 0.2
10294 1 14.4 18 0
Shippers
Primary Key Field 0 = ShipperlD ShipperlD CompanyName Phone
1 Speedy Express (503) 555-9831
2 United Package (503) 555-3199
3 Federal Shipping (503) 555-9931
А вот и перевод:
Подробности заказа
Поле первичного ключа 0 = OrderlD
Поле первичного ключа 1 = ProductID
Идентификатор заказа Идентификатор продукта Цена Количество Скидка
10285 1 14.4 45 0.2
10294 1 14.4 18 О
Грузоотправители
Поле первичного ключа 0 = ShipperlD
Идентификатор грузоотправителя Название компании Телефон
1 Быстрый экспресс (503) 555-9831
2 Объединение пакет (503) 555-3199
3 Федеральная отгрузка (503) 555-9931
Транзакции только помогают сохранить непротиворечивость базы данных. Если вы переводите деньги со сберегательного счета на текущий для оплаты счета за телефон, транзакции помогут гарантировать, что деньги будут сняты с одного счета и появятся на другом (или же не произойдет ни того, ни другого). Вы не столкнетесь ни с ситуацией, когда деньги придут на текущий счет, но не будут сняты со сберегательного (это было бы неплохо для вас, но плохо для банка), ни с противоположной ситуацией (неприятной для вас, но хорошей для банка). Ничто не помешает вашей супруге потратить эти деньги на ужин в модном ресторане.
При оптимистическом блокировании предполагается, что ничего подобного не случится, но вам следует быть готовым к такой ситуации, если она все-таки возникнет. Использование пессимистического блокирования требует координации действий всех пользователей таблицы базы данных таким образом, чтобы предотвратить подобную ситуацию. Разумеется, чем меньше блокировок накладывается на столбец базы данных, тем шире возможности использования вашего приложения.
Следует понимать, что такая ситуация влияет и на считывание данных, и на их обновление. Скажем, если ваша супруга видит, что на счету есть деньги, и строит свои планы относительно этих денег, это может привести к не меньшим проблемам, чем просто потеря денег с общего текущего счета.
Хотя обсуждение способов решения таких проблем выходит далеко за пределы рассматриваемого в этой главе материала, важно помнить, что они возникают, если записи, считанные в объект DataSet (Набор данных), не блокированы. Использование SqlDa-taAdapter при работе с DataSet (Набор данных) предполагает применение оптимистической стратегии блокировки.
Почему это так важно? Прежде всего потому, что от этого зависит производительность и масштабируемость вашего приложения. А почему это так сложно? Потому что нельзя дать совета, подходящего для всех приложений в любой ситуации. Когда пользователи не обращаются одновременно к одним и тем же данным, использование оптимистической стратегии блокировок является наилучшим вариантом. Если необходимо заблокировать доступ к записи на долгий период времени, время ожидания получения доступа к этой записи значительно увеличится, понижая тем самым производительность и масштабируемость приложения.
Вы должны понимать, что такое уровни локализации транзакций, администратор блокировок базы данных, и что существует возможность конфликта при доступе к данным, а такой конфликт может привести к зависанию приложения. Вы должны понимать, сколько времени и ресурсов может потратить ваше приложение на разрешение конфликтов, как оно должно поступать с несогласованными или некорректными данными. Все это необходимо для принятия решения, в каких ситуациях допускается попытка избежать зависания любой ценой, и как следует поступать при возникновении последовательности конфликтующих операций.
Иногда может понадобиться использовать объект DataSet (Набор данных) с дополнительно реализованными возможностями для проверки того, были ли изменены записи, содержащиеся в нем, со времени их последней выборки или модификации. А можно просто использовать SqlDataReader и заново произвести выборку. Все это зависит от ситуации.
Так, при бронировании комнат в нашем примере HotelBroker (Посредник, бронирующий места в гостинице) нельзя делать оптимистических предположений о наличии свободных мест. Это равносильно предположению о бесконечном количестве комнат в отеле и приведет к ситуации, когда администратор должен будет распределить ограниченное количество комнат на гораздо большее количество желающих. В нашем примере для проверки того, зарезервирована ли комната, используется хранимая процедура MakeReservation.
Иногда, даже при отсутствии одновременных запросов, объект DataSet (Набор данных) нельзя использовать для добавления новой строки без установления связи с базой данных. В нашем примере HotelBroker (Посредник, бронирующий места в гостинице) нельзя использовать произвольный первичный ключ. Бронирование могут производить одновременно несколько пользователей. Поэтому идентификаторы бронирования не могут быть локальными. Их определение должно производиться самой базой данных. В нашем примере это делает хранимая процедура MakeReservation.
Способ использования отсоединенных операций в вашем приложении следует определить еще прежде, чем вы решите, каким образом будут использоваться объекты SqlDataReader и DataSet (Набор данных).
Зачем вообще в приложении HotelBroker (Посредник, бронирующий места в гостинице) используется DataSet (Набор данных)? Фактически, в реализации объекта Customer (Клиент) DataSet (Набор данных) никак не используется. Но его использует объект HotelBroker (Посредник, бронирующий места в гостинице), и делается это по двум причинам. Первая — педагогическая. Мы хотели показать, как объект DataSet (Набор данных) может быть использован в полноценном приложении, а не только в простой программе. Во-вторых, в Web-ориентированной версии приложения, реализованной в последующих главах книги, удобно производить кэширование некоторых данных. Например, вполне разумно сделать так, чтобы пользователь мог работать с локальной копией системы бронирования. С другой стороны, такую информацию, как электронный адрес пользователя, достаточно запросить один раз — при его регистрации в системе. Поэтому в нашем случае нет необходимости в сложном механизме кэширования информации о пользователе, так что реализованный объект Customer (Клиент) использует методы объекта SqlCommand.
Каким образом метод SqlDataAdapter: :Update (Обновить) передает источнику данных информацию о произведенных изменениях? Изменения, внесенные в объект Da-taSet (Набор данных), передаются базе данных с помощью свойств InsertCommand, UpdateCommand (Команда обновления) и DeleteCommand класса SqlDataAdapter. Каждому из этих свойств присваивается экземпляр SqlCommand, который может быть параметризован для того, чтобы поставить в соответствие переменные программы частям SQL-запроса. Продемонстрируем это на примере кода, взятого из реализации конструктора класса HotelBroker (Посредник, бронирующий места в гостинице).
Экземпляр SqlCommand создается для представления параметризованного SQL-запроса, который используется при вызове метода SqlDataAdapter:: Update (Обновить) для добавления в базу данных новой строки. В момент вызова метода вместо параметров будут подставлены фактические значения.
SqlCommand *cmd = new SqlCommand(
"insert Hotels(City, HotelName, NumberRooms, RoomRate)
// "вставить Гостиницы (Город, HotelName,
// NumberRooms, RoomRate)
values(@City, @Name, SNumRooms, @RoomRate)", // значения
conn);
Параметры должны быть связаны с соответствующими столбцами в DataRow. Во фрагменте кода метода AddHotel, рассмотренного ранее, столбцы различались по именам: HotelName, City (Город), NumberRooms, RoomRate. В конструкторе SqlParame-ter им соответствуют параметры @Name, @City, @NumRooms, @RoomRate. Последний аргумент инициализирует свойство Source (Источник) объекта SqlParameter. Свойство Source (Источник) определяет столбец объекта DataSet (Набор данных), которому соответствует параметр. Метод Add (Добавить) помещает параметр в коллекцию объектов Parameter (Параметр), связанную с экземпляром SqlCommand.
SqlParameter *param = new SqlParameter(
"@City", SqlDbType::Char, 20, "City");
cmd->Parameters->Add(param); // Параметры-> Добавить
cmd->Parameters->Add(new SqlParameter( // Параметры-> Добавить
"@Name", SqlDbType::Char, 20, "HotelName"));
cmd->Parameters->Add (new SqlParameter( // Параметры-> Добавить
"@NumRooms", SqlDbType::Int, 4, "NumberRooms"));
cmd->Parameters->Add(new SqlParameter( // Параметры-> Добавить
"@RoomRate", SqlDbType::Money, 8, "RoomRate"));
И, наконец, свойству InsertCommand класса SqlDataAdapter присваивается указатель на экземпляр класса SqlCommand. Отныне именно эта команда будет использоваться при вставке строки в базу данных:
hotelsAdapter->InsertCommand = cmd;
Аналогичный исходный код есть в конструкторе класса HotelBroker (Посредник, бронирующий места в гостинице). Различие лишь в том, что там устанавливаются значения свойств UpdateCommand (Команда обновления) и DeleteCommand для определения команд обновления и удаления строк.
hotelsAdapter->UpdateCommand = new SqlCommand(
"update Hotels set NumberRooms = @NumRooms, RoomRate = @RoomRate where City = @City and HotelName = @Name",
// где Город = @City и HotelName = @Name ", conn); hotelsAdapter->UpdateCommand->Parameters->Add(
// Параметры-> Добавить new SqlParameter(
"@City", SqlDbType::Char,20, "City")); hotelsAdapter->UpdateCommand->Parameters->Add(
// Параметры-> Добавить new SqlParameter(
"@Name", SqlDbType:-.Char, 20, "HotelName")); hotelsAdapter->UpdateCommand->Parameters->Add(
// Параметры-> Добавить new SqlParameter(
"@NumRooms", SqlDbType::Int, 4, "NumberRooms")); hotelsAdapter->UpdateCommand->Parameters->Add(
// Параметры-> Добавить new SqlParameter(
"@RoomRate",SqlDbType::Money, 8, "RoomRate"));
hotelsAdapter->DeleteCoiranand = new SqlCommand(
"delete from Hotels where City = @City and HotelName = // "удалить из Гостиниц где Город = @City и HotelName = @Name", conn);
hotelsAdapter->DeleteCommand->Parameters->Add(
// Параметры-> Добавить new SqlParameter(
"SCity", SqlDbType::Char, 20, "City"));
hotelsAdapter->DeleteCommand->Parameters->Add(
// Параметры-> Добавить new SqlParameter(
"@Name", SqlDbType::Char, 20, "HotelName"));
Все изменения, внесенные в объект DataSet (Набор данных), будут переданы базе данных при выполнении метода SqlDataAdapter: : Update (Обновить). Как принять или отменить внесенные изменения до вызова этого метода, будет рассмотрено в следующем разделе.
Каждый объект DataTable (Таблица данных) содержит коллекцию объектов Da-taRow. Каждый такой объект представляет строку таблицы. Добавление нового объекта DataRow влияет на ограничения объекта DataColumn (мы предполагаем, что свойство Enf orceConstraints объекта DataSet (Набор данных) имеет значение true (истина)).
Первичные ключи
Существует несколько типов ограничений. Первичный ключ — уникальный ключ для строк таблицы. Другие ограничения единственности определяют единственность каждого значения в столбце (или столбцах). Внешний ключ используется для обозначения того, что значения в столбце являются первичными ключами для другой таблицы объекта DataSet (Набор данных). Первичный ключ объекта DataTable (Таблица данных) является свойством:
// Определить РК для таблицы BookCategories DataColumn *bookcategoriesPK [] =
new DataColumn*[2]; bookcategoriesPK[0] = en; bookcategoriesPK[l] = loc; bookcategories->PrimaryKey = bookcategoriesPK;
// Определить РК для таблицы Authors (Авторы) DataColumn *authorsPK [] =
new DataColumn*[1]; authorsPK[0] = auid; authors->PrimaryKey = authorsPK; // авторы
// Определить РК для таблицы Books (Книги) DataColumn *booksPK [] =
new DataColumn*[1]; booksPK[0] = ISBN; books->PrimaryKey = booksPK; // книги
Ограничения
Для работы со всеми ограничениями помимо первичных ключей используются абстрактный базовый класс Constraint (Ограничение) и его производные классы: UniqueConstraint и ForeignKeyConstraint. Базовый класс обеспечивает возможность помещения ограничения в коллекцию ограничений таблицы. Первичный ключ также регистрируется в этой коллекции как ограничение единственности с именем, генерируемым системой. Для определения, является ли ограничение первичным ключом, используется свойство UniqueConstraint::IsPrimaryKey.
Определим уникальность значений столбца Category таблицы Categories (Категории). Так как последний аргумент метода Add (Добавить) имеет значение false (ложь), это ограничение не будет первичным ключом таблицы. Для этой таблицы мы не определяем первичного ключа, а задаем только ограничения единственности. Вообще говоря, задавать ограничения для значений таблицы не обязательно. Хотя это и нарушает правила реляционной целостности, никто не заставляет вас использовать объект DataSet (Набор данных) реляционным способом. // Определим ограничение единственности // для таблицы Categories (Категории) categones->Constraints->Add ( // категории-> Ограничениям Добавить
"Unique CategoryName Constraint",
// "Уникальное ограничение CategoryName ",
categoryname,
false); // ложь
При использовании внешнего ключа можно определить действия, которые следует выполнить при изменении первичного ключа, с которым он связан. Выбор здесь стандартен: None (Ничего), Cascade (Каскад), SetNull. Для установки значения, принимаемого по умолчанию для этого параметра (он описывается в свойстве Def aultValue объекта DataColumn), используется метод SetDefault. Эти параметры могут быть определены как для условий обновления, так и для условий удаления данных.
В нашем примере внешний ключ определяется таким образом, чтобы все идентификаторы авторов, содержащиеся в таблице Books (Книги), были описаны также и в таблице Authors (Авторы). Другими словами, у каждой книги, зарегистрированной в базе, есть автор, который также зарегистрирован в этой же базе. Мы назвали это ограничение '"Authors->Books". При изменении идентификатора автора правила обновления данных вынуждают объект DataSet (Набор данных) изменить этот идентификатор и во всех остальных строках таблиц на новое значение. Когда идентификатор удаляется, значение для этого идентификатора в строках, описывающих книги, будет установлено пустым.
Если при этом свойство DeleteRule имеет значение Cascade (Каскад), то каскадное удаление будет выполнено для всех таких строк из таблицы Books (Книги). Свойство Ас-ceptRejectRule используется при транзакционном изменении объекта DataSet (Набор данных) и будет рассмотрено ниже. Значение этого свойства определяет, что произойдет при вызове метода AcceptChanges объектов DataSet (Набор данных), Da-taRow или DataTable (Таблица данных). В нашем примере изменения будут произведены последовательно со всеми данными. Другое возможное значение этого свойства — None (не совершать никаких действий).
// Определить FK для таблицы Books (Книги) // (Authorld должен быть в таблице Authors (Авторы)) DataColumn *bookauthorFK [] =
new DataColumn*[1]; bookauthorFK[0] = booksauid; ForeignKeyConstraint *fk =
new ForeignKeyConstraint(
"Authors->Books", authorsPK, bookauthorFK); // Авторы-> Книги
fk->AcceptRe;jectRule = AcceptRejectRule::Cascade; // Каскад fk->DeleteRule = Rule::SetNull; fk->UpdateRule = Rule:rCascade; // Каскад books->Constraints->Add(fk); // книги-> Ограничениям Добавить
Связи между данными
Кроме ограничений для данных, можно задавать связи между ними, для хранения которых используется коллекция DataRelation объекта DataSet (Набор данных). Связи соединяют таблицы таким образом, что вы можете перемещаться от предка к потомку и наоборот. При добавлении связи в коллекцию ограничений автоматически добавляется и соответствующий внешний ключ.
В нашем примере таблица Categories (Категории) сделана предком таблицы Book-Categories (Категории книг) через столбцы Categories (Категории) и CategoryName. Оба столбца, между которыми определяется связь, должны содержать данные одного типа. Эту связь можно использовать для нахождения строк в таблице-потомке или строки в таблице-предке по значению поля, соответствующего связанному столбцу. В нашем примере необходимо также установить связь между столбцами, описывающими номер книги в Библиотеке Конгресса в таблицах Books (Книги) и BookCategory.
// Установим связь между столбцом Categories (Категории) // в таблице BookCategories (Категории книг) и
// столбцом Categories (Категории) в таблице Categories (Категории) ds->Relations->Add( // Отношения-> Добавить
"Category->BookCategories Relation",
// "Категория-> Отношение BookCategories ",
categoryname,
en) ;
// Установим связь между столбцом Library of Congress Number // (Номер книги в Библиотеке Конгресса) таблицы Books (Книги) и // столбцом LOC таблицы BookCategories (Категории книг) ds->Relations->Add( // Отношения-> Добавить
"Book Category LOC->Book LOC Relation",
loc,
bloc);
Можно также выбрать подмножество данных из объекта DataSet (Набор данных). Метод Select (Выбрать) класса DataTable (Таблица данных) имеет синтаксис, совпадающий с синтаксисом фразы "where" в SQL-запросах. Для доступа к полям строки используются имена столбцов Ниже приведен пример из описания класса HotelBroker (Посредник, бронирующий места в гостинице), в котором этот метод используется для получения списка отелей определенного города.
ArrayList *GetHotels(String *city)
{
try
{
DataTable *t = hotelsDataset->
Tables->get_Item("Hotels"); // Гостиницы
DataRow *rows [] = t->Select( // Выбор
String::Format("City = '{0}'", city)); // Строка:: Формат ("Город = ' {0} ' ", город));
ArrayList *hotels = new ArrayList;
for (int i=0; i < rows->Length; i++)
{
String *name = rows[i]->get_Item(
"HotelName") ->ToString {) ->Tnm() ; // Вырезка
hotels->Add(name); // гостиницы-> Добавить (название);
}
return hotels; // гостиницы
}
catch(Exception *e) // Исключение
{
throw e;
}
}
Метод AddHotel класса HotelBroker (Посредник, бронирующий места в гостинице) иллюстрирует, как добавляется новая строка в объект DataSet (Набор данных) При этом создается новый экземпляр класса DataRow и для добавления данных в соответствующие поля используются имена столбцов
Если необходимо сохранить созданную строку в базе данных, используется метод Update (Обновить) класса SqlDataAdapter Он является промежуточным звеном между объектом DataSet (Набор данных) и базой данных Позже мы обсудим, как производить транзакционное редактирование набора данных для того, чтобы принять или отвергнуть изменения до их передачи в базу данных
String *AddHptel( // Строка
String *city, // Строка
String *name, // Строка
int number, // номер
Decimal rate) // Десятичная цена
{
try
{
DataTable *t = hotelsDataset->Tables->get_Item( // Таблицы
"Hotels"); // Гостиницы
DataRow *r = t->NewRow();
r->set_Item("HotelName", name); // название
r->set_Item("City", city); // ("Город", город)
r->set_Item("NumberRooms", _box(number));
r->set_Item("RoomRate", _box(rate));
t->Rows->Add(r); // Строки-> Добавить
hotelsAdapter->Update(hotelsDataset, "Hotels"); // Обновить "Гостиницы"
}
catch(Exception *e) // Исключение
{
throw e;
}
}
Для удаления строки из объекта DataSet (Набор данных) прежде всего необходимо найти эту строку или строки, а затем вызвать метод Delete (Удалить) для каждого из удаляемых экземпляров DataRow. Метод Remove (Удалить) удаляет экземпляр DataRow из коллекции Этот экземпляр не помечается как удаленный, так как он уже не является частью объекта DataSet (Набор данных) При вызове метода Update (Обновить) преобразователя данных соответствующие данные не будут удалены из базы данных Приведем фрагмент метода DeleteHotel класса HotelBroker (Посредник, бронирующий места в гостинице)
String *DeleteHotel(String *city, String *name) // Строка
*DeleteHotel (Строка *city, Строка *name)
{
try
{
t = hotelsDataset->Tables->get_Item("Hotels"); // Таблицы-> get_Item ("Гостиницы")
r = t->Select ( // Выбор String::Format(
// Строка:: Формат (
"City = '{0}' and HotelName = '{!}'",
// "Город = ' {0} ' и HotelName ='{!}' ",
city, name)); // город, название
for (i=0; i<r->Length; i++)
r[i]->Delete (); // Удалить
}
Для изменения строки набора данных достаточно просто найти эту строку и внести необходимые изменения в поля строки. В качестве примера ниже приведен фрагмент реализации метода ChangeRooms класса HotelBroker (Посредник, бронирующий места в гостинице). При вызове метода Update (Обновить) преобразователя данных, все изменения, сделанные в этом фрагменте, будут переданы базе данных.
String *ChangeRooms( // Строка
String *city, // Строка
String *name, // Строка
int numberRooms,
Decimal rate) // Десятичная цена
{
DataTable *t = 0;
try
{
t = hotelsDataset->Tables->get_Item("Hotels"); // Таблицы-> get_Item ("Гостиницы")
DataRow *r [] = t->Select( // Выбор
String::Format(
// Строка:: Формат (
"City = '{0}' and HotelName = '{I}1",
// "Город = ' {0} ' и HotelName = ' {1} ' ",
city, name)); // город, название
for (int i = 0; i < r->Length; i++) {
r[i]->set_Item("NumberRooms", _box(numberRooms));
r[i]->set_Item("RoomRate", _box(rate)) ; }
} }
Режим работы с базами данных при отсутствии постоянного соединения с базой данных называют отсоединенным (disconnected). Соединенный режим представляет собой сильносвязанную среду, которая может содержать состояния и соединения. Среда клиент/сервер является тому подтверждением. Именно для такого подхода и были разработаны ADO и OLEDB (OLE для баз данных). В среде соединенного режима можно использовать устройства считывания данных. При необходимости для этих целей можно использовать, посредством обеспечивающих взаимодействие СОМ-компонентов, ADO. Фактически, специально для применения в .NET, изменения в ADO не вносились, так что здесь есть полная обратная совместимость, включая также ошибки и прочее.
Однако держать соединение постоянно открытым слишком дорого в среде, в которой требуется обеспечить возможность работы нескольким пользователям. Это относится к многоузловым и Internet-ориентированным решениям. В таких средах часто нет необходимости блокировать доступ к таблицам баз данных. А это способствует масштабируемости, так как уменьшает вероятность конфликтов. Объекты DataSet (Набор данных) из коллекции таких объектов Tables (Таблицы), с их ограничениями, могут имитировать таблицы исходной базы данных и взаимосвязи между ними. В приложениях, полностью реализованных в .NET, одна часть приложения может передавать или получать экземпляр DataSet (Набор данных). Конкурентоспособным разработкам это может дать большое преимущество в масштабируемости и производительности, что справедливо также для многих типов Internet-приложений и приложений, ориентированных на внутрисете-вое применение.
При работе в отсоединенном режиме соединение осуществляется таким же образом, как и в соединенном режиме. Данные получают с помощью классов преобразования данных источников данных. Свойство SelectCommand определяет SQL-запрос, используемый для передачи данных в набор данных. В отличие от устройства считывания данных, которое связано соединением с определенной базой данных, набор данных не имеет связей ни с какой базой данных, даже с той, из которой были получены хранящиеся в нем данные.
Рассмотрим, как можно получить информацию об объекте DataTabie (Таблица данных), точнее, об ограничениях и ключах этого объекта. В предыдущем примере уже было показано, как получить доступ к объектам Data-Column объекта DataTabie (Таблица данных). Обратите внимание на использование свойства IsPrimaryKey объекта UniqueConstraint для определения, является ли ограничение первичным ключом. Следующий фрагмент взят из примера DataEditing.
pEnum = ds->Tables->GetEnumerator(); // Таблицы
while (pEnum->MoveNext() )
{
DataTabie *t =
dynamic_cast<DataTable *>(pEnum->Current);
Console::WriteLine(" {0}", t->TableName);
Console : :WriteLine ( "\tPrimary Key:")/' // Первичный ключ
for (int i = 0; i < t->PrimaryKey.Length; i++)
{
DataColumn *c = t->PrimaryKey[i]; Console::WriteLine("\t\t{0}", c->ColumnName); }
Console::WriteLine("\tConstraints:" ) ;
lEnumerator *pEnum = t->Constraints->GetEnumerator(); // Ограничения while (pEnum->MoveNext()) {
Constraint *c = // Ограничение
dynamic_cast<Constraint *>(pEnum->Current); // Ограничение
String *constraintName; // Строка // если (с - ForeignKeyConstraint) if (dynamic_cast<ForeignKeyConstraint *>(c) != 0) constraintName =
String::Concat("Foreign Key:", // Строка:: Concat ("Внешний ключ: ", c->ConstraintName);
else if (dynamic_cast<UniqueConstraint *>(c) != 0) {
UniqueConstraint *u = dynamic_cast<UniqueConstraint *>(c); if (u->IsPrimaryKey)
constraintName = "Primary Key"; // Первичный ключ else
constraintName = u->ConstraintName; } else
constraintName = "Unknown Name"; // Неизвестное имя Console::WriteLine("\t\t{0, -40}", constraintName); } }
Напечатанные программой строки приведены ниже. Обратите внимание на то, что определение связей, осуществленное нами с помощью объектов DataRelation, приводит к появлению объектов ForeignKeyConstraint в коллекции ограничений таблицы.
Первичный ключ также появляется в коллекции ограничений как объект UniqueCon-straint. Ограничения, определенные как ограничения единственности или внешние ключи, появляются в коллекции так, как ожидается.
Categories
Primary Key: Constraints:
Unique CategoryName Constraint BookCategories Primary Key:
CategoryName
LibraryofCongressNumber Constraints:
Primary Key
Foreign Key:Category->BookCategories Relation
Constraint2 Authors
Primary Key:
Authorld Constraints:
Primary Key Books
Primary Key:
ISBN Constraints:
Primary Key
Foreign Key:Authors->Books
Foreign Key:Book Category LOC->Book LOG Relation
Вот перевод этой выдачи:
Категории
Первичный ключ: Ограничения:
Ограничение единственности CategoryName Категории книг
Первичный ключ:
Название категории
Номер в Библиотеке Конгресса Ограничения:
Первичный ключ
Внешний ключ: Отношение Category-> BookCategories
Constraint2 Авторы
Первичный ключ:
Authorld Ограничения:
Первичный ключ Книги
Первичный ключ:
ISBN Ограничения:
Первичный ключ:
Внешний ключ:Авторы-> Книги
Внешний ключ: Отношение Book Category LOC->Book LOG
Обратите внимание на ограничение в таблице BookCategories (Категории книг) с именем, сгенерированным системой. Когда вы внимательно просмотрите исходный код программы, то убедитесь, что эти ограничения в ней не добавляются. Откуда же они берутся? Если бы вы просмотрели содержимое соответствующего объекта, то увидели бы, что ограничение наложено на столбец LibraryofCongressNumber. Система посчитала, что, поскольку столбец CategoryName является внешним ключом для другой таблицы, то значения в столбце LibraryOfCongressNumber должны быть уникальными.
Можно также просмотреть коллекцию связей Relations (Отношения) объекта DataSet (Набор данных). При этом можно узнать, какие таблицы являются предками, и какие именно столбцы в них участвуют в образовании связей. То же самое можно сделать и для таблиц-потомков. Приведем соответствующий фрагмент примера DataEditing.
pEnum = ds->Relations->GetEnumerator(); // Отношения
while (pEnum->MoveNext())
{
DataRelation *dr =
dynamic_cast<DataRelation *>(pEnum->Current); DataTable ^parentTable = dr->ParentTable; DataTable *childTable = dr->ChildTable; Console::WriteLine(
" Relation: {0} ", dr->RelationName); // Отношение Console::WriteLine(
ParentTable: {0, -10}", parentTable); Console::Write(" Columns: "); // Столбцы forfint j =0; j < dr->ParentColumns.Length; j++) Console::Write( // Запись
{0, -10}",
dr->ParentColumns[j]->ColumnName); Console::WriteLine(); Console::WriteLine(
ChildTable: (0, -10}", childTable);
Console::Write(" Columns: "); // Запись forfint j = 0; j < dr->ChildColumns.Length; j++) Console::Write( // Запись
{0, -10}",
dr->ChildColumns[j]->ColumnName); Console::WriteLine(); }
Программа напечатает:
Output Relations between tables in the DataSet... Relation: Category->BookCategones Relation ParentTable: Categories
Columns: Category ChildTable: BookCategories
Columns: CategoryName Relation: Book Category LOC->Book LOG Relation ParentTable: BookCategories
Columns: LibraryofCongressNumber
ChildTable: Books
Columns: LibraryofCongressNumber
А вот и перевод:
Отношения между таблицами в Наборе данных...
Отношение: Категория-> Отношение Категории книг ParentTable: Категории
Столбцы: Категория ChildTable: Категории книг
Столбцы: CategoryName Отношение: Отношение Категория книги LOC->Book LOC ParentTable: Категории книг
Столбцы: Номер в Библиотеке Конгресса ChildTable: Книги
Столбцы: Номер в Библиотеке Конгресса
К этому моменту мы изложили более чем достаточно материала, необходимого для понимания классов Customer (Клиент) и HotelBroker (Посредник, бронирующий места в гостинице) из версии приложения Acme Travel Agency (Туристическое агентство Acme), ориентированной на работу с базами данных. Как обычно, файлы с исходным кодом для этой версии находятся в папке CaseStudy. Если вы использовали программы, изменяющие содержимое базы данных HotelBroker (Посредник, бронирующий места в гостинице), не забудьте запустить макрос SQL, приводящий эту базу в исходное состояние.
В связи с тем, что у нас не было необходимости хранить какое-либо состояние объекта Customer (Клиент), в нем для доступа к базе данных и получения данных используется объект SqlDataReader Любое состояние, которое может понадобиться программе (например, список клиентов), легко может быть получено у программы-клиента, а не у объекта среднего яруса. Объекты HotelBroker (Посредник, бронирующий места в гостинице) и HotelBookings немного более сложны. Как уже было сказано, из педагогических побуждении эти объекты были реализованы с использованием объекта Data-Set (Набор данных). Так сделано для того, чтобы продемонстрировать использование этой технологии в приложениях. Тем не менее, мы увидим, что при разработке Web-ориентированых приложении есть причины сохранять некоторые состояния в среднем ярусе. В этом случае объект DataSet (Набор данных) служит интеллектуальным кэшем.
А теперь отвлечемся от примера и рассмотрим интеграцию ХМL с базой данных.
Классы каркаса, предназначенного для работы с базами данных, собраны в ADO.NET. Класс DataSet (Набор данных) позволяет работать с реляционными данными реляционным же способом, независимо от того, есть ли в текущий момент соединение с источником данных. Разъединенный (disconnected) доступ к данным становится все более значимым в многоярусном и Internet-ориентированном мире данных. При использовании такого типа доступа к данным необходимо установить соединение с базой данных только для изменения или получения ее содержимого. Конечно, при желании, можно работать и обычным соединенным (connected) способом.
Источники данных ADO.NET позволяют задавать команды непосредственно источнику данных. При этом не используются промежуточные объекты, такие, как объекты OLEDB (OLE для баз данных), которые находятся между ADO и источником данных. Класс DataAdapter эмулирует источник данных (как набор команд базы данных) и соединение с этим источником данных. Класс DataAdapter реализует интерфейс IDa-taAdapter, являющийся связующим звеном между объектом DataSet (Набор данных) и источником данных. Различия между источниками данных скрыты интерфейсом I DataAdapter. Источники данных OLEDB (OLE для баз данных) позволяют использовать вложенные (nested) транзакции; а источники данных SqlServer этого не позволяют.
Источники данных .NET передают данные в набор данных или в устройство считывания данных. Набор данных— резидентная упрощенная реляционная база данных, не соединенная прямо ни с какой другой базой данных. Набор данных можно даже преобразовать в документ XML, и наоборот. Это позволяет работать с данными как с реляционными или как с иерархическими XML-данными. Устройства считывания данных моделируют обычный способ работы с базами данных.
Классы доступа к данным, поставляемые вместе с каркасом, находятся в пространствах имен System: :Data (Система: Данные), System: : Data: :SqlClient, System:: Data::01eDb (Система::Данные::ОЬЕ для баз данных), System: : Data: :Common (Система::Данные::Обшие) и System: : Data: :SqlTypes. Пространства имен OleDb (OLE для баз данных) и Sql содержат классы, используемые при работе с источниками данных OleDb (OLE для баз данных) и SqlServer соответственно. Уже разработан источник данных ODBC, а другие драйверы доступа будут созданы в ближайшем будущем.
В этой главе мы изменим реализацию классов Customer (Клиент) и Hotel (Гостиница) для того, чтобы ближе познакомиться с использованием SQL Server. Для демонстрации использования XML в наш пример туристического агентства Acme Travel Agency добавим возможность бронирования авиабилетов.
В своих примерах мы будем использовать SQL Server 2000 и источник данных SQL Server. Несмотря на это, большинство материала, изложенного в главе, можно отнести и к источнику данных OleDb (OLE для баз данных).
Кроме того, для понимания примеров читателю необходимо понимать принципы работы баз данных
Базы данных, используемые в примерах
В главе предполагается, что SQL Server установлен в конфигурации Local System account, причем в качестве режима аутентификации выбран Mixed Mode (Смешанный режим). Предполагается, что имя пользователя — sa, а поле пароля не заполнялось. В некоторых примерах используется база данных Northwind Trader, инсталлируемая в качестве образца базы данных в составе SQL Server. Кроме того, в некоторых примерах используются базы данных HoteLBroker (Посредник, бронирующий места в гостинице) и AirlineBroker, созданные исключительно как иллюстративный материал к данной книге. Некоторые из иллюстративных программ изменяют используемые базы данных, в то время как в других предполагается, что эти базы имеют первоначальный вид. В результате какие-то программы не будут работать подобающим образом, пока вы не восстановите исходный вид используемых в них баз данных. Это можно сделать с помощью прилагаемых к программам макросов SQL. Более подробную информацию можно найти в файле readme.txt.
Проводник Visual Studio.NET no серверу, Server Explorer— полезная утилита при работе с базами данных. Хотя и не такая мощная, как SQL Server Enterprise Manager, она обеспечивает базовые возможности, необходимые при создании и отладке приложений, работающих с базами данных.
Для того чтобы запустить Server Explorer, выберите пункт меню View=>Server Explorer. Окно Server Explorer можно прикрепить и при необходимости перемещать. На Рисунок 9.1 представлено окно Server Explorer.
С помощью Server Explorer можно легко получить информацию о любом поле таблицы, просмотреть или изменить данные в ней. Можно также создавать или изменять хранимые процедуры и разрабатывать таблицы. Далее мы рассмотрим Server Explorer в нескольких примерах для того, чтобы ближе познакомить читателя с его использованием.
Использовавшийся в предыдущем примере режим называют соединенным. Программа соединяется с базой данных, выполняет все необходимые действия, а затем отсоединяется. При этом перемещаться по данным базы можно только в одном направлении. Это соответствует однонаправленному курсору/набору записей в классической технологии доступа к данным ADO. При использовании соединенного режима следует открывать и закрывать соединение явно.
Держать соединение постоянно открытым — не лучший способ работы, если вы хотите минимизировать потребление ресурсов (соединение само по себе недешево) для обеспечения масштабируемости. Тем не менее, как мы увидим позже, именно использование SqlDa-taReader может, в зависимости от ваших потребностей, оказаться правильным подходом.
Далее будет показано, что SqlConnection используется вместе с DataSet (Набор данных) и SqlDataReader для установления соединения с базой данных так же, как это сделано выше с помощью SqlCommand. Объект SqlConnection, кроме того, управляет свойствами базы данных, такими, как транзакции и уровни изоляции. Основная (root) транзакция начинается вызовом метода BeginTransaction класса SqlConnection". Аналогичная строка соединения с SQL Server с использованием объекта класса OleDbConnection будет такой:
"Provider=SQLOLEDB.1;server=localhost;uid=sa;pwd=;
database=Northwind";
В приведенной строке следует изменить на корректные имя сервера, идентификатор и пароль пользователя.
Как уже было сказано, SqlCommand применяется для выполнения команд при использовании и DataSet (Набор данных) и SqlDataReader, только действует немного по-разному. Это станет более понятным после рассмотрения класса SqlDataAdapter.
Свойство CommandType определяет тип команды, хранимой в SqlCommand. Для источника данных Sql это может быть Text (Текст) (принятое по умолчанию значение) или StoredProcedure (Хранимая процедура). CommandText также можно определить как свойство. Вскоре мы научимся использовать параметры при работе с командами, которые передаются базе данных.
Экземпляр класса SqlDataReader возвращается посредством метода Ехе-cuteReader экземпляра класса SqlCommand. Если программа должна быть независима от используемого источника данных, вместо указанного метода следует использовать интерфейс IDataReader. При этом можно вызывать методы интерфейса, а не самого экземпляра класса.
IDataReader *idr = command->ExecuteReader() ;
Этот же прием можно использовать и для других классов источника данных, где реализованы интерфейсы, которые поддерживаются несколькими источниками данных. Пока экземпляр класса SqlDataReader не будет закрыт, никакие действия над объектом SqlCommand, кроме его закрытия, недоступны.
Язык XML не навязывает принцип организации данных или суть документа XML. Он лишь определяет правила сопоставления документов. С другой стороны, схема XML описывает метаданные, т.е. способ организации данных внутри документа XML. Схемы XML пишутся на XML.
Например, сам по себе XML можно использовать для описания данных реляционной базы данных, а схема XML может использоваться для описания связей между данными, такими, как первичные или внешние ключи. Гораздо проще использовать схему XML и данные в одном документе или текстовом потоке, чем загружать каждую таблицу в набор данных, а затем программно устанавливать связи между таблицами.
Используя объект DataSet (Набор данных) можно создать новый документ XML. Используя запрос XPath, можно перейти в начало документа, а затем, с помощью объекта XmlNodeReader прочитать весь документ. Мы выведем содержимое документа на экран. Класс XmlNodeReader обеспечивает перемещение по документу. Приведем фрагмент кода из примера DataSetXML:
XmlDataDocument *xmlDataDoc = new XmlDataDocument(d);
XmlNodeReader *xmlNodeReader = 0;
try
{
XmlNode *node = xmlDataDoc->SelectSingleNode("/");
XmlNodeReader = new XmlNodeReader (node);
FormatXml (XmlNodeReader); }
catch (Exception *e) // Исключение {
Console::WriteLine (
"Exception: {0}", e->ToString()); // Исключение
}
finally // наконец
r
if (XmlNodeReader != 0) // если (XmlNodeReader! = 0)
xmlNodeReader->Close(); }
static void FormatXml (XmlReader *reader) {
while (reader->Read()) // читатель-> Чтение () {
switch (reader->NodeType) // переключатель
//(читатель-> NodeType) {
case XmlNodeType::Element: // случай
// XmlNodeType::Элемент Format (reader, "Element"); // Формат (читатель, "Элемент"); while(reader->MoveToNextAttribute() ) Format (reader, "Attribute"); // Формат (читатель, "Атрибут"); break;
case XmlNodeType::Text: // случай XmlNodeType:: Текст Format (reader, "Text"); // Формат (читатель, "Текст"); break;
static String *lastNodeType = ""; // статическая Строка
static void Format(XmlReader *reader, String *nodeType) // Формат
{
if (nodeType->Equals("Element"))
// если (nodeType-> Равняется ("Элемент"))
{
if (lastNodeType->Equals("Element"))
// если (lastNodeType-> Равняется ("Элемент"))
{
Console::WriteLine();
}
for (int i=0; i < reader->Depth; i++)
{
Console::Write(" "); // Запись
}
Console::Write(reader->Name) ;
// Запись:: (читатель-> Название); }
else if (nodeType->Equals("Text")) // если (nodeType-> Равняется ("Текст")) Console::WriteLine("={0}", reader->Value); // Значение else
{
Console::Write(String::Format( // Запись:: (Строка:: Формат ( "{0}<{1}>{2}", nodeType, reader->Name, // Название reader->Value)); // читатель-> Значение Console::WriteLine (); }
lastNodeType = nodeType; }
Вот какой документ XML будет записан объектом DataSet (Набор данных) в файл:
AirlineBroker
Airlines <!— Авиалинии -->
Name=America West
Abbreviation=AW
WebSite=www.americawest.com
ReservationNumber=555-555-1212
Airlines <!-- Авиалинии —>
Name=Delta
Abbreviation=DL
WebSite=www.delta.com
ReservationNumber=800-456-7890
Airlines <!-- Авиалинии -->
Name=Northwest
Abbreviation=NW
WebSite=www.northwest.com
ReservationNumber=888-111-2222
Airlines <!— Авиалинии —>
Name=Piedmont
Abbreviation=P
WebSite=www.piedmont.com
ReservationNumber=888-222-333
Airlines <!-- Авиалинии -->
Name=Southwest
Abbreviation's
WebSite=www.southwest.com
ReservationNumber=l-800-111-222
Airlines <!-- Авиалк-^'и -->
Name=Unitea
Abbreviation=UAL
WebSite=www.ual.com
ReservationNumber=800-123-4568
Flights <'-- Рейсъ. -->
Airline=DL
FlightNumber=987
StartCity=Atlanta
EndCity=New Orleans
Departure=2001-10-05T2G:15:СС.ООСГПСО-04:00
Arnval=2001-10-05T22:30:ОС.ОЭООССО-04:СО
PlaneType=737
FirstCost=1300
BusinessCost=0
EconomyCost=450
Flights <!-- Рейсы -->
Airline=UAL
FlightNumber=54
StartCity=Boston
EndCity=Los Angeles
Departure=2001-10-01T10:00:OO.OOCOOOO-r4:00
Arriva1=2001-10-01T13:00:00.0000000-04:00
PlaneType=767
FirstCost=1500
BusinessCost=1000
EconomyCost=300
PlaneType
PlaneType=737
FirstClass=10
BusinessCldss=0
EconomyСlass=200
PlaneType
PlaneType=767
FirstClass=10
BusinessClass=30
EconomyCiass=300
Customers !'-- 1лкеггы -->
LastName=Adams
FirstName=John
EmailAddress=adans@presidents.erg
Customerld
DataSet (Набор данных) можно использовать как резидентную реляционную базу данных, не связанную ни с какой другой базой данных. Теперь на примере программы DataEditing, мы рассмотрим несколько возможностей объекта DataSet (Набор данных), связанных с добавлением данных и отношений непосредственно в набор данных, без обращения к какой бы то ни было внешней базе данных.
Прежде всего создадим новый объект DataSet (Набор данных) и включим проверку ограничений. Затем добавим в DataSet (Набор данных) четыре объекта DataTable (Таблица данных): Books (Книги), Categories (Категории), Authors (Авторы) и BookCate-gories (Категории книг).
DataSet *ds = new DataSet; // новый Набор данных ds->EnforceConstraints = true; // истина
// Добавить (Add) таблицы (tables) к Набору данных (DataSet) DataTable *categories =
ds->Tables->Add("Categories");
// Таблицы-> Добавить ("Категории"); DataTable *bookcategories =
ds->Tables->Add("BookCategories");
// Таблицы-> Добавить ("BookCategories"); DataTable *authors = ds->Tables->Add("Authors");
// Таблицы-> Добавить ("Авторы"); DataTable *books = ds->Tables->Add("Books"); // Таблицы-> Добавить ("Книги");
Объект DataTable (Таблица данных) содержит коллекцию объектов DataColumn, каждый из которых представляет собой столбец таблицы. Теперь добавим столбцы в определения таблиц.
// определить типы для определений столбцов
Type *stringType = Туре::GetType("System.String");
// Система.Строка
Type *intType = Туре::GetType("System.Int32");
// Определить столбцы для таблиц
// Добавить столбец (column) в таблицу Category (Категория) DataColumn *categoryname =
categories->Columns->Add( // категории-> Столбцы-> Добавить "Category",stringType); // Категория
// Добавить (Add) столбцы (columns) для таблицы BookCategories DataColumn *cn = bookcategories->Columns->Add( // Столбцы-> Добавить
"CategoryName", stringType);
DataColumn *loc = bookcategories->Columns->Add( // Столбцы-> Добавить
"LibraryofCongressNumber", stringType);
// Добавить (Add) столбцы (columns) для таблицы Authors (Авторы) DataColumn *auid = authors->Columns->Add( // авторы-> Столбцы-> Добавить
"AuthorId", intType); authors->Columns->Add( // авторы-> Столбцы-> Добавить
"AuthorLastName", stringType); authors->Columns->Add( // авторы-> Столбцы-> Добавить
"AuthorFirstName", stringType);
// Добавить (Add) столбцы (columns) для таблицы Books (Книги) DataColumn *ISBN = books->Columns->Add( // книги-> Столбцы-> Добавить
"ISBN", stringType);
DataColumn *booksauid = books->Columns->Add( // книги-> Столбцы-> Добавить
"AuthorId", intType);
books->Columns->Add("Title", stringType); // книги-> Столбцы-> Добавить ("Название", stringType); DataColumn *bloc = books->Columns->Add( // книги-> Столбцы-> Добавить
"LibraryofCongressNumber", stringType);
Когда преобразователь данных обновляет содержимое базы данных, это не делается одной транзакцией. Если необходимо, чтобы несколько операций выполнялись за одну транзакцию, в программе следует предусмотреть управление транзакциями.
Объект SqlConnection содержит метод BeginTransaction, возвращающий объект SqlTransaction. При вызове метода BeginTransaction следует определить уровень локализации (выполняемых операций). Когда вы точно знаете, что делаете, и понимаете внутреннюю суть вещей, вы можете повысить производительность и масштабируемость приложения установкой соответствующего уровня локализации (выполняемых операций). Если установить уровень локализации (выполняемых операций) некорректно или даже просто неподходящим образом, это может привести к некорректности или несогласованности полученных данных.
Для выполнения или отмены транзакции в классе имеются методы Commit (Фиксировать) и Rollback (Откат). Вы открываете SqlConnection, вызываете метод BeginTransaction, используете SqlDataAdapter как обычно, а затем вызываете SqlTransaction::Commit (Фиксировать) или SqlTransaction::Rollback (Откат), в зависимости от необходимости. Затем закрываете соединение. Для установки точки сохранения (save point) транзакции используется метод Save (Сохранить).
В целях минимизации используемых ресурсов, а, следовательно, для повышения масштабируемости вашего приложения, может оказаться желательным минимизировать промежуток времени между вызовами методов BeginTransaction и Commit (Фиксировать) или Rollback (Откат).
Приведем фрагмент кода из примера Transaction. В нем используется база данных AirlineBroker, описанная в предыдущей главе. Для иллюстрации здесь используется объект SqlCommandBuilder, рассмотренный выше.
conn = new SqlConnection(ConnString);
conn->0pen(); // Открыть
trans = conn->BeginTransaction();
da = new SqlDataAdapter;
ds = new DataSet; // новый Набор данных
da->SelectCommand =
new SqlCommand(and, conn, trans);
SqlCommandBuilder *sb = new SqlCommandBuilder(da);
da->Fill(ds, "Airlines"); // Авиалинии
DataRow *newRow = ds->Tables->get_Item( "Airlines")->NewRow(); // Авиалинии
newRow->set_Item("Name", S"Midway"); // Название, "На полпути" newRow->set_Item("Abbreviation", S"M"); // Сокращение newRow->set_Item("WebSite", S"www.midway.com"); // Web-узел newRow->set_Item("ReservationNumber", S"555-555-1212"); ds->Tables->get_Item("Airlines")->Rows->Add(newRow); // Авиалинии
Console::WriteLine(
sb->Get!nsertCommand()->CommandText); Console::WriteLine (
sb->GetDeleteCommand()->CommandText); Console::WriteLine(
sb->GetUpdateCommand()->CommandText); pEnum =
sb->GetInsertCommand()->
Parameters->GetEnumerator(); // Параметры while (pEnum->MoveNext()) {
SqlParameter *p =
dynamic_cast<SqlParameter *>(pEnum->Current); Console::WriteLine (
"{0, -10} {I, -10}", p->ParameterName, p->SourceColumn); }
da->Update(ds, "Airlines"); // Авиалинии trans->Commit(); trans = 0; conn->Close () ;
Для полной уверенности в корректности работы источника данных SQL Server следует использовать методы Commit (Фиксировать) и Rollback (Откат) объекта SqlTrans-action для подтверждения или отмены транзакции, выполнение которой начато вызовом метода SqlConnection: : BeginTransaction. При этом не стоит использовать операторы транзакций SQL Server.
Если вы в своей работе с базой данных используете хранимые процедуры, вы можете, конечно, использовать операторы транзакций SQL Server внутри хранимых процедур вместо объекта SqlTransaction. Хранимые процедуры могут инкапсулировать изменения, произведенные в результате транзакций. Это делает, в частности, хранимая процедура MakeReservation базы данных HotelBroker (Посредник, бронирующий места в гостинице).
Начнем с небольшой программы JustConnect, единственная задача которой — просто устанавливать соединение с базой данных. Пример поможет также проверить, корректно ли установлен SQL Server и существует ли запрашиваемая база данных (в нашем случае — Northwind, входящая в состав SQL Server как ее стандартная часть)
SqlConnection *conn = 0;
String *ConnString =
"server=localhost;
uid=sa;
pwd=;
database=Northwind";
try
{
conn = new SqlConnection(ConnString);
conn->0pen(); // Открыть
Console::WriteLine(
"Connection to {0} opened successfully.", // "Соединение с {0} открыто успешно. ",
conn->Database); // База данных
}
catch(Exception *e) // Исключение
{
Console::WriteLine(e->Message); // Сообщение
}
_finally // наконец
{
if (conn->State == ConnectionState::Open) // если открыто
conn->Close();
}
Если СУБД SQL Server установлена и работает корректно, причем база данных Northwmd существует, результатом работы программы JustConnect будет следующее сообщение:
Connection to Northwmd opened successfully.
(Соединение с Northwmd открылось успешно.)
Если же что-то происходит не так, как должно, при выполнении метода Open (Открыть) возникает исключение и пользователь увидит сообщение, определенное в обработчике исключений. Например, если закрыть SQL Server, программа выведет следующее сообщение:
General network error. Check your network documentation.
(Общая сетевая ошибка. Сверьтесь с вашей сетевой документацией.)
Если изменить имя базы данных, заданное в строке соединения, на имя несуществующей базы, например, Southwind, будет выведено следующее сообщение:
Cannot open database requested in login 'Southwind'. Login fails.
Login failed for user 'sa'.
(He могу открыть базу данных, требуемую в регистрационном имени
'Southwind'. Вход в систему невозможен.
Вход в систему был безуспешным для пользователя 'за'.)
Следующим примером станет использование классов ADO.NET для получения доступа к данным, хранящимся в базе данных. Соответствующие файлы находятся в подпапке Connected.
Нам необходимы объекты для соединения, хранения команд, передаваемых базе данных, и хранения самих данных, поэтому мы определяем три указателя на объекты классов SqlConnection, SqlCommand и SqlDataReader:
SqlConnectlon *conn = 0;
SqlCommand * command = 0;
SqlDataReader *reader = 0;
Далее инициализируется строка соединения с базой данных Вы можете изменить значение поля, предназначенного для хранения имени сервера, на имя своего компьютера Необходимо также определить имя пользователя и пароль для получения доступа к базе данных Строку соединения можно устанавливать и как свойство объекта SqlConnection В качестве команды, которая будет передаваться базе данных в нашем примере, выбран простой оператор отбора данных:
String *ConnString =
"server=localhost;
uid=sa;
pwd=;
database=Northwind";
String *cmd =
"select Customerld,
CompanyName from Customers";
На Рисунок 9.2 приведены списки таблиц и хранимых процедур базы данных Northwmd В теле блока try создается объект класса SqlConnection. Затем открывается соединение с базой данных, ведь это должно быть сделано до передачи базе данных какой-либо команды После этого создается объект класса SqlCommand, связанный с созданным ранее соединением.
conn = new SqlConnection(ConnString);
conn->0pen(); // Открыть
command = new SqlCommand(cmd, conn);
Если команда выполняется посредством использования метода ExecuteReader объекта SqlCommand, то при этом возвращается экземпляр класса SqlDataReader Этот объект можно использовать для перемещения по полученному набору данных Для извлечения данных из текущей строки набора можно использовать имя столбца
reader = command->ExecuteReader();
// читатель = команда-> ExecuteReader ();
if (reader != 0)
{
Console::WriteLine(
"CustomerldXtCompanyName");
while (reader->Read()) // Чтение
Console::WriteLine (
"{0}\t\t{l}",
reader->get_Item("Customerld"),
reader->get_Item("CompanyName"));
}
И в заключение, в блоке finally закрываются считывающее устройство и соединение.
if (reader != 0)
reader->Close() ; if (conn->State == ConnectionState::0pen) // если открыто
conn->Close();
Если соединение не закрыть явно, завершитель объекта SqlConnection, рано или поздно запущенный, закроет соединение. Но из-за того, что сборщик мусора не является детерминированным, никто не сможет сказать, когда это произойдет. Поэтому всегда закрывайте соединение явно. Если этого не сделать, будет использоваться больше соединений, чем необходимо (даже если вы организуете связной пул), что может снизить масштабируемость приложения. Кроме того, может исчерпаться запас соединений.
Приведем результат работы программы:
Customerld CompanyName
ALFKI Alfreds Futterkiste
ANATR Ana Trujillo Emparedados у helados
ANTON Antonio Moreno Taqueria
AROUT Around the Horn
BERGS Berglunds snabbkop
BLAUS Blauer See Delikatessen
BLONP Blondesddsl pere et fils
BOLID Bolido Comidas preparadas
BONAP Bon app'
BOTTM Bottom-Dollar Markets
BSBEV B's Beverages
. . .
Для проверки корректности работы программы можно использовать Server Explorer среды разработки Visual Studio.NET. Выберите в базе данных Northwind таблицу Customers (Клиенты) и щелкните на ней правой кнопкой для того, чтобы вызвать всплывающее меню. Выберите в нем пункт Retrieve Data from Table (Получить данные из таблицы) и, просмотрев данные, хранящиеся в таблице, сравните их с результатом работы программы. Наверняка вы заметите поразительное сходство (Рисунок 9.3).
Метод ExecuteReader класса SqlCommand возвращает экземпляр класса Da-taReader. Данные возвращаются, если в качестве команды задан запрос на выборку. Этот же метод можно использовать для обновления, вставки или удаления данных. Метод SQLCommand: : ExecuteReader использует хранимую процедуру sp_executesql. Некоторые команды, использующие операторы SET (оператор Установить), могут работать неправильно. Другие драйверы могут иметь иные ограничения на использование метода ExecuteReader.
Обычно для команд, при выполнении которых данные не возвращаются, используется метод SqlCommand::ExecuteNonQuery. Пример NonQuery демонстрирует работу этого метода. Кроме того, соединение с SQL Server осуществляется в нем с помощью источника данных OleDb (OLE для баз данных).
String *cmd = "update Customers set ContactName =
// Строка *cmd = "обновить Клиентов, установить ContactName =
Too' where ContactName = 'Maria Anders'";
// Too', где ContactName = 'Мария Андерс";
try {
conn = new OleDbConnection(ConnString);
conn->0pen(); // Открыть
command = new OleDbCommand(cmd, conn);
int NumberRows = command->ExecuteNonQuery(); // команда
Console::WriteLine(
"Number Rows: {0}", NumberRows.ToString());
}
Количество измененных строк, которое должно быть равным 1, показано в окне, предназначенном для консольного вывода. Если запустить программу еще раз, она не сможет найти необходимую ей запись, так как та была изменена при первом запуске программы (нет больше в базе данных Марии Андерс (Maria Anders)!), и выведет значение 0. Для приведения базы данных в исходное состояние необходимо запустить макрос SQL, как это описано в файле readme.txt для этой главы. На Рисунок 9.4 показан результат изменения первой строки. Значение поля ContactName изменено с Maria Anders на Foo.
При выполнении вставки, обновления и удаления данных возвращается количество строк, которых коснулись изменения. Для всех остальных операторов SQL Server возвращает значение -1 (при использовании родного источника данных или OLEDB (OLE для баз данных)). Другие драйверы доступа могут возвращать 0 или -1.
Для получения одного значения (например, результата вычислений) используйте метод ExecuteScalar. При работе с источниками данных, способными генерировать данные XML, более эффективным будет использовать метод SqlCommand: : ExecuteXmlReader, a не получать данные в объект DataSet (Набор данных), а затем преобразовывать их в ХМL.
Документы могут содержать в себе результат вычислений, полученный от базы данных. Например, отчет о продажах содержит, кроме данных о продажах, полученных от источника данных, и некоторые пояснения. Для представления данных в виде документа ХМL используется класс XmlDataDocument.
Класс XmlDataDocument является производным от класса XmlDocument, который представляет документы XML в библиотеке классов .Net Xml Framework. Особенно удобным делает класс XmlDataDocument то, что экземпляр этого класса можно получить из объекта DataSet (Набор данных) посредством простой передачи объекта Data-Set (Набор данных) конструктору класса XmlDataDocument в качестве аргумента. XmlDataDocument имеет свойство DataSet (Набор данных), так что вы можете работать с документом XML как с реляционными данными, если это имеет смысл.
Web-форма состоит из двух частей:
отображаемого содержимого, или презентации формы. Как правило, отображаемое содержимое описывается на языке HTML; программного кода, который описывает логику взаимодействия с визуальными элементами.Физически Web-форма представлена файлом с расширением . aspx. Расширение любой HTML-страницы можно изменить на .aspx. При этом представление страницы с новым расширением будет тождественно представлению исходной страницы. Иными словами, Web-формы совместимы снизу вверх с обычными HTML-страницами.
Особенность Web-форм состоит в способе, при помощи которого программный код отделяется от самой формы. Исходный код (написанный не на C++) может храниться в отдельном не скомпилированном файле. Или же программный код (снова таки, не на C++) может быть вложен в . aspx-файл. И, наконец, код (написанный на С#, VB или C++) может храниться в предварительно скомпилированной сборке, содержащей динамически подключаемую библиотеку (DLL). Когда страницы загружаются Web-сервером, выполняется код, описывающий пользовательский интерфейс. Этот код динамически формирует отображаемую клиентом страницу.
Чтобы четче представить архитектуру Web-форм, написанных на C++, мы рассмотрим пример HelloCodebehind. Эта программа отображает информацию, введенную пользователем. Чтобы запустить приложение, введите в адресной строке броузера унифицированный указатель информационного ресурса (URL) http: //localhost/NetCpp/HeiloCodebehind. aspx. Код, написанный на C++, содержится в файле HelloCodebehind.aspx.h. Обратите внимание, что данный проект создает динамически подключаемую библиотеку (DLL) — файл HelloCcdebehind.dl]. Эта динамически подключаемая библиотека (DLL) затем копируется в подкаталог bin виртуального каталога. (Виртуальный каталог мы создали раньше для хранения примеров программ, которые рассматриваются в главе 10 "ASP.NET и Web-формы". Так было сделано потому, что информационный сервер Internet (US) ищет загружаемые динамически подключаемые библиотеки (DLL) в этом подкаталоге.) Код, который описывает видимые эпементы страницы (презентацию страницы), приведен ниже. Он содержится в файле HelloCodebehind.aspx:
<!-- KelloCoaebehind.aspx -->
<%@ Assembly Name=" HelloCodebehind" °>
<!-- Имя сборки -->
<%@ Page Inherits= MyWebPage ">
<HTML>
<HEAD>
</HEAD>
<BODY> <!-- ТЕЛО -->
<FORM RUNAT="SERVER">YOUR NAME:
<asp:textbox id=txtName Runat="server"></asp:textbox>
<p>asp:button id=cmdEcho onclick=cmdEcho__Click Text="Echo"
runat="server" tooltip="Click to echo your name">
</asp :buttonx/p>
<asp:lacel id=lblGreeting runat="server">
</asp:label>
<P></P>
</FORM>
</BODY>
<!-- тело -->
</HTML>
Код, который создает интерфейс пользователя, содержится в файле HelloCodebehind. h:
//HelloCodebehind.h
fusing <System.dll>
fusing <System.Web.dll>
using namespace System;
// использование пространства имен Система;
using namespace System::Web;
// использование пространства имен Система::Сеть;
using namespace System::Web::UI;
// использование пространства имен
// Система::Сеть:пользовательский интерфейс;
using namespace System::Web::UI::WebControls;
// использование пространства имен
// Система::Сеть::Пользовательский интерфейс::WebControls;
public _gc class MyWebPage : public System::Web::UI::Page
// класс сборщика мусора MyWebPage : общедоступная Система::
// Сеть:: Пользовательский интерфейс:: Страница
{
protected: // защищенный
TextBox *txtName;
Button *cmdEcho; // Кнопка
Label *lblGreeting; // Метка
public:
void cmdEcho_Click(Object *Source, EventArgs *e)
{
IblGreeting-XText = // Текст
String::Format( // Строка:: Формат "Hello, (0).
Welcome to Managed C++ ASP.", // "Привет, {О}. Добро пожаловать
// в Управляемый C++ ASP. ",
txtName->Text); // Текст
}
};
Этот код нужно скомпилировать отдельно и развернуть полученную сборку HelloCodebehind. dll в каталоге \OI\NetCpp\ChaplO\bin. Информационный сервер Internet (US) автоматически ищет загружаемые файлы в подкаталоге bin виртуального каталога.
Технология ASP.NET, предназначенная для создания Web-приложении, является важной частью платформы .NET. По сравнению с очередной усовершенствованной версией ASP (Active Server Pages — Активные страницы сервера), эта новая технология представляет собой более унифицированную платформу, которая значительно упрощает реализацию сложных Web-приложений В данной главе мы ознакомимся с основами технологии ASP.NET, а также рассмотрим Web-формы, облегчающие создание интерактивных Web-страниц. В главе 11 "Web-службы" мы изучим построение Web-служб на основе технологии ASP.NET. Web-службы позволяют создавать Web-приложения, которые могут совместно обрабатывать данные, будучи развернутыми в неоднородных (гетерогенных) системах В главе 12 "Web-узлы и Web-службы, работающие на основе ATL Server" иллюстрируется создание Web-страниц и Web-служб на основе сервера ATL Server.
Изучение технологии ASP.NET мы начнем с рассмотрения очень простого Web-приложения. На примере этого приложения мы изучим систему отладки, используемую при программировании на основе технологии ASP.NET. Кроме того, коротко будут рассмотрены основы обработки данных в Web. Изучая этот маленький пример, мы столкнемся с некоторыми сложными задачами, которые решаются в процессе разработки Web-приложений. Благодаря этому мы сможем по достоинству оценить возможности и преимущества технологии ASP.NET; о них и будет речь в оставшейся части данной главы
Конечно, используя ASP.NET, вы можете реализовать Web-приложение и на языке C++. Но, как мы уже убедились, язык C++ далеко не оптимальный язык для написания кода с целью создания тех элементов Web-приложения, которые используются при визуальном взаимодействии. Для этого гораздо более подходят языки С# и VB.NET. Чтобы узнать больше об ASP.NET, вы можете обратиться к следующим книгам из серии The Integrated .NET Series, выпущенной издательствами Object Innovations и Prentice Hall PTR:
Application Development Using C# and .NET. (Разработка приложений с помощью С# и.NET.) Application Development Using Visual Basic and .NET. (Разработка приложений с помощью Visual Basic и .NET.) Fundamentals of Web Applications Using .NET and XML. (Основные принципы создания Web-приложений с помощью .NET и XML.)Язык C++ не имеет себе равных при создании вызываемых с Web-страницы эффективных компонентов для вычислительных машин баз данных. Высокопроизводительные Web-приложения на C++ можно создать также и при помощи технологии, которая имеет название ATL Server. Введение в технологию ATL Server содержится в главе 12 "Web-узлы и Web-службы, работающие на основе ATL Server".
Сведения о конфигурации сервера и приложения хранятся в файлах в формате XML. Содержимое файлов можно легко прочесть, и, в случае необходимости, изменить.
Файл конфигурации сервера
Файл конфигурации сервера имеет название machine.config. Этот файл находится в каталоге \WINNT\Microsoft..NET\Framework. Каждая версия платформы .NET имеет свой каталог, в котором хранится файл конфигурации сервера. За счет этого можно одновременно использовать различные версии ASP.NET. Иными словами, вы можете продолжать использовать Web-приложения, которые работают с предыдущей версией платформы .NET, и одновременно разрабатывать приложения, использующие более новую версию.
Файлы конфигурации приложения
Чтобы сохранить значения параметров, которые используются конкретным Web-приложением, в корне виртуального каталога нужно создать файл web. conf ig. Если этот файл отсутствует, будут использоваться значения параметров конфигурации приложения, принятые по умолчанию. Они хранятся в файле machine. conf ig. Если же файл web. conf ig существует, то будут использованы значения параметров, которые содержатся в нем.
Формат файлов конфигурации
Файл web. conf ig, также как и файл machine, conf ig, хранится в формате XML. Файл состоит из разделов. В каждом разделе объединены взаимосвязанные параметры. Чтобы ознакомиться со структурой файла конфигурации и параметрами, значение которых можно изменить, просмотрите файл web. conf ig, он был создан средой Visual Studio, когда мы создавали новый проект ASP.NET Web-приложения.
<?xml version="l.0" encoding="utf-8" ?>
<!— версия = "1.0" KOflnpOBKa="utf-8" —>
<configuration> <!— конфигурация -->
<system.web>
<!— CUSTOM ERROR MESSAGES
Set mode="on" or "remoteonly" to enable custom
error messages, "off" to disable. Add
<error> tags for each of the errors you want to
handle.
-->
<!-- ПОЛЬЗОВАТЕЛЬСКИЕ СООБЩЕНИЯ ОБ ОШИБКАХ
Установите режим набора = "on" или "remoteonly",
чтобы разрешить пользовательские
сообщения об ошибках, "off" чтобы отключить их. Добавьте
тэги <error> (<ошибка>) для каждой из ошибок,
которую вы хотите обрабатывать.
-->
<customErrors
mode="0ff"
/>
<!-- AUTHENTICATION
This section sets the authentication policies of the application.
Possible modes are "Windows", "Forms", "Passport" and "None"
-->
<!-- ОПОЗНАВАНИЕ
Этот раздел устанавливает политику опознавания
приложения. Возможные режимы - "Windows", *
"Forms" ("Формы"), "Passport" ("Паспорт")
и "None" ("Никакой")
—>
<authentication mode="None" />
<!-- режим опознавания = "Никакой" -->
</system.web>
</configuration> <!— конфигурация —>
В состав ASP.NET-приложения может входить файл Global. asax. В нем содержится код, который обрабатывает события уровня приложения, инициируемые ASP.NET. Этот файл расположен в корневом каталоге приложения. Если файл Global.asax в приложении отсутствует, ASP.NET считает, что обработчики событий уровня приложения не определены.
В данном коде приведены наиболее часто используемые события уровня приложения. Обычно за время существования Web-приложения происходят следующие события
Application_Start Это событие возникает лишь один раз за все время работы приложения, когда создается первый экземпляр класса HttpApplication Приложение запускается первый раз тогда, когда его запускает информационный сервер Internet (I1S) для первого пользователя В обработчике событий можно инициализировать состояние, которое будет использоваться всем приложением Session_Start возникает в начале каждого сеанса На этом этапе можно инициализировать переменные сеанса Application_BeginRequest инициируется в начале каждого отдельного запроса Как правило, обработка запроса производится классом Page (Страница) Application_EndRequest инициируется в конце запроса Session_End инициируется в конце каждого сеанса. Как правило, не нужно освобождать переменные, инициализированные в начале сеанса (при возникновении события Session_Start) Они будут освобождены автоматически в процессе сборки мусора Но если открыт дорогостоящий ресурс, например, соединение с базой данных, тогда при возникновении этого события можно вызвать метод Dispose (Освободить ранее выделенную область памяти) Application_End инициируется в самом конце срока существования приложения, когда удаляется последний экземпляр HttpApplicationМы ознакомились с основными возможностями ASP.NET и создали несколько простых Web-страниц, используя шаблоны библиотеки классов управляемого C++. Дальнейшее изучение материала главы 10 "ASP.NET и Web-формы" мы продолжим на конкретном примере. Для языков С# и VB.NET существует специальный шаблон проекта, имеющий название ASP.NET Web Application (Web-приложение на основе ASP.NET), который формирует каркас приложения. Кроме того, конструктор форм (Forms Designer) позволяет легко создавать Web-формы путем перетаскивания необходимых элементов управления с панели инструментов. Конструктор форм (Forms Designer) поддерживает языки С# и VB.NET. Если приложение создается на C++, код соответствующей формы, к сожалению, придется создавать самостоятельно.
Пространство имен System: :Web (Система Сеть) содержит потезнын класс HttpRequest. Данный класс используется для считывания различных значении, которые клиент отсылает в запросе по протоколу передачи гипертекстовых фантов HTTP Эти значения, принимаемые по протоколу передачи гипертекстовых фантов HTTP, затем будут использованы классическими (те использующими общий шлюзовой ишер-фейс CGI (Common Gateway Interface)) программами или программами, использующими интерфейс прикладного программирования Internet-сервера ISAPI (Internet Server API), которые обрабатывают Web-запрос. На этой основе строится обработка данных на более высоком уровне. В табл. 10.1 приведены некоторые наиболее часто используемые свои-ства класса HttpRequest. Если вы знакомы с протоколом передачи гипертекстовые файлов HTTP, смысл этих свойств должен быть вам понятен Исчерпывающая инфор мация об этих и других свойствах класса HttpRequest содержится в документации по .NET Framework.
Свойство Request (Запрос) класса Page (Страница) возвращае! объект 'tt; quest. Из свойств объекта HttpRequest можно извлечь любую необходимую информацию. Например, следующий код (этот код не содержится ни в одном примере) определяет длину (в байтах) содержимого, отосланного клиентом, и записывает поточенную информацию в объект Response (Ответ). Затем эта информация отображается в окне-броузера.
Таблица 10.1. Общедоступные свойства экземпляра класса HttpReguest
Свойство | Смысл |
AcceptTypes | Строковый массив принимаемых клиентом MIME-типов |
Browser (Броузер) | Информация о возможностях броузера клиента |
ContentLength | Длина (в байтах) содержимого, отосланного клиентом |
Cookies (Небольшие фрагменты данных о предыстории обращений конкретного пользователя к конкретному Web-серверу, автоматически создаваемые сервером на машине пользователя) | Коллекция небольших фрагментов данных о предыстории обращений конкретного пользователя к конкретному Web-серверу, автоматически создаваемых сервером на машине пользователя (cookies), отосланных клиентом |
Form (Форма) | Коллекция переменных формы |
Headers (Заголовки) | Коллекция заголовков для протокола передачи гипертекстовых файлов HTTP |
HttpMethod | Метод передачи по протоколу передачи гипертекстовых файлов HTTP, используемый клиентом (например, СЕТ (Получить) или POST (Отправить почтовое сообщение)) |
Params | Объединенная коллекция, которая состоит из элементов Querystnng, Form (Форма), Server-Variables И Cookies (Небольшие фрагменты данных о предыстории обращений конкретного пользователя к конкретному Web-серверу, автоматически создаваемые сервером на машине пользователя) |
Path (Путь) | Виртуальный запрос текущего пути |
OueryString | Коллекция строковых переменных запроса, посылаемого по протоколу передачи гипертекстовых файлов HTTP |
ServerVanables | Коллекция переменных Web-сервера |
Page *p = dynamic_cast<Page *>(sender);
// Страница *р = dynamic_cast <Страница *> (отправитель);
HttpRequest *request = p->get_Request();
int length = request->ContentLength; // длина
HttpResponse *response = p->get_Response();
response->Write(String::Format( // ответ-> Запись (Строка:: Формат
"ContentLength = {0}<br>", _box(length))); // длина
Коллекции
Многие полезные коллекции представлены как свойства объекта HttpRequest. Эти коллекции принадлежат типу NamedValueCollection (в пространстве имен System: :Collections: :Specialized (Система::Коллекции::Специализированное пространство имен)). Получить значение переменной из коллекции можно при помощи строкового ключа. Например, следующий код извлекает из коллекции ServerVan-ables значение переменных сервера QUERY_STRING и HTTP_USER_AGENT.
Page *p = dynamic_cast<Page *>(sender);
// Страница *р = dynamic_cast <Страница *> (отправитель);
HttpRequest *request = p->get_Request();
HttpResponse *response = p->get_Response();
String *strQuery = // Строка
request->ServerVariables->get_Item( // запрос
"QUERY_STRING");
response->Write(String::Format( // ответ-> Запись (Строка:: Формат (
"QUERY_STRING = {0}<br>",strQuery));
String *strAgent = // Строка
request->ServerVariables->get_Item( // запрос
"HTTP_USER_AGENT");
response->Write(String::Format( // ответ-> Запись (
Строка:: Формат ( "HTTP_USER_AGENT = {0}<br>",
strAgent));
Если вслед за унифицированным указателем информационного ресурса (URL) соответствующего .aspx-файла ввести строку запроса ?foo=3, приведенный выше код отобразит в окне броузера что-то примерно следующее:
QUERY_STRING = foo=3
HTTP_USER_AGENT = Mozilla/4.0
Подобные переменные при программировании классического (т.е основанного на общем шлюзовом интерфейсе CGI (Common Gateway Interface)) Web-сервера играют решающую роль. Используя переменные среды, Web-сервер передает информацию CGI-скрипту или программе. На всякий случай ASP.NET обеспечивает доступ и к этой низкоуровневой информации.
Стандартная задача состоит в том, чтобы извлечь информацию из элементов управления формы. В HTML-коде элементы управления идентифицируются атрибутом name (имя). Этот атрибут сервер использует для определения соответствующего значения. Способ передачи данных из формы серверу зависит от того, какой метод использует форма в протоколе передачи гипертекстовых файлов HTTP: GET (Получить) или POST (Отправить почтовое сообщение).
Если используется метод GET (Получить), данные, введенные в форму, кодируются как часть строки запроса. Чтобы затем извлечь нужные значения, используется коллекция QueryString Если же используется метод POST (Отправить почтовое сообщение), данные, введенные в форму, передаются как содержимое после заголовка протокола передачи гипертекстовых файлов HTTP. В этом случае для извлечения значения элементов управления используется коллекция Forms (Формы). Чтобы узнать, какую коллекцию следует использовать, нужно определить значение (GET (Получить) или POST (Отправить почтовое сообщение)) переменной сервера REQUEST_METHOD. (Если переменная REQUEST_METHOD имеет значение GET (Получить), используется коллекция QueryString Если же ее значение равно POST (Отправить почтовое сообщение), используется коллекция Forms (Формы))
Если вы используете ASP NET, тогда вам не стоит волноваться о том, какой метод протокола передачи гипертекстовых файлов HTTP использовался при запросе В ASP NET имеется коллекция Params, которая представляет собой объединение (в математическом смысле) коллекций ServerVariables, QueryString, Forms (Формы) и Cookies (Небольшие фрагменты данных о предыстории обращений конкретного пользователя к конкретному Web-серверу, автоматически создаваемые сервером на машине пользователя)
Пример программы
Идеи, о которых мы рассказали выше, проиллюстрируем на примере простой страницы Squares. aspx Эта страница отображает столбец с квадратами натуральных чисел Числа, квадраты которых будут выведены, ограничиваются значением, вводимым в форме Страница GetSquares aspx отсылает запрос, используя метод GET (Получить), а страница PostSquares.aspx отсылает запрос, используя метод POST (Отправить почтовое сообщение) Пользовательский интерфейс обоих приложений тождественен (Рисунок 10.11)
Ниже приведен HTML-код страницы GetSquares.aspx Обратите внимание, что мы используем чистый HTML-код И, за исключением директивы Page (Страница), которая используется для включения режима трассировки, признаки использования ASP NET отсутствуют
<! - - GetSquares.aspx - ->
<%@ Page Trace = "false" %>
<!—- Трассировка Страницы --->
<html> <head> </head>
<body> <!-— тело -—>
<P>This program will print a column of square numbers</P>
<!--- Эта программа будет печатать столбец квадратов чисел --->
<form method="get" action = Squares.aspx>
<!--- метод формы = "получить" действие = Squares aspx -->
How many
<!—- Сколько -->
<INPUT type=text size=2 value=5 name=txtCount>
<P></P>
<INPUT type=submit value=Squares name=cmdSquares>
</form>
</body> <!— тело -->
</html>
Тэг form (форма) имеет атрибуты, при помощи которых указывается метод (в данном случае GET (Получить), а не POST (Отправить почтовое сообщение)) и действие (запрос на страницу Squares aspx) Элементы управления имеют атрибут name (имя), с помощью которого сервер извлекает нужное значение
Запустите страницу GetSquares aspx и щелкните на кнопке "Squares" ("Квадраты чисел") Будет отображена некоторая служебная информация, относящаяся к протоколу передачи гипертекстовых файлов HTTP За ней последует столбец с квадратами чисел Поскольку режим трассировки включен, то ASP NET отображает на странице также и подробности запроса На рис 1012 приведены результаты выполнения запроса, в котором используется метод GET (Получить)
Можно видеть, что данные, введенные в форму, закодированы как часть строки запроса Длина содержимого равна нулю, поскольку используется метод GET (Получить), а не POST (Отправить почтовое сообщение) Если прокрутить трассировочную информацию дальше, можно увидеть еще много интересных сведений Например, там приводится коллекция QueryString
А теперь давайте рассмотрим страницу PostSquares aspx Обратите внимание, — в этот раз тэг form (форма) указывает, что используется метод POST (Отправить почтовое сообщение) Остальные параметры такие же, как и в случае страницы GetSquares . aspx
<!-- postSquares.aspx -->
<%@ Page Trace = "false" %>
<html>
<head>
</head>
<body> <!— тело —>
<P>This program will print a column of squares</P>
<!— Эта программа будет печатать столбец квадратов —>
<form method="post" action = Squares.aspx> How many:
<!— Сколько: —>
<INPUT type=text size=2 value=5 name=txtCount> <P></P>
<INPUT type=submit value=Squares name=cmdSquares> </form>
</body> <i— тело —> </html>
Запустите страницу PostSquares.aspx и щелкните на кнопке Squares (Квадраты чисел) Снова будет отображена некоторая информация, относящаяся к протоколу передачи гипертекстовых файлов HTTP. За ней последует столбец, содержащий квадраты чисел Поскольку режим трассировки включен, то ASP.NET отображает на странице также и подробности запроса. На Рисунок 10.13 приведены результаты выполнения запроса POST (Отправить почтовое сообщение).
Поскольку используется метод POST (Отправить почтовое сообщение), то строка запроса пуста Длина содержимого равна 29. Данные, введенные в форму, передаются в текстовом виде вслед за заголовком протокола передачи гипертекстовых файлов HTTP.
Если прокрутить трассировочную информацию дальше, то теперь можно увидеть коллекцию Form (Форма) Именно с помощью этой коллекции ASP.NET обеспечивает доступ к данным, введенным в форме в случае использования метода POST (Отправить почтовое сообщение)
Класс HttpResponse инкапсулирует информацию ответа, полученного по протоколу передачи гипертекстовых файлов HTTP, притом информация содержится в операции ASP NET Каркас Framework использует данный класс при формировании ответа клиенту. Формирование ответа включает запись элементов управления сервера для отправки клиенту. Созданный вами код сервера может также использовать метод Write (Запись) объекта Response (Ответ). Использование метода Response: :Write (Ответ' Запись) иллюстрировалось уже несколько раз.
Метод Redirect (Переадресовать)
Класс HttpResponse имеет полезный метод Redirect (Переадресовать) Благодаря этому методу сервер может переадресовать запрос, передаваемый по протоколу передачи гипертекстовых файлов HTTP на другой унифицированный указатель информационного ресурса (URL). Простая переадресация, без передачи каких-либо данных, является тривиальной задачей. Все что для этого нужно сделать— это вызвать метод Redirect (Переадресовать) и передать ему требуемый унифицированный указатель информационного ресурса (URL) Примером ситуации, когда используется метод Redirect (Переадресовать), служит реорганизация Web-узла. В процессе реорганизации некоторые страницы могут содержать недостоверную информацию Кроме того, содержимое Web-узла может быть перемещено Чтобы сохранить доступ к старым страницам, достаточно просто переадресовать трафик
Обратите внимание, что при переадресации запроса всегда используется метод GET (Получить) протокола передачи гипертекстовых файлов HTTP Это подобно установлению связи по указанному унифицированному указателю информационного ресурса (URL) (Метод POST (Отправить почтовое сообщение) может использоваться, если данные отсылаются из формы, — тогда можно указать одно из двух действий — GET (Получить) или POST (Отправить почтовое сообщение)) Более интересный случай представляет передача данных на новую страницу. Один из способов передачи данных состоит в том, что передаваемая информация кодируется в строке запроса. При кодировании строки запроса следует придерживаться общепринятых соглашений для протокола передачи гипертекстовых файлов HTTP Класс HttpUtility имеет метод UrlEncode, который корректно кодирует отдельные составляющие элементы строки запроса Вам нужно самостоятельно написать код, который отделяет унифицированный указатель информационного ресурса (URL) от строки запроса при помощи символа "знак вопроса" (9), а также разделяет отдельные составляющие элементы строки запроса при помощи символа "амперсанд" (&).
В папке Hotel (Гостиница) имеется простое Web-приложение, где иллюстрируется использование этого метода передачи данных при переадресации запроса. Файл de fault. aspx содержит форму, при помощи которой собираются данные, необходимые для бронирования мест в гостинице Само бронирование выполняется на странице Reservationl. aspx. Чтобы получить доступ к начальной странице default. aspx, используйте унифицированный указатель информационного ресурса (URL)http://localhost/NetCpp/Hotel/.
Как обычно, ссылка на эту страницу содержится на начальной странице с примерами программ На Рисунок 10.14 показана начальная страница нашего приложения, которое бронирует место в гостинице
Ниже приведен ASP NET-код на C++, который выполняется после щелчка на кнопке Make Reservation (Забронировать)
void cmdMakeReservation_Click(
Object *sender, EventArgs *e)
{
HttpUtility *utility = new HttpUtility;
String *query = String:.Concat ( // Строка
"City=", utility->UrlEncode(txtCity->Text)); // Город
query = String::Format(
// запрос = Строка:: Формат ( "{0}&Hotel={l}",
query, utility->UrlEncode(txtHotel->Text));
query = String::Format(
// запрос = Строка:: Формат ( "{0}&Date={l}",
query, utility->UrlEncode(txtDate-XText));
query = String::Format( // запрос = Строка:: Формат ( "{0}&NumberDays={1}",
query, utility->UrlEncode(txtNumberDays->Text));
Response->Redirect(String::Concat(
// Ответ-> Переадресовать (Строка:: Concat (
"Reservation!.aspx?", query)); // запрос
}
Метод cmdMakeReservation_Click строит строку запроса. Эта строка присоединяется к унифицированному указателю информационного ресурса (URL) страницы Reservation! .aspx От унифицированного указателя информационного ресурса (URL) ее отделяет символ 9 В качестве разделителя элементов строки запроса используется символ & Для кодирования отдельных элементов используется метод HttpUtility: :UrlEncode Символы "слэш" (в дате) и пробелы, например в названии "San Jose" кодируются отдельно Если щелкнуть на кнопке, будет вызван метод Page_Load класса Reservationl, отображающий страницу, с помощью которой можно забронировать место в гостинице Этот метод считывает название города, название гостиницы, дату и количество дней, которое вы планируете провести в гостинице, а затем отображает полученные данные на возвращаемой Web-странице
void Page_Load(Object *sender, EventArgs *e)
{
Page *p = dynamic_cast<Page *>(sender);
// Страница *р = dynamic_cast <Страница *> (отправитель);
HttpRequest *request = p->get_Request();
HttpResponse *response = p->get_Response() ;
response->Write("Making reservation for ...");
// ответ-> Запись ("Делаю резервирование для ... "};
response->Write("<br>"); // ответ-> Запись
String *city = request->Params->get_Item("City");
// Строка *city = запрос-> Params-> get_Item ("Город");
response->Write(String::Concat("City = ", city));
// ответ-> Запись (Строка:: Concat ("Город = ", город));
response->Write("<br>"); // ответ-> Запись
String *hotel = request->Params->get_Item("Hotel");
// Строка *hotel = запрос-> Params-> get_Item ("Гостиница");
response->Write(String::Concat("Hotel = ", hotel));
// ответ-> Запись (Строка:: Concat
// "Гостиница = ", гостиница));
response->Write("<br>"); // ответ-> Запись
String *strDate = request->Params->get_Item("Date");
// Строка *strDate = запрос-> Params-> get_Item ("Дата");
response->Write(String::Concat("Date = ", strDate));
// ответ-> Запись (Строка:: Concat ("Дата = ", strDate));
response->Write("<br>"); // ответ-> Запись
String *strDays = // Строка
request->Params->get_Item("NumberDays" ) ;
response->Write(String::Concat(
// ответ-> Запись (Строка:: Concat ( "NumberDays = ", strDays));
response->Write("<br>"); // ответ-> Запись
}
Строку запроса можно увидеть в адресной строке броузера На Рисунок 10.15. показана выходная информация, выводимая броузером (На самом деле, наша программа не бронирует место в гостинице, она всего лишь выводит переданные ей параметры )
Включите трассировку Вывод трассировки продемонстрирует идеи, которые мы обсудили, когда говорили о программировании запросов и ответов в Web-программах В частности, стоит изучить коллекцию Query String, показанную на Рисунок 10.16.
Ключевым (базисным) пространством имен для Web-форм и Web-служб является пространство имен System: :Web (Система::Сеть). Поддержка Web-форм реализована в пространстве имен System: :Web: :UI (Система::Сеть::Пользовательский интерфейс). Поддержка элементов управления сервера, например текстовых полей и кнопок, реализована в пространстве имен System: :Web: :UI: :WebControls (Система::Сеть:: Пользовательский интерфейс:^еЬСоп1го1$). Выходная . aspx-страница динамически генерируется классом Page (Страница) пространства имен System: :Web: :UI (Система::Сеть::Пользовательский интерфейс), а также классами, производными от класса Page (Страница), что иллюстрируется на примере последней рассмотренной нами страницы с фоновым кодом.
Наследование от класса Page (Страница)
Выходная страница генерируется в результате взаимодействия элементов, которые содержатся в .aspx-файле, кода, который содержится в файле с фоновым кодом (или сценария в случае С# или VB.NET) и базового класса Page (Страница). Возможность такого взаимодействия обеспечивает ASP.NET. Для .aspx-файла ASP.NET динамически создает класс, производный от класса, реализованного с помощью фонового кода или сценария, который, в свою очередь, является производным от класса Page (Страница). Иерархия классов, производных от класса Page (Страница) показана на Рисунок 10.6. Созданный нами класс MyWebPage является производным от класса Page (Страница).
Последним потомком (листом) класса Page (Страница) является класс My .aspx Page (Рисунок 10.6). Он динамически создается средой выполнения ASP.NET. Данный класс расширяет возможности класса MyWebPage (Рисунок 10.6). Он объединяет элементы управления и HTML-текст Web-формы. В результате компиляции данного класса создается исполняемый модуль. Когда страницу запрашивает броузер, загружается этот модуль. Он создает HTML-страницу, которая затем отсылается броузеру.
Обсуждая состояние сеанса, мы столкнулись с рядом случаев, в которых желательно конфигурировать ASP NET. Существует два типа конфигурации.
Server configuration (Конфигурация сервера). В этой конфигурации определяются стандартные значения параметров, которые будут использоваться всеми ASP NET-приложениями. Application configuration (Конфигурация приложения) В этой конфигурации определяются значения параметров, которые будут использоваться конкретным ASP.NET-приложением.С точки зрения программиста, модели событий Web-форм и Windows-форм очень похожи Именно благодаря этому сходству программирование Web-форм оказывается таким легким Но, по сути, события Web-форм сильно отличаются от событий Windows-форм Самое существенное отличие состоит в том, что события Web-форм инициируются клиентом, а обрабатываются сервером.
Мы уже создали простую форму Она состоит из одного текстового поля и одной кнопки Эта форма не обладает достаточным разнообразием элементов управления для того, чтобы подробно проиллюстрировать обработку событий. Представим себе форму, более насыщенную элементами управления. Пусть она содержит несколько текстовых полей, несколько списков, несколько флажков, кнопок и т.п. Поскольку полный обход (путешествие на сервер и обратно) стоит дорого, не каждое событие вызывает автоматическое обращение к серверу. Элементы управления сервера имеют внутренний набор событий сервера. Возникновение таких событий приводит к автоматическому обращению к серверу. Наиболее часто из этих внутренних событий используется событие щелчка на кнопке Другие события, например выбор элемента из списка, не приводят к немедленному обращению к серверу Такого рода события накапливаются в буфере до тех пор, пока событие щелчка на кнопке не вызовет отправку "почтового" сообщения на сервер. Затем сервер обрабатывает различные события, изменяющие состояние формы, причем последовательность обработки событий произвольная Наконец, после этого обрабатывается и событие щелчка на кнопке.
Глобальная информация приложения хранится во встроенном объекте Application (Приложение), который является экземпляром класса HttpApplicationState. Удобно получать доступ к этому объекту через свойство Application (Приложение) класса Page (Страница) Класс HttpApplicationState содержит словарь в формате "ключ — значение", используемый для хранения как объектов, так и скалярных величин.
Информация о сеансе индивидуальных пользователей может храниться во встроенном объекте Session (Сеанс), который является экземпляром класса HttpSession-State. Доступ к этому объекту удобно получать с помощью свойства Session (Сеанс) класса Page (Страница) Класс HttpSessionState, аналогично классу HttpApplicationState, имеет словарь в формате "ключ — значение", который можно использовать для хранения как объектов, так и скалярных величин.
При реализации переменных сеанса возникают некоторые интересные вопросы.
Состояние сеанса и небольшие фрагменты данных о предыстории обращений конкретного пользователя к конкретному Web-серверу, автоматически создаваемые сервером на машине пользователя (cookies)
По умолчанию небольшие фрагменты данных о предыстории обращений конкретного пользователя к конкретному Web-серверу, автоматически создаваемые сервером на машине пользователя (cookies), в ASP.NET используются для идентификации сеанса, в котором отправлен запрос. Но ASP.NET можно сконфигурировать так, чтобы при выполнении не создавались эти небольшие фрагменты данных о предыстории обращений конкретного пользователя к конкретному Web-серверу, обычно автоматически создаваемые сервером на машине пользователя (cookies). В такой модели идентификатор сеанса (Session ID), который обычно сохраняется в небольшом фрагменте данных о предыстории обращений конкретного пользователя к конкретному WWW-серверу, автоматически создаваемом сервером на машине пользователя (cookie), внедряется в унифицированный указатель информационного ресурса (URL). Конфигурация, где не используются небольшие фрагменты данных о предыстории обращений конкретного пользователя к конкретному Web-серверу, автоматически создаваемые сервером на машине пользователя (cookies), обсуждается в следующем разделе.
Предельное время ожидания для состояния сеанса
По умолчанию, предельное время ожидания для состояния сеанса истекает после 20 минут. Это означает, что если данный пользователь находится в состоянии ожидания (простоя) в течение указанного периода времени, сеанс прекращается, и запрос от клиента теперь будет обрабатываться как запрос от нового пользователя, причем будет создан новый сеанс. Как и другие параметры, предельное время ожидания указывается в файле конфигурации, — это мы обсудим в разделе "Конфигурация ASP.NET".
Хранение состояния сеанса
С помощью модели состояния сеанса, которая отделяет хранение информации от использования сохраненной информации приложениями, ASP.NET полностью решает не только проблему группы серверов, но и много других проблем. Модели состояния позволяют реализовать различные сценарии хранения так, чтобы не затрагивать при этом код приложения. Сервер состояния .NET не имеет "активных" объектов для запросов Вместо этого в конце каждого сетевого запроса все объекты в коллекции сеанса преобразуются в последовательную форму, чтобы запомнить состояние сеанса. Когда тот же самый клиент возвращается к странице, объекты сеанса преобразуются из последовательной формы в параллельную.
По умолчанию, состояния сеанса кэшируются, и поэтому, как правило, хранятся в оперативной памяти Конечно, в конфигурации можно указать, что в качестве кэша следует использовать память определенной машины или же что состояния сеанса нужно сохранять в базе данных SQL-сервера В этих случаях данные не привязаны к определенному серверу, и потому данные сеанса могут безопасно использоваться группой серверов
Страница совместно обрабатывается Web-сервером, средой выполнения ASP NET и написанным вами кодом. Класс Page (Страница) содержит ряд событий, с помощью которых можно управлять обработкой страницы. При этом также можно использовать свойства и методы класса Page (Страница). В данной главе возможности класса Page (Страница) будут проиллюстрированы на примере программ Мы рассмотрим лишь некоторые основные возможности. Для получения исчерпывающих сведений обратитесь к описанию каркаса .NET Framework.
События страницы
В процессе стандартной обработки страницы на сервере происходит ряд событий. Фактически эти события определены в базовом классе Control (Элемент управления), и, таким образом, могут быть вызваны также элементами управления сервера. Ниже приведены наиболее значимые события.
Init (Инициализация). Событие возникает на первом этапе жизненного цикла страницы, при ее инициализации Информация о состоянии элементов управления еще отсутствует. Load (Загрузка) Событие возникает на этапе загрузки элементов управления в страницу. На данном этапе уже имеется информация о состоянии представления этих элементов управления. PreRender (Предвыполнение). Событие возникает непосредственно перед воспроизведением (отображением) элементов управления в выходном потоке. Как правило, страница это событие не обрабатывает Тем не менее, оно имеет важное значения для реализации ваших собственных элементов управления сервера. Unload (Разгрузка). Событие возникает тогда, когда элементы управления выгружаются из страницы Записывать ваши собственные данные в выходной поток теперь уже поздно.Свойства страницы
Класс Page (Страница) имеет ряд важных свойств. Некоторые наиболее полезные свойства приведены ниже.
EnableViewState. Указывает, разрешено ли сохранение состояния представления самой страницы и ее элементов управления Можно получить или установить значение этого свойства. По умолчанию оно принимает значение true (истина), т.е. сохранение состояния разрешено ErrorPage. Указывает страницу, на которую будет перенаправлен броузер, если возникнет непредвиденная исключительная ситуация. IsPostBack. Указывает, загружается страница впервые или повторно. I sValid. Указывает, была ли успешной проверка правильности страницы Request (Запрос). Получает HTTP-объект Request (Запрос), который предоставляет доступ к данным входных запросов, поступающих по протоколу передачи гипертекстовых файлов HTTP. Response (Ответ). Получает HTTP-объект Response (Ответ), который отсылает запрошенные данные броузеру. Session (Сеанс). Получает текущий объект Session (Сеанс), в котором ASP.NET хранит состояние сеанса. Trace (Трассировка). Получает объект TraceContext страницы. В этот объект записывается трассировочная информация.Пример программы
Чтобы продемонстрировать отдельные возможности, которые используются при обработке страниц, мы расширим нашу программу Echo (Эхо). Сборка Hello-Page имеет ряд обработчиков событий страницы. Для записи простого текста в выходной поток используется свойство Response (Ответ). После возникновения каждого события мы отображаем текущий текст в элементах управления сервера txtName и IblGreeting. Обработчик события Load (Загрузка) выводит текущее значение свойства IsPostBack. Если страница запрашивается впервые, свойство IsPostBack имеет значение false (ложь). Если же страница запрашивается повторно, оно принимает значение true (истина).
<!-- HelloPage.aspx -->
<-d@ Assembly Name = "HelloPage" %>
<V:P Page Inherits=MyHelloPage %>
<HTML>
< HEAD>
</HEAD>
<BODY>
<!-- ТЕЛО -->
<FORK RUNAT="SERVER">Your name:
<asp:textbox id=txtName Runat="server">
</asp:textbox>
<p><asp: button id=cmdEcho onclick=cmdEcho_Click Text="Echo" •runat-"server"
tooitip="Click to echo your name">
<!-- подсказка --> </a.-;p : bu11оn></p>
Greeting runat = "server "></asp : label>
</FORMi>
</Body>
< !-- тело -->
</HTML>
В .aspx-файле, который приведен выше, имеется ссылка на сборку HelloPage. Названная сборка содержит класс MyHelloPage. В этом файле также указано, что класс r.vj'.' (Страница) является производным от класса MyHelloPage. Это означает, что события данной страницы обрабатываются методами класса MyHelloPage.
Если страница запрашивается впервые, текстовые поля и подписи не содержат никаких значений, поскольку никакой информации в форму мы еще не вводили. Свойство IsPostBack имеет значение false (ложь). Теперь введите имя Robert (Роберт), и щелкните на кнопке Echo (Эхо). Обработчики событий страницы выведут следующую информацию:
Page_Init
txtName =
IblGreeting =
Page_Load
IsPostBack = True // Истина
txtName = Robert // Роберт
iblGreeting =
Page_PreRender
txtName = Robert // Роберт
IblGreeting = Hello, Robert. Welcome again
// Привет, Роберт. Добро пожаловать снова
В функции Page_Init элементы управления не содержат никаких данных, поскольку состояние представления на этапе инициализации страницы не доступно. В функции Page_Load текстовое поле содержит данные. Надпись не содержит никаких данных, так как обработчик события (щелчка мыши) еще не вызван. Свойство IsPostBack теперь имеет значение true (истина). В функции Page_PreRender оба элемента управления содержат данные.
Щелкните на кнопке Echo (Эхо) еще раз. Данные в элементах управления функции Page_Init опять отсутствуют. А вот в функции Page_Load оба элемента управления используют данные состояния представления. На Рисунок 10.9 представлено окно броузера, в котором отображено то, что после второго щелчка на кнопке Echo (Эхо) вывели обработчики события страницы.
Директива Page (Страница) и директива Assembly (Сборка)
Файл с расширением . aspx может содержать директиву Assembly (Сборка) и директиву Page (Страница). Директива Assembly (Сборка) связывает уже существующую скомпилированную сборку с текущей страницей. Директива Page (Страница) определяет различные атрибуты, которые управляют обработкой ASP.NET-страницы. Каждая директива содержит одну или несколько пар атрибут/значение. Синтаксис директивы Assembly (Сборка) и директивы Page (Страница) приведен ниже.
<@ Assembly Name="HelloCodebehind" @>
<@ Page Inherits=MyWebPage @>
Программа HelloCodebehind.aspx служит примером .aspx-страницы, в которой отсутствует код сценария. Если использовать другие языки программирования, например, С# или VB.NET, сценарий можно вложить непосредственно в . aspx-файл. В таком случае использовать директиву Assembly (Сборка) нет необходимости. Указанные языки программирования также позволяют использовать файлы с фоновым исходным кодом. Для этого в директиве Page (Страница) используется атрибут Src, который указывает на нескомпилированный исходный файл. Язык C++ такие возможности не поддерживает. Если используется язык C++, директива Assembly (Сборка) должна содержать атрибут Name (Имя), который идентифицирует скомпилированную сборку. При помощи атрибута Inherits (Наследуется) указывается класс, производный от класса Page (Страница), производным от которого в свою очередь является класс данной . aspx-страницы.
Web-приложение состоит из страниц с документом и страниц с кодом Документ и код хранятся в своем собственном формате Простейшим документом является статическая HTML-страница. Она содержит информацию, которая будет отформатирована и затем отображена Web-броузером. HTML-страница может также содержать гиперссылки на другие HTML-страницы. Гиперссылка (или просто ссылка) содержит адрес, либо унифицированный указатель информационного ресурса (Uniform Resource Locator — URL), определяющий местонахождение искомого документа. Объединение информации, которая формирует страницу, и ссылок иногда называют гипертекстом. С помощью гипертекста обеспечивается удобство навигации в безбрежном океане информации, которая содержится во всемирной паутине (World Wide Web — WWW)
Поддержка управляемого C++в ASP.NET
Управляемый С+-Ь поддерживается технологией ASP.NET лишь частично. Сценарий ASP.NET, написанный на некоторых других языках платформы .NET, например на С# или VB.NET, может быть внедрен ' в Web-страницу. Такой сценарий динамически компилируется в процессе выполнения приложения. Однако использовать управляемый C++ для написания внедряемых сценариев ASP.NET нельзя. На управляемом C++ можно писать лишь предварительно компилируемые сборки, которые используются ASP.NET-странидей. Такие ASP.NET-страницы называют страницами с "предварительно компилируемым фоновым кодом" ("precompiled code-behind"). Инструментальные средства разработки графического интерфейса пользователя (Graphical User Interface, GUI) Web-страниц на основе ASP.NET, входящие в состав среды Visual Studio.NET, позволяют генерировать код на С# и VB.NET. Код на C++ они не генерируют. Тем не менее, практически все элементы пользовательского интерфейса могут быть реализованы на C++, но при этом соответствующий код нужно писать самостоятельно.
Установка примеров web-приложений
Как обычно, все примеры программ, которые рассматриваются в данной главе, расположены в папке, соответствующей данной главе. Но прежде чем вы сможете запустить программу, вам необходимо инсталлировать информационный сервер Internet (Internet Information Services— US) на вашей машине. Информационный сервер Internet инсталлируется по умолчанию вместе с операционной системой Windows 2000 Server. Если на вашей машине установлена операционная система Windows 2000 Workstation, информационный сервер Internet придется инсталлировать отдельно. После инсталляции информационного сервера Internet, вы получите доступ к его описанию с помощью броузера Internet Explorer. Для этого используйте унифицированный указатель информационного ресурса (URL) http: //localhost. В окне броузера откроется начальная страница документации информационного сервера Internet (Рисунок 10.1).
Средством управления информационного сервера Internet является диспетчер служб Internet (Internet Services Manager) — интегрируемый модуть пульта управления Microsoft (Microsoft Management Console, MMC). Он находится в папке Administrative Tools (Средства администрирования) Панели управления (Control Panel). На Рисунок 10.2 показано главное окно диспетчера служб Internet (Internet Services Manager). Чтобы запустить или остановить работу Web-сервера, а также выполнить некоторые другие задачи, следует щелкнуть правой кнопкой мыши на элементе Default Web Site. Чтобы выполнить конфигурацию Web-сервера, нужно выбрать из контекстного меню команду Properties (Свойства).
По умолчанию Web-файлы, предназначенные для публикации, хранятся в катаюге \Inetpub\wwwroot. Он находится на том же логическом диске, на котором инсталлирована операционная система Windows. Диспетчер служб Internet (Internet Services Manager) позволяет переопределить этот каталог. Чтобы получить доступ к Web-страницам, расположенным в любом каталоге вашего жесткого диска, необходимо создать на его базе виртуальный каталог Проще всего это сделать в Проводнике Windows (Windows Explorer) Щелкните правой кнопкой мыши на нужном кататоге. Из контекстного меню выберите пункт Sharing (Совместное использование) Затем перейдите на вкладку Web Sharing (Совместное использование в Web), щелкните на кнопке Add (Добавить) и введите альтернативное имя (псевдоимя, или псевдоним). Альтернативное имя будет использоваться в качестве имени виртуапьного каталога. На Рисунок 10.3 иллюстрируется создание псевдоимени NetCpp для виртуального кататога на основе кататога \Ol\NetCpp\ChaplO. Для того чтобы можно было выполнять дальнейшие примеры, иллюстрирующие материал данной главы, создайте этот виртуальный каталог на вашей машине сейчас
Создав виртуальный каталог, вы получите доступ к хранящимся в нем файлам. Для этого следует указать псевдоимя каталога в унифицированном указателе информационного ресурса (URL). В частности, чтобы получить доступ к файлу default.htm, который расположен в созданном виртуальном каталоге, используйте унифицированный указатель информационного ресурса (URL) http: //localnost/NetCpp/. Файл default.htm представляет собой начальную страницу, где содержатся ссылки на все примеры ASP.NET-программ, рассматриваемые в данной главе (Рисунок 10.4).
ASP.NET-приложение состоит из всех Web-страниц и файлов с кодом, которые хранятся на Web-сервере в виртуальном каталоге или его подкаталогах. Как мы уже убедились, кроме . aspx-файлов и файлов с фоновым кодом в состав приложения также входят файлы global.азах и config.web. В этом разделе главы мы рассмотрим возможности приложений ASP.NET. Затем мы изучим механизмы, которые обеспечивают работу с состоянием приложения и состоянием сеанса, а также конфигурирование Web-приложений.
Теперь нужно заполнить первый раскрывающийся список (DropDownList) названиями городов. Необходимые для этого данные могут быть получены с помощью метода GetCi-ties объекта HotelBroker. Мы воспользуемся свойством привязка данных (data binding), которым обладает раскрывающийся список. Возможно, вы были уверены, что привязка данных используется исключительно при работе с базами данных. При разработке приложений на основе платформы .NET понятие привязка данных принимает более широкий смысл. Здесь привязка данных используется не только при работе с базами данных, но и с другими источниками данных. Привязка элементов управления к базе данных играет очень важную роль при построении двухзвенных приложений типа клиент/сервер. Мы же создаем трехзвенное приложение. В этом приложении код, описывающий логику представления, которое реализовано с помощью Windows-форм либо Web-форм, взаимодействует с компонентом бизнес-логики, а не непосредственно с базой данных.
Каркас .NET Framework имеет ряд возможностей связывания данных. Они облегчают привязку данных, полученных при помощи промежуточного звена. Очень просто привязать данные к списку массивов (ArrayList). Эта возможность превосходно подходит для нашего примера, потому что мы должны заполнить строками раскрывающийся список (DropDownList) городов, а метод GetCities как раз возвращает список массива строк.
В самой нижней строке уместился весь код, который мы добавили к методу Page_Load класса WebForml, для того, чтобы заполнить раскрывающийся (DropDownList) список listCities.
void Page_Load(Object *sender, System::EventArgs *e)
{
if ( HsPostBack)
{
hotelBroker = new HotelBroker;;
ArrayList *cities = hotelBroker->GetCities(};
listCities->DataSource = cities; // города
ArrayList *hotels = hotelBroker->GetHotels(
dynamic_cast<String *> // Строка
(cities->get_Item(0))); // города
BindHotels(hotels); // гостиницы
DataBind();
}
}
Вызов метода DataBind() связывает все элементы управления сервера с соответствующими им источниками данных. В результате все элементы управления заполняются соответствующими данными. Метод DataBind может быть вызван для каждого элемента управления сервера отдельно. Метод DataBind принадлежит классу Control (Элемент управления). Он наследуется классом Page (Страница) и классами конкретных элементов управления сервера.
Инициализация гостиниц
Аналогично мы можем заполнить второй раскрывающийся список (DropDownList) названиями гостиниц. Но это немного сложнее, потому что GetHotels возвращает список массивов структур HotelListltem, а не строк. Нам необходимо заполнить раскрывающийся список listHotels названиями гостиниц. Здесь нам поможет метод BindHotels, который просматривает в цикле список гостиниц и создает список массивов с названиями гостиниц. Этот список затем привязывается к раскрывающемуся списку listHotels. Ниже приведен законченный код, содержащий логику инициализации гостиниц для первого города (который имеет индекс 0).
void BindHotels(ArrayList *hotels)
{
ArrayList *hotelNames =
new ArrayList(hotels->Count); // гостиницы
lEnumerator *pEnum = hotels->GetEnumerator(); // гостиницы
while (pEnum->MoveNext())
{
HotelListltem *hotel =
dynaraic_cast<HotelListItem *>
(pEnum->Current) ;
hotelNames->Add(hotel->HotelName->Trim());
// Добавить гостиницу;
}
listHotels->DataSource = hotelNames;
}
Выбор города
И, наконец, мы реализуем следующую особенность: при выборе города отображаются гостиницы в выбранном городе. Мы добавляем обработчик события для выбора города. Ниже приведен код для обработки события SelectedlndexChanged.
void listCities_Selected!ndexChanged(
Object *sender, System::EventArgs *e)
{
String *city = listCities->Selected!tem->Text; // Строка
ArrayList *hotels = hotelBroker->GetHotels(city); // город
BindHotels(hotels); // гостиницы
DataBind();
}
Добавление класса Global (Глобальный)
Добавьте к проекту новый файл Global .азах.h. Как обычно, нет необходимости самостоятельно писать соответствующий код. Его можно скопировать из файла решения, который находится в каталоге CaseStudyXAcmeWeb. Для этого выполните следующие пошаговые инструкции:
1. Скопируйте файл Global.asax.h из каталога CaseStudy\AcmeWeb в каталог Demos\AcmeWeb, где расположен новый проект.
2. В окне Solution Explorer (Поиск решения) щелкните правой кнопкой мыши на узле Header Files (Заголовочные файлы) под проектом AcmeWeb, и выберите Add => Add Existing Item (Добавить => Добавить существующий элемент). Затем дважды щелкните на файле Global.азах.h.
Обратите внимание, что в Global.азах.h метод Application_Start создает экземпляр класса HotelBroker и присваивает его статическому полю hotelBroker. Это поле затем извлекается в исходном файле WebForml.aspx.h методом Page_Load, который был приведен ранее.
//Global.asax.h
#using <.\Hotel.dll>
#using <System.dll>
#using <System.Web.dll>
using namespace System;
// использование пространства имен Система;
using namespace OI::NetCpp::Acme;
namespace AcmeWeb
// пространство имен AcmeWeb
{
public _gc class Global :
// класс сборщика мусора Глобальный:
public System::Web::HttpApplication
{
public:
static HotelBroker *hotelBroker; // статическое поле protected: // защищенный
void Application_Start(
Object *sender, EventArgs *e)
{
hotelBroker = new HotelBroker;
}
}; }
Файл Global. asax И Объект Global (Глобальный)
Чтобы вызвать методы класса Global (Глобальный), который определен в файле Global.asax.h, необходим файл Global.азах. Например, если в каталоге AcmeWeb файл Global.asax отсутствует, информационный сервер Internet (US) не сможет вызвать метод Application_Start. А это очень важный метод, так как именно он создает экземпляр класса HotelBroker. Файл Global.азах находится в папке Cas-eStudy\AcmeWeb. Скопируйте этот файл в папку Demos\AcmeWeb. Нет необходимости добавлять его в проект. Но удобства ради мы это сделаем. Чтобы добавить данный файл в проект, выполните следующие пошаговые инструкции.
1. Скопируйте файл Global.азах из каталога CaseStudyXAcmeWeb в каталог DemosXAcmeWeb, в котором расположен ваш новый проект.
2. В окне Solution Explorer (Поиск решения) щелкните правой кнопкой мыши на узле проекта AcmeWeb. Из контекстного меню выберите команду Add (Добавить), затем Add Existing Item (Добавить существующий элемент). Установите *.* в качестве фильтра типа файла, а затем дважды щелкните на файле Global. asax.
Файл Global.asax содержит единственную строку (она приведена ниже), которая указывает, что объект приложения является производным от класса AcmeWeb.Global. Этот класс определен в исходном файле Global. asax. h.
<%@ Application Inherits="AcmeWeb.Global" %>
AcmeWeb.aspx
Другой ASP.NET-файл, который должен обязательно присутствовать в каталоге AcmeWeb, имеет название AcmeWeb. aspx. В этом файле указана сборка, где содержится реализация класса AcmeWeb: :WebForml. Исходный код этого класса содержится в исходном файле WebForml.aspx.h. В файле AcmeWeb.aspx, кроме того, указано, что страница наследует класс AcmeWeb: :WebForml, а также некоторый HTML-код, который предназначен для создания представления, отображаемого для пользователя. Файл AcmeWeb.aspx находится в каталоге CaseStudy\AcmeWeb. Вы можете скопировать его из этого каталога в каталог Demos\AcmeWeb. Добавлять его в проект не обязательно. Но удобства ради мы сделаем это. Чтобы добавить данный файл в проект, выполните следующие пошаговые инструкции:
1. Скопируйте файл AcmeWeb.aspx из каталога 4aseStudy\AcmeWeb в каталог Demos \AcmeWeb, в котором расположен новый пюект
2. В окне Solution Explorer (Поиск решения) щелкште правой кнопкой мыши на узле проекта AcmeWeb Из контекстного меню вьберите команду Add (Добавить), затем Add Existing Item (Добавить существующШ элемент) В качестве фильтра типов файлов используйте * * Затем дважды щелшите на файле AcmeWeb. aspx <!— AcmeWeb.aspx —>
<%@ Assembly Name="AcmeWeb" %>
<%@ Page Inherits=AcmeWeb.WebForml %>
<HTML>
<HEAD>
</HEAD>
<BODY> <'— ТЕЛО —>
<form id="Forml" method="post" runat="servor">
<asp:Label id=Labell style="Z-INDEX: 101; EFT: 52px;
POSITION: absolute; TOP: 84px" runat="serv«r">City</asp:Label>
<' - Город —>
<asp:Label id=Labe!2 style="Z-INDEX: 102, EFT. 55px, POSITION: absolute; TOP: 135px" runat="sener">Hotel</ asp:Label>
Osp.DropDownList id=listCities style="Z-HDEX: 103; LEFT: 134px; POSITION: absolute; TOP: 80px" runa1="server" Width="120px" Height="22px" AutoPostBack="rrue"x/asp: Drop-DownList>
<asp-DropDownList id=listHotels style="Z-I!DEX: 104; LEFT-134px; POSITION: absolute; TOP- 128px" run<t="server" Width="120px" Height="22px"x/asp:DropDown]ist> </form>
</BODY> <<— тело —> </HTML>
Построение AcmeWeb
Чтобы построить проект, скопируйте файл Hotel dll в каталог Demos\AcmeWeb Это необходимо сделать потому, что проект содержит /ирективу #using для указанной сборки Скопировав этот файл из каталога CaseStudy^AcmeWeb, можно построить проект, в результате чего будет создан файл AcmeWeb. dll
Просмотр AcmeWeb
Прежде чем вы сможете просмотреть приложение AcmeWeb в броузере, вам нужно сначала скопировать файл AcmeWeb.dll в подкатало bin каталога \Demos\AcmeWeb Вы уже это делали в случае каталога CaseStudy\Acmefeb
1. В каталоге DemosXAcmeWeb, в котором расположи проект, создайте подкаталог bin
2. Скопируйте файл AcmeWeb dll из каталога Dtmos\AcmeWeb\Debug в каталог Demos\AcmeWeb\bin
3. Кроме того, скопируйте файл Hotel. dll в катаюг Demos\AcmeWeb\bin
Но даже после копирования AcmeWeb. dll в катало bin вы все же не можете отобразить http: //localhost/NetCpp/Demos/AcmeWeb/JcmeWeb.aspx в вашем броузере Ведь для этой Web-страницы нужно еще сконфигур1ровать виртуальный каталог как приложение для информационного сервера Internet (II>) Чтобы сделать это, выполните следующие инструкции
4. Щелкните на кнопке Start (Пуск) и выберите команду Programs (Программы) Затем выберите команду Administrative Tools (Средства администрирования) и запустите Internet Server Manager (Диспетчер серверов Internet)
5. Откройте дерево под стандартным Web-узлом (Default Web Site) до узла NetCpp\Demo s\AcmeWeb
6. Щелкните правой кнопкой мыши на каталоге NetCpp \Demos \AcmeWeb Из контекстного меню выберите команду Properties (Свойства)
7. В диалоговом окне щелкните на кнопке Create (Создать), Рисунок 10.20.
8. Появится диалоговое окно, показанное на Рисунок 10.21.
9. Щелкните на кнопке ОК
Только теперь http://localhost/NetCpp/CaseStudy/AcmeWeb/AcmeWeb.aspx можно отобразить в окне броузера (Рисунок 10.22.) Эту страницу можно отобразить непосредственно в броузере или выполнить ее в Visual Studio, которая вызовет Internet Explorer, чтобы обратиться к приложению по протоколу передачи гипертекстовых файлов HTTP При прокрутке списка городов вы увидите города, возвращенные компонентом HotelBroker
Отладка AcmeWeb
Чтобы отладить проект AcmeWeb, необходимо создать файл конфигурации и в нем определить используемый режим отладки Необходимо также включить режим отладки в проекте, как показано на рис 10 23 Чтобы сделать это, выполните следующие шаги
1. Скопируйте файл Web.config из каталога CaseStudy\AcmeWeb в каталог Demos\AcmeWeb. В этом файле конфигурации можно указать несколько возможностей, но для отладки наиболее важен тэг Compilation debug="tme"/> (<отладка сборки = "истина"/>).
2. В окне Solution Explorer (Поиск решения) щелкните правой кнопкой мыши на проекте AcmeWeb. Из контекстного меню выберите команду Properties (Свойства).
3. В папке Configuration Properties (Свойства конфигурации) щелкните кнопкой мыши на узле Debugging (Отладка).
4. В качестве значения свойства Command (Команда) укажите aspnet_wp. ехе.
5. В качестве значения свойства Attach (Присоединить) укажите Yes (Да).
6. Укажите http: //localhost/NetCpp/Demos/AcmeWeb/AcmeWeb.aspx в качестве значения свойства HTTP URL.
Теперь можно открыть решение в отладчике. Для этого выберите из меню Debug (Отладка) команду Start (Пуск). На Рисунок 10.24 и Рисунок 10.25 показан отладчик, остановившийся в точках останова, содержащихся в файлах Global. азах. h и WebForml. aspx. h, соответственно.
Первая программа, которую мы рассмотрим в данной главе, называется Hello, aspx. На начальной странице она представлена соответствующей ссылкой. Пример называется "монолитным'', так как он выполнен в виде одного файла. В этот файл вложен код сценария для сервера ASP.NET. Поскольку писать код вложенного сценария на языке C++ нельзя, он написан на С#. На этом примере будет рассмотрена работа приложений с вложенным кодом сценария. Исходный код программы приведен ниже. Это обычный HTML-текст, в который внедрен код определенного сценария, написанный на С#. Кроме того, в нем имеются некоторые специальные тэги, которые распознает ASP. NET, описывающие элементы управления сервера
<!—- Hello.aspx -->
<%@ Page Language="Cft" %>
< !-- Язык Страницы -->
<HTML>
<HEAD>
<SCRIPT RUNAT="SERVER">
<'-- СЦЕНАРИЙ RUNAT = "СЕРВЕР" —>
protected void cmdEcho__Click(obiect Source, EventArgs e)
{
IblGreeting.Text="Hello, " + txtName.Text; // Текст = "Привет", + txtName.Текст;
}
</SCRIPT>
<!-- сценарий -->
</HEAD>
<BODY>
<!-- ТЕЛО -->
<FORM RUNAT="SERVER">Your name:
<asp:textbox id=txtNdme Runat="server"></asp:textbox>
<p><asp: button ld=cmdEcho onclick=cmdEcho_Click Text "Echo" runat="server" tooltip="Click to echo your name">
</asp :buttonx/p>
<asp:label id=lblGreeting runat="server">
</asp:]abcl* <P></P>
</FORM>
</BODY>
<!-- тело -->
</HTML>
Чтобы запустить программу, укажите унифицированный указатель информационного ресурса (URL) http://local.iost/NetCpp/Hello.aspx или шепкните кнопкой мыши на ссылке Hello, aspx, расположенной на начальной странице с примерами программ. Вы увидите страницу с текстовым полем, в которое вы можете ввести свое имя, и кнопкой Echo (Эхо). Введите имя и затем щелкните на кнопке Echo (Эхо) Теперь появится введенное вами имя с предшествующим ему приветствием "Hello" ("Привет"). Пока на экране отображается эта простая форма, вы сможете ввести другое имя. Если указатель мыши расположить над кнопкой Echo (Эхо), в желтом прямоугольнике будет выведена подсказка "Click to echo your name" ("Щеткните, чтобы отобразить ваше имя"). Работа данной программы иллюстрируется на Рисунок 10.5.
Написание этой маленькой программы при помощи других средств построения Web-приложений, включая ASP (Active Server Pages), является не совсем тривиальной задачей. Характерная особенность этого приложения — его пользовательский интерфейс, полностью реализованный на основе формы Пользователь представляется с помощью формы и взаимодействует с этой же формой. Введенные данные обрабатываются сервером, и пользователь продолжает видеть ту же форму. Такая мелеть пользоватетьского интерфейса— вторая сущность настольных приложений. Но в Web-приложениях подобная модель пользовательского интерфейса используется не слишком часто Как правило, Web-сервер отсылает обратно клиенту уже другую страницу. Конечно, для создания приложения с интерфейсом в виде формы можно было бы использовать и технологию наподобие ASP, но код получится несколько громоздким. Ведь серверу пришлось бы синтезировать новую страницу, идентичную старой. Для этого в новую страницу нужно включить тэги, описывающие исходную страницу, а также некоторую отсылаемую клиенту дополнительную информацию (в нашем примере Echo (Эхо) это приветствие, которое выводится внизу страницы) Иными словами, необходим механизм запоминания текущих данных, введенных в элементах управления формы Другая особенность названного Web-приложения состоит в том, что некоторая обработка данных происходит также и на стороне клиента. А именно, броузер выводит подсказку в желтом прямоугольнике Такую расширенную обработку данных в состоянии выполнить броузер Internet Explorer Некоторые другие броузеры эту функцию не поддерживают
Если вы посмотрите на код программы, то убедитесь, что реализовать подобные Web-приложения с помощью ASP.NET совсем несложно (по крайней мере, на языке С# или VB.NET).
Архитектура элементов управления сервера строится на верхнем слое более фундаментальной архитектуры обработки данных, которую можно назвать архитектурой запросов и ответов. Понимание запросов и ответов протокола передачи гипертекстовых файлов HTTP поможет нам получить целостное представление о технологии ASP NET Кроме того, при программировании иногда возникают такие ситуации, когда использование запросов и ответов является естественным подходом
Чтобы по достоинству оценить поддержку Web-приложений в ASP.NET, нужно иметь четкое представление о концепции Web-сеанса. Протокол передачи гипертекстовых файлов HTTP (HyperText Transfer Protocol) не хранит состояние приложения. Это значит, что не существует прямого способа, при помощи которого можно было бы узнать, исходит последовательность запросов от одного или от разных клиентов. Web-сервер, например информационный сервер Internet (US), может иметь механизм, при помощи которого исходящие от одного клиента запросы объединяются в логический сеанс. Работать с сеансами в ASP.NET очень легко.
Любая информация, которая вводится в форму, "запоминается" Web-сервером Это важное свойство Web-форм Протокот передачи гипертекста HTTP не хранит информацию о состоянии формы Иными словами, сохранение текущего состояния Web-формы происходит не автоматически, а должно быть запрограммировано. Информация о состоянии Web-формы называется состоянием представления, или состоянием вида (view state) ASP.NET может с помощью каркаса Framework автоматически сохранять состояние представления (вида) Web-формы. При этом используется "скрытый" элемент управления Для удобства восприятия, значительная часть длинного значения, которое описывает состояние представления формы, в приведенном ниже коде опущена.
<input type="hidden" name="_VIEWSTATE"
value="dDw2M]kzODE3NTtOPDtsPGk8M]47P ... +0z4+0z4=" />
Позже в этой главе мы рассмотрим другие возможности ASP.NET, используя которые можно управлять состоянием сеанса и состоянием приложения.
Сохранение состояния при запросах, посылаемых по протоколу передачи гипертекстовых файлов HTTP, — главная проблема в Web-программировании ASP.NET предоставляет для этого несколько удобных функций Необходимо сохранять два типа состояния.
Состояние приложения — глобальная информация, которая совместно используется всеми пользователями Web-приложения Состояние сеанса используется для хранения данных конкретного пользователя, который многократно обращается к Web-приложениюСтатические элементы данных класса совместно используются всеми экземплярами класса Следовательно, статические элементы данных могут использоваться для хранения состояния приложения
Технология ASP.NET предоставляет широкие возможности для трассировки программы. Если атрибуту Trace (Трассировка) страницы присвоить значение true (истина), то выходная трассировочная информация, генерируемая ASP.NET, будет выводиться в окне броузера. Кроме того, можно вывести свою трассировочную информацию. Для этого используется метод Write (Запись) объекта TraceContext. Чтобы получить доступ к этому методу, используется свойство Trace (Трассировка) класса Page (Страница).
Страница HelloTrace.aspx иллюстрирует использование трассировки при записи информации в объект Response (Ответ).
<!—- HelloTrace.aspx -—>
<%@ Assembly Name="HelloTrace" %>
<%@ Page Inherits=MyHelloTrace Trace = "true" %>
<!-- Трассировка Страницы --> <HTML>
<HEAD>
</HEAD>
<BODY> <!— ТЕЛО —>
<FORM RUNAT="SERVER">Your name:
<asp:textbox id=txtName Runat="server"> </asp:textbox>
<p><asp:button id=cmdEcho onclick=cmdEcho_Click Text="Echo" runat="server"
tooltip="Click to echo your name">
<!-- подсказка -->
</asp:button></p>
<asp:label id=lblGreeting runat="server">
</asp:label> <P></P>
</FORM>
</BODY> <!-- тело -->
</HTKL>
Класс MyHelloTrace, реализованный в виде сборки rielloTrace.dll, содержит следующий код, который записывает вывод трассировки:
void Page_Init(Gtoect *senaer, EventArgs *e)
{
Page *p = dynamic_cast<Page *>(sender);
// Страница "р = dynarpic__cast <Страница *> (отправитель);
TraceContext *trace = p->get_Trace();
trace->Write("Page_Init<br>"); // трассировка-> Запись
tгасе->Write (String::Concat(
// трассировка-> Запись (Строка)
"txtName = ", txtName->Text, "<br>")); // Текст
trace->Wnte (String: :Concat (
// трассировка-> Запись (Строка)
"IblGreeting = ",iblGreeting->Text, "<br>")); // Текст
}
void Page_Load(Object *sender, EventArgs *e)
{
Page *p = dynamic__cast<Page *>(sender);
// Страница *р = dynamic_cast <Страница *> (отправитель);
TraceContext *trace = p->get_Trace();
trace->Write("Page_Load<br>"); // трассировка-> Запись
trace->Write (String: :Format(
// трассировка-> Запись (Строка:: Формат )
"IsPostBack = (0}<br>", _box (IsPostBack) )
};
trace->Write(String::Concat(
// трассировка-> Запись (Строка)
"txtName = ", txtName->Text, "<br>")); // Текст
trace->Write(String::Concat(
// трассировка-> Запись (Строка)
"IblGreeting = ", lblGreeting->Text, "<br>")); // Текст
}
void Page_PreRender(Object ^sender, EventArgs *e) -
{
Page *p = dynamic_cast<Page *>(&ender);
// Страница *р = aynamic_cast <Страница *> (отправитель);
TraceContext wtrace = p->get_Trace();
trace->Write("Fage_PreRendei<br>"); // трассировка-> Запись
trace->Wnte (String: : Concat
// трассировка-> Запись (Строка)
"txtName == ", txtName->Text, "<br>")); // Текст
trace->Write(String::Concat(
// трассировка-> Запись (Строка)
"IblGreeting = ", lblGreeting->Text, "<br>")); // Текст
}
На Рисунок 10.10 показано, что отображает броузер после первого запроса данной страницы Обратите внимание, что вывод трассировки и трассировочная информация, генерируемая ASP.NET, отображается после формы
ASP.NET предоставляет модель программирования и соответствующую инфраструктуру, которая облегчает разработку Web-приложений нового типа. Частыо этой инфраструктуры является среда выполнения платформы .NET и ее каркас .NET Framework Серверная программа пишется на одном из языков программирования платформы .NET, которые компилируются либо предварительно, либо оперативно, т.е в процессе выполнения приложения. ASP.NET поддерживает две основные модели программирования.
Web-формы облегчают создание Web-страниц на основе форм. Среда разработки в режиме полного соответствия WYSIWYG (What You See Is What You Get — что видишь на экране, то и получишь при печати) позволяет просто перетаскивать соответствующие элементы управления на Web-страницу Специальные элементы управления сервера предоставляют программистам модель событий, похожую на модель событий, которая используется при программировании Windows-приложений. В данной главе подробно обсуждаются Web-формы. Web-службы позволяют через программный интерфейс приложения (API) представить на Web-узле функции, которыми могут воспользоваться другие удаленные приложения. Для обмена данными используются стандартные Web-протоколы и форматы, например, протокол передачи гипертекстовых файлов HTTP (Hypertext Transfer Protocol) и язык XML (extensible Markup Language), что позволяет избежать проблем, связанных с прохождением пакетов данных через системы сетевой защиты (брандмауэры). Web-службы обсуждаются в следующей главеWeb-формы и Web-службы могут использовать дополнительные возможности, предоставляемые платформой NET, например, оттранслированные программы и среду выполнения платформы NET. Кроме того, в самой ASP NET предусмотрен ряд служб инфраструктуры, к числу которых принадлежит управление состоянием, защита, конфигурирование, кэширование и трассировка (отладка) программ
Оттранслированные программы
Web-формы (и Web-службы) могут быть написаны на любом языке, который поддерживается платформой .NET и интерпретируется верхним слоем общеязыковой среды выполнения CLR (Common Language Runtime) К числу таких языков принадлежат С#, VB.NET, и C++ с управляемыми расширениями. Полученная программа затем транслируется За счет этого обеспечивается более высокая производительность по сравнению с ASP-страницами, написанными на интерпретируемом языке сценариев, например, VBScnpt. Оттранслированным программам доступны все дополнительные возможности среды NET, например, управляемая среда выполнения, и, конечно же, библиотека классов .NET Framework. Традиционный неуправляемый код может быть вызван посредством .NET-служб, обеспечивающих возможность взаимодействия управляемого и неуправляемого кода. Эти службы обсуждаются в главе 15 "Смешивание управляемого и неуправляемого кода".
Элементы управления сервера
В ASP.NET введено существенное новшество, известное как элементы управления сервера Элементы управления сервера описываются специальными тэгами, например <asp:textbox>. Серверная программа взаимодействует с элементами управления сервера. Затем среда выполнения ASP.NET генерирует обычную HTML-страницу, которая отсылается Web-броузеру. С помощью такой модели программирования легко получить стандартный HTML-код. Этот код может быть интерпретирован любым броузером.
Независимость кода от броузеров
Всемирная сеть (World Wide Web — WWW) построена на стандартах, но используемые броузеры не полностью совместимы. Некоторые из них поддерживают специфические функции и это является неутешительным житейским фактом. Перед дизайнером, создающим Web-страницу, открываются две, не очень-то привлекательные, возможности: писать простейший код, понятный каждому броузеру, или писать индивидуальный код для каждого броузера Использование элементов управления сервера позволяет существенно облегчить решение этой проблемы. ASP.NET учитывает совместимость броузеров на этапе генерации кода элементов управления сервера. Если страница запрошена мощным многофункциональным броузером, тогда генерируется HTML-код, использующий дополнительные функции броузера. В противном случае генерируется простейший HTML-код. Возможности используемого броузера ASP.NET определяет автоматически.
Отделение кода от содержимого
Стандартные ASP-страницы представляют собой смесь кода сценариев и элементов языка HTML. В ASP.NET код сценариев четко отделен от представляемого содержимого. Если код сценария написан на языке, который поддерживает создание вложенных сценариев ASP.NET, например, на С# (но не на C++), тогда этот код можно заключить в блок <SCRIPT RUNAT="SERVER"> ... </SCRlPT>. Или, что еще лучше, код сценария можно разместить на отдельной, так называемой, странице с "фоновым кодом". Именно таким образом следует поступить, если код сценария написан на C++. Страницы с "фоновым кодом" обсуждаются в следующей, основной части данной главы.
Управление состоянием
Протокол передачи гипертекстовых файлов (HyperText Transfer Protocol — HTTP) не хранит информацию о состоянии страницы. Это означает следующее. Пусть пользователь ввел в элементы управления формы некоторую информацию и отослал заполненную форму на сервер. Еспи затем форма отобразится снова, то введенная пользователем информация, как правило, будет потеряна. Чтобы сохранить информацию, введенную пользователем, в Web-приложение нужно добавить специальный код, который запоминает состояние формы. В ASP.NET сохранение этих составляющих частей состояния страницы полностью прозрачно. В ASP.NET также имеются удобные средства управления состоянием другого сеанса и состоянием приложения.
Чтобы четко представить и глубоко понять архитектуру Web-форм, мы рассмотрим, что происходит во время существования простого приложения HelloCodeBehind, реализованного на C++. В этом приложении используется предварительно скомпилированный файл HelloCodebehind. aspx, который содержит фоновый код. Исходный код файла написан на C++. Если используется другой язык, тогда компилировать файл не нужно. Достаточно лишь иметь файл с исходным фоновым кодом. То, что происходит во время существования Web-формы, которая использует предварительно скомпилированный фоновый код, можно описать следующим образом.
1. Приложение запрашивает WеЬ-страницу HelloCodebehind. aspx посредством броузера.
2. Среда выполнения ASP NET создает класс Page (Страница), производный от класса реализованного в предварительно скомпилированной сборке В этом классе объединены визхальные элементы, которые содержатся в aspx-файле, и код, содержащийся в предварительно скомпилированной сборке Затем класс компи-лирхется и выполняется Резхльтатом выполнения является HTML-страница, которая отсылается Web-броузеру Чтобы увидеть HTML-код в броузере Internet Explorer в меню View (Вид) выберите команду Source (В виде HTML) Проделав это, вы \бедитесь что э шменты управления сервера заменены обычным HTML-кодом Он приведен ниже
3. Броузер интерпретирует HTML-код и отображает простую форму (Рисунок 10.7) Чтобы отличить этот пример от аналогичного ему первого примера этой главы, фраза "YOUR NAME" (' ВАШЕ ИМЯ') отображается прописными (заглавными, большими) буквами Поскольку форма отображается впервые, текстовое поле еще не зшолнено и отсутствует приветствие
4. Потьзователь вводит свое имя например Peter и щелкает на кнопке Echo (Эхо) Броузер распознает, что была нажата кнопка Submit (Отослать) Форма использует метод post ("отправить почтовое сообщение") и выполняет действие "HelloCodebehind aspx" Таким образом, мы отправили в первоначальный aspx-фаил 'почтовое сообщение с уведомлением"
5. Теперь сервер обрабатывает страницу В этот раз событие было инициировано щелчком пользователя на кнопке Echo (Эхо) Дальше вызывается обработчик события из класса MyWebPage
6. Для считывания имени, введенного пользоватепем, применяется свойство Text (Текст) элемента управления сервера txtName (тип TextBox (Текстовое поле)) Далее формируется строка с приветствием, которая затем присваивается этемент> управления lolGreeting (тип Label (Надпись)) При этом снова испотьзуется нотация для свойства
7. Сервер снова генерирует стандартный HTML-код для элементов управления сервера и отсылает полный ответ броузеру Ниже приведен окончательный HTML-код
<fcrm rame="ctrlO" method="post" action="HelloCodebehind.aspx" id="ctrl()">
< i-- действие "nelloCodebehind Qspx" идентификатор - "ctrl()" -->
<input type="hidden" narae="_VIEWSTATE"
value="dDw2MkzODE3NTtOPDtsPGk8M]47P . . +0z4+0z4=" />
YOUR NAME:
<input name="txtName" type="text" value="Peter" id="txtName" />
<p>
<input type="subnu t" name="cmdEcho" value="Echo" id="cmdEcho" title="Click to echo your name">
<-/p>
<ьрап id="lblGreeting">
Hello, Peter. Welcome to Managed C++ ASP.NET
</span> <P></P> </form>
8. Броузер воспроизводит страницу (Рисунок 10.8.) На этот раз отображается также и приветствие
На языке C++ мы создадим простую Web-страницу на основе ASP.NET, на которой отображается информация о гостиницах. Названия городов и гостиниц хранятся в раскрывающихся списках. Выбор названия города в первом раскрывающемся списке приводит к тому, что во втором раскрывающемся списке будут перечислены названия гостиниц, которые расположены в выбранном городе. Информацию о гостиницах содержит компонент Hotel.dll, расположенный в каталоге CaseStudy\AcmeWeb. В свою очередь, этот каталог находится в папке к данной главе. Для заполнения списков мы используем привязку данных. Завершенный проект расположен в каталоге Cas-eStudy\AcmeWeb в папке к данной главе. Вы можете также самостоятельно создать приложение, выполнив все необходимые действия. Сохраните созданное вами приложение в папке Demos (Демонстрации), хранящейся в папке к данной главе.
Чтобы увидеть результаты работы приложения, введите в адресной строке броузера унифицированный указатель информационного ресурса (URL) http://localhost/NetCpp/ CaseStudy/AcmeWeb/AcmeWeb.aspx. Но перед этим еще необходимо создать виртуальный каталог и, используя диспетчер служб Internet (Internet Services Manager), сконфигурировать его в информационном сервере Internet (US) в качестве приложения. Конфигурирование виртуального каталога описывается ниже.
Конфигурирование соединения с Web-сервером
Перед тем как писать приложение, вы, возможно, пожелаете проверить, а, может быть, есть смысл изменить настройки соединения Web-сервера. Имеются два переключателя: File share (Совместное использование файлов) и FrontPage. Если приложение полностью разрабатывается на локальном компьютере, для удобства и ускорения работы рекомендуется выбрать переключатель File share (Совместное использование файлов). Чтобы получить доступ к этому параметру настройки, в меню Tools (Сервис) выберите команду Options (Параметры). Затем выберите папку Projects (Проекты), а в ней — пункт Web Settings (Параметры Web). Наконец, в группе Preferred Access Method (Предпочтительный метод доступа) выберите нужный переключатель. Процесс конфигурации соединения иллюстрируется на Рисунок 10.17.
Создание сетевого приложения с помощью C++ на основе ASP.NET
1. В Visual Studio в меню File (Файл) выберите команду New (Создать), а затем пункт Project (Проект).
2. В диалоговом окне New Project (Новый проект) в качестве типа проекта (Project Туре) укажите Visual C++ Projects (Проекты на Visual C++). В качестве используемого шаблона (Template) укажите Managed C++ Class Library (Библиотека классов управляемого C++).
3. Укажите AcmeWeb в качестве имени проекта.
4. Укажите путь к проекту С: \OI\NetCpp\ChaplO\Demos (Рисунок 10.18).
5. Щелкните на ОК При этом в каталоге С: \OI\NetCpp\ChaplO\Demos будут созданы файлы проекта, включая файл решения Acme Web sin
6. Создайте на основе каталога C:\OI\NetCpp\ChaplO виртуальный каталог с псевдонимом NetCpp (рис 10 3), если вы не создали его раньше
Конструирование формы
Поскольку в среде Visual Studio NET конструктор форм не поддерживает язык C++, то форму лучше спроектировать на С#, а затем полученный код (на С#) перенести на язык C++ Сначала вы могли бы создать на С# фиктивный проект с формой и сконструировать саму форму Но это все уже сделано за вас Вы можете воспользоваться файлом WebForml.aspx.h, содержащим готовый код на C++ Он находится в каталоге Cas-eStudy\AcmeWeb Добавьте его в ваш проект, который расположен в каталоге Demo s\AcmeWeb
Форма содержит две надписи (Label (Надпись)), два раскрывающихся списка (DropDownList) и одну кнопку (Button) Свойство Text (Текст) первой надписи имеет значения City (Город), а второй — Hotel (Гостиница) Свойство Text (Текст) единственной кнопки имеет значение Make Reservation (Забронировать) Идентификатор (ID) первого раскрывающегося списка принимает значение listCities, второго— listHo-tels Идентификатор (ID) кнопки имеет значение cmdMakeReservation Данная форма представлена на Рисунок 10.19 Размер раскрывающихся списков, которые показаны на рисунке, был изменен
Чтобы в новом проекте (он расположен в каталоге Demos\AcmeWeb) воспользоваться кодом, перенесенным на C++, (этот код содержится в файле WebForml.aspx.h, хранящемся в папке CaseStudy\AcmeWeb), выполните следующие действия
1. Скопируйте файл WebForml. aspx. h из каталога CaseStudy\AcmeWeb в каталог Demos\AcmeWeb, в котором расположен новый проект
2. Откройте новый проект, если он еще не открыт Для этого войдите в каталог Demos\AcmeWeb идважды щелкните на файле Acme Web. sin
3. В окне поиска решения (Solution Explorer), щелкните правой кнопкой мыши на узле Header Files (Заголовочные файлы) (Этот узел находится в проекте Acme Web ) Теперь из контекстного меню выберите команду Add (Добавить) и затем Add Existing Item (Добавить существующий элемент) После этого дважды щелкните левой кнопкой мыши на файле WebForml. aspx. h
4. Удалите файл Acme Web. h, который в проекте не используется Для этого выделите этот файл в окне поиска решения (Solution Explorer) и нажмите клавишу Delete (Удалить) Поскольку файл AcmeWeb h больше не требуется вообще, его можно удалить и из каталога \Demos\AcmeWeb С этой целью можно использовать и Проводник Windows (Windows Explorer)
5. Отредактируйте файл AcmeWeb срр Удалите директиву iinclude, которая включает файл AcmeWeb h Добавьте директиву # include, чтобы включить файл WebForml aspx.h Вскоре мы добавим в проект еще один заголовочный файл, который называется Global. азах. h Включите его в текущий файл при помощи директивы #include
// Это - основной файл динамически подключаемой библиотеки (DLL).
#include "stdafx.h"
#include "Global.asax.h"
#include "WebForml.aspx h"
Ниже приведен исходный файл WebForml. aspx. h Обратите внимание, что для извлечения объекта HotelBroker используется статическое поле Global: : hotelBroker Вскоре мы увидим, что этот объект создается методом Application_Start объекта Global (Глобальный), который определен в исходном файле Global. asax. h
//WebForml.aspx.h
namespace AcmeWeb
// пространство имен AcmeWeb
{
public _gc class WebForml :
public System::Web::UI::Page
// класс сборщика мусора WebForml:
// общедоступная Система::Сеть::Пользовательский
// интерфейс::Страница
{
protected: // защищенный
System::Web::UI::WebControls::Label *Labell;
System::Web::UI::WebControls::Label *Label2;
System::Web::UI::WebControls::DropDownList
*listCities; System::Web::UI::WebControls::DropDownList
*listHotels;
private: // частный
static HotelBroker *hotelBroker; // статический
public:
WebForml()
{
Page::Init += new System::EventHandler(
this, Page_Init);
}
private: // частный
void Page_Load(Object *sender, System::EventArgs *e)
{
if ( HsPostBack)
{
hotelBroker = Global:rhotelBroker;
ArrayList *cities = hotelBroker->GetCities();
listCities->DataSource = cities; // города
ArrayList *hotels = hotelBroker->GetHotels;
dynamic_cast<String *> // Строка
(cities->get_Item(0))); // города
BindHotels(hotels); // гостиницы
DataBind();
}
}
};