Text
                    Дино Эспозито
Microsoft
ASP.NET 2.0
Базовый курс
а РУССКАЯ РЕДАКД ИЯ
Microsoft
С^ППТЕР

Глава 1 Модель программирования ASP.NET ASP.NET — платформа разработки приложений для Web — включает в себя сервисы, программную инфраструктуру и модель программирования, необходимые для создания приложений масштаба предприятия. Хотя синтаксически ASP.NET совместима со своей предшественницей Active Server Pages (ASP), на самом деле это совершенно новая, революционная программная среда, ориентированная на быструю разработку Web-приложений. Будучи частью платформы Microsoft .NET, ASP.NET позволяет с исключительной легкостью создавать и развертывать расширяемые приложения с компонентной структурой для любого целевого браузера или мобильного устройства. ASP.NET 1.1 очень популярна, она используется в тысячах разнообразных проектов. Но создание этой системы — лишь первый этап большого пути, и чем дольше вы с ней работаете, тем больше понимаете, как многого ей еще недостает. Упростив решение множества задач, ASP.NET стала настоящим раем для программистов, перешедших на нее от классической ASP, Internet Server Application Programming Interface (ISAPI) и других Web-платформ. Но ее версия 1.1 лишь подогрела аппетиты разработчиков, по- казала заманчивые перспективы будущего Web-программирования, и после несколь- ких месяцев ее эксплуатации разработчики уже желали большего — много большего! Следующей базовой версией данной платформы стала ASP.NET 2.0, хотя в ней не вводится никакая новая революционная парадигма программирования. Вы не увидите здесь ни радикально нового подхода к проектированию и написанию кода, ни нового синтаксиса. Тем не менее это крупная веха в развитии технологий Web-разработки от Microsoft и для разработчиков, и для архитекторов приложений. В ASP.NET 2.0 пере- работаны многие классы, и ее внутренняя реализация претерпела ряд существенных изменений. Появилось несколько новых элементов управления, призванных повысить продуктивность разработки, усовершенствован ряд существовавших ранее системных модулей и добавлена группа новых, делающих систему гибкой, настраиваемой, надеж- ной и защищенной. В результате перед ведущими разработчиками и архитекторами открылись новые, более эффективные пути решения множества разнообразных задач, а также разрешения проблем, возникавших при разработке приложений в предыду- щей версии ASP.NET. Поэтому, для того чтобы получить максимум преимуществ от использования ASP.NET, желательно изучить всю ее программную модель — набор компонентов, программные средства и инфраструктуру. Обзору этой модели и посвящена данная глава. А начнем мы с анализа базовых концепций платформы ASP.NET и ее программной модели. Что такое ASP.NET До появления ASP.NET существовали три ведущие технологии и платформы разра- ботки Web-приложений: ASP, Java Server Pages QSP) и Web-платформа с открытым исходным кодом, известная под названием LAMP (Linux, плюс Apache, плюс MySQL, плюс Perl, Python или PHP в качестве языка программирования).
Модель программирования ASP.NET Глава 1 3 Примечание Для полноты картины следует упомянуть и о нескольких платформенно- зависимых низкоуровневых технологиях, на которых основываются ASP и JSP. Первая является расширением ISAPI, а вторая реализована как приложение-сервлет. Расширения ISAPI на платформе IIS и сервлеты в Java-системах позволяют создавать серверные приложения для Web на основе подхода, более близкого к классическому. Согласно этому подходу вы создаете страницу не декларативным способом, сочетая разметку со встроенным кодом, а путем написания программного модуля, который сам формирует ее структуру и затем осуществляет рендеринг страницы. Хотя у каждой из перечисленных платформ имеются элементы и характеристики, специфические для конкретного языка и определенной архитектуры, все эти плат- формы предназначены для создания интерактивных страниц, совокупность которых образует Web-приложение. Кроме того, все эти платформы в той или иной степени дают разработчику возможность отделить программную логику от структуры стра- ницы путем использования компонентов, за вызов и рендеринг которых отвечает сама страница. Но если не учитывать этой объединяющей цели, все платформы очень отличаются друг от друга, и большинство их различий связано с программной мо- делью и языками программирования. Например, в JSP определен комплекс классов, которые вместе с JavaBeans составляют эффективную расширяемую модель повтор- ного использования компонентов. Кроме того, JSP поддерживает пользовательские тэги и позволяет разработчику связывать код с их определениями. Наконец, будучи ключевым элементом платформы Java 2 Enterprise Edition (J2EE), JSP ориентирована на использование первоклассного компилируемого языка Java, тогда как на платфор- мах ASP и LAMP применяются языки сценариев. Какое же место среди упомянутых технологий занимает ASP.NET? Подобно ASP и всем прочим средам разработки и выполнения Web-приложений ASP.NET работает поверх протокола HTTP, используя достоинства этого протокола и его правила для организации двунаправленного взаимодействия между сервером и браузером. От множества других технологий разработки Web-приложений ее отли- чает абстрактная программная модель Web Forms. Кроме того, платформа ASP.NET является интегральной частью Microsoft .NET Framework. А это очень важное обсто- ятельство. Приложение ASP.NET представляет собой расширяемый набор повторно используемых компонентов. Их код компилируется, причем его можно писать на таких высококлассных языках программирования, как С#, Microsoft Visual Basic .NET, Mi- crosoft JScript .NET и J#, используя при этом иерархию классов .NET Framework. Таким образом, ASP.NET соединила в себе все лучшее, что создано в области раз- работки Web-приложений. Семантически (и до некоторой степени в отношении языков программирования) эта система совместима с ASP.NET. Она предоставляет такие же возможности объектно-ориентированного программирования, как JSP (определение пользовательских тэгов, применение компилируемых языков, создание приложений с компонентной структурой, расширяемость и возможность повторного использования кода). А кроме того, в ASP.NET имеется обширный набор средств, инструментов и си- стемных функций, которые можно объединить под общим названием средства абстра- гирования программной модели HTTP. Целый комплекс дружественных к программисту классов позволяет разрабатывать страницы теми же методами, что и при создании настольных приложений. А модель Web Forms обеспечивает реализацию классической модели приложения, управляемого событиями, но с развертыванием через Web. ASP.NET поддерживается на множестве платформ, к числу которых относятся Mic- rosoft Windows 2000 с Service Pack 2, Windows XP Professional и Windows Server 2003 Для разработки серверных приложений ей требуется Web-сервер Internet Information Services (IIS) версии 5.0 или выше, а также пакет Microsoft Data Access Components
4 Часть I Разработка страниц ASP.NET (MDAC) 2.7, который автоматически устанавливается вместе с .NET Framework. С точ- ки зрения производительности, надежности и защищенности идеальным сочетанием серверного программного обеспечения для хостинга приложений ASP.NET является Windows Server 2003 (лучше всего с Service Pack 1) и IIS 6.0. Программирование в эру Web Forms Идея, положенная в основу модели ASP.NET Web Forms, является результатом поиска наилучшей стратегии дешевого, но эффективного Web-взаимодействия Все основные достоинства Web-приложений и самые существенные их недостатки связаны с про- токолом HTTP. Он работает без сохранения состояния, поэтому в приложениях на его основе приходится применять ряд специализированных программных технологий, не требующихся настольным приложениям, и в первую очередь технологии управления состоянием. Однако простота и масштабируемость HTTP, обеспечившие его эффек- тивность, позволили этому протоколу получить всемирное распространение — ведь именно благодаря ему Интернет стал тем, чем он является сейчас. И теперь, когда стремительно возрастает спрос на мощные Web-приложения с богатыми возмож- ностями, программистам приходится искать новые пути обеспечения эффективного взаимодействия клиента и сервера Предлагалось множество технологий, призванных обеспечить связь между раз- ными страницами приложения, а также последовательными вызовами одной и той же страницы одним клиентом. Большинство программистов привыкли мыслить ка- тегориями выполняемых на клиенте действий, вызывающих ответные действия на сервере. Однако эта фундаментальная схема не может быть реализована в Web-среде непосредственно, без специальных средств, действующих с обеих сторон. Необходимы определенный уровень абстрагирования и системные сервисы, которые обеспечивали бы непрерывность взаимодействия сервера и клиента. В ASP (в большей степени, нежели в JSP) реализован декларативный подход к решению этой задачи; объектная же модель ASP не отличается богатством и раз- нообразием. Программистам, которые собираются освоить технологию разработки Web-приложений, необходимо научиться мыслить по-иному, оставить за порогом привычную парадигму действия-ответа. Событийно-управляемое программирование в НТТР-среде Программная модель ASP.NET Web Forms привнесла в Web не присущую ей изна- чально концепцию событийно-управляемого взаимодействия. Для реализации этой концепции нужно, чтобы данные, связанные с действиями пользователя на клиент- ском компьютере, передавались на сервер — это обязательное условие осуществления работы приложения с сохранением состояния. Сервер обрабатывает информацию о действиях клиента и генерирует ответные действия. Общее состояние приложения включает информацию двух типов: состояние клиента и состояние сеанса. Состо- яние клиента — это главным образом содержимое полей формы, которое именует- ся состоянием страницы. Доступ к нему осуществляется посредством серверных коллекций, в которых хранятся данные, полученные в результате возврата формы. Но как обстоит дело с общим состоянием сеанса? Пользователь ожидает, что информация, отправленная серверу с одной страницы, будет использована на следующих страницах, которые с ней логически связаны. Возьмем, к примеру, приложение для электронной коммерции, в котором пользователь наполняет корзину для покупок. Какой компонент этого приложения помнит, что именно пользователь положил в свою корзину? Сам по себе протокол HTTP не дает возможности отслеживать подобную информацию. Поэтому необходима дополнительная серверная инфраструктура, надстроенная по- верх данного протокола и отвечающая за сохранение состояния сеанса.
Модель программирования ASP.NET Глава 1 5 Разработчику Web-приложений исключительно важно понимать суть концеп- ции программирования без сохранения состояния сеанса. Мы говорили, что такова природа протокола HTTP, в котором два последовательных запроса, направленных клиентом серверу, никак не связаны друг с другом и обрабатываются сервером со- вершенно независимо. Он даже не знает, что они пришли от одного и того же клиента в ходе одного сеанса работы с приложением, и не сохраняет никакой информации, связанной с этим сеансом. Единственным связующим звеном запросов может быть информация приложения, сохраняемая в глобальных объектах. В ASP.NET типичным методом обхода описанного системного ограничения является применение реентера- бельных форм. Реентерабельная форма — это HTML-элемент <form>, выполняющий возврат данных формы той странице, которая его содержит. Сами по себе такие формы не решают проблему, однако в сочетании с блоками кода и скрытыми полями, где хранится важная для страницы информация состояния, они позволяют это сделать разработчикам, причем весьма элегантно. Такое решение стало стандартным, интегрированным в исполняющую среду ASP.NET, и теперь именно таким способом все приложения ASP.NET сохраняют свое состояние. Возможность сохранения состояния страницы между запросами обеспечивает ис- полняющая среда ASP.NET. Генерируя HTML-код определенной страницы, ASP.NET кодирует состояние серверных объектов и помещает его в несколько скрытых, про- зрачно создаваемых полей формы. Когда поступает запрос на предоставление данной страницы, исполняющая среда анализирует включенную в состав запроса информацию состояния (содержимое скрытых полей формы той клиентской страницы, от которой поступил запрос) и учитывает ее при формировании новых экземпляров серверных объектов. Схема описываемого процесса представлена на рис. 1-1. Как видите, он очень отличается от процесса работы приложений Windows Forms. Рис. 1-1. Сравнение моделей Windows Forms и Web Forms из .NET Framework Модель Windows Forms основана на классическом событийно-управляемом стиле программирования настольных приложений. Каким бы ни было соединение между клиентскими и серверными компонентами, сервер всегда реагирует на клиентский ввод и ему известно состояние приложения. А само приложение является двухслойным, причем его слои тесно связаны между собой. Модели Web Forms для поддержки такой событийно-управляемой схемы работы требуется некий дополнительный механизм.
6 Часть I Разработка страниц ASP.NET На рис. 1-1 его представляют блоки десериализации состояния страницы, выполня- емый при получении запроса, и сериализации состояния страницы, выполняемый при подготовке ответа. За выполнение обеих операций отвечает исполняющая среда ASP.NET — допол- нительный код, который специальным образом расширяет возможности Web-сервера. Реентерабельные формы и скрытые поля являются низкоуровневыми средствами, используемыми при работе с состоянием. Такая схема работы не была бы достаточ- но эффективной, если бы для ее поддержки не создали богатую объектную модель, охватывающую весь контент серверной страницы. Эта объектная модель — ключевой элемент платформы ASP.NET. Компонентная модель ASP.NET идентифицирует и описывает строительные блоки страниц ASP.NET. Она реализована как объектная модель, где практически каждый HTML-элемент страницы (речь идет о тэгах типа <form> или <input>) имеет свой сер- верный аналог. Кроме того, объектная модель ASP.NET включает множество компонен- тов — так называемых серверных элементов управления, или Web-элементов, которые представляют более сложные части пользовательского интерфейса. Многим из этих элементов управления соответствуют не отдельные HTML-элементы, а их сочетания. Типичными примерами являются элементы управления Calendar и DataGrid. Страница ASP.NET состоит из некоторого количества серверных элементов управ- ления, литерально заданного текста, разметки и изображений. Важные данные, кото- рые составляют состояние страницы и ее элементов управления, хранятся в скрытых полях и образуют контекст запроса страницы. Соответствие между экземпляром страницы и ее состоянием является однозначным, программным образом модифици- ровано быть не может, а управляет им исполняющая среда ASP.NET. Знакомство с платформой ASP.NET имеет смысл начать с компонентной модели ASP.NET. С нею вы будете иметь дело на протяжении всего цикла разработки, от про- ектирования страниц до конфигурирования исполняющей среды (рис. 1-2). Компонентная модель ASP.NET Рис. 1-2. Общая схема стека разработки ASP.NET (стрелка указывает направление от пользовательского интерфейса к системным сервисам)
Модель программирования ASP.NET Глава 1 7 Прежде чем перейти к подробному описанию представленных на рис. 1-2 элемен- тов, мы рассмотрим основные принципы работы протокола HTTP, в соответствии с которым осуществляется взаимодействие компонентов приложения, а после этого по- говорим о структуре страницы ASP.NET и о том, как на данной платформе создаются и развертываются приложения. Протокол HTTP В этом разделе приводится краткий обзор базовых принципов функционирования Web-приложений. Если вы уже знакомы с основами работы Web, то можете пропу- стить его и сразу переходить к следующему разделу. Аббревиатура HTTP настолько хорошо всем знакома и мы так к ней привыкли, что многие из нас уже не помнят, что она означает. Поэтому прежде всего напомню: HTTP является сокращенным названием протокола передачи гипертекста — Hyper- text Transfer Protocol. Это текстовый протокол, который определяет, как взаимо- действуют между собой браузеры и серверы Web. Формат пакета HTTP описан в документе RFC 2068, который можно загрузить по адресу http://www.w3.org/Protocols/ rfc2068/rfc2068.txt. Пакеты HTTP передаются через соединение TCP (Transmission Control Protocol — протокол управления передачей), а местом их назначения является стандартный порт 80 компьютера, расположенного по целевому IP-адресу (IP, Internet Protocol — протокол Интернета). Запрос HTTP Когда вы задаете URL в адресной строке браузера, последний, используя систему доменных имен (Domain Name System, DNS), транслирует указанное в составе URL имя сервера в его IP-адрес. Браузер открывает сокет и соединяется с портом 80, расположенным по данному адресу. Ниже приведен пример простейшего запроса на получение страницы, URL которой является http://www.contoso.com/default.aspx: GET /default.aspx НТТР/1.1 Host: www.contoso.com Первая строка текста запроса — это так называемая стартовая строка (start line). Она должна содержать имя подлежащей выполнению команды HTTP (в данном случае GET), URL ресурса и номер версии протокола HTTP, которую решил исполь- зовать клиент. Запрос HTTP может содержать (и обычно содержит) несколько заголовков. Заго- ловком HTTP называется текстовая строка, в которой хранится дополнительная ин- формация запроса. В приведенном выше примере заголовком является строка, на- чинающаяся с Host:. Типичные заголовки HTTP-запросов перечислены ниже. User-Agent — идентифицирует тип браузера, направившего запрос. Connection — указывает, как серверу следует поступить с подключением: закрыть его или оставить активным. If-Modified-Since — используется для проверки клиентом того, является ли дей- ствительным содержимое кэша. Наиболее распространенными командами HTTP, также называемыми глаголами HTTP, являются GET и POST. Команда GET позволяет извлечь информацию, иденти- фицируемую URL запроса, а команда POST— передать включенный в запрос контент сёрверу для обработки. Обычно глагол POST используется для отправки серверу определенного блока данных, например информации, которую пользователь ввел в размещенную на Web-странице форму.
8 Часть I Разработка страниц ASP.NET Ответ HTTP Ответ сервера содержит строку состояния, где указывается номер версии протокола и код возврата (успех/неудача выполнения запроса). За строкой состояния следу- ет группа заголовков (они идентифицируют тип контента, определяют его длину, содержат другую информацию), а за ними — тело (контент) ответа, отделенное пустой строкой. Рассмотрим пример простейшего HTML-вывода, возвращенного сервером, у которого клиент запросил страницу ASP.NET: НТТР/1.1 200 ОК Server: Microsoft-IIS/5.0 Content-Type: text/html Content-Length: 51 <htmlxbody><h1>ASP. NET is cool! </h1></body></html> Следует понимать, что запросы и ответы — это текстовые строки, отформатиро- ванные в соответствии с правилами HTTP, которые передаются через соединение TCP. Код возврата 200, который вы видите в первой строке приведенного выше от- вета, означает, что запрос выполнен успешно. Сервер обработал его и вернул контент определенной длины — 51, имеющий заданный MIME-тип — text/html. (Аббревиатура MIME расшифровывается как Multipurpose Internet Mail Extensions — многоцелевые расширения электронной почты в сети Интернет.) Допустимые коды ответов HTTP перечислены в спецификации этого протокола. Пустая строка, отделяющая контент от последнего заголовка, вставлена не для удобства чтения: это обязательная пара символов возврат каретки-перевод строки, присутствия которой в данном месте от- вета требует стандарт HTTP. Итак, браузер получает ответ сервера, имеющий описанную структуру. Что про- исходит дальше, зависит от MIME-типа контента ответа и возможностей браузера. Например, контент типа text/html все браузеры выводят одинаково — как HTML, а вот с контентом типа text/xml поступают по-разному: одни выводят его в виде обычного текста, тогда как другие (например, Microsoft Internet Explorer 6.0) применяют к нему перед выводом встроенные таблицы стилей. Построение серверного слоя абстрагирования Любое взаимодействие между браузерами и серверами Web заключается в обмене пакетами, подобными только что рассмотренным. Если заданный в запросе URL указывает на HTML-страницу, сервер обычно считывает содержимое ее файла и просто переписывает его в тело ответа. Если же указана страница ASP.NET, в работу включается особый модуль — ISAPI-расширение IIS. ISAPI-расширением называется библиотека динамической компоновки (DLL), за- регистрированная в IIS как обработчик запросов ресурсов с заданным расширением имени файла. Получив запрос ресурса с расширением .aspx, IIS вызывает соответству- ющее ему ISAPI-расширение. Оно анализирует запрос и конфигурирует серверную среду, которая будет формировать ответ для браузера. После извлечения связанных с запросом данных состояния и выполнения ряда других необходимых операций активизируется .aspx-страница, которая и формирует HTML-вывод. Возврат формы Единственным элементом клиентской страницы, который может передавать данные серверу, является HTML-тэг <form>. Когда пользователь щелкает расположенную в форме кнопку типа submit, браузер собирает содержимое всех элементов управления этой формы в одну строку и передает ее серверу в составе команды GET или POST.
Модель программирования ASP.NET Глава 1 9 Ниже приведено определение простейшей HTML-формы, содержащей текстовое поле и кнопку типа submit: <form method-"post” action="default.aspx”> <input type="text" name="EmpCode" /> <input type="submit" value="Send" /> </form> Здесь указано, что введенные в форму данные должны передаваться серверу с по- мощью команды POST, и задан целевой URL соответствующего запроса — defa.ult.spx. Ниже продемонстрирован запрос с командой POST, который будет сгенерирован, когда пользователь щелкнет submit-кнопку: POST /default aspx НТТР/1.1 Host: www.contoso.com Content-Type: application/x-www-form-urlencoded Content-Length: 12 EmpCode=1001 Обрабатывая этот запрос, расширение ISAPI проанализирует его тело и представит содержащуюся в запросе информацию с использованием дружественной к програм- мисту объектной модели. Переменная EmpCode будет представлена не строкой имя- значение, а элементом коллекции Request.Form, определенной на уровне приложения. Это первый уровень абстракции, надстроенный поверх программной модели HTTP Такие объекты, как Request, Response и Server, образуют HTTP-контекст запроса. Они поддерживаются большинством платформ разработки Web-приложений, включая JSP и ASP. Однако в ASP.NET существует и ряд других полезных системных объектов. Структура файла страницы ASP.NET Страница ASP.NET представляет собой серверный текстовый файл с расширением .aspx. Он имеет модульную структуру и состоит из трех разделов: директив страницы, ее кода и пользовательского интерфейса. Директивы страницы Раздел предназначен для конфигурирования окруже- ния, в котором будет выполняться страница, и определения способа обработки страницы исполняющей средой ASP.NET. Кроме того, эти директивы позволяют сделать относительно страницы определенные предположения. С их помощью можно импортировать пространства имен, необходимые для программного кода, загрузить сборки, которые в данный момент отсутствуют в глобальном кэше сборок (Global Assembly Cache, GAC), а также зарегистрировать новые элементы управ- ления с пользовательскими именами тэгов и префиксами пространств имен. Программный код В данном разделе размещаются обработчики событий страницы и ее элементов управления, а также вспомогательные подпрограммы. Однако не обязательно, чтобы связанный со страницей программный код находился непо- средственно в ее файле — он может храниться и отдельно, в так называемом файле отделенного кода страницы. Если код находится в основном файле страницы, то он должен содержаться в тэге с именем <script>. (Данное имя, которое переводится как «сценарий», не совсем точно отражает назначение тэга, оно было выбрано из соображений обратной совместимости.) Серверные тэги <script> отличаются от одноименных клиентских тэгов наличием атрибута runat^server, о котором я рас- скажу далее в этой главе. Перед выполнением код страницы обязательно компи- лируется. Обычно это делается при первом обращении к данной странице. Однако в ASP.NET 2.0 весь код приложения может быть предкомпилирован и развернут в виде двоичных сборок.
10 Часть I Разработка страниц ASP.NET Интерфейс страницы В этом разделе файла определяется структура страницы. Здесь содержатся серверные элементы управления, литеральный текст и тэги HTML. Для управления пользовательским интерфейсом, генерируемым сервер- ными элементами управления, могут использоваться их атрибуты и свойства. Определять все разделы не обязательно — страница, содержащая только раздел кода или только раздел пользовательского интерфейса, будет вполне функциональной, а в некоторых особых случаях она может состоять из одной-единственной директивы. Однако реальные страницы обычно включают все три раздела. В главах 2 и 3 мы сможем подробнее рассмотреть структуру и возможности страниц ASP.NET и изучить их строительные блоки. Демонстрационная страница ASP.NET Пришло время показать вам, как выглядит страница ASP.NET. Простейшие страницы можно создавать в обыкновенном текстовом редакторе, поэтому откройте Notepad, и пусть спящий великан (я имею в виду Microsoft Visual Studio .NET) пока отдыхает. Приведенный ниже текст составляет простую страницу ASP.NET, которая предостав- ляет пользователю возможность ввести одно или несколько слов и одним щелчком кнопки перевести их в верхний регистр. Чтобы не усложнять пример, я включил весь необходимый программный код прямо в файл страницы. (Как вы узнаете поз- же, в реальных приложениях код страниц обычно содержится в отдельных файлах.) <!— Раздел директив —> <% ©Page Language="C#" %> <!-- Раздел кода —> <script runat="server > private void MakeUpper(object sender, EventArgs e) { string but = TheString.Value; TheResult InnerText = buf.ToUpper(); 1 </script> <!— Раздел пользовательского интерфейса страницы --> <html> <head><title>Pro ASP.NET (Ch 01)</title></head> <body> <h1>Make It Upper</h1> <form runat=”server"> <input runat="server" id= TheString" type="text” /> <input runat="server" id="Button1" type="submit" value="Proceed..." OnServerClick="MakeUpper" /> <hr> <h3>Results:</h3> <span runat="server" id="TheResult" /> </form> </body> </html> Пустыми строками и комментариями я отделил друг от друга три раздела файла страницы: директивы, код и интерфейсную часть. Обратите внимание на то обстоя- тельство, что многие тэги страницы содержат атрибут runat. Это один из ключевых элементов страницы ASP.NET В следующем разделе о нем будет рассказано под-
Модель программирования ASP.NET Глава 1 11 робнее, а пока вам достаточно знать, что благодаря данному атрибуту при обработке файла страницы тэг превращается в экземпляр серверного компонента. Раздел пользовательского интерфейса страницы состоит из литералов и тэгов HTML, многие из которых содержат упомянутый атрибут runat. Тэги, помеченные данным атрибутом, не являются HTML-элементами. Это представления серверных компонентов — элементов управления ASP.NET, отвечающих за формирование отправ- ляемой браузеру разметки. Тэги исходного файла ASP.NET, помеченные атрибутом runat, не выводятся в формируемый выходной поток, а проходят сложный процесс трансформации, в результате которого генерируется разметка. За создание экзем- пляров элементов управления, соответствующих тэгам исходного файла страницы, отвечает исполняющая среда ASP.NET. Краткий обзор кода страницы Благодаря атрибуту runat при обработке исходного файла страницы на сервере текстовое поле ввода превращается в экземпляр класса HtmllnputControl. Свойство HtmllnputControl этого класса определяет текст, который по умолчанию будет содер- жаться в поле. Когда пользователь щелкает submit- кнопку, страница автоматически выполняет возврат данных формы самой себе. Чтобы так происходило, необходимо установить атрибут runat тэга <form>. После возврата формы страницей значение поля ввода прочитывается сервером и автоматически присваивается свойству Value новосозданного экземпляра класса HtmllnputControl. Затем выполняется код, связан- ный с событием OnSeruerClick. Этот код принимает текущее содержимое текстового поля (возвращенную страницей строку) и переводит ее символы в верхний регистр. Далее эта строка присваивается свойству InnerText серверного элемента управления, связанного с HTML-тэгом <span>. Когда завершается выполнение обработчика со- бытия MakeUpper, страница готова для рендеринга — останется лишь отправить об- новленный HTML-код браузеру. Для того чтобы протестировать созданную в данном примере страницу, скопируйте ее .aspx-файл в корневой каталог Web-сервера. Обычно это c:\inetpub\wwwroot. При желании вы можете создать произвольный виртуальный каталог. Присвойте странице имя hello.aspx, откройте ее в браузере и щелкните содержащуюся в ней кнопку. Что вы увидите, показано на рис. 1-3. Рис. 1-3. Наша первая (совсем простая) страница ASP.NET в действии
12 Часть I Разработка страниц ASP.NET Прежде чем переводить текст в верхний регистр, взгляните на HTML-код исходной страницы, отображенной в браузере. Вот что вы увидите: <!-- Раздел директив --> <!— Раздел кода —> <!-- Раздел пользовательского интерфейса страницы --> <html> <headxtitle>Pro ASP.NET (Ch 01)</title></head> <body> <h1>Make It Upper</h1> <form method="post" action="hello.aspx" id="Form1"> <div> <input type="hidden" name="_EVENTTARGET" value="" /> <input type="hidden name=”__EVENTARGUMENT'’ value="" /> <input type="hidden" name="_VIEWSTATE" value="/wEPDwUJNzM4N...==" /> </div> <sc ript type="text/j avasc ript"> el- ver theForm = document.forms['Form1']; if (ItheForm) { theForm = document Forml; } function __doPostBack(eventTarget, eventArgument) { if (ItheForm.onsubmit || (theForm.onsubmit() != false)) { theForm. „EVENTTARGET, value = eventTarget; theForm.__EVENTARGUMENT.value = eventArgument; theForm. submitO; } } // -> </script> <input name="TheString" type="text" id=”TheString" value="Hello, world" /> cinput name="Button1" type="submit" id="Button1" value="Proceed .." /> <hr> <h3>Results &nbsp;</h3><span id="TheResult"x/span> </form> </body> </html> Как видите, в состав тэга <form> включен жестко закодированный атрибут action, обеспечивающий возврат формы той же странице. Он помещается сюда автоматиче- ски и играет в ASP.NET очень важную роль. Скрытые поля, которые содержатся в приведенном листинге, также генерируются автоматически — они необходимы для работы механизма возврата формы. Сказанное касается и встроенного кода сценария. Тэги <input> почти идентичны своим аналогам из исходного файла .aspx, из них лишь исчез атрибут runat. Ну а теперь давайте рассмотрим те программные слои ASP.NET, которые обеспе- чивают функционирование страниц в составе приложений. Компонентная модель ASP.NET ASP.NET является базовой технологией всей связанной с Web функциональности .NET Framework. Сама .NET Framework представляет собой иерархию классов, охва-
Модель программирования ASP.NET Глава 1 13 тывающих все аспекты программирования для операционной системы Windows. Web- приложение состоит из страниц, которые пользователь запрашивает у сервера, а сервер обрабатывает, чтобы вернуть код разметки, главным образом состоящий из HTML- кода. Способ обработки запрошенного ресурса и генерирования разметки зависит от конкретного сервера. Если ресурс имеет расширение .aspx, IIS поручает его обработку исполняющей среде ASP.NET. Исполняющая среда ASP.NET преобразует исходный код запрошенной страницы .aspx в «живой» экземпляр класса .NET Framework, наследующего базовый класс с именем Page. Выполняющаяся страница ASP.NET — это объект; объектами являются и все ее компоненты, то есть серверные элементы управления. Многие новые функции ASP.NET стали явными или неявными дополнениями инфраструктуры .NET. В ASP.NET используются такие функции этой платформы, как межъязыковая интеграция, обработка исключений, сборка мусора, защита на уровне доступа к коду, ее технологии развертывания и конфигурирования, а также ее невероятно обширная библиотека классов Все это доступно нам благодаря тому, что приложения ASPNET являются особой разновидностью приложений .NET. Модель взаимодействия компонентов Любому элементу страницы ASPNET, помеченному атрибутом runat, можно присво- ить уникальный идентификатор, который позволяет обращаться к этому элементу из серверного кода. Доступ к элементам по идентификаторам естественен для клиента (именно так работают страницы с динамическим HTML), но для серверных при- ложений это нечто совершенно новое. Реализация такого революционного подхода стала возможной благодаря двум факторам компонентной архитектуре платформы .NET, составной частью которой является ASPNET; встроенному в ASPNET механизму управления состоянием приложения. Компонентная архитектура самой .NET облегчает взаимодействие компонентов приложений и делает его простым и эффективным, что распространяется и на прило- жения ASPNET Компоненты ASP.NET обращаются к элементам страницы, вызывают методы и устанавливают свойства друг друга. Все элементы страницы являются полноценными компонентами, а не просто ана- лизируемым текстом. Такая модель страницы обладает исключительной мощью и гибкостью. Для создания нового элемента управления достаточно создать новый класс, производный от одного из существующих. А для построения иерархии на- следования страницы достаточно задать для нее родительский класс, отличный от базового класса Page. О Внимание! Если каждому элементу управления страницы ASP.NET явно не присвоить уникальный идентификатор, Visual Studio .NET 2005 вернет ошибку времени разработки, что, однако, не помешает успешному выполнению этой страницы. Атрибут runat Атрибут runat определяет, как должен обрабатываться элемент страницы: выводиться как есть на этапе рендеринга или преобразовываться в экземпляр определенного клас- са .NET, сохраняющий свое состояние. Такой класс сам отвечает за вывод необходимой разметки. Все элементы разметки, атрибуту runat которых присвоено значение server, интерпретируются как серверные элементы управления. Классы таких элементов управления имеют свойства и методы, позволяющие конфигурировать их состояние. Во время подготовки отправляемого браузеру ответа элемент управления генерирует
14 Часть I Разработка страниц ASP.NET необходимый HTML-код. Рассмотрим следующую простую команду, которая выводит в составе клиентской страницы якорный элемент: Response.Write(”<A id=myAnchor href=www.asp.net>Click me</A>") В данном случае якорный элемент создается программным способом, а не опреде- ляется в разметке страницы. В классической ASP блоки кода и метод Response. Write яв- ляются единственными средствами, с помощью которых можно динамически создавать и конфигурировать элементы управления. В некоторых средах разработки, например в Microsoft Visual InterDev, поддерживаются элементы управления времени разработки, позволяющие выводить динамически генерируемый HTML код объектным способом Однако они предназначены только для генерирования разметки и кода сценариев во время разработки (о чем и говорит их название). В ASP.NET появилось совершенно новое семейство элементов управления, которые можно назвать элементами управ- ления времени выполнения, подчеркнув тем самым их принципиальное отличие от элементов управления времени разработки. Работа с серверными элементами управления При создании страницы ASP у вас нет возможности программно управлять элементом myAnchor. На сервере это фиксированный, безжизненный текст, годящийся лишь для отправки браузеру, и только на клиенте он оживает и может выполнять инструкции сценария. Предположим, что вам нужно установить атрибут href якоря с учетом условий времени выполнения. В классической ASP для этого сначала пришлось бы получить или сформировать значение, предназначенное для присвоения атрибуту, азатем вызвать метод Response.Write: strHref = "www.asp.net" strHtml = "<A id=myAnchor " strHtml = strHtml + "href=" + strHref strHtml = strHtml + ">Click me</A>” Response.Write(st rHtml) Приведенный код будет работать и в составе страницы ASP.NET, но в данном случае это не лучшее решение. Объявив тэг <А> с атрибутом runat, якорный элемент можно оживить прямо на сервере: <А runat=”server" id="myAnchor">Click me</A> Загрузив запрошенную браузером страницу, исполняющая среда ASP.NET произво- дит разбор ее исходного кода и создает экземпляры всех элементов управления, поме- ченных атрибутом runat. В пределах страницы экземпляр серверного элемента управле- ния, созданного для тэга <А>, идентифицируется именем myAnchor. программной установки атрибута href после загрузки страницы может использоваться такой код: <script runat="server” language="C#"> void Page_Load(object sender, EventArgs e) { myAnchor.HRef = "http://www.asp.net"; 1 </script> Элементам разметки из файла .aspx, имена которых совпадают с именами элемен- тов HTML, ставятся в соответствие серверные элементы управления. Однако не у всех тэгов HTML имеются аналоги среди серверных элементов управления ASP.NET. Для тех тэгов, которым специальные элементы управления не сопоставлены, используется универсальный элемент управления Список тэгов и соответствующих им элементов управления жестко закодирован в исполняющей среде ASP.NET. Элементам разметки,
Модель программирования ASP.NET Глава 1 15 принадлежащим к пространству имен <asp>, сопоставляются серверные элементы управления, а остальным ставятся в соответствие сборка и класс, объявленные с ис- пользованием директивы ©Register. Тэги уровня страницы Атрибут runat может использоваться в таких тэгах уровня страницы, как <head> и <body>. Эти тэги представлены экземплярами класса HtmlGenericControl. HtmlGenericControl — это класс .NET, который ставится в соответствие серверным тэгам HTML, не имеющим представлений среди специализированных классов .NET Fra- mework. Список таких тэгов включает, в частности, <span>, <font> и <iframe>. При загрузке следующей страницы программно устанавливается цвет ее фона: <%@ Page Language="C#” %> <script runat=”server"> private void Page_Load(object sender, EventArgs e) TheBody.Style[HtmlTextWriterStyle.BackgroundColor] = "lightblue": } </script> <html> <body id="TheBody" runat="server"> <h3>The background color of this page has been set programmatically. Open View|Source menu to see the source code.</h3> </body> </html> The resulting HTML code is as follows: <html> <head><title>Pro ASP.NET (Ch 01)</title></head> <body id="TheBody" style="background-color:lightblue;”> <form method="post" action="Body.aspx” id=”Form1"> <div> <input type="hidden" name="__VIEWSTATE" value="/wEPD... RVC+" /> </div> <h3>The background color of this page has been set programmatically. Open View|Source menu to see the source code.</h3> </form> </body> </html> Аналогичным образом можно установить любые атрибуты тэга <body>, скажем, выбрать программным способом таблицу стилей или фоновое изображение. Для установки внутреннего текста тэга используется свойство InnerText, а для создания атрибутов тэга — коллекция Attributes класса HtmlGenericControl'. TheBody.Attributes["Background"] = "/proaspnet20/images/body.gif"; Подробнее о программном интерфейсе класса HtmlGenericControl мы поговорим в главе 4. Примечание В ASP.NET 2.0 содержимое тэга <head> доступно программно лишь при условии, что он помечен атрибутом runat. У класса Раде имеется набор специализиро- ванных свойств и методов, которые будут рассмотрены в главе 3. Неизвестные тэги Встретив неизвестный тэг, то есть такой, который не является предопределенным в текущей схеме или определенным пользователем, исполняющая среда ASP.NET
16 Часть! Разработка страниц ASP.NET может действовать одним из двух способов. Если тэг не содержит информации о про- странстве имен, ASP.NET интерпретирует его как универсальный элемент управления HTML. Иными словами, отсутствие указания на пространство имен рассматривается как указание на пространство HTML, и исполняющая среда ASP.NET считает тэг HTML-элементом. Она не выбрасывает исключение, а просто генерирует текст раз- метки. В качестве примера рассмотрим такую страницу ASP.NET: <%@ Page Language="C#” %> <script runat=”server"> void Page_Load(object sender, EventArgs e) { __ dinoe.Attributes["FavoriteFood"] = "T-bone steak"; } </script> <html> <head><title>Pro ASP.NET (Ch 01)</title></head> <body> <form runat="server"> <Person id="dinoe" runat="server" /> Click the <b>View|Source</b> menu item.. </form> </body> </html> Тэг <Person> обрабатывается так, словно это обычный HTML-тэг, и при выполне- нии сценария к нему добавляется атрибут FavoriteFood. Каким получается результи- рующий HTML-код страницы, показано на рис. 1-4. В приведенном примере типом объекта dinoe является HtmlGenericControl. Рис. 1-4. ASP.NET обрабатывает пользовательские тэги, для которых не указано пространство имен, ставя их в соответствие классу HtmlGenericControl Однако если тэг содержит информацию о пространстве имен, он принимается лишь при условии, что этим пространством является <asp:> либо пространство, явно ассоциированное с именем тэга посредством директивы ©Register. Когда явно заданное пространство имен системе неизвестно, генерируется ошибка компиляции.
Модель программирования ASP.NET Глава 1 17 Серверные элементы управления ASP.NET Все серверные элементы управления ASP.NET делятся на два семейства: элементы управления HTML и элементы управления Web. Элементы, относящиеся к первому семейству, определены в пространстве имен System.Web.UI.HtmlControls, а относящиеся ко второму — в пространстве имен System.Web.UI.WebControls. Серверные элементы управления HTML Серверные элементы управления HTML — это классы, представляющие стандартные HTML-тэги, поддерживаемые большинством браузеров. Набор свойств такого элемен- та управления соответствует набору атрибутов представляемого им тэга. У серверного элемента управления HTML имеются простые свойства (InnerText, InnerHtml, Style и Value) и свойства-коллекции (Attributes'). Экземпляр серверного элемента управления автоматически создается исполняющей средой ASP.NET, когда она встречает в обра- батываемом исходном файле страницы тэг, помеченный атрибутом runat' "server". Как уже упоминалось, набор серверных элементов управления не охватывает всего набора HTML-тэгов каждой конкретной версии схемы HTML. В пространстве имен System.Web.UI.HtmlControls представлены лишь наиболее часто используемые тэги, а такие тэги, как <iframe>, <frameset>, <body>, <hn>, <fieldset>, <marquee> и <pre>, не имеют специализированных серверных представлений. Однако то обстоятельство, что набор специальных серверных элементов управле- ния неполон, не ограничивает ваших программистских возможностей, когда речь идет об использовании и конфигурировании тэгов на сервере. Вам нужно лишь использо- вать более универсальный интерфейс программирования — класс HtmlGenericControl, о котором уже упоминалось в этом разделе Серверные элементы управления Web Серверные элементы управления Web обладают более широкими возможностями, чем серверные элементы управления HTML. К их числу относятся не только тексто- вые поля и кнопки, но и такие специализированные элементы, как календарь, баннер со сменяющейся рекламой, раскрывающийся список, древовидное представление данных и таблица данных. Кроме того, среди Web-элементов есть компоненты, очень похожие на некоторые серверные элементы управления HTML. Однако Web-элементы более абстрактны, чем соответствующие им HTML-элементы, поскольку их объект- ная модель не обязательно должна соответствовать синтаксису HTML Для примера можно сравнить серверный текстовый элемент управления HTML с серверным Web- элементом TextBox HTML-элемент имеет следующую разметку: cinput runat="server" id="FirstName" type="text" value="Dino” /> А разметка Web-элемента TextBox такова: <asp:textbox runat="server" id="FirstName" text="Dino" /> Оба элемента управления генерируют одинаковый код разметки, отправляемой браузеру. Однако программный интерфейс серверного текстового элемента HTML максимально приближен к интерфейсу HTML-тэга <input>, тогда как методы и свой- ства Web-элемента TextBox имеют более абстрактные имена. Так, для задания контента серверного текстового элемента HTML используется свойство Value, соответствующее одноименному атрибуту HTML, тогда как у элемента управления TextBox для той же цели применяется свойство Text. За некоторыми исключениями (о них я расскажу в главе 3) выбор между серверными элементами управления типа Web и HTML за- висит от ваших предпочтений, а также от того, как вам удобнее разрабатывать и со- провождать страницы.
18 Часть I Разработка страниц ASP.NET Стек разработки ASP.NET Если говорить на самом высоком уровне абстракции, разработка любого приложения ASP.NET осуществляется в два этапа: создание страниц и конфигурирование среды их выполнения. Сначала вы создаете страницы приложения, реализуете требования пользователей, а затем настраиваете окружение, в котором оно будет функциониро- вать, чтобы предоставление страниц осуществлялось максимально эффективным и безопасным способом. Как видно из рис. 1-2, компонентная модель ASP.NET является основой всех приложений ASP.NET и их строительных блоков. Руководствуясь этим рисунком, мы проанализируем разные логические уровни, чтобы разобраться, что и для чего они содержат. Уровень представления Страница ASP.NET состоит из элементов управления, текста и разметки. Когда ее исходный код преобразуется в функционирующий экземпляр класса страницы, ис- полняющая среда ASP.NET перестает различать эти три составляющие — для нее все они являются элементами управления, в том числе литеральный текст и символы воз- врата каретки. Во время выполнения страница ASP.NET является просто иерархией элементов управления. Элементы управления с богатым интерфейсом Богатство интерфейсов программирования ASP.NET обеспечивается наличием боль- шой библиотеки серверных элементов управления, выполняющих базовые задачи HTML-взаимодействия — от простейших, таких как ввод текста с использованием стандартных тэгов, до довольно сложных, скажем, отображения данных в виде табли- цы. Встроенный набор элементов управления настолько обширен, что удовлетворяет любым требованиям. Кроме того, в последней версии ASP.NET появились новые элементы управления с богатым интерфейсом (rich controls), называемые также эле- ментами rich-типа, которые позволяют существенно повысить производительность труда разработчиков страниц. В ASP.NET 2.0 вы найдете элементы управления для создания Web-мастерор и представлений иерархических данных со свертываемыми ветвями, типичные фор- мы, элементы для декларативного связывания данных, меню, средства навигации по сайту. Есть здесь даже мини-API для создания сайтов портального типа. Применение элементов управления rich-типа позволяет сократить время разработки и уменьшить количество ошибок, без усилий внедрить передовой опыт сообщества разработчиков и предоставить более гибкие и эффективные средства работы конечному пользователю. О таких элементах управления мы детально поговорим в главах 4, 6 и 10. Специализированные элементы управления Набор встроенных элементов управления ASP.NET достаточно обширен и разно- образен, чтобы с его помощью можно было реализовать любую необходимую функ- циональность Web-приложения. В их основе лежит компонентная модель ASP.NET, существенно упрощающая применение общих принципов объектно-ориентированного программирования. Функции существующих элементов управления можно расширять. Можно также создавать новые элементы управления путем объединения нескольких существующих элементов — такие составные элементы управления называются пользовательскими (user). Кроме того, в ASP.NET 1.x определен небольшой набор базовых классов, на основе которых можно создавать совершенно новые, так называемые специализиро- ванные (custom) элементы управления. В ASP.NET 2.0 этот набор классов расширен,
Модель программирования ASP.NET Глава 1 19 в частности с целью упростить разработку новых элементов управления, связанных с данными Адаптивный рендеринг В ASP.NET 2.0 реализована новая, так называемая адаптерная архитектура элементов управления, позволяющая одному и тому же элементу по-разному осуществлять свой рендеринг в зависимости от типа целевого браузера. Заметьте, однако, что новая адаптерная модель ASP.NET 2.0 не используется в мобильных элементах управления. Последние составляют особое семейство, раз- работанное специально для создания приложений, предназначенных для мобильных устройств. Такие элементы являются производными от MobileControl и размещаются на страницах, наследующих MobilePage. В мобильных элементах управления по-преж- нему используется старая адаптерная модель, разработанная специально для них и введенная еще в ASP.NET 1.1. Таким образом, в отношении создания приложений для мобильных устройств в ASP.NET 2.0 ничего не изменилось — для этой цели ис- пользуются те же мобильные элементы управления, что и в ASP.NET 1.1. Так в чем же суть новой адаптерной модели ASP.NET 2.0? Данная модель адаптив- ного рендеринга позволяет вам писать собственные адаптеры, настраивая серверные элементы управления для использования с определенными браузерами. Например, можно написать адаптер, генерирующий для элемента управления Calendar особую HTML-разметку, предназначенную для вывода конкретным целевым браузером. Внутренняя организация страницы и процесс ее выполнения Любая страница ASP.NET является экземпляром класса, наследующего класс Page. Класс страницы — это конечная точка конвейера модулей, через который проходит запрос HTTP в процессе его обработки. Различные системные компоненты, работа- ющие над исходным запросом, шаг за шагом формируют информацию, необходимую для поиска объекта страницы, который сгенерирует результирующую разметку, пред- назначенную для отправки браузеру. Объектная модель страницы поддерживает ряд функций и возможностей, каждая из которых относится к одной из следующих базовых категорий: события, сценарии, персонализация, стилевое оформление, прототипы. События страницы Жизненный цикл страницы в исполняющей среде ASP.NET отмечен серией событий. Путем написания кода обработки этих событий разработчики могут динамически модифицировать вывод страницы и состояние составляющих ее элементов управле- ния. В ASP.NET 1.x страница генерирует такие события, как Init, Load, Pre Render и Unload, соответствующие ключевым моментам ее жизненного цикла. В ASP.NET 2.0 добавлен ряд новых событий, позволяющих точнее отслеживать процесс обработки запроса. В частности, введены новые события, сигнализирующие о начале и окончании фаз инициализации и загрузки страницы. Жизненный цикл страницы ASP.NET мы детально проанализируем в главе 3. Сценарии страницы Объектная модель сценариев страницы позволяет разработчику управлять кодом сценариев и скрытыми полями, вставляемыми в клиентские страницы. Эта объект- ная модель генерирует код на языке JavaScript, с помощью которого связываются между собой HTML-элементы, генерируемые серверными элементами управления. Таким способом программируются функции, которые на сервере иначе реализовать невозможно. Например, используя данный подход, вы можете запрограммировать
20 Часть I Разработка страниц ASP.NET страницу таким образом, чтобы при ее отображении в браузере фокус ввода получил заданный вами элемент. Страницы ASP.NET имеют такую архитектуру, при которой они способны вызы- вать серверные методы без осуществления полного возврата формы и последующего обновления всей страницы. Данный механизм удаленного вызова сценариев реали- зован на основе механизма обратного вызова, и такое решение дает разработчикам определенные преимущества. Применением метода обратного вызова сценария (script callback) обеспечивается прямая передача функции JavaScript результатов выпол- нения серверного метода, и эта функция может затем обновить пользовательский интерфейс страницы, используя динамический HTML. При таком способе работы передача данных серверу и обратно все равно происходит, но полного обновления страницы удается избежать. Обратный вызов сценария — не единственная из числа давно ожидаемых сообще- ством разработчиков новых технологий ASP.NET 2.0. Еще одной такой технологией является межстраничный постинг (cross-page posting), позволяющий передать контент формы другой странице. Не обучаем ли мы нового пса старым трюкам ? Может быть. Как упомйналось в этой главе, одной из наиболее важных особенностей ASP.NET является то, что каждая страница содержит только один тэг <form>, который осущест- вляет возврат формы той же самой странице. Это решение заложено в архитектуре ASP.NET, и оно имеет свои достоинства. В предыдущих версиях ASP.NET межстраничный постинг можно было реализо- вать тем же способом, что и в классической ASP, — с использованием тэга <form>, не помеченного атрибутом runat, то есть тэга <form> чистого HTML. Данный метод работает прекрасно, но он не вписывается в объектно-ориентированный и строго типизированный мир ASP.NET. Поэтому в ASP.NET 2.0 реализован новый, более со- временный метод межстраничного постинга. Персонализация страницы В ASP.NET 2.0 можно сохранять и извлекать информацию, связанную с конкрет - ным пользователем (скажем, сведения о его предпочтениях), с помощью встроенных средств системы, так что вам для этого больше не придется писать специальный инфраструктурный код. Приложение лишь определяет собственную модель персо- нализированных данных, а остальное делает исполняющая среда ASP.NET, которая анализирует эту модель и генерирует представляющий ее компилируемый класс. Каждый член класса персонализированных данных соответствует элементу инфор- мации, связанной с текущим пользователем. Процессы загрузки и сохранения персо- нализированных данных абсолютно прозрачны для конечного пользователя, и даже автору страницы не требуются особые знания о том, как реализованы эти операции. Ему указанные данные доступны через одно из свойств объекта страницы. Каждая страница может использовать сохраненную ранее информацию и сохранять новую, которая будет доступна при выполнении будущих запросов. Стилевое оформление страницы Подобно темам Microsoft Windows ХР, тема ASP.NET представляет собой комплекс настраиваемых стилей и визуальных атрибутов элементов сайта. Сюда относятся свойства элементов управления, таблицы стилей страницы, изображения и шаблоны страниц. Таким образом, тему можно считать чем-то вроде супер-CSS (CSS, Cascading Style Sheet — каскадная таблица стилей). Она идентифицируется именем и состоит из * Игра слов. В английском языке есть пословица: «Старого пса новым трюкам не научишь».
Модель программирования ASP.NET Глава 1 21 CSS-файлов, изображений и обложек элементов управления. Обложка (skin) элемента управления — это текстовый файл, который содержит используемое по умолчанию объявление данного элемента со значениями его свойств. Если, к примеру, разработ- чик размещает на странице элемент управления DataGrid и не задает его визуальные свойства, при рендеринге этого элемента используются значения свойств, заданные в файле темы. Использование тем — замечательное решение, позволяющее одним махом изменять внешний вид и поведение страниц и, что более важно, делать их единообразными. Прототипы страниц Страницы практически любого современного сайта имеют единообразную структуру и однотипное оформление. На одних сайтах общими элементами структуры страниц являются только заголовок и нижний колонтитул, на других же могут использоваться сложные навигационные меню и разнообразные элементы управления, в которые за- ключен контент сайта. В ASP.NET 1.x разработчикам рекомендуется заключать такие блоки пользовательского интерфейса в пользовательские элементы управления и использовать эти элементы в составе страниц. Однако очевидно, что такая модель подходит лишь для сайтов небольшого размера; если же сайт содержит сотни страниц, то управлять им будет слишком сложно. Для сайтов с богатым контентом подход, основанный на применении пользова- тельских элементов управления, не всегда приемлем, так как сопряжен с рядом про- блем. Во-первых, ссылаясь на пользовательские элементы управления, необходимо дублировать код страниц. Во-вторых, для того чтобы применить новый шаблон, раз- работчику приходится возиться с каждой страницей. А в-третьих, HTML-элементы, которые покрывают всю область контента, часто оказываются разделенными между несколькими пользовательскими элементами управления. В ASP.NET 2.0 задача создания прототипов страниц была решена путем введе- ния концепции эталонной страницы (master page). Разработчики сайтов с большим количеством страниц, имеющих единообразную схему и однотипную функциональ- ность, могут теперь программировать все это в одном эталонном файле, вместо того чтобы добавлять информацию об общей структуре в каждую страницу и при этом распределять соответствующую разметку между несколькими пользовательскими элементами управления. Основываясь на единой эталонной странице, разработчики могут создавать любое количество единообразных страниц контента, просто ссыла- ясь в них на эталонную страницу с помощью специального атрибута. Об эталонных страницах я расскажу в главе 6. Исполняющая среда HTTP Процесс, в результате которого в ответ на Web-запрос генерируется текст для браузера на чистом HTML, в ASP.NET 2.0 не особенно отличается от аналогичного процесса в ASP.NET 1.1. IIS принимает запрос, присваивает ему идентификационный токен и передает этот запрос ISAPI-расширению ASP.NET (aspnet_isapi.dll) — точке входа процедуры обработки запроса, осуществляемой ASP.NET. Это общая схема, а ее детали зависят от версии IIS и используемой модели процесса. Моделью процесса называется последовательность операций, осуществляемых с целью обработки запроса. Когда исполняющая среда ASP.NET функционирует по- верх IIS 5.x, в основе модели процесса лежит отдельный рабочий процесс с именем aspnet_wp.exe, Этот процесс Microsoft Win32 получает управление непосредственно от IIS через ISAPI-расширение ASP.NET. Данному расширению передаются все запросы на получение ресурсов ASP.NET, и оно, в свою очередь, передает их рабочему процессу.
22 Часть I Разработка страниц ASP.NET Рабочий процесс загружает общеязыковую среду (Common Language Runtime, CLR) и запускает конвейер управляемых объектов, которые преобразуют исходный запрос в полнофункциональную страницу, предназначенную для отправки браузеру Модуль aspnetisapi и рабочий процесс реализуют такие системные функции, как повторное использование процессов (process recycling), кэширование выводимых страниц, мониторинг использования памяти и пулинг потоков. Каждое Web-приложение выполняется в отдельном домене приложения (AppDomain), поддерживаемом рабочим процессом. По умолчанию рабочий процесс выполняется от имени учетной записи ASPNET, имеющей очень ограниченные разрешения. Примечание Домен приложения, он же AppDomain, — это специфическая сущность обще- языковой среды .NET. AppDomain обеспечивает изоляцию управляемого кода во время его выполнения, соблюдение для него ограничений защиты и его выгрузку. Домен приложения является своего рода легковесным процессом, в который загружается несколько сборок (для надежного выполнения кода). В рамках одного процесса ЦПУ может выполняться несколько доменов приложений. Также не существует соответствия один-к-одному между доменами приложения и потоками. Одному домену приложения может принадлежать не- сколько потоков, и хотя каждый конкретный поток не связан с каким-то одним доменом приложения, в каждый конкретный момент он выполняется в одном домене. Когда ASP.NET функционирует под управлением IIS 6.0. по умолчанию исполь- зуется иная модель процесса, в которой место aspnet_wp.exe занимает стандартный рабочий процесс IIS 6.0 (w3wp.exe). Он анализирует URL запроса и загружает необ- ходимое расширение ISAPI. В частности, для запросов, связанных с ASP.NET, рабочий процесс загружает aspnet_isapi.dll. В модели процесса IIS 6.0 расширение aspnet_isapi отвечает за загрузку общеязыковой среды и запуск конвейера HTTP. Поступив в HTTP-конвейер ASP.NET, запрос проходит через ряд системных и оп- ределенных пользователем компонентов, осуществляющих его последовательную обработку. Целью выполнения этого процесса является нахождение класса страницы, способного сформировать ответ на запрос, и создание экземпляра данного класса. Раз- работчик приложения может до некоторой степени модифицировать исполняющую среду: изменить список установленных модулей HTTP, внести изменения в конфи- гурационные файлы, определить провайдеры состояния и персонализации, а также другие прикладные сервисы. Системные модули HTTP Модули HTTP — это ASP.NET-аналоги фильтров IS API. Каждый такой модуль пред- ставляет собой класс .NET Framework, реализующий определенный интерфейс. Всеми приложениями ASP.NET наследуется ряд системных модулей HTTP, указанных в файле machine.config. Предустановленные модули реализуют такие функции, как ау- тентификация и авторизация, а также сервисы, связанные с состоянием сеанса Модуль HTTP может выполнять предобработку и постобработку запроса, перехватывать и об- рабатывать системные события, а также события, генерируемые другими модулями. Вы можете писать и регистрировать собственные модули HTTP, интегрируя их в исполняющий конвейер ASP.NET, обрабатывать системные события и генерировать собственные. Кроме того, у вас есть возможность редактировать для каждого приложе- ния в отдельности список используемых по умолчанию модулей HTTP. Сюда можно добавлять пользовательские модули и удалять те, которые вам не требуются. Конфигурация приложения Поведение приложения ASP.NET регулируется множеством параметров, одни из которых задаются на уровне системы, другие зависят от характеристик конкретного
Модель программирования ASP.NET Глава 1 23 приложения. Единый набор системных параметров определяется в файле machine, config. Этот файл содержит используемые по умолчанию и специфические для ком- пьютера значения всех поддерживаемых установок. Машинные установки обычно контролируются системным администратором, и приложениям не предоставляется право доступа к файлу machine.config для записи. Файл machine.config хранится вне Web-пространства приложений и недоступен даже в том случае, когда в результате успешной атаки в систему внедряется злонамеренный код. Большинство значений, хранящихся в файле machine.config, разрешается пере- определять на уровне приложения, для чего нужно создать один или несколько файлов web.config, связанных с конкретным приложением. Главный из них, область действия которого распространяется на все приложение, помещается в корневую папку этого приложения. Он содержит подмножество установок из файла machine.config и соот- ветствует той же самой XML-схеме. Назначение файла web.config — переопределение некоторых установок из числа задаваемых по умолчанию. Однако имейте в виду, что не все установки из файла machine.config можно переопределять в дочерних конфи- гурационных файлах. В частности, информация о модели процесса ASP.NET может быть задана только на уровне компьютера, то есть в файле machine.config. Если приложение содержит дочерние каталоги, для каждого из них можно создать свой файл web.config. Таким образом, конфигурационные файлы образуют иерархию, на вершине которой расположен файл machine.config, ниже — файл web.config из кор- невой папки приложения, а далее — файлы machine.config из его вложенных папок. Установками, заданными ниже по иерархии, переопределяются установки, заданные выше. При этом с помощью специальных тэгов можно не только переопределять значе- ния одноименных параметров, но и добавлять новые параметры или удалять существу- ющие. Набор установок, воздействующих на конкретную страницу, определяется как сумма установок из файла верхнего уровня и изменений, вносимых на низших уровнях иерархии. Если в той или иной папке приложения конфигурационный файл отсутству- ет, для содержащихся в ней страниц применяются установки высших уровней. Прикладные сервисы Примерами важных сервисов, которые предоставляются приложениям исполняю- щей средой ASP.NET, могут служить сервисы аутентификации, управления состо- янием и кэширования. В ASP.NET 2.0 их число увеличилось — появились серви- сы администрирования, управления членством и ролями, сервисы персонализации (рис. 1-5). Большинство прикладных сервисов должны сохранять и восстанавливать данные, используемые приложением для своих внутренних целей. Модель данных и контей- нер для их хранения определяются конкретным сервисом, равно как и технология их сохранения и восстановления. Приложение же принимает все это как данность. Но как быть, если такие ограничения вам не подходят? Возможность настраивать конфигурацию времени выполнения посредством устано- вок, задаваемых в файлах machine.config и web.config, придает вашему коду известную гибкость. Однако этот механизм не обеспечивает возможности полноценной настройки сервисов. Поэтому в ASP.NET 2.0 интегрирована новая программная модель, добав- ляющая в общую систему классов шаблоны для создания пользовательских сервисов. Первоначально эта модель — модель провайдеров — была разработана для использова- ния в нескольких образцах приложений ASP.NET, так называемых Starter Kits. Она определяет общий API компонентов различного назначения, именуемых провайдерами тех или иных функций. Этот API позволяет разработчику управлять поведением про- вайдера и выбирать подходящую схему данных и хранилище информации.
24 Часть I Разработка страниц ASP.NET 1 Компонентная модель ASP.NET Декларативное связывание с данными Иерархические и табличные представления Меню и типичные формы Web Parts Сложные элементы управления и слой представления Разработка Эталонные страницы Темы и обложки Адаптивный рендеринг Объектная модель страницы Структура страницы Приложение Система Управление состоянием Персонализация Аутентификация Членство Конфигурация времени выполнения Прикладные сервисы Модель провайдеров Рис. 1-5. Более детальная схема стека разработки ASP.NET (стрелка указывает направление от пользовательского интерфейса к системным сервисам) Внимание! Модель провайдеров является одним из ключевых архитектурных элементов ASP.NET, и поэтому для эффективной разработки приложений важно понимать базовые принципы, положенные в ее основу. Данная модель существовала в ASP.NET и раньше, но при создании ASP.NET 2.0 была формализована и полностью отделена от платформы и среды исполнения, так что теперь ее можно использовать в любом приложении .NET, а не только в ASP.NET. Модель провайдеров ASP.NET В основу модели провайдеров ASP.NET положена известная архитектурная концеп- ция — модель, которую называют «стратегия». Под стратегией в данном случае по- нимается необходимая приложению функциональность (скажем, сортировка данных), которая может быть реализована с использованием различных алгоритмов (напри- мер, быстрой сортировки или сортировки слиянием). Каждое приложение выбирает алгоритм, который максимально соответствует его нуждам, и для управления им использует стандартный API, общий для всех алгоритмов, реализующих данную функциональность. Важной особенностью модели «стратегия» является то, что она дает объекту или целой подсистеме возможность открыть свою внутреннюю организацию таким об разом, чтобы клиент мог отключить используемую по умолчанию реализацию той или иной функции и подключить другую ее реализацию, в том числе собственную. Концепция стратегии реализована также в модели провайдеров ASP.NET.
Модель программирования ASP.NET Глава 1 25 Для чего введена модель провайдеров Для конечного пользователя приложения применение мидели провайдеров является абсолютно прозрачным; сам по себе этот факт не означает, что у приложения будет бо- лее богатый контент или что оно будет работать быстрее и гибче. Данная модель — это прежде всего элемент инфраструктуры, позволяющий разработчикам и архитекторам совершенствовать архитектуру приложения, внося определенные изменения на уровне системных компонентов. Кроме того, она дает разработчикам возможность создавать новые компоненты, параметры и поведение которых могут настраиваться клиентами. Реализация данной модели не превращает приложение в проект с открытым исходным кодом, в котором каждый может модифицировать все что угодно. Вы лишь откры- ваете некоторые части приложения для настройки клиентами, делая это простым, элегантным и эффективным способом. Кроме того, реализация модели провайдеров в самой ASP.NET дает вам возможность настраивать определенные компоненты ее исполняющей среды. Для этой цели здесь определены специальные провайдерные классы, которые вы можете использовать в качестве базовых классов при создании собственных провайдеров. Пример применения модели провайдеров Как применяется модель провайдеров и каковы ее основные преимущества, я проде- монстрирую на примере схемы классической процедуры аутентификации пользовате- ля, представленной на рис. 1-6. Последовательность блоков этой схемы соответствует потоку операций ASP.NET 1 1. Классический сценарий проверки членства Фиксированное поведение Фиксированное хранилище Рис. 1-6. Классическая схема проверки членства в приложениях ASP.NET 1.1 Пользователю, пытающемуся подключиться к защищенной странице, предлага- ется ввести на странице входа свое имя и пароль. Введенные данные передаются функции, которая отвечает за подтверждение личности пользователя. ASP.NET 1.x может автоматически сверить их с учетными записями Windows или списком имен, содержащимся в файле web.config. Оба эти решения не очень хорошо подходят для реальных Web-приложений, поэтому в большинстве случаев разработчикам при- ходится самостоятельно писать аутентификационный код, сверяющий введенные пользователем идентификационные значения с информацией из «доморощенного» источника данных. Схема и контейнер такого источника данных, как и алгоритм аутентификации, фиксированы и определяются разработчиком.
26 Часть I Разработка страниц ASP.NET Что плохого в таком решении? Да, в общем, ничего. Оно нормально работает, дает программисту полный контроль над происходящим и может быть адаптировано для других приложений. Единственный его недостаток — это отсутствие универсальной, раз и навсегда определенной модели аутентификации. Конечно, его можно перено- сить из одного приложения в другое, но более универсальное решение, построенное по модели провайдеров, портировать несравненно легче. Эти две схемы соотносятся между собой примерно так же, как метод вырезания и вставки соотносится с объек- тно-ориентированным наследованием. Рассмотрим другой сценарий — управление состоянием сеанса. В ASP.NET 1.x со- стояние сеанса можно хранить в базе данных SQL Server или в памяти специального процесса, внешнего по отношению к приложению. В обоих случаях приходится при- держиваться схемы данных, жестко закодированной в ASP.NET. Предположим, что вам больше подходит решение на основе базы данных, как более надежное, но в вашей системе не используется SQL Server. Тогда придется либо отказаться от идеи хранения состояния сеанса в базе данных, либо приобрести лицензию на SQL Server. Повлиять на поведение модуля ASP.NET, управляющего состоянием сеанса, вы никак не можете. Надеюсь, идея вам уже понятна: в ASP.NET есть модули, для использования кото- рых приходится соглашаться на определенную схему данных, определенное их храни- лище и определенное внутреннее поведение модуля. Если что-либо из предлагаемого вам не подходит, то ничего не остается, как отказаться от использования этих модулей и реализовать их функции самостоятельно (как в описанном выше примере с сервисом управления членством). В результате у вас получится собственная, специфическая для конкретного приложения система, автоматический перенос которой из одного при- ложения в другое будет невозможен. Кроме того, если вы наймете новых сотрудников, их придется обучать работе с этой системой. Наконец, вам придется приложить не- мало усилий, если вы захотите сделать свой API достаточно универсальным — таким, который можно расширять и повторно использовать в разных контекстах. (А иначе вы будете вынуждены снова и снова изобретать колесо.) Гораздо лучшим решением является применение модели провайдеров. В этом случае, во-первых, вы получаете хорошо документированные универсальные про- граммные интерфейсы для выполнения типичных задач. А во-вторых, у вас появля- ется возможность полностью контролировать внутреннее поведение и логику доступа к данным каждого API, реализованного согласно этой модели. Итак, в ASP.NET 1.1 у вас часто нет другого выхода, как писать собственный API для выполнения определенных функций желаемым способом. В ASP.NET 2.0 же под- держивается модель провайдеров, предлагающая лучшую альтернативу, так что грех ее не использовать. На рис. 1-7 приведена схема того же процесса, который был проиллюстрирован на рис. 1-6, но на этот раз реализованного по модели провайдеров. В ASP.NET 2.0 определен глобальный класс с именем Membership, содержащий группу статических методов. (Об API членства я подробно расскажу в главе 15.) Эти методы вызываются из приложения для выполнения различных операций, связанных с управлением член- ством, в том числе для проверки введенных пользователем имени и пароля, создания новых учетных записей пользователей и изменения паролей. Данный API расположен на верхнем уровне, а ниже вы можете подключать собственные провайдеры, которые выполняют указанные задачи тем способом, какой вам требуется. Написать новый про- вайдер очень просто — для этого достаточно создать класс, производный от известного базового класса, и переопределить в нем несколько методов. Провайдер, ответствен- ный за выполнение конкретной задачи, задается в конфигурационном файле.
Модель программирования ASP.NET Глава 1 27 Сценарий с использованием провайдеров Рис. 1-7. Сервис членства, реализованный с помощью модели провайдеров в ASP.NET 2.0 Преимущества модели провайдеров Реализация в ASP.NET модели «стратегия» дает вам два преимущества: возможность адаптации среды выполнения приложения к собственным нуждам и возможность по- вторного использования кода. Эта модель распространяется на несколько функцио- нальных областей ASP.NET. Вы можете писать провайдеры для управления членством пользователей, ролями, состоянием сеанса, для управления пользовательскими про- филями с применением технологии персонализации и для загрузки информации карты сайта из подходящих источников. Например, написав соответствующий провайдер, можно изменить схему данных, предназначенную для хранения идентификационной информации пользователей, с тем чтобы она содержалась, например, в базе данных Oracle или DB2, а пароли хранить хэшированными, а не в виде чистого текста. Столь беспрецедентно высокий уровень настройки системных компонентов открывает перед разработчиками множество новых возможностей. Вы можете писать совершенно новые провайдеры, а также расширять существующую модель новыми компонентами. Если посмотреть на ASP.NET 2.0 с точки зрения существующих приложений, модель провайдеров покажется еще более привлекательной, поскольку она является основой повторного использования кода и сохранения вложений в его разработку. Для создания реальных систем управления членством в ASP.NET 1.1 приходилось писать собственные API — об этом уже говорилось ранее. Как же следует поступить при переходе на ASP.NET 2.0? Отказаться от имеющихся наработок в пользу нового API членства или пользоваться собственным устаревшим API? Ни то, ни другое! Модель провайдеров открывает перед вами уникальную возмож- ность интегрировать имеющийся код в новую систему, построенную по модели про- вайдеров, адаптировав его таким образом, чтобы его можно было подключить к новой системе стандартным способом. Для этой цели достаточно написать адаптер, то есть оболочку для старого кода, имеющую интерфейс провайдера. Концепция адапте- ра — это еще одна базовая концепция программных систем, суть которой заключается в том, что класс А снабжается интерфейсом Б, понятным клиентскому классу В.
28 Часть I Разработка страниц ASP.NET В нашем случае это означает, что существующий код заключается в оболочку в ви- де нового провайдерного класса, бесшовно интегрирующегося в среду ASP.NET 2 О Таким образом внутренняя реализация определенного сервиса, такого как сервис членства, меняется, а его API остается неизменным, равно как и код приложений, которые его используют. Тем самым достигается возможность повторного использо- вания всего ранее написанного кода. Реализация модели провайдеров в ASP.NET Реализация модели провайдеров в ASP.NET состоит из трех элементов: класса про- вайдера, слоя конфигурации и слоя хранения. Класс провайдера — это компонент, который вы интегрируете в систему для получения требуемой функциональности. Конфигурационный слой предоставляет информацию, необходимую для иденти- фикации нужного провайдера и создания его экземпляра. Слоем хранения является физическое хранилище данных. В зависимости от того, какой функциональностью должен обладать конкретный провайдер, этим хранилищем может быть Active Direc- tory, таблица Oracle или SQL Server, XML-файл или что-либо еще. Класс провайдера Класс провайдера реализует известный его клиентам интерфейс, с помощью которого им предоставляется требуемая функциональность. Клиентам не обязательно знать де- тали реализации данного интерфейса. Такая прозрачность кода провайдера позволяет одному программному компоненту управлять другим, о коде которого он даже ничего не знает. Единственное отличие модели провайдеров ASP.NET от классической модели «стратегия» состоит в том, что вместо интерфейсов используются базовые классы. В ASP.NET не всякий класс, реализующий заданный интерфейс, может выступать в роли класса провайдера. Он обязательно должен быть производным от определен- ного базового класса. Каждому из поддерживаемых ASP.NET типов провайдеров соответствует свой базовый провайдерный класс. Этот класс определяет интерфейс провайдера в виде набора абстрактных методов. Все базовые провайдерные классы являются производными от одного общего класса ProviderBase. У него имеется един- ственный переопределяемый метод, Initialize, через который исполняющая среда пере- дает провайдеру необходимые установки из конфигурационных файлов. На рис. 1-8 представлена иерархия провайдерных классов для сервиса членства. Рис. 1-8. Иерархия провайдерных классов
Модель программирования ASP.NET Глава 1 29 Интерфейсы и базовые классы в сравнении Пусть поднимут руки те разработчики, которые ни разу не участвовали в много- часовых дебатах по поводу выбора между интерфейсами и базовыми классами. Та- кие дискуссии по определению бесконечны, и ни один из двух противоположных лагерей не желает сдавать свои позиции. Что лучше использовать — интерфейсы или базовые классы? Какие соображения следует принять во внимание при поиске ответа на этот вопрос? Для начала давайте рассмотрим следующий факт. В выпусках ASP.NET 2.0, предшествующих версии Beta, была реализована модель провайдеров, в точности соответствующая модели «стратегия», то есть основанная на интерфейсах. В выпуске Beta 1 интерфейсы заменили базовыми классами, на которых в конце концов и решено было остановиться. Похоже, ко- манда разработчиков самой ASP.NET нашла ответ на поставленный выше вопрос? Как известно, интерфейс представляет собой набор логически связанных между собой методов, содержащий только их объявления. Таким образом, ин- терфейсный тип — это частичное описание типа, которому потенциально может удовлетворять любое количество классов, Соответственно, хорошим интерфей- сом можно считать такой интерфейс, который может быть реализован группой разных типов и инкапсулирует некую обобщенную полезную функциональность, необходимую клиентам. Вот почему имена многих интерфейсов оканчиваются на «able» (в переводе с англ. — способный): IDisposable, I Comparable, IFormattable. Если интерфейс реализуется только одним классом, то, скорее всего, он неудачно спроектирован. Возможностью создания интерфейсов не следует злоупотреблять, чтобы не плодить ненужных типов; как правило, новые интерфейсы должны вводиться только после тщательного обдумывания. Базовый класс определяет общее поведение и общий программный интерфейс дерева дочерних классов. Классы гибче интерфейсов, и для них поддерживается версионность. Например, когда вы добавите новый метод в версию 2.0 опреде- ленного класса, существующие производные классы будут функционировать так же, как раньше, если только новый метод не является абстрактным. В случае с интерфейсами такое невозможно. В свете сказанного очевидным становится следующее универсальное правило: по возможности вместо интерфейсов следу- ет использовать базовые классы (что ни в коем случае не должно читаться как: «базовые классы следует использовать всегда»). По моему мнению, для модели провайдеров базовые классы являются лучшим выбором. Слой конфигурации Каждый поддерживаемый провайдерный тип ассоциируется с определенным разделом конфигурационных файлов, в котором задается используемый по умолчанию провай- дер данной функциональности и перечисляются все доступные ее провайдеры. Если у провайдера имеются открытые свойства, посредством атрибутов раздела, в котором он определен, задаются значения этих свойств по умолчанию. Содержимое данного раздела передается в виде аргумента методу Initialize класса ProviderBase — един- ственному общему для всех провайдеров методу. В нем каждый провайдер использует данную информацию для инициализации своего состояния. Приведем фрагмент кода конфигурационного раздела провайдера членства: <membership defaultProvider="AspNetSqlProvider”> <providers> <add name="AspNetSqlProvider” type="System.Web.Security.SqlMembershipProvider, System.Web"
30 Часть I Разработка страниц ASP.NET connections!ringName="LocalSqlSeever" enablePasswordRetrieval="false" enablePasswordReset="true“ requiresQuestionAndAnswer="true" passwordFormat="Hashed" /> </providers> </membership> Слой хранения Всем провайдерам нужно записывать информацию в постоянную память и считывать ее оттуда. Во многих случаях два провайдера одного типа различаются только тем, где они хранят свои данные. Информация о хранилище содержится в атрибутах про- вайдера в разделе <providers>, как в приведенном выше фрагменте кода. Заданный в нем провайдер SqlProfileProvider — это предопределенный провайдер профилей, использующий для хранения своих данных таблицу SQL Server. Его строка под- ключения задается в атрибуте connectionStringName, который содержит ссылку на другой раздел конфигурационных файлов, предназначенный для централизованного хранения строк подключения. Для того чтобы провайдер мог выполнять свою работу, ему необходима опреде- ленная инфраструктура (база данных, таблицы, отношения и т. п.). Соответствующее конфигурирование рабочего окружения обычно осуществляется на этапе развертыва- ния. ASP.NET упрощает эту задачу, предлагая в помощь администратору специальную консоль управления сайтом (рис. 1-9). Рис. 1-9. Консоль администрирования Web-сайта, вызываемая из Visual Studio .NET 2005 Существующие типы провайдеров Модель провайдеров использована в ASP.NET для реализации целого ряда функций, среди которых важнейшими являются следующие: реализация механизма чтения-записи для хранения информации пользовательских профилей;
Модель программирования ASP.NET Глава 1 31 создание определяемого пользователем репозитария учетных записей пользо- вателей, поддерживающего наиболее типичные операции, как то: проверка су- ществования заданного пользователя, добавление и удаление учетных записей пользователей, изменение паролей; создание определяемого пользователем репозитария ролей пользователей; • определение карты сайта; введение новых типов хранилищ данных состояния сеанса. Провайдерные классы, доступные в ASP.NET, перечислены в табл. 1-1. Табл. 1-1. Базовые провайдерные классы ASP.NET Класс Описание MembershipProvider Базовый класс провайдеров членства, используемых для управления информацией учетных записей пользователей ProfileProvider Базовый класс провайдеров персонализации, используе- мых для сохранения и восстановления информации поль- зовательских профилей RoleProvider Базовый класс провайдеров ролей, используемых для управления информацией о ролях пользователей SessionStateStoreProviderBa.se Базовый класс провайдеров состояния сеанса, используе- мых для сохранения соответствующей информации в по- стоянной памяти и для восстановления ее оттуда SiteMapProvider Базовый класс провайдеров карты сайта В составе каждого их этих классов определены абстрактные методы, соответствую- щие различным аспектам функционирования провайдеров, доступным для настройки пользователями. Например, у класса MembershipProvider имеются методы ValidateUser, CreateUser, DeleteUser, ChangePassword и т д Заметьте, что сам класс MembershipPro- vider в коде приложений никогда не используется, поскольку является абстрактным. Сказанное верно и в отношении других базовых провайдерных классов. Вместо них используются производные классы, такие как SqlMembershipProvider или, скажем, ActiveDirectoryMembershipProvider. Итак, если вы собираетесь писать пользовательский провайдер членства, служа- щий оболочкой для существующего кода, вам следует сделать его производным от MembershipProvider (или другого подобного класса, если требуются дополнительные, отсутствующие у MembershipProvider функции). Примечание Архитектура провайдеров, будучи одним из самых важных нововведений ASP.NET 2.0, является также и одним из наиболее сложных ее функциональных эле- ментов, требующим грамотного использования. Чтобы разработчики допускали меньше ошибок, команда ASP.NET предлагает большое количество демонстрационного кода и набор провайдеров, показывающий, что можно и чего нельзя делать с их помощью. На- писать пользовательский провайдер с нуля непросто по нескольким причинам. Во-первых, провайдеры должны поддерживать многопоточность, во-вторых, следует предпринять специальные меры, чтобы на этапе инициализации провайдера не мог произойти повтор- ный вход. Поэтому, прежде чем начинать свой первый проект по разработке провайдера, обязательно ознакомьтесь с материалами, представленными в разделе Provider Toolkit в ASP.NET Developer Center на сайте Microsoft. Заключение Будучи частью .NET Framework, ASP.NET позволяет использовать все преимуще- ства ее общеязыковой среды (CLR), такие как строгая типизация, наследование, взаимодействие кода, написанного на разных языках, и версионность. А поскольку
32 Часть I Разработка страниц ASP.NET ASP.NET — новейшая платформа Web-приложений, в ней использовано все лучшее из других платформ, включая классическую ASP, JSP и LAMP В ASP.NET реали- зована программная модель, которая, хотя и построена поверх протокола HTTP, не сохраняющего состояние, обеспечивает сохранение состояния и позволяет создавать приложения, управляемые событиями. В этой главе мы проанализировали компонентную модель, лежащую в основе Web страниц ASPNET, а затем исследовали стек разработки, от его вершины (слоя представления и элементов управления с богатым интерфейсом) и до дна (инфра- структуры и провайдеров). Модель провайдеров, по сути являющаяся реализацией концепции стратегии, стала ключевым элементом новой архитектуры ASP.NET и тем столпом, на котором основана архитектура новых приложений. Будучи довольно широко используемой, она позволяет настраивать и адаптировать для собственных нужд ряд низкоуровневых аспектов функционирования исполняющей среды, а также обеспечивает возможность повторного использования больших массивов существую- щего кода. Изучив ее, вы сможете создавать новые компоненты, обладающие неверо- ятной гибкостью и расширяемостью, с легкостью подключаемые к разным проектам и предоставляющие клиентам удобные возможности настройки. Только факты Разрабатывая приложения для ASP.NET, вы можете использовать все преимуще- ства общеязыковой среды .NET. такие как строгая типизация, наследование, надеж- ная защита кода, а также взаимодействие кода, написанного на разных языках. Страница ASP.NET во время выполнения представлена экземпляром класса, про- изводного от класса Page. Класс Page является конечной точкой конвейера модулей, обрабатывающих по- лученный сервером НТТР-запрос. Только к тем элементам исходной страницы ASP.NET, которые помечены атри- бутом runat, возможен программный доступ во время выполнения страницы на сервере. Элементы страницы, не имеющие атрибута runat, не обрабатываются на сервере и выводятся как есть. Атрибут runat применим практически ко всем тэгам, которые можно использовать в составе страницы ASP.NET, включая пользовательские и неизвестные тэги. Моделью процесса называется последовательность операций, выполняемых для обработки запроса. Эта модель определяется IIS и, в свою очередь, определяет, в каком рабочем процессе и от имени какой учетной записи будут выполняться приложения ASP.NET. Приложения ASP.NET работают от имени учетной записи с очень ограниченными разрешениями. Поведение приложений ASP.NET можно настраивать, используя иерархический набор конфигурационных файлов. Модель провайдеров ASP.NET является инфраструктурным элементом, благода- ря которому архитектура приложений стала более эффективной, а разработчики и архитекторы получили возможность действовать на уровне системных компо- нентов. Модель провайдеров ASP.NET обладает двумя важными достоинствами: она по- зволяет настраивать и адаптировать исполняющую среду и обеспечивает возмож- ность повторного использования кода.
Глава 2 Разработка Web-приложений в Microsoft Visual Studio .NET 2005 Независимо от того, как именно вы проектируете и реализуете Web-приложение, в конечном счете оно всегда состоит из определенного количества страниц, связанных с неким общедоступным URL. Непрерывный прогресс Web-технологий не влияет на такое положение вещей по той простой причине, что оно является естественным следствием простоты протокола HTTP. Пока базовым транспортным протоколом будет оставаться HTTP, Web-приложение будет набором страниц, доступных через Web. Какова же в этом контексте роль Microsoft ASP.NET и Visual Studio .NET 2005? ASP.NET является абстрактным программным слоем, функционирующим поверх HTTP и позволяющим разработчикам создавать Web-сайты и основанные на Web клиентские составляющие систем предприятия. Благодаря ASP.NET разработчики могут оперировать такими высокоуровневыми сущностями, как классы и компоненты, в рамках парадигмы объектной ориентированности. Средства разработки помогают программистам и архитекторам и обеспечивают максимально гладкое и эффективное взаимодействие с исполняющей средой ASP.NET. Эти средства используются для создания и развертывания приложений, имеют собственную программную модель и заставляют разработчиков играть по собственным правилам. Главным средством разработки приложений ASP.NET и клиентских составляющих систем масштаба предприятия является Visual Studio .NET 2005. У данной IDE по сравнению с ее предыдущей версией много новых функций, специально предназна- ченных для разработчиков Web-приложений и устраняющих некоторые ограничения Visual Studio .NET 2003. В настоящей главе мы рассмотрим важнейшие характеристики и функции Vi- sual Studio NET 2005, связанные с разработкой приложений ASP.NET. Вы узнаете, какие изменения внесены в проект как организационную структуру, используемую при разработке приложения, что нового предлагает IDE и какие в ней появились до- полнительные возможности редактирования, а также познакомитесь со средствами развертывания приложений. Введение в Visual Studio .NET 2005 Visual Studio .NET — это контейнерная среда, в которую интегрирован ряд визуальных дизайнеров различного назначения. В частности, здесь есть дизайнеры для разработ- ки приложений Windows Forms, сайтов ASP.NET, Web-сервисов. Все необходимые для работы элементы, такие как ссылки, соединения с источниками данных, папки и файлы, группируются на двух уровнях: решения и проекта. Контейнер решения содержит несколько проектов, тогда как контейнер проекта — несколько элементов. Грамотно используя эти контейнеры, можно эффективно управлять настройками от- дельных проектов и решения как целого. Для каждого элемента проекта в отдельном окне с именем Properties выводится полный набор его свойств.
34 Часть I Разработка страниц ASP.NET Прежде чем мы приступим к непосредственному изучению Visual Studio .NET 2005, имеет смысл коротко перечислить недостатки ее предыдущей версии, чтобы вы смогли в полной мере оценить достоинства нового продукта. Ограничения Visual Studio .NET 2003 Как вы, вероятно, знаете, Visual Studio .NET 2003 поддерживает единственную модель разработки приложений, в основе которой лежит использование проектов. Однако опыт показывает, что это не всегда наилучший подход, по крайней мере в отношении приложений Web и ASP.NET. Под проектом понимается логическая сущность, представляющая любое прило- жение для .NET, будь то приложение Windows Forms, приложение Web, консольное приложение или Web-сервис. Приступая к разработке очередного приложения, вы первым делом создаете новый проект, конфигурируете его, а затем добавляете в него элементы будущего приложения — страницы, ресурсы, классы и элементы управле- ния. При разработке Web-приложения в Visual Studio .NET возникает целый ряд проблем, по крайней мере на двух уровнях: компьютера и интегрированной среды разработки (IDE). Ограничения уровня компьютера Для того чтобы Visual Studio .NET 2003 успешно функционировала на компьютере разработчика, в дополнение к ней необходимо установить Microsoft FrontPage Server Extensions (FPSE). Это единственное средство, с помощью которого можно добраться до файлов проекта, размещенных на Web-сервере, поскольку Visual Studio .NET не поддерживает FTP и не имеет непосредственного доступа к IIS. Кроме того, при по- пытке установить Windows SharePoint Services (WSS) начинаются проблемы. Чтобы Visual Studio .NET и ASP.NET могли сосуществовать на одном компьютере с тесто- выми сайтами WSS, приходится производить дополнительную настройку. Visual Studio .NET 2003 зависит от IIS, который должен быть установлен либо на том компьютере, где осуществляется разработка, либо на сервере, с которым имеется связь. Каждое создаваемое вами приложение должно быть связано с виртуальной папкой IIS. Эти ограничения отражаются на процессе разработки гораздо сильнее, чем может показаться на первый взгляд. Например, для создания новых проектов разработ- чику требуются административные привилегии, поэтому нужно выработать эффектив- ную корпоративную политику безопасности. Кроме того, затруднена, хотя и возможна, отладка приложения для разных конфигураций и сценариев использования. Ограничения уровня IDE Главной проблемой, связанной с разработкой Web-приложений в Visual Studio .NET, является неспособность этой IDE работать с отдельными страницами ASP.NET, не входящими в состав проекта. Вы можете открыть такую страницу и даже редакти- ровать ее, но Microsoft IntelliSense с ней работать не будет, как не будут работать и другие важные функции среды разработки, такие как выполнение и отладка страницы. Короче говоря, у Visual Studio .NET 2003 в такой ситуации перед программой Блокнот только одно преимущество — цветная раскраска синтаксиса. Единым и единственным центром управления элементами приложения в Visual Studio .NET 2003 является файл проекта. Чтобы сделать файл частью проекта, нужно явно добавить его в проект и сконфигурировать — вы не можете просто указать на существующий виртуальный каталог и приступить к работе над его содержимым. Та- ким образом, информация, хранящаяся в файле проекта, — это не просто содержимое каталога. Следствием такой организации работы является то, что забытые програм- мистом ненужные файлы остаются в каталоге, а потом оказываются на сайте.
Разработка Web-приложений в Microsoft Visual Studio .NET 2005 Глава 2 35 Описываемая модель создает проблемы и на уровне управления исходным кодом. Когда используется система управления этим кодом, вы не можете регистрировать в ней файлы непосредственно. Все они регистрируются автоматически, когда вы добав- ляете их в проект, и дальнейшие операции по получению файлов для редактирования и сохранению изменений производятся автоматически, во время выполнения соот- ветствующих команд меню Visual Studio. Иными словами, и здесь проект является единственным центром доступа к файлам. Visual Studio .NET всеми возможными средствами старается принудить вас исполь- зовать для каждой добавляемой в проект страницы класс отделенного кода. В общем случае держать код (файл .cs или .vb) отдельно от разметки страницы (файла .aspx) удобно, и такой подход является предпочтительным. Однако эта функция реализова- на в Visual Studio .NET 2003 так, что в файлах проекта оказывается слишком много инструментально-генерируемого кода. В результате труднее становится поддерживать файлы кода и элементы управления синхронизированными. Более того, поскольку содержимое проекта компилируется в одну сборку, совместно используемый проект становится предметом состязания, домен приложения перезагружается при каждом изменении в исходных файлах, а этап компиляции в больших проектах оказывается неоправданно длительным. Наконец, вы не найдете в Visual Studio .NET 2003 поддержки декларативного определения ресурсов, и поэтому для добавления в проект нового ресурса, такого как файл WSDL или XSD, вам придется явно генерировать необходимый код. Подводя итог, можно констатировать, что хотя разработчики успешно пользуются Visual Studio .NET 2003 при создании реальных приложений, этот инструмент далеко не идеален (особенно когда речь идет о простых проектах) и имеет множество недо- статков и ограничений. Достоинства Visual Studio .NET 2005 В отличие от своего предшественника Visual Studio .NET 2005 является более про- стым и дружественным к разработчику средством создания приложений ASP.NET. Ключевые его усовершенствования касаются недостатков Visual Studio .NET 2003, описанных в предыдущем разделе. Давайте выясним, как именно они были устранены и каково нынешнее положение дел. Устранение зависимости от IIS Наличие отдельно установленного Web-сервера IIS больше не является обязатель- ным. В состав Visual Studio .NET 2005 входит собственный локальный Web-сервер, так что по крайней мере для быстрого тестирования и отладки страниц IIS теперь не требуется. Пользовательский интерфейс встроенного Web-сервера Visual Studio представлен на рис. 2-1. Этот Web-сервер является переработанной версией бесплатного мини-сервера Cas- sini, первоначально продававшегося с Web Matrix — поддерживаемым сообществом разработчиков бесплатным редактором для приложений ASP.NET. Важно отметить, что локальный Web-сервер используется по умолчанию при тестировании страниц. Если же открыть проект из существующего виртуального каталога IIS, то Visual Stu- dio .NET будет использовать для тестирования приложения IIS. Возможности встроенного Web-сервера очень скромны, и он, конечно же, не может заменить полнофункциональное серверное программное обеспечение, например IIS. Встроенный Web-сервер работает только с отдельными страницами и не содержит никаких дополнительных компонентов вроде метабазы IIS.
36 Часть I Разработка страниц ASP.NET Рис. 2-1. Локальный Web-сервер Visual Studio .NET 2005 Способы доступа к Web-сайтам Visual Studio .NET 2005 поддерживает несколько способов открытия Web-сайтов. По- мимо FPSE вы можете использовать для этой цели FTP или непосредственно задавать путь к требуемым файлам в файловой системе сервера. Теперь у вас имеется прямой доступ к локальной инсталляции IIS; вы можете просматривать иерархию виртуальных папок и создавать в ней новые папки. Как показано на рис. 2-2, для открытия Web-сайта достаточно указать папку в файловой системе или виртуальный каталог IIS. В первом случае для тестирования сайта будет использоваться локальный Web-сервер. Рис. 2-2. Если сайт открывается из файловой системы, приложение ASP.NET выполняется под управлением локального Web-сервера Взаимодействие с IIS теперь очень упростилось (рис. 2-3). Когда вы даете команду открыть сайт, Visual Studio .NET 2005 выводит диалоговое окно, в левой части которо- го на выбор предоставляется несколько опций, позволяющих найти проект в файловой
Разработка Web-приложений в Microsoft Visual Studio .NET 2005 Глава 2 37 системе, в иерархии виртуальных каталогов IIS (это возможно только при наличии локального IIS), используя FTP или просто введя URL сайта, сконфигурированного с помощью FrontPage Server Extensions. На вкладке IIS содержатся также кнопки для создания новых виртуальных каталогов и приложений. Рис. 2-3. Поиск в иерархии IIS существующего виртуального каталога, который требуется открыть t Примечание Вы можете открывать существующие Web-сайты, используя протокол FTP, а затем создавать и редактировать их файлы. Однако для этого необходимо наличие доступа к FTP-серверу и разрешения на чтение и запись каталога FTP. Этот каталог уже должен существовать, поскольку Visual Studio .NET 2005 не может создавать через FTP новые сайты. Файлы проекта Visual Studio .NET 2005 не компилирует все содержимое сайта в одну сборку, как это делала Visual Studio .NET 2003. Она действует с учетом новой модели компиляции ASP.NET и динамически распознает типы файлов на основе имен папок, в которых они содержатся. Благодаря этому немедленно обнаруживаются изменения, вносимые в файлы .aspx, а также в сопутствующие файлы кода (,cs и .vb) и ресурсов, и произ- водится динамическая компиляция этих файлов. У новой модели компиляции ASP.NET есть положительные и отрицательные стороны, и, прежде чем делать о ней собственное заключение, следует все их тща- тельно взвесить. Однако несомненными являются следующие два ее преимущества. Во-первых, модель компиляции ASP.NET 2.0 позволяет развертывать большее число типов исходных файлов (например, классы на C# и VB.NET), с тем чтобы система осуществляла мониторинг вносимых в них изменений и автоматически их переком- пилировала. Во-вторых, такое поведение системы никому отнюдь не навязывается. Если вы хотите оставить в Web ценные файлы исходного кода, написанного на С#, на милость хакеров, то можете использовать старую модель, то есть создавать из внешних классов отдельные сборки путем явной компиляции. Так что выбор остается за вами: ASP.NET 2.0 и Visual Studio .NET 2005 предоставляют вам альтернативу.
38 Часть I Разработка страниц ASP.NET Файлы решений (.sin) все еще поддерживаются в Visual Studio NET 2005, но теперь их использование больше не является обязательным — создать Web-проект и эффективно управлять им можно и без их помощи. Например, чтобы включить тот или иной файл в проект, достаточно просто поместить его в каталог этого проекта. (Если файл не будет немедленно отображен в Visual Studio, щелкните правой кнопкой мыши окно Solution Explorer и выберите команду Refresh Folder.) Файлы решений облегчают управление группами проектов, но вовсе не обязательно помещать такой файл в каждый Web-каталог. Копирование Web-проекта Еще одной долгожданной функцией, заслуживающей упоминания, является функция копирования сайта. В ранних версиях Visual Studio .NET задача дублирования Web- проекта на втором компьютере и синхронизации двух его копий была непростой. Если серверный хост поддерживал FPSE, можно было обратиться к интегрированному мастеру Visual Studio .NET 2003, вызываемому командой Project\Copy. В противном случае единственным средством копирования и синхронизации служил протокол FTP. Аналогичным образом осуществлялось и перемещение Web-сайта в пределах локаль- ной сети, только в этом случае можно было пользоваться Проводником Windows. Процедура копирования или переноса сайтов сама по себе не такая уж и сложная, но она усложняется, как только вводятся дополнительные условия, например когда требуется скопировать только модифицированные файлы или файлы, отвечающие определенному критерию. В Visual Studio .NET 2005 для копирования текущего Web-сайта в другое место на локальном или удаленном компьютере достаточно воспользоваться командой Сору Web Site из меню Website. Данная команда активизирует встроенное FTP-средство переноса файлов, окно которого показано на рис. 2-4. Рис. 2-4. Окно, открываемое командой Copy Web Site
Разработка Web-приложений в Microsoft Visual Studio .NET 2005 Глава 2 39 Щелкнув кнопку Connect, вы подключаетесь к целевому сайту (рис. 2-5), вы- бираете файлы целевого или исходного сайта и с помощью кнопок, размещенных между двумя списками файлов, выполняете синхронизацию или копируете файлы с одного сайта на другой. Причем вы можете копировать файлы между физическими или виртуальными папками и даже между компьютерами. Рис. 2-5. Подключение к удаленному сайту для копирования локального проекта Как видите, функция копирования Web-сайта является идеальным инструмен- том развертывания, особенно в тех случаях, когда приходится работать с файлами функционирующего сервера. Кроме того, она может использоваться как средство синхронизации, кода требуется быстро протестировать работу приложения в разных ситуациях и конфигурациях. Интеллектуальное редактирование с использованием IntelliSense Последней в нашем списке, но отнюдь не последней по значимости, является вве- денная в Visual Studio .NET 2005 функция поддержки редактирования независимых файлов, благодаря которой теперь, чтобы отредактировать тот или иной файл .aspx, вовсе не обязательно создавать проект. Достаточно дважды щелкнуть на таком фай- ле в Проводнике Windows, и запустится Visual Studio .NET 2005, которая позволит вам отредактировать исходный код страницы. При этом в отличие от предыдущей версии IDE вам будут доступны все ее функции, включая IntelliSense, и вы сможете увидеть результат своей работы во встроенном браузере при поддержке со стороны локального сервера. Примечательно, что функция IntelliSense (рис. 2-6) теперь поддерживается для всех элементов кода страницы, в том числе для выражений связывания с данными, директив страницы и кода, включаемого непосредственно в .aspx-файлы. В Visual Studio .NET 2003 было сложно организовать поддержку IntelliSense в HTML-представлении страницы, отображаемом во время работы с кодом пользова- тельских элементов управления. Для этого приходилось вручную создавать XSD-файл, описывающий открытый интерфейс элемента управления, а затем инсталлировать этот файл в определенную папку и связывать его со страницей посредством атрибута
40 Часть I Разработка страниц ASP.NET xmlns. Теперь редактировать файлы схемы вручную вам больше не придется. В Visual Studio .NET 2005 они генерируются автоматически, как только автор страницы опу- скает на нее элемент управления, для чего генератор схемы использует связанные с этим элементом управления метаданные. Однако новые атрибуты метаданных при этом не добавляются. Генератор получает всю необходимую ему информацию из су- ществующих атрибутов, определяющих ожидаемое поведение элемента управления, связанное с синтаксическим разбором и сохранением состояния. WebSitel Microsoft Development Environment File Edit yiew Website Build Debug Joels JMndw Client Objects Be Events li <^8 Page Language»"C#” 2i <1 _________________ 3 d2p%® Assembly_______________| %® Implements 5 S2 %® Import 6 [£8 %® Mas ter Type 32 %® Outputcache *3 32 %® Page .. g 32 %® PreviousPageType Ю 32 %® Reference ЗД 32 %® Register 1 ^32 asp:Conterit Styleshee \1" conte -shim-str -photos"» .e-galler to My Pt rum dolor .1 laoreet nostrud exercitatior Рис. 2*6. Функция IntelliSense поддерживается для всех элементов кода страницы Внимание! Учитывая сказанное выше, во время создания специализированных элемен- тов управления для ASP.NET 2.0 следует проверять, как с ними будет работать функция IntelliSense в Visual Studio .NET 2005. Для того чтобы обеспечить правильную работу этой функции, элемент управления необходимо пометить определенными атрибутами-мета- данными. Создание проекта ASP.NET Мы познакомились с базовыми функциями Visual Studio .NET 2005 и теперь можем двигаться дальше, к разработке в этой IDE своего первого учебного проекта. Новый Web-сайт создается с помощью команды File\New\Web Site. После ее выбора откры- вается диалоговое окно, показанное на рис. 2-7, в котором Visual Studio предлагает вам указать тип нового сайта. При выборе первой опции, ASP.NET Web Site, для нового сайта генерируется минимальный набор файлов. В частности, создается страница .aspx и пустой каталог Арр_Data. Если выбрать установку Personal Web Site Starter Kit, вы получите пол- ностью функционирующий сайт с несколькими стандартными функциями. Давайте остановимся на первой опции. Visual Studio .NET 2005 создаст файл проекта, но не будет использовать его для отслеживания файлового состава приложения. Корневой каталог файла неявно определяет Web-проект. Любой файл или папка, добавленные сюда или здесь созданные, автоматически становятся частью проекта.
Разработка Web-приложений в Microsoft Visual Studio .NET 2005 Глава 2 41 Рис. 2-7. Опции создания нового Web-сайта в Visual Studio .NET 2005 Средства разработки страницы Та часть приложения ASP.NET, которая формирует его интерфейс, может состоять из элементов нескольких типов, среди которых наиболее важными являются страницы. Редактировать страницу можно в одном из двух режимов-представлений: Design (режим дизайнера) или Source (редактирование исходного кода). В режиме дизай- нера отображается графическое представление страницы, и вы можете выделять и редактировать элементы управления и статические элементы страницы, а в режиме исходного кода выводятся HTML-разметка страницы и ее встроенный код. Здесь вы- водятся также всплывающие подсказки и действуют функции IntelliSense, цветового выделения элементов синтаксиса и автозавершения. Вы выбираете шаблон требуемого элемента и добавляете его в состав сайта, поль- зуясь окном, представленным на рис. 2-8. Это окно открывается из меню File по команде New\File. Обратите внимание на два флажка, которые размещены внизу окна. Первый из них позволяет поместить код страницы в отдельный файл (подобно модели отделен- ного кода из Visual Studio .NET 2003), а второй дает возможность связать текущую страницу с эталонной. Эталонные страницы — это замечательное новое средство, появившееся в ASP.NET 2.0, о котором мы поговорим в главе 6. Модель отделенного кода, использовавшаяся в Visual Studio .NET 2003, в Visual Studio .NET 2005 суще- ственно переработана и реструктурирована. Как результат, теперь код страниц не обязательно должен быть отделенным (то есть страницу можно не разделять на два файла, .aspx и .cs). Хотя отделение кода по-прежнему полностью поддерживается и настоятельно рекомендуется поступать именно так, это больше не является обяза- тельным требованием. Прежде чем мы приступим к добавлению в состав нашей страницы программного кода, давайте познакомимся с некоторыми средствами, используемыми при разра- ботке страниц.
42 Часть I Разработка страниц ASP.NET Рис. 2-8. Шаблоны элементов, поддерживаемые Visual Studio .NET 2005 Эталонные страницы Эталонная страница — это файл, определяющий шаблон для группы страниц. По- добно обычной странице .aspx, она содержит заменяемые разделы, каждый из кото- рых помечен уникальным идентификатором. Для того чтобы страница наследовала эталонную страницу, первая должна содержать ссылку на вторую Эта ссылка может быть включена в директиву ©Page или задана программно. Страницы, основанные на эталонной, называются страницами контента. Одна эталонная страница может быть связана с любым количеством страниц контента. Применение шаблонов, со- держащихся в эталонных страницах, осуществляется абсолютно прозрачно для ко- нечного пользователя. Работая с приложением, пользователь видит и вводит лишь URL страниц контента. Исполняющая среда ASP.NET, получив запрос страницы контента, применяет соответствующий алгоритм компиляции и динамически фор- мирует класс этой страницы, соединяя ее собственную информацию с информацией эталонной страницы. Введение механизма эталонных страниц стало ответом на одну из самых насущных потребностей разработчиков, горячо обсуждавшихся в группах новостей ASP.NET 1.x. Этот механизм позволяет разработчику создавать сайты с единообразно оформлен- ными страницами, один раз запрограммировав общую часть их пользовательского интерфейса и общую функциональность в файле эталонной страницы и разместив в нем именованные заполнители для того контента, который будет определен в произво дных страницах. Основное достоинство такого подхода заключается в том, что общая информация содержится в одном месте — в эталонной странице, а не реплицируется в каждой странице сайта. Контракт между эталонной страницей и страницей контента фиксирован и опреде- ляется исполняющей средой ASP.NET. Никакие изменения в приложении или состав- ляющих элементах управления не могут нарушить связь между эталонной страницей и страницами контента.
Разработка Web-приложений в Microsoft Visual Studio .NET 2005 Глава 2 43 Внимание! Применение эталонных страниц — лишь один из способов разработки Web- сайтов, отнюдь не являющийся обязательным или даже предпочтительным. Данную тех- нологию следует использовать только в тех случаях, когда необходимо продублировать части пользовательского интерфейса на нескольких страницах или когда структура при- ложения такова, что для него наиболее естественной является реализация в виде набора эталонных страниц и страниц контента. Страницы контента Как уже было сказано, в файле эталонной страницы определяются общие части группы страниц сайта и размещаются заменители тех частей интерфейса, которые являются индивидуальными для каждой страницы сайта. Что касается страницы контента, то в ее файле определяется содержимое ее индивидуальных частей. Страница контента относится к особому типу страниц ASP.NET и может содержать тэги только одного вида, а именно: <asp:Content>. Никакие классические тэги HTML, включая клиент- ские тэги <script> и комментарии, размещать в ней нельзя, и встретив их, компилятор выдаст сообщение об ошибке. Причина такого рода ограничения кроется в реализации механизма эталонных страниц. Поскольку области контента обозначаются в эталонной странице замени- телями, литеральная разметка страницы контента (то есть комментарии, сценарии и некоторые другие тэги) может конфликтовать с той, которая содержится в эталонной странице. В Visual Studio .NET имеется особый дизайнер для работы со страницами контен- та (рис. 2-9). В нем отображается столько рабочих поверхностей, сколько областей контента определено в эталонной странице. Структура эталонной страницы здесь показана светло-серым цветом и для редактирования недоступна. Рис. 2-9. Страница контента в Visual Studio .NET 2005
44 Часть I Разработка страниц ASP.NET Обнимание! Страницы контента могут использоваться лишь совместно с эталонными страницами. Чтобы вместо страницы Web Forms создать страницу контента, достаточно установить флажок Select master page (рис. 2-8). Классы отделенного кода Когда при создании новой страницы Web Forms вы устанавливаете флажок Place code in separate file, в одной папке создаются файл .aspx и файл класса на языке C# или Visual Basic .NET. Второй файл получает то же имя, что и основной файл страницы, и отличается от такового лишь расширением, соответствующим языку программи- рования. Например, если страница Web Forms называется WebForml.aspx, файл от- деленного кода имеет имя WebForml.aspx.cs. Это имя задается по умолчанию, в со- ответствии со стандартным соглашением об именах. При желании файл отделенного кода можно переименовать, задав для него любое другое имя, хотя поступать так не рекомендуется, чтобы не нарушать согласованности именования. Если вы не станете отделять код страницы от ее разметки, ничего плохого с вашим приложением не случится. Однако поскольку реальные страницы обычно содержат довольно много серверного кода, при размещении его в тэге <script> файла .aspx этот файл становится чересчур тяжеловесным и его чтение, редактирование и поддерж- ка затрудняются. Если же вы следуете принципу отделения кода, каждая страница приложения имеет сопутствующий файл класса, содержащий весь необходимый ей код. Указанный класс становится основой динамически генерируемого исполняющей средой ASP.NET класса страницы, создаваемого для представления запрошенного ресурса .aspx. В таком случае лучше реализуется объектно-ориентированный подход к разработке страниц, код становится более модульным, а кроме того, облегчается совместная работа над страницей программистов и дизайнеров. В Visual Studio .NET 2005 модель страницы была усовершенствована, хотя ее об- щий синтаксис по сравнению с предыдущей версией остался практически неизменным. В первую очередь обращают на себя внимание два изменения. Во-первых, отделение кода больше не осуществляется принудительно (что видно на рис. 2-8). Во-вторых, благодаря поддержке частичных классов — нововведения .NET Framework 2.0 — не- сколько разработчиков (и дизайнеров) могут теперь работать над страницей одновре- менно. (Частичными классами называются классы .NET, определенные в нескольких файлах исходного кода, содержимое которых объединяется для составления полного определения класса.) Примечание Файлы отделенного кода необходимо создавать для всех страниц про- екта, за исключением тех тестовых страниц, которые создаются для проверки той или иной функциональности и в конечное приложение не включаются. Применение модели отделенного кода в сочетании с принципом наследования класса позволяет создавать иерархии классов, сокращая таким образом сроки разработки и максимизируя повторное использование кода. Панели элементов управления Страница Web Forms состоит главным образом из элементов управления — предо- пределенных элементов управления HTML и Web, а также пользовательских (user) и специализированных (custom) элементов управления. За исключением пользова- тельских элементов управления, все остальные представлены на панели элементов редактора (рис. 2-10). С упомянутой панели, которую можно выводить на экран и скрывать, элементы управления перетаскивают в Web-форму с помощью мыши Как в режиме Design, так и в режиме Source, панель элементов отображается только по- сле выбора ресурса .aspx.
Разработка Web-приложений в Microsoft Visual Studio .NET 2005 Глава 2 45 Панель элементов является настраиваемой, на нее можно добавлять новые эле- менты управления и создавать на ней новые вкладки. Элементы управления, опреде- ляемые в проекте в рамках текущего решения, добавляются на панель элементов автоматически. T oolboK ** j-nt о ММ r Data - VaMacion Havkgatton - login WebParts flfrjtllli - HTML Рис. 2-10. Панель элементов Visual Studio .NET 2005 A Label W Textbox @ Button ® UrMutton ® ImagaButton A Hyperlink, I DropOovmUst Й5 ustBox й Chaddtac |= ChackBoxUst 0 RadoButton | ~ RadoButtonUst Щ Image Щ ImageMap EJ Table BuhtedM Ж mdd« леИ Pjut-ral ggJCahndw £3 AdRotator tjiBWJptoad V wizard SI xml © MuBVtow О Panel Thwe we rwusebte controls m this Dr аз an ttem Специальные функции редактора Редактор кода Visual Studio .NET 2005 обладает несколькими интересными функци- ями, демонстрирующими стремление команды разработчиков этого продукта сделать все для максимального удобства пользователей. Я хочу обратить ваше внимание на четыре такие функции: проверки доступности, сохранения разметки, упрощенной расстановки табуляций и отступов, проверки целевой схемы. Специальная кнопка на панели инструментов исходного кода HTML — Check page for accessibility — позволяет выбрать несколько требований доступности и проверить на соответствие им код страницы (рис. 2-11). Даже для пустой страницы система вы- водит несколько рекомендаций.
46 Часть I Разработка страниц ASP.NET Visual Studio .NET 2005 сохраняет форматирование написанного вами HTML-кода и не меняет его, когда вы переключаетесь между представлениями. (В Visual Studio .NET 2003 автоформатирование выполнялось каждый раз, когда вы переключались между представлениями страницы, что было очень неудобно.) Кроме того, в ваше распоряжение предоставляется удобная функция расстановки отступов и формати- рования тэгов, которую можно включать и отключать по своему желанию. Рис. 2-11. Диалоговое окно для выбора параметров проверки доступности На рис. 2-12 показан список поддерживаемых целевых клиентов. Стоит только выбрать конкретный клиент, и весь процесс редактирования будет адаптирован к его возможностям. Для примера представьте, что вы выбрали в качестве целевого клиента Netscape Navigator 4.0 (NN4). Этот браузер не распознает тэги <iframe>, но поддер- живает тэг <1ауег>, который имеет почти идентичные характеристики. На рис. 2-13 видно, что Visual Studio .NET знает об этой его особенности и формирует HTML-код страницы соответствующим образом. Если вы все же пожелаете ввести тэг <iframe>, редактор примет его и даже автоматически завершит ваш ввод, но подчеркнет тэг красной волнистой линией, привлекая ваше внимание к потенциальной проблеме. [HTML Source Editing X ; S] £=: ^Internet Explorer 6.0 "BaJ Jpternft Explorer 6.П Internet Explorer 3.02 / Netscape Navigator 3.0 Netscape Navigator 4.0 HTML 4.01 XHTML 1.0 Transitional (Netscape 7, Opera 7, Internet Explorer 6) XHTML 1.0 Frameset XHTML 1,1 Strict Mobile cHTML 1.0 Template Mobile HTML 3.2 Template Рис. 2-12. Список поддерживаемых целевых клиентов, для которых Visual Studio .NET может проверять вашу разметку |%"| Примечание В Visual Studio .NET 2005 количество поддерживаемых схем целевых клиентов увеличилось. Теперь в их число входят схемы Internet Explorer 6.0, HTML 3.2 (действующая в Internet Explorer 3.x и Netscape Navigator 3.x), а также схемы мобильных устройств (Compact HTML 1.0 и Mobile HTML 3.2). Netscape 4 0 и XHTML 1.0 Transitional (используется в Netscape 7.0 и Opera 7.0).
Разработка Web-приложений в Microsoft Visual Studio .NET 2005 Глава 2 47 WebSite! Microsoft Development Environment 0t Edit J0ew Website Quid Qebug tools JftrJ'» tjdp * LsJ§Ll,Netscape Navigator 4.0 д test.espx*Г Albums.aspx g-’>../;...,..y:.,,7.....r.fc,,~..X~“.„,...........,1,^, Client Objects 8c Events li <%@ Page Language»"C#" CornpileWith“"test. eu i ‘ 2! 31 <!DOCTYPE html PUBLIC /W3C//DTD XHTML 1 4| Si <html xmlng°"httP: //wwv.w3 . orcf/ 1Э99/Xhtml" 6| «head runat»"server"> 7i <title>Untitled Page</title> 8i </ head> 9j <body> 10i «form id“"forml” runat»"server"> Hi <div> 12= <lfra®e> <1] 13| «/ifra'Bahr 14i </div> 2a | 1S| </ :d3 (layer 16i «/body .EH img :;i 17j c/html SStsindex Igi О kbd , “ЧИ^ИИИИИИИИИИ1^^^И^^ИИ1ж») aaistrg S3 map 33 menu Vi & i Рис. 2-13. Редактор кода действует в соответствии с выбранной целевой схемой Рефакторинг кода Когда вы открываете в Visual Studio .NET 2005 для редактирования файл .vb или .cs, в программе появляется новое меню, получившее название Refactor (рис. 2-14) ProAspNet/O Microsoft Visual Studio Efe Edit Ref •M actor Wet Ле Quid Debra look ®ndow Community Renanje... Ctrl+R, Ctrl+R Extract Method... Ctri+R, Ctri+M Toolbox General There ore no usabia Item onto ths tt. * *1. a Encapsulate Held... Ctrl+R, Ctrl+F Promote Local Variable to P arameter Ctrl+R, Ctrl+P Remoye Parameters... Ctrl+R, Ctrl+V Regrder Parameters... Ctrl+R, Ctrl+O » aspx Рис. 2-14. Меню Refactor, которое помогает разработчикам быстро перерабатывать код классов Как видите, в этом меню представлены продвинутые функции редактирования кода. В частности, с их помощью можно извлечь блок кода и преобразовать его в но- вый метод или же переименовать тот или иной член, заменив его старое имя новым везде, где оно встречается. Функция рефакторинга пригодится вам и при работе со свойствами Одной из самых утомительных задач при написании классов является превращение поля в свойство с аксессором get и мутатором set. Представьте, что в со- ставе вашего класса имеется такое поле: private int .counters;
48 Часть I Разработка страниц ASP.NET В определенный момент вы можете обнаружить, что для нужд приложения луч- ше подошло бы полнофункциональное свойство. Но не обязательно самостоятельно вводить необходимый программный код — достаточно просто выделить строку объ- явления поля и с помощью команды Refactor\Encapsulate превратить это поле в свойство. В результате вместо исходной строки будет сгенерирован следующий код: public int Counters { get { return .counters; } set { .counters = value; } 1 После этого вы сможете изменить открытое имя свойства и, конечно, написать для аксессора и мутатора дополнительный код. Импорт и экспорт установок Разработчикам часто приходится перемещать проект с одного компьютера на другой. Делается это по разным причинам. Например, у вас может быть несколько тестовых компьютеров разного назначения, или вы постоянно переключаетесь между сайтом клиента и собственным тестовым сайтом, или же вы такой трудоголик, что, придя домой, снова садитесь за работу. Обычно на разных компьютерах Visual Studio .NET устанавливается с одним и тем же набором функций, а иногда с одинаковыми установками IDE. Для того чтобы чувствовать себя комфортно и иметь под рукой все необходимое, разработчики часто создают макросы, переупорядочивают меню и панели инструментов, а также до- бавляют на панель элементов новые элементы управления, создают новые шаблоны проектов, задают предпочтительные цвета и шрифты. Очевидно, что такого рода информацию непросто каталогизировать, организовывать и сохранять вручную, но, к счастью, в Visual Studio .NET 2005 эта задача автоматизирована. На рис. 2-15 показано диалоговое окно нового мастера импорта и экспорта уста- новок, которое вызывается из меню Tools. С его помощью можно выбрать установки IDE, которые требуется сохранить, и записать их в файл XML- формата. Этот файл можно создать в любом удобном месте и присвоить ему любое имя с расширением .vssettings. Добавление кода в проект Добавив в проект Web-форму, программист обычно пишет обработчики событий страницы и ее элементов управления. Кроме того, приложению могут потребо- ваться различные пользовательские классы, реализующие функциональность, для которой не существует готовых стандартных классов или классов сторонних про- изводителей. Наполнение формы элементами управления — задача несложная, и большинство выполняемых при этом операций являются интуитивными. Вы открываете форму в режиме Design и мышью размещаете на ней элементы управления, перетаскивая их с панели элементов, а затем настраиваете их свойства. При желании можно пере- ключиться в режим Source и вручную ввести HTML-код разметки.
Разработка Web-приложений в Microsoft Visual Studio NET 2005 Глава 2 49 Рис. 2-15. Мастер импорта и экспорта установок IDE Приятным сюрпризом для многих разработчиков станет возможность перетаски- вать элементы управления с панели элементов на страницу в режиме Source и в этом же режиме редактировать свойства элементов управления в окне Properties, выделяя их в HTML-коде страницы. Кроме того, все элементы управления, включая создавае- мые вами специализированные элементы, могут иметь собственный пользовательский интерфейс времени разработки, посредством которого конфигурируются их свойства времени выполнения. Определение обработчиков событий Обычно включение программного кода в состав страницы Web Forms осуществляется с целью обработки тех или иных событий самой формы либо ее элементов управления. Как же пишутся обработчики событий страницы? Поместите в форму кнопку и дважды ее щелкните. Visual Studio переключится в режим Source и создаст пустой обработчик события элемента управления по умолчанию. Для кнопки этим событием является Click. Код обработчика, который вы увидите, будет выглядеть примерно так: protected void Button1_Click(object sender, EventArgs e) { } Создав этот обработчик, Visual Studio автоматически модифицирует разметку, добавив в нее атрибут OnClick: <asp:Button ID="Button1" runat="server" Text="Button" OnClick="Button1_Click" /> Заметьте, что связывание событий с их обработчиками всегда выполняется в теле страницы .aspx. В отличие от своей предшественницы Visual Studio NET 2005 не
50 Часть I Разработка страниц ASP.NET вставляет в страницы автоматически сгенерированный код связывания с событиями Напомню, что в Visual Studio .NET 2003 по двойному щелчку кнопки в дизайнере формы в класс отделенного кода вставлялся следующий оператор: // VS.NET вставляет этот код в класс отделенного кода страницы, когда вы // дважды щелкаете кнопку, чтобы написать обработчик ее события по умолчанию Buttonl.Click += new EventHandler(this.Button1_Click); Конечно, если языком страницы является Visual Basic .NET, этот код будет не- много другим. Если вы имеете дело со страницей, у которой есть файл отделенного кода, обра- ботчик события определяется в этом файле, а не в основном файле страницы. Когда вы дважды щелкаете элемент управления или тело страницы, генерируется обработчик события данного объекта по умолчанию. Но как быть, если вам требуется обработчик другого события? В таком случае вы выбираете желаемый элемент управ- ления и щелкаете в окне Properties значок Events (рис. 2-16), после чего выбираете требуемое событие. Рис. 2-16. Окно Properties в режиме отображения Events Создание вспомогательных классов Создать вспомогательный класс несложно: достаточно добавить в проект соответ- ствующий элемент, как показано на рис. 2-17. Отметим, что в файле кода, компилируемом в одну сборку, может быть определено любое количество классов, причем здесь могут содержаться и частичные определения. Как осуществляется развертывание такого файла? У вас есть две возможности: создать дополнительный проект, на основе которого будет генерироваться DLL-библиотека компонентов, или поместить этот файл в папку с именем App_Code, содержащуюся в корневой папке приложения. В первом случае вы можете добавить новый проект к решению с помощью коман- ды File\Add. Выберите в предложенном списке типов проектов пункт Class Library, после чего добавьте в проект необходимое количество файлов классов. Завершив
Разработка Web-приложений в Microsoft Visual Studio .NET 2005 Глава 2 51 формирование проекта, включите ссылку на него в свой основной проект типа Web Site. Когда вы будете работать над последним, IntelliSense обнаружит новые классы и станет помогать вам при написании операторов доступа к их членам. Рис. 2-17. Добавление нового класса в проект ASP.NET Вы спрашиваете, что собой представляет папка Арр Code? Она имеет стандартное имя и предназначена для хранения повторно используемых компонентов, автомати- чески компилируемых ASP.NET и связываемых с кодом страницы. Здесь хранятся файлы исходного кода классов (.vb или .cs), которые перед выполнением страниц компилируются в сборку. Созданная в определенной папке и видимая всем страницам сайта, эта сборка обновляется каждый раз, когда в один из ее исходных файлов вно- сятся изменения. Важно отметить, что любые файлы, которые вы поместите в папку App Code, при развертывании приложения будут рассматриваться как его исходный код и развертываться соответствующим образом. (Об этой и других специализи- рованных папках ASP.NET более подробно рассказывается в следующем разделе.) Создание демонстрационного совместно используемого класса Чтобы на практике познакомиться с преимуществами повторно используемых компо- нентов исходного кода, мы создадим страницу, где будет присутствовать необычный компонент, который утомительно вставлять в каждую страницу, где он требуется. Страница с этим компонентом показана на рис. 2-18. Для доступа ко многим продуктам и сервисам, предоставляемым через Web автори- зированным пользователям, необходимо указывать так называемый надежный пароль. Определение понятия «надежный пароль» у каждого сервиса свое, но обычно таким считается пароль длиной не менее 8 символов, включающий не менее одного симво- ла каждой из следующих категорий: символы верхнего регистра, символы нижнего регистра, цифры, специальные символы. Этим определением мы и воспользуемся в нашем примере. Демонстрационная страница, которую предстоит создать, попросит
52 Часть I Разработка страниц ASP.NET пользователя задать желаемую длину пароля и предложит пароль, соответствующий упомянутым выше правилам. Мы создадим новый файл StrongPassword.cs и поместим его в специально подготовленный подкаталог App_Code. Структура этого класса показана ниже: public class StrongPassword { public StrongPasswordO {...} public string GenerateO {..} public string Generate(int passwordLength) } Рис. 2-18. Страница PswdGen.aspx, предназначенная для генерирования надежного пароля заданной длины У класса имеется единственный метод, Generate, генерирующий новый надежный пароль. (Конечно, определение такого пароля в общем случае является произволь- ным.) Поскольку наш класс помещен в папку App_Code, он будет компилироваться по требованию и доступ к нему получат все страницы. В демонстрационной странице код, генерирующий и проверяющий пароль, довольно прост и вполне читабелен: void buttonGenerate_Click(Object sender, System.EventArgs e) { // Получаем введенное пользователем значение желаемой длины пароля И и проверяем, является ли оно числом. Эта простая, но весьма // эффективная проверка позволяет воспрепятствовать атаке, // совершаемой путем внедрения кода или данных (code/data injection) int pswdLen = 8; bool result = Int32.TryParse(PswdLength.Text, out pswdLen);
Разработка Web-приложений в Microsoft Visual Studio .NET 2005 Глава 2 53 // Генерируем и выводим новый пароль Strong Password pswd = new StrongPasswordO; labelPassword.Text = pswd Generate(pswdLen); } Результат выполнения этого кода вы видели на рис. 2-18. Обратите внимание: того же результата можно было достичь, поместив приведенный код прямо в основной файл страницы или выделив класс StrongPassword в отдельную сборку. Заглядываем в файл web.config Поведение приложения ASP.NET во многом определяется установками, заданными в конфигурационных файлах machine.config и web.config. В machine.config содержатся задаваемые по умолчанию и действующие на уровне всего компьютера значения всех поддерживаемых параметров. Как правило, установки, действующие на уровне компьтюера, задаются системным администратором и приложениям никогда не предоставляется доступ к ним для записи. Однако приложение может переопределять большинство задаваемых по умолчанию установок, для чего используются файлы web.config. Приложение включает как минимум один файл web.config, который хранится в его корневой папке и содержит установки некого подмножества параметров из файла machine.config, заданные согласно той же XML-схеме. Еще раз хочу подчеркнуть: хотя в файле web.config и разрешается переопределять многие задаваемые по умолчанию установки, некоторые из параметров могут быть определены только в файле machine.config. Если приложение имеет дочерние каталоги, в каждом из них может содержаться свой файл web.config Область действия такого файла определяется иерархически и распространяется на все страницы, которые содержатся в одном с ним подкаталоге и подкаталогах последнего. При этом в данном файле могут переопределяться, удаляться и дополняться установки из файлов, расположенных выше него по иерархии. Visual Studio .NET автоматически генерирует для приложения файл web.config с установками, используемыми по умолчанию, однако его наличие не является обязательным и приложение вполне может работать без этого файла. Правда, в таком случае вы не сможете данное приложение отлаживать. Зарезервированные папки ASP.NET В ASP.NET используется ряд специализированных подкаталогов, которые располо- жены в корневом каталоге приложения и предназначены для хранения определен- ного контента и данных. В ASP.NET 1.x использовалась только папка Bin, однако в ASP.NET 2.0 введено семь дополнительных защищенных папок. Ни одна из них не создается ASP.NET 2.0 или Visual Studio .NET 2005 автоматически и ни одна не явля- ется необходимой для работы приложения. Эти папки либо создаются разработчиком вручную, либо по его требованию их создает Visual Studio .NET, для того чтобы можно было использовать в приложении ту или иную функцию. Специализированные папки приложения В табл. 2-1 перечислены специализированные папки, которые можно создавать в ос- новном каталоге приложения. Еще раз подчеркну, что все они, кроме папки Bin, присутствуют в нем только в том случае, когда требуются конкретному приложению. Причем на практике обычно используются только две-три из них — редкое приложе- ние содержит все семь дополнительных папок.
54 Часть I Разработка страниц ASP.NET Табл. 2-1. Специализированные зарезервированные папки ASP.NET Имя папки Содержимое Bin Все предкомпилированные сборки, необходимые для работы при- ложения App_Browsers App_Code Файлы с информацией о возможностях браузера Файлы исходного кода классов (.cs или .vb), которые будут ис- пользоваться страницами. Все классы должны быть написаны на одном языке, нельзя в одну папку поместить файлы на C# и Visual Basic.NET App_Data Файлы данных приложения. Это могут быть XML-файлы или базы данных Access, содержащие персонализированные данные App_GlobalResources App_LocalResources App_Themes Глобальные для приложения файлы ресурсов (.resx) Файлы ресурсов (.resx) для отдельных страниц Определения поддерживаемых приложением тем (подробнее о темах рассказывается в главе 6) App_WebReferences Файлы .wsdl, необходимые для связывания Web-сервисов с при- ложением Содержимое перечисленных каталогов недоступно клиентам посредством НТТР- запросов. Единственное исключение составляет содержимое папки App_Themes. О Внимание! Имена указанных специализированных папок ASP.NET зарезервированы, и изменять их нельзя. Причина данного ограничения связана с особенностью функциони- рования фильтра ISAPI, отвечающего за блокирование запросов к этим папкам. Из сооб- ражений производительности данный фильтр не может обращаться к файлу web.config за информацией об именах каталогов, так как ему пришлось бы анализировать этот файл для каждого запроса, что весьма накладно. В качестве альтернативы можно было бы хранить имена каталогов в реестре, доступ к которому осуществляется значительно быстрее. Однако применение такого подхода не позволило бы развертывать приложения методом ХСору и потребовало бы внесения существенных изменений в архитектуру ASP.NET. (Подробнее о развертывании методом ХСору рассказывается далее в этой главе, в разделе «Развертывание приложения».) Содержимое многих папок, перечисленных в табл. 2-1, при первом обращении к ним компилируется в динамическую сборку. Сказанное касается тем, программного кода, ресурсов и Web-ссылок. (Подробнее о модели компиляции ASP.NET 2.0 рас- сказывается далее в этой главе.) Папка App_Code Как упоминалось выше, в папку App_Code можно помещать создаваемые для прило- жения вспомогательные и бизнес-классы. Вы развертываете их в виде исходного кода, а исполняющая среда ASP.NET берет на себя заботу о том, чтобы они автоматически компилировались при первом к ним обращении. Ссылка на результирующую сборку автоматически включается в приложение, и эта сборка совместно используется всеми страницами сайта. Не следует хранить в папке App_Code ничего кроме компонентов, — здесь не ме- сто ни страницам, ни пользовательским элементам управления Web, ни каким-либо файлам, которые не содержат программного кода. Областью действия сборки, созда- ваемой на основе содержимого данной папки, является все приложение, а создается она в папке Temporary ASP.NET Files, то есть вне Web-пространства приложения.
Разработка Web-приложений в Microsoft Visual Studio .NET 2005 Глава 2 55 Примечание Если вы волнуетесь по поводу того, что ценные исходные файлы на C# или VB.NET будут развертываться на сервере Web, то примите во внимание, что всякий (повторяю: всякий) доступ к папке App_Code, осуществляемый через HTTP, контролируется и блокируется упоминавшимся выше ISAPI-фильтром ASP.NET. Еще раз обращаю ваше внимание на тот факт, что все файлы классов, содержа- щиеся в папке AppCode, должны быть написаны на одном языке программирова- ния — Visual Basic.NET или С#, поскольку все они компилируются в одну сборку и обрабатываются одним компилятором. Если же вы хотите использовать разные языки, создайте для каждого из них свою подпапку и внесите в конфигурационные файлы специальные записи, которые будут указывать системе компиляции, что для каждой из этих папок нужно создать отдельную сборку. Таким образом, у вас получится по одной сборке для каждого языка программирования. Предположим, вы создали два файла, с именами source.cs и source.vb. Поскольку они написаны на разных языках, хранить их вместе в папке Арр Code нельзя. Однако вы можете создать две подпапки, назвав их, скажем, App_Code\VB и App_Code\CS, и поместить каждый файл в ту папку, которая соответствует его языку Затем нужно будет добавить в файл web.config следующие записи: <configuration> <system.web> <compilation> ccodeSubDi rectories> Odd di rectoryName="VB" /> Odd directoryName="CS" /> </codeSubDi recto ries> </compilation> </system.web> </configuration> Учтите, что раздел <codeSubDirectories> может содержаться только в том файле web.config, который находится в корневом каталоге приложения. Каждый его под- раздел указывает системе компиляции на необходимость создать отдельную сборку. При этом все файлы, находящиеся в заданном в этом подразделе подкаталоге, должны быть написаны на одном языке, но в разных подкаталогах могут находиться файлы, написанные на разных языках. Примечание В папке App_Code могут также храниться файлы XSD (такие файлы генерируются в частности, для типизированных объектов DataSet). Файл XSD содержит строго типизированную схему таблицы данных. В .NET Framework 1.1 типизированный DataSet создается вручную, с помощью утилиты xsd.exe, а в ASP.NET 2.0 для этого до- статочно переместить исходный XSD-файл в папку App_Code. Папки ресурсов Если Web-страница является локализуемой, то вместо жестко закодированного текста во время формирования пользовательского интерфейса в ней используются строки, которые хранятся отдельно в виде ресурсов. Связав сборку ресурсов с приложением, ASP.NET может динамичкски извлекать из нее ресурсы для адаптации пользова- тельского интерфейса приложения к языку и культуре пользователя. В ASP.NET 1. х разработчикам приходилось создавать подобные сопутствующие сборки вручную, но теперь, в ASP.NET 2.0, они создаются автоматически — система компилирует все файлы ресурсов, найденные в папках App_LocalResources и App_GlobalResources. В первой из указанных выше папок содержатся файлы ресурсов, которые связаны с конкретными страницами. Связь между файлом страницы и файлом ее ресурсов
56 Часть I Разработка страниц ASP.NET устанавливается с использованием очень простого соглашения об именах: если стра- ница называется sample.aspx, то соответствующий файл ресурсов будет иметп имя sample.aspx.resx. Данный файл не связан с определенным языком и культурой. Что- бы создать сборку ресурсов для какой-либо культуры, скажем, итальянской, Нужно добавить в имя файла ее идентификатор, вот так: sample.aspx.it.resx. Здесь элемент it является идентификатором итальянской культуры, и его можно заменить иден- тификатором fr (французская), еп (английская) и т. д. На рис. 2-19 показан пример содержимого папки локальных ресурсов. Рис. 2-19. Папка локальных ресурсов страницы respage.aspx Локальные ресурсы обеспечивают возможность неявной локализации страницы, при которой каждому элементу управления ставится в соответствие определенный элемент файла .resx. Вот каким станет код нашей демонстрационной страницы после его переработки для поддержки локальных ресурсов: <%@ Page Language="C#" meta:resourcekey="PageResoureel” UICulture=”auto” %> <html> <head id="Head1" runat₽="server"> <title>Pro ASP.NET (Ch 02)</title> </head> <body>
Разработка Web-приложений в Microsoft Visual Studio .NET 2005 Глава 2 57 <Ы> ь <asp:Label runat="server” id="H1” meta:resourcekey="LabelResoureel" /> </h1> <form id=".Form1" runat="server"> <asp:Button ID="btn" Runat="server" meta:resourcekey="BtnResource1" /> </form> </body> </html> Самой странице и каждому входящему в ее состав элементу управления назначается ключ ресурса. Файл .resx содержит набор элементов, идентификаторы которых имеют форму ключ_ресурса.имя_свойства. Например, свойство Text кнопки неявно связано с элементом BtnResourcel.Text файла .resx. Для установления такого соответствия вам не придется писать ни строчки кода, а лишь заполнить определенным образом файл ресурсов. Присваивая атрибуту UlCulture значение auto, вы тем самым указываете исполняющей среде ASP.NET, что для выбора культуры она должна использовать языковые установки текущего браузера. /г| Совет Чтобы быстро протестировать работу страницы с разными языками, откройте окно Аг свойств Internet Explorer и на вкладке Общие щелкните в нем кнопку Языки, после чего добавьте в список интересующие вас языки, а тот, который собираетесь протестировать, переместите в начало списка. После этого Internet Explorer будет отправлять в каждом запросе идентификатор выбранного вами языка. На рис. 2-20 показано, как одна и та же страница выглядит на разных языках. Рис. 2-20. Страница respage.aspx на английском и итальянском языках Неявная локализация действует автоматически, то есть вам не нужно определять, как должна считываться из файла ресурсов информация каждого свойства. Однако
58 Часть I Разработка страниц ASP.NET иногда необходим некоторый контроль над процессом установки свойств, и в таком случае можно обратиться к глобальным ресурсам. Когда вы добавляете в приложение файл ресурсов, Visual Studio .NET создает папку AppGlobalResources и помещает туда новый файл .resx. Вы можете переименовать его по своему усмотрению и заполнить строками, изображениями, звукозаписями — иными словами, всем, что необходимо для вашего приложения (рис. 2-21). ProAspNet20 Microsoft Visual Studio File Edit yiew websfte guild gebug Resources Joels Window > Debug »- Strings Мали New Image Add New Ijpn Add New Text Fie App _GlobalRes_./Reeource.resx Add New String вя‘ J Add Existing File... PNG Image.. BMP Image... £IF Image. JPEG Image. JIFF Image. Рис. 2-21. Редактор ресурсов Visual Studio .NET В коде страницы или элемента управления для ссылки на ресурсы используются выражения, как в следующем примере. <asp:Label Runat="server” Text="<%$ Resources:Resource, Msgl %>" /> Здесь Resources — имя пространства имен объекта, Resource — имя содержащего ресурсы файла .resx, a Msg1 — имя используемого ресурса. О возможности явной локализации ресурсов вы вспомните в том случае, когда понадобится локализовать большие блоки текста или пользовательские сообщения. ।__] Примечание Областью действия результирующей сборки ресурсов является все при- ложение, и ссылки на нее помещаются в другие сборки, генерируемые для приложения. Все типы, определяемые в сборках ресурсов принадлежат к пространству имен Resources и являются статическими объектными типами. Связанные Web-сервисы Когда вы добавляете в приложение ссылку на Web-сервис, в папку AppWebReferences загружается его .wsdl-файл. Во время выполнения все файлы WSDL, обнаруженные в указанной папке, динамически компилируются в прокси-классы C# или Visual Ba- sic. NET подобно классам бизнес-логики из папки App Code. Заметьте, что ASP.NET 1.x требует, чтобы при наличии ссылки на Web-сервис вы с помощью Visual Studio .NET 2003 явно создали соответствующий прокси-класс. Если вы получили файл WSDL иным способом (не путем загрузки с помощью мастера Add Web Reference), то можете поместить его в папку App WebReferences вручную. Доступные темы Папка App Themes предназначена для хранения файлов тем элементов управления Темой называется набор обложек и связанных с ними файлов, таких как таблицы стилей и изображения. Этот набор используется в приложении для создания едино-
Разработка Web-приложений в Microsoft Visual Studio .NET 2005 Глава 2 59 образного пользовательского интерфейса элементов управления. Для каждой темы в папке AppThemes создается своя подпапка, имя которой совпадает с именем темы. Здесь хранятся все связанные с данной темой файлы. При загрузке темы содержимое этой папки анализируется и компилируется в класс, который является производным от известного базового класса. Темы, определенные вне папки App Themes, игнорируются системой компиляции ASP.NET. Компиляция проекта Для того чтобы откомпилировать и запустить приложение ASP.NET, достаточно щел- кнуть кнопку Start, расположенную на панели инструментов, или нажать клавишу F5 и дождаться появления окна браузера. Этап компиляции, в ходе которого обрабатыва- ются классы отделенного кода и вспомогательные классы, в Visual Studio .NET 2005 отсутствует. Все динамически сгенерированные и предварительно откомпилированные сборки развертываются в папку Bin и связываются с приложением только тогда, когда они требуются той или инрй запрошенной странице. После компиляции всех необходимых сборок Visual Studio .NET для отладки автома- тически подсоединяется к исполняющему процессу ASP.NET, которым обычно является w3wp.exe. В завершение программа открывает начальную страницу приложения. ©Внимание! Исполняющий процесс ASP.NET может быть и другим — все зависит от того, какая модель процесса используется на данном компьютере. Причем модель про- цесса можно выбирать; установка же по умолчанию зависит от операционной системы и установок Web-сервера. Если Web-сервер функционирует под управлением Windows 2000 Server или одной из версий Windows ХР, исполняющим процессом является aspnet_wp.exe. Он работает от имени учетной записи ASPNET, имеющей весьма ограниченные разре- шения, и взаимодействует с IIS 5.x. Если же у вас установлены Windows 2003 Server и IIS 6.0 и используется модель процесса по умолчанию, исполняющим процессом является w3wp.exe — стандартный рабочий процесс IIS 6.0. Процесс w3wp.exe действует от имени учетной записи NETWORK SERVICE. Этот процесс ничего не знает об ASP.NET, но его поведение легко настраивается с помощью копии ISAPI-расширения ASP.NET, зависящего от версии IIS. В IIS 6.0 можно даже переключиться на модель процесса IIS 5, но в таком случае значительно снизится производительность системы. При компиляции проекта Visual Studio .NET 2005 может пожаловаться на от- сутствие файла web.config, необходимого для отладки приложения. Если вы хотите просто выполнить страницу, не отлаживая ее, щелкните в окне сообщения кнопку Run. В качестве альтернативы Visual Studio может сгенерировать для вас необходимый файл web.config. Если вы будете создавать этот файл самостоятельно, не забудьте включить в него строку, разрешающую отладку. Compilation debug="true" /> После этого вы сможете приступить к отладке приложения. Функции отладки Если вы откомпилировали проект в режиме отладки (этот режим используется по умолчанию), то можете устанавливать в исходном коде точки останова и выполнять его в пошаговом режиме, как показано на рис. 2-22. В меню Debug в Visual Studio .NET 2005 содержится немного больше команд, чем в предыдущей версии этой системы. В частности, расширены возможности, связанные с обработкой исключений и точками останова. Теперь вы можете настроить IDE та- ким образом, чтобы она автоматически выполняла останов при выбросе исключения. Данная функция выполнена очень продуманно, и она действительно удобна. Функция позволяет проанализировать все исключения, сконцентрироваться на исключениях из заданной группы или исключениях, не обработанных приложением.
60 Часть I Разработка страниц ASP NET 121В public partial class PswdGen_aspx 13jl { 14h void buttonGenerate_Click(Object sendur, System. Event Args e> 15^ { 16 17 18 19 20 21 22l 23 24! // Get the password length ini pswdLen = 8; bool result *• Int32.TryParse (PswdLength.Text, out pswdLen); StrongPassword pswd ” new StrongPassword(); labelPassword.Text “ pswd.Generate(pswdLen); Рис. 2-22. Пошаговое выполнение с заходом с использованием интегрированного отладчика Visual Studio .NET Для точек останова можно задавать конкретное абсолютное местоположение или же относительное, то есть позицию в коде определенной функции. Предусмотрена даже возможность выполнить останов при изменении содержимого памяти по за- данному адресу. Пользовательский интерфейс окон Watch стал богаче, и теперь в его основу по- ложена концепция визуализаторов. Визуализатором называется всплывающее окно, в котором данные определенного типа представлены в более дружественной и удо- бочитаемой форме — в виде XML, текста или DataSet (рис. 2-23). Рис. 2-23. Текстовый визуализатор, вызванный из окна QuickWatch во время отладки
Разработка Web-приложений в Microsoft Visual Studio .NET 2005 Глава 2 61 ’ Т •kjs ф Как показано на рис. 2-24, визуализаторы могут также вызываться из специального окна подсказки (code tip), выводимого при наведении указателя мыши на ту или иную переменную в программном коде и содержащего значение этой переменной. >;Е) public partial class PwwdGen_awpx : Page ected void buttonGenerate_Click(Object sender. System. EventArgs e) // Get the password length int pswdLen *6; bool result " Int32.TryParse(PswdLength.Text, out pswdLen); StrongPawcword pswd “ new StrongPas*word(); |iabelPassword. T2xtj - prwd. Generate ( pswdLen) j laoelFassword.Text a - "[P isswords display here]" • - > -» ipt i * ✓ Text Visualizes XML Visualizes HTML Visualizes Рис. 2-24. Вызов визуализатора из окна подсказки Визуализаторы определены для нескольких типов данных. Лично мне больше всего нравится визуализатор типа DataSet — с его помощью очень легко увидеть, что содержит экземпляр объекта DataSet. Тестирование приложения Как уже упоминалось, в Visual Studio .NET 2005 имеется два способа тестирования страниц: их может предоставлять либо IIS (если он установлен), либо встроенный локальный Web-сервер. По умолчанию Visual Studio .NET использует IIS, когда вы открываете проект как Web-сайт и указываете его URL; в противном случае использу- ется локальный Web-сервер. Важная особенность процесса выполнения приложения на локальном Web-сервере связана с контекстом защиты. Под управлением IIS при- ложение ASP.NET обслуживается рабочим процессом, который выполняется от имени определенной для приложения реальной учетной записи — обычно это учетная запись ASPNET или NETWORK SERVICE с очень ограниченными разрешениями. Встроенный же Web-сервер использует токен защиты текущего пользователя, то есть ваш. Это означает, что если разработчик в данный момент вошел в систему как администратор (а так бывает гораздо чаще, чем следовало бы), приложение получает административные привилегии. И проблема здесь даже не в риске атаки системы; главная проблема в том, что вы тестируете приложение в условиях, отличных от тех, в которых оно будет эксплуатироваться. Та страница, которая будет успешно выполнена локальным Web-сервером, может «с треском провалиться» при попытке запуска под управлением IIS. Для простого приложения, которое лишь читает и выполняет страницы ASP.NET, эта проблема не столь существенна. Однако результаты тестирования на локальном сервере будут менее надежны, если приложение осуществляет доступ к файлам, от- личным от Web-страниц, файлам, расположенным на других компьютерах, реестру Windows, локальной либо удаленной базе данных. Во всех этих случаях необходимо убедиться, что реальная учетная запись ASP.NET имеет разрешения, достаточные для работы с указанными объектами. Итог всех приведенных выше рассуждений таков: хотя вы можете использовать локальный Web-сервер для тестирования страниц, сценарий тестирования при этом не всегда получается реалистичным.
62 Часть I Разработка страниц ASP.NET Развертывание приложения Инсталляция любого приложения .NET, и в частности приложения ASP.NET, вы- полняется предельно просто — путем рекурсивного копирования всех файлов в це- левую папку на целевом компьютере. Данную процедуру называют развертыванием методом ХСору, по имени Windows-утилиты, с помощью которой осуществляется рекурсивное копирование файлов. Это название подчеркивает, что развертывание Web- приложения является не чем иным, как копированием файлов, не требующим ни редактирования реестра, ни регистрации и конфигурирования компонентов, ни создания локальных файлов. Или, во всяком случае, что ничего из перечисленного делать не нужно, поскольку .NET Framework делает все необходимое сама. Развертывание методом ХСору Задачу развертывания Web-приложения можно выполнить разными способами в за- висимости от конкретной ситуации: с помощью FTP, любого средства управления сервером, осуществляющего интеллектуальную репликацию, или же инсталляцион- ного приложения MSI. Можно даже использовать функцию Copy Web Site из Visual Studio .NET 2005, описанную ранее в этой главе. Каждый из перечисленных способов имеет свои достоинства и недостатки, и выбор между ними осуществляется с учетом конкретной хост-среды и конкретных обстоя- тельств. Однако имейте в виду, что если вы собираетесь развертывать приложение на площадке провайдера интернет-услуг, то будете вынуждены играть по его правилам и использовать указанные им средства. В случае, когда требуется развернуть интерфейс- ную часть существующей системы на серверах разного типа, пожалуй, проще всего создать инсталляционный проект. Что касается FTP-средств, то они удобны своей универсальностью и подходят для сопровождения приложения и быстрого внесения изменений. Наконец, специализированные средства обеспечивают возможность ав- томатической синхронизации реплик. Так что в итоге — выбор за вами. Копирование файлов Протокол FTP дает вам значительную свободу действий, позволяет модифицировать и заменять отдельные файлы. Но такое решение не является автоматизированным: все приходится делать вручную Если у вас имеется полный доступ к удаленному сайту, FTP используется примерно так же, как Проводник Windows в локальной сети. На мой взгляд, с появлением в Visual Studio .NET 2005 функции Copy Web Site потребность в FTP-доступе вообще в значительной мере уменьшается. Ведь по своей сути это FTP-подобное средство, предназначенное для доступа к удаленным файловым системам. Новое средство копирования сайта обладает и функцией синхронизации. Конечно, ему далеко до специализированных средств управления сервером, но оно прекрасно подходит для использования во многих реальных ситуациях. В конце концов, ведь и специализированное средство репликации сайта всего лишь копирует файлы из одного места в другое. Его достоинствами по праву считаются удобный пользовательский ин- терфейс и интеллектуальность, однако они надстроены поверх этой базовой функции. Такое средство поддерживает базу данных файлов со штампами времени, атрибутами, свойствами и может автоматически синхронизировать версии сайта, благодаря чему минимизируются трудозатраты разработчиков и администраторов. Создание инсталляционного проекта Довольно типичным способом развертывания приложения является использование инсталляционного файла. В данном случае развертывание осуществляется в два этапа:
Разработка Web-приложений в Microsoft Visual Studio .NET 2005 Глава 2 63 вначале создается и конфигурируется виртуальный каталог, а затем туда копируются необходимые файлы. Visual Studio .NET предельно облегчает создание инсталляци- онного файла. Вы просто создаете проект типа Web Setup Project (его интерфейс показан на рис. 2-25), выбираете подлежащие копированию файлы, после чего ком- пилируете проект. Рис. 2-25. Создание инсталляционного проекта Web-приложения Узел Web Application Folder представляет виртуальный каталог нового приложения на целевом компьютере. Свойства этого каталога настраиваются в окне Properties. На- пример, свойство AllowDirectoryBrowsing позволяет разрешить или запретить просмотр содержимого каталога. Вы можете задать имя виртуального каталога, разрешение на выполнение приложения, уровень изоляции и страницу, открываемую по умолчанию. Подпапка Bin создается в нем автоматически; кроме того, могут создаваться и запол- няться любые другие указанные вами папки. В результате компиляции такого проекта вы получите .msi-файл Windows Installer, содержащий все необходимое для инсталляции приложения у ваших клиентов. По умолчанию инсталляционный пакет поддерживает функции восстановления и деин- сталляции приложения и не содержит .NET Framework. Предполагается, что она уже установлена на целевом компьютере, но если этот не так, ее можно явно включить в инсталляционный пакет. Что еще вам нужно сделать Одной из замечательных особенностей сборок .NET является то, что это компоненты с самоописанием. Если какому-либо приложению потребуется информация о вну- тренних характеристиках сборки, ему достаточно лишь запросить ее у самой сборки!
64 Часть I Разработка страниц ASP.NET Необходимый для этого API определяет поддерживаемая .NET технология рефлексии (.NET reflection). Таким образом устраняется необходимость в использовании реестра (или другого централизованного репозитария), где хранились бы пути к двоичным ком- понентам и их атрибуты. Еще одним достоинством сборок .NET является то, что теперь они могут выполняться параллельно, и приложения ASP.NET активно этим пользуются В частности, когда вы обновляете страницу, две версии «одной» сборки сосуществуют в течение некоторого времени, не мешая друг другу и не конфликтуя. Благодаря такой структуре приложения для него подходит модель развертывания ХСору. А что еще вам нужно сделать для завершения развертывания приложения? Если приложение содержит файлы, используемые как для чтения, так и для записи (XML, конфигурационные базы данных Access), то вам необходимо предоставить разрешения на их запись. Точно так же, если приложение или элемент управления генерирует временные файлы, необходимо позаботиться о наличии папок с соответствующими разрешениями, где эти файлы будут создаваться. Указанные задачи можно выполнить разными способами, но сделать это нужно обязательно и, естественно, до того, как приложение будет запущено. Если вы пользуетесь услугами интернет-провайдера, то вам обычно предоставляется изолированное поддерево папок на диске с полными разрешениями на запись туда для учетной записи ASP.NET Поэтому разрабатываемое приложение следует сделать достаточно гибким, чтобы впоследствии можно было конфигурировать пути к временным файлам. [ ^,1 Примечание Мы ничего не говорим здесь о конфигурировании баз данных, предполагая, что все необходимые базы данных созданы, сконфигурированы и уже работают. Если это не так, то вы должны будете решить и эту задачу. А еще вам придется позаботиться о доступе к удаленным приложениям и сервисам, которые могут потребоваться вашему приложению, в частности к Web-сервисам и компонентам СОМ+. Конфигурирование исполняющей среды Еще одним важным аспектом, о котором вам необходимо позаботиться, является конфигурирование исполняющей среды. В процессе разработки приложения вы те- стируете его на одном компьютере, а эксплуатировать будете на другом, с другим файлом machine.config, и едва ли вам удастся в точности перенести в него все уста- новки из своего файла machine.config. Скорее всего, администратор не позволит вам модифицировать данный файл, поскольку содержащиеся в нем установки подобраны так, чтобы обеспечить оптимальную работу остальных приложений. (Особенно это верно, если вы пользуетесь услугами интернет-провайдера.) Решить указанную проблему можно путем репликации необходимых устано- вок из файла machine.config в файл web.config. Однако если вы развертываете код у провайдера, может оказаться, что и это невозможно, поскольку установки в файле machine.config заблокированы и не могут быть переопределены. В таком случае нужно очень-очень попросить администратора внести изменения в конфигурацию сервера необходимым для вашего приложения образом, но так, чтобы не помешать работе других приложений. Обычно для этой цели в серверном файле machine.config создается связанный с конкретным приложением раздел <location>. Развертывание приложения ASPNET в Web-ферме также связано с определенными конфигурационными сложностями. Все файлы machine.config Web-фермы должны быть синхронизированы. Проще всего поместить необходимые установки в файл web.config приложения, что очень упростит развертывание, поскольку вам останется лишь выполнить установку приложения на каждом из компьютеров, а никаких других изменений в их конфигурацию вносить не придется. Если же необходимые разделы заблокированы, что опять-таки характерно для интернет-провайдеров, то вы
Разработка Web-приложений в Microsoft Visual Studio .NET 2005 Глава 2 65 окажетесь в описанной выше ситуации, и вам ничего не останется, как уговаривать администратора создать для вас в файлах machine.config новый раздел <location>. Примечание Раздел <location> может использоваться в файлах обоих типов, machine.config и web.config, и позволяет ограничить действие заданных в нем установок определенной папкой (то есть приложением или его частью). В файл machine.config указанный раздел обычно помещают в случаях, подобных описанному выше, для чего требуются разрешения администратора. В файлах web.config он используется в сложных системах, состоящих из главного и нескольких подчиненных приложений. Предкомпиляция сайта Как упоминалось выше, динамически создаваемые сборки помещаются во внутри- системную папку, которой управляет исполняющая среда ASP.NET. Если исходные файлы приложения не изменяются, компиляция каждой страницы выполняется лишь однажды — при первом к ней обращении. И хотя во многих случаях связанная с этим задержка между получением запроса страницы и ее выдачей незначительна, ее можно устранить и тем самым несколько оптимизировать производительность сайта. Для этого нужно заранее откомпилировать весь сайт и развернуть его уже в виде готовых сборок. Предкомпилированное приложение также содержит исходные файлы, но первое (фиктивное) обращение к страницам и ресурсам было произведено еще до развертывания, так что все его сборки уже созданы. Это делается на том компьютере, где ведется разработка или тестирование приложения, потом готовые сборки упаковы- ваются и инсталлируются на целевой компьютер. Предкомпиляция сайта позволяет не копировать на целевой компьютер ценные исходные файлы, оберегая тем самым вашу интеллектуальную собственность. ©Внимание! Исходные файлы, содержащие классы C# или сценарии WSDL, защищены от доступа через HTTP. Однако если хакеру удастся проникнуть в систему, он сможет получить к ним доступ. Предкомпиляция сайта была возможна и в ASP.NET 1.x, но в версии 2.0 она стала системной функцией, полностью поддерживаемой исполняющей средой. Ее осущест- вление дает вам два важных преимущества: запросы к сайту выполняются без задержки, поскольку страницы и код заранее откомпилированы в сборки; сайты можно развертывать без исходного кода, оберегая таким образом интеллек- туальную собственность — реализованные в них технологические и архитектурные решения. Поддерживаются две базовые формы предкомпиляции: на месте и для развер- тывания Примечание Для защиты интеллектуальной собственности в дополнение к предкомпиля- ции сайта можно применить метод запутывания (obfuscation). Он заключается в изменении имен метаданных сборки с целью воспрепятствования сканированию файлов, осуществляе- мому потенциальными взломщиками кода в поиске важных строк. Запутывание никак не влияет на выполнение кода, зато исполняемые файлы сжимаются и загружаются немного быстрее. В случае декомпиляции сборки, к которой применен метод запутывания, получа- ется несколько менее читабельный промежуточный код. Данный метод применим ко всем приложениям .NET, и в Visual Studio .NET 2005 для его реализации имеется специальный инструмент — версия Community Edition коммерческого средства Dotfuscator. Предкомпиляция на месте Данный способ предкомпиляции позволяет разработчику или администратору сай- та использовать любую страницу приложения так, как ее использовал бы конечный
66 Часть I Разработка страниц ASP.NET пользователь. Иными словами, страницы компилируются, как при обычном исполь- зовании, но сайт перед вводом в эксплуатацию компилируется полностью, и поль- зователь не испытывает задержек при первом обращении к страницам, как в версии ASP.NET 1.x. Такая предкомпиляция осуществляется уже после развертывания сайта, но до его открытия для общего доступа. Для предкомпиляции сайта на месте исполь- зуется следующая команда, в которой /proaspnet20 — это имя виртуальной папки приложения: aspnet^compiler -v /proaspnet20 Если выполнить предкомпиляцию сайта еще раз, компилятор пропустит актуаль- ные страницы и обработает только новые и измененные файлы, а также те, которые от них зависят. Благодаря такой оптимизации можно перекомпилировать сайт после каждого, даже самого малого, изменения. Предкомпиляция на месте является своего рода пакетной компиляцией, при ко- торой генерируются все необходимые сборки, помещаемые в фиксированный каталог ASP.NET на серверном компьютере. Если один из файлов откомпилировать не удается, вся процедура предкомпиляции завершается неудачей. Предкомпиляция для развертывания В случае выполнения предкомпиляции для развертывания генерируется представле- ние сайта, состоящее из сборок, статических и конфигурационных файлов, — своего рода манифест сайта. Такое представление создается на исходном компьютере, где осуществляется разработка или тестирование сайта, а затем копируется на тот ком- пьютер, где он будет эксплуатироваться. Возможно также создание инсталляционного пакета MSI. Предкомпиляция для развертывания также осуществляется с помощью утилиты с интерфейсом командной строки aspnet_compiler: aspnet_compiler -m путь_к_метабазе -с виртуальный_путь -р физический_путь целевой_путь Назначение параметров этой команды описано в табл. 2-2. Табл. 2-2. Параметры утилиты aspneLcompiler Параметр Описание путь к метабазе Необязательный параметр, указывающий полный путь к IIS-метаба- зе приложения виртуальный путь Обязательный параметр, задающий виртуальный путь к компилиру- емому приложению физический_путъ Необязательный параметр, указывающий физический путь к компи- лируемому приложению целевой_путь Необязательный параметр, задающий целевой путь к откомпилиро- ванному приложению Если целевой путь не задан, предкомпиляция выполняется по исходному пути приложения, благодаря чему в результирующем приложении сохраняются исходные файлы. Если же задан целевой путь, отличный от исходного, по этому целевому пути размещаются только сборки, и новое приложение выполняется без использования файлов исходного кода. Например, следующая команда позволяет откомпилировать приложение ProAspNet20, разместив результат по заданному дисковому пути: aspnet_compiler -v /ProAspNet20 c:\ServerPath
Разработка Web-приложений в Microsoft Visual Studio NET 2005 Глава 2 67 Статические файлы, такие как файлы изображений, web.config и HTML-файлы страниц, не компилируются — они просто копируются по целевому пути. ©Внимание! Если вы не хотите развертывать HTML-страницы в виде чистого текста, из- мените их расширение на .aspx и откомпилируйте. Так же можно поступить и с файлами изображений. Однако если вы решите скрыть изображения и HTML-файлы за расшире- нием ASP.NET, снизится производительность приложения, поскольку обычно эти файлы предоставляет по запросу сам IIS, а это, конечно же. происходит быстрее, чем обработка запроса конвейером ASP.NET. Предкомпиляция и развертывание могут осуществляться в двух разных формах: с поддержкой и без поддержки обновлений. Сайты, упакованные для развертывания, не позволяют динамически изменять свое содержимое. Когда необходимо внести из- менение, вы модифицируете исходные файлы, перекомпилируете весь сайт и повторно его развертываете. Исключение составляет конфигурация сайта: вы можете обновить на эксплуатационном сайте файл web.config, не перекомпилируя сам сайт. Сайты, предкомпилированные для развертывания с поддержкой обновлений, со- держат полный набор исходных файлов, включая файлы классов и ресурсов. Ком- пилятор, однако, не обрабатывает файлы .aspx, а просто копирует их из одного места в другое. Благодаря этому вы получаете возможность вносить в сайт некоторые из- менения даже после его компиляции. Например, можно изменять местоположение элементов управления, а также настройки цветов и шрифтов, другие визуальные установки. Можно даже включать в состав существующих страниц новые элементы управления, если только для них не требуются обработчики событий или иной код. Однако вам не удастся добавить в состав предкомпилированного сайта новые стра- ницы — для этого придется перекомпилировать его заново. Из двух подходов к предкомпиляции для развертывания первый, без поддержки обновлений, обеспечивает наилучшую защиту страниц и наилучшую начальную про- изводительность. Второй подход, с поддержкой обновлений, требует выполнения части работы по компиляции страницы перед первым ее запуском. Он ближе к модели компи- ляции и развертывания ASP.NET 1.1, когда файлы .aspx развертываются в виде исходного кода и все классы (включая классы отделенного кода) компилируются в сборки. Администрирование приложения ASP.NET В дополнение к работающим страницам, графике, а также компонентам и сервисам, используемым серверной частью приложения, реальное Web-приложение содержит ад- министративные средства для управления учетными записями пользователей, функци- ями защиты и конфигурацией. В большинстве случаев эти средства имеют простейший пользовательский интерфейс, надстроенный поверх таблиц базы данных, за создание которых отвечает разработчик приложения. Для экономии времени такие средства создаются как приложения Web Forms. Если приложение спроектировано правильно, многие объекты доступа к данным и бизнес-логики могут использоваться повторно. Но всегда ли необходимо создавать такие внешние дополнительные приложения? Хотя в отдельных случаях специализированные служебные приложения действи- тельно нужны, Visual Studio .NET 2005 предоставляет в ваше распоряжение столь богатый набор разнообразных средств, что ничего другого вам, скорее всего, не по- требуется. В частности, здесь есть целое Web-приложение, предназначенное для ад- министрирования различных аспектов функционирования сайта. Это приложение, называемое Web Site Administration Tool (WSAT), создано на основе модели провай- деров ASP.NET и запускается с помощью команды Website\ASP.NET Configuration или одноименной кнопки панели инструментов в окне Solution Explorer.
68 Часть I Разработка страниц ASP.NET Web Site Administration Tool На рис. 2-26 представлено средство администрирования Web-сайта. Его пользователь- ский интерфейс разбит на четыре блока, в соответствии с областями администриро- вания: членство, пользовательские профили, параметры приложения и провайдеры. Рис. 2-26. Средство администрирования Web-сайта из Visual Studio .NET 2005 WSAT — это отдельное приложение, которое устанавливается вместе с ASP.NET 2.0 с полным своим исходным кодом, который вы найдете в подпапке ASP.NETWebAd- minFiles инсталляционной папки ASP.NET. Путь к последней таков: %WINDOWS%\Microsoft.NET\Framework\[BepcM«] Эту утилиту можно запустить и извне Visual Studio, но тогда нужно задать пара- метр, указывающий, какое приложение вы собираетесь конфигурировать. Вот полный URL, который нужно ввести в браузере для работы с приложением ProAspNet20: http://localhost :ХШ/аэр. netwebadminfiles/default.aspx?applicationUrl=/ProAspNet20 Здесь XXXX — номер порта, используемого вашим локальным Web-сервером. При- ложение WSAT из очевидных соображений, связанных с безопасность#), закрыто для публичного доступа через IIS. В табл. 2-3 перечислены группы установок, которые определяются посредством данного приложения. Табл. 2-3. Группы установок, определяемых посредством WSAT Вкладка Назначение Security Добавление и редактирование учетных записей пользователей, ролей и раз- решений на доступ к сайту Application Управление конфигурационными установками приложения, такими как пара- метры отладки и SMTP Provider Выбор провайдеров, которые будут использоваться для каждой из функций ASP.NET
Разработка Web-прило^ний в Microsoft Visual Studio .NET 2005 Глава 2 69 Управление членством и ролями На вкладке Security приложения WSAT осуществляется управление всеми параметрами защиты приложения. Вы можете выбрать метод аутентификации, настроить учетные за- писи пользователей и пароли, создать роли и группы пользователей, а также определить правила управления доступом к отдельным частям приложения. Специальный мастер проведет вас по всем этапам настройки учетных записей пользователей и ролей. По умолчанию информация о членстве и ролях хранится в локальной базе данных SQL Server (aspnetdb.mdf), которая находится в папке App_Data вашего Web-сайта. Если необходимо, чтобы эта информация хранилась в другом месте, выберите соответству- ющий провайдер на вкладке Provider. В ASP.NET 1.1 типичной практикой было создание пользовательской базы дан- ных для хранения сведений об авторизированных пользователях. Ее необходимо в какой-то момент заполнить, а кроме того, администратор базы данных должен иметь возможность управлять учетными записями пользователей и ролями. В ASP.NET 1.1 существовало несколько путей решения этой проблемы: озадачить собственных раз- работчиков, обратиться к внешним консультантам (разумеется, небезвозмездно) или же приобрести продукт сторонних производителей. Если удастся найти продукт, ко- торый полностью отвечает вашим требованиям как в отношении функциональности, так и в вопросе цены, то лучше на нем и остановиться. Доморощенный код наверняка будет обладать более скромной функциональностью, будет менее надежным, и, скорее всего, при его написании вам придется отказаться от реализации некоторых важных элементов защиты (таких как требование изменения пароля каждые п дней) При всем этом он обойдется дороже готового. С другой стороны, разработка аналога WSAT не является такой уж невыполнимой задачей, и, вполне вероятно, вы сможете выделить деньги на ее решение из бюджета проекта. Но наличие готового средства такого рода, интегрированного в среду разра- ботки, — это же просто замечательно! Оно позволяет вам выполнять базовые задачи администрирования вообще без лишних затрат, а когда требуются еще какие-либо функции, отсутствующие у WSAT, вы, имея ее исходный код, можете бесшовно ин- тегрировать необходимые дополнения. Управление параметрами приложения Иногда приложения ASP.NET используют информацию (установки пользовательского интерфейса, предпочтения пользователей, общие предпочтения, строки подключения), которую вы не хотите «зашивать» в страницы. И хотя приложение может содержать собственные средства конфигурирования данных (например, хранить их в базе данных или XML-файле), удобным решением во многих случаях является использование спе- циального раздела <appSettings> из файла web.config. Этот раздел предназначен для хранения параметров приложения, которые могут быть выражены в простой форме имя-значение. Для редактирования данного раздела удобно пользоваться вкладкой Application утилиты WSAT. Как видно на рис. 2-27, данная вкладка позволяет также устанавливать параме- тры отладки и трассировки приложения, управлять установками SMTP. Последние, в частности, определяют, как ваше Web-приложение будет отправлять сообщения электронной почты. Если почтовый сервер, которым вы пользуетесь, требует, чтобы вы перед тем, как отправить сообщение, указывали имя и пароль, то здесь можно задать тип требуемой сервером аутентификации и, при желании, эти самые имя и пароль. ., На вкладке Application имеется также раздел, где вы можете указать, какие стра- ницы сообщений будут выводиться для определенных ошибок HTTP.
70 Часть I Разработка страниц ASP.NET Рис. 2-27. Вкладка Application утилиты WSAT Выбор и конфигурирование провайдеров Информацию о членстве и профилях нужно где-то хранить. Описанная в главе 1 модель провайдеров ASP.NET 2.0 предоставляет вам механизм подключения компо- нентов, позволяющий выбрать такое средство поддержки хранения данных, которое лучше всего подходит для вашего конкретного приложения. В составе ASP.NET 2.0 имеются предопределенные провайдеры членства, ролей и персонализации, использу- ющие локальные файлы SQL Server. Но поскольку ключевой характеристикой модели провайдеров является расширяемость, ничто не мешает вам написать и интегрировать в систему собственные провайдеры. Если вы хотите заменить используемый по умолчанию провайдер определенной функции, воспользуйтесь вкладкой Provider, показанной на рис. 2-28. Здесь же можно зарегистрировать новый провайдер. Редактирование конфигурационных файлов WSAT — прежде всего административное средство, и хотя оно позволяет редактиро- вать определенные области конфигурационных файлов, его нельзя рассматривать как полноценный редактор файла web.config. В Visual Studio .NET для этого предусмотрен специальный текстовый редактор. В версии 2005 он был усовершенствован и теперь полностью поддерживает функцию IntelliSense. Но хотя эта функция, безусловно, помогает делу, редактирование файлов web.config средствами IDE по-прежнему тре- бует ввода с клавиатуры — в частности, вам без конца приходится вводить угловые скобки. Возникает вопрос, не существует ли возможности еще больше облегчить редактирование этих файлов? Визуальный редактор файлов web.config В ASP.NET имеется интерактивное средство для конфигурирования исполняющей среды и редактирования файла web.config. Это средство реализовано как расширение
Разработка Web-приложений в Microsoft Visual Studio .NET 2005 Глава 2 71 IIS Microsoft Management Console (ММС), в которую при его установке добавляется новая вкладка — ASP.NET (рис. 2-29). Рис. 2-28. Вкладка Provider утилиты WSAT Рис. 2-29. ММС-оснастка ASP.NET Для доступа к этой оснастке нужно открыть консоль IIS ММС и выбрать желаемое Web-приложение, а затем вывести окно его свойств и перейти на вкладку ASP.NET.
72 Часть I Разработка страниц ASP.NET Чтобы запустить редактор файлов web.config, щелкните кнопку Edit Configuration. Qt; кроется окно с новым набором вкладок, составляющих пользовательский интерфейс для редактирования файлов web.config. При реализации данного административного средства использовался новый конфигурационный API, который позволяет считывать и записывать содержимое файлов .config. На рис. 2-30 показано, как конфигурируется функция управления состоянием сеанса для текущего Web-приложения. Рис. 2-30. Визуальный редактор файлов web.config Редактор позволяет работать с любым разделом файла web.config. Все изменения, которые вы здесь вносите, сохраняются в файле web.config в текущем каталоге, будь то корневой каталог приложения или тот подкаталог, который был выбран перед от- крытием оснастки ASP.NET. Иными словами, если вы хотите создать либо отредакти- ровать файл web.config подкаталога приложения, выберите этот подкаталог консоли управления IIS, щелкните его правой кнопкой мыши и откройте окно свойств, после чего перейдите в окно редактирования конфигурации. В каких случаях используется редактор конфигурации Описанной выше оснасткой можно воспользоваться при редактировании расположен- ных на локальном компьютере файлов web.config. С ее помощью можно редактировать файл web.config, который уже открыт в Visual Studio .NET, при условии, что у вас имеется локальная инсталляция IIS и вы открыли проект через IIS. В остальном же этот редактор остается серверным административным средством, предназначенным скорее для настройки, чем для создания и редактирования файлов web.config на те- стовом или эксплуатационном компьютере. Примечание Мне очень хотелось бы, чтобы этот редактор был интегрирован в Visual Studio .NET, хотя бы как внешнее средство. К сожалению, не существует возможности запуска оснастки ASP.NET для определенного приложения с использованием командной строки. В будущих версиях IIS (начиная с версии 7) планируется ввести более продвину- тый административный пользовательский интерфейс, который интегрировался бы прямо в Visual Studio. Поживем — увидим.
Разработка Web-приложений в Microsoft Visual Studio .NET 2005 Глава 2 73 Заключение Visual Studio .NET 2005 является специализированным средством разработки при- ложений ASP.NET. В нем в единой IDE объединена функциональность нескольких визуальных дизайнеров, благодаря чему разработчики получили уникальные возмож- ности для проектирования и редактирования приложений. В версии 2005 исправлены многие недостатки, имевшиеся в версии 2003, и новые решения просто впечатляют эффективностью. В этой главе мы проанализировали основные этапы разработки Web-приложений с использованием Visual Studio .NET 2005, такие как формирование пользовательского интерфейса страницы и написание ее кода, поддержка и развитие Web-проекта, раз- вертывание и администрирование итогового набора страниц, файлов и сборок. Я хотел в общем продемонстрировать процесс разработки в данной среде, показать, что тут хорошо, что было улучшено, что появилось нового, а также обратить ваше внимание на некоторые важные моменты этого процесса. Если вы успешно пользовались пре- дыдущими версиями Visual Studio .NET, то эту обязательно полюбите. Если же у вас были нарекания на работу предыдущей версии, Visual Studio .NET 2003, вы будете приятно удивлены, обнаружив, что все основные ее недостатки устранены. Были затронуты и такие темы, как развертывание и админйстрирование прило- жения. Это важные этапы завершения проекта, которым не всегда уделяют должное внимание, что приводит к возникновению проблем как у пользователей, так и у раз- работчиков. В некоторых случаях для эффективного развертывания и администри- рования приложения требуются специализированные средства; однако во многих других случаях достаточно нескольких стандартных средств, чтобы сделать работу программистов и администраторов гладкой и эффективной. Мы говорили о том, что главный вопрос, связанный с административными средствами, таков: кто будет их писать и во сколько это обойдется? С выходом Visual Studio .NET 2005 на этот вопрос появился простой ответ: базовые задачи уже автоматизированы, продукт содержит все нужное для их выполнения, а дальше все зависит от ваших запросов и наличных ресурсов. Только факты Для разработки приложений ASP.NET сервер IIS больше не нужен, поскольку в составе Visual Studio .NET 2005 имеется собственный локальный мини-Web- сервер, который может использоваться для тестирования приложения в ходе его разработки. Visual Studio .NET 2005 поддерживает несколько способов открытия Web-сайта. Как и раньше, вы можете использовать для доступа к исходным файлам средство FPSE, а кроме того, доступ к ним возможен через FTP, посредством IIS и даже файловой системы. Visual Studio .NET 2005 позволяет редактировать отдельные файлы страниц, не входящие в состав проекта. Приложение ASP.NET 2.0 может содержать папки, имеющие особое значение для исполняющей среды ASP.NET: App_Code для классов, App_Themes для тем, App Global Resources для ресурсов и т. д. Хотя страницы можно тестировать и на локальном Web сервере, следует иметь ;il в виду, что такой сценарий тестирования недостаточно, реалистичен, поскольку на локальном компьютере у вас другие учетные записи, дру] ие конфигурационные установки и т. д. Так что не стоит в полной мере полагаться на адекватность по- добного тестирования. . .
74 Часть I Разработка страниц ASP.NET ASP.NET поддерживает две формы предкомпиляции сайта: на месте, и для раз- вертывания. Предкомпиляция на месте осуществляется для уже развернутого приложения и за- ключается в компиляции всех его страниц с целью устранения задержки первого обращения, В случае предкомпиляции для развертывания на том компьютере, где разрабаты- вался сайт, создается его файловое представление, состоящее из сборок и стати- ческих файлов. Это представление при желании может быть упаковано в инстал- ляционный пакет MSI и развернуто на эксплуатационных компьютерах. Предкомпиляция для развертывания позволяет не переносить на эксплуатаци- онные серверы исходные файлы и тем самым оберегает вашу интеллектуальную собственность.
Глава 3 Анатомия страницы ASP.NET Страницы ASP.NET динамически компилируются по требованию, то есть в тот момент, когда при выполнении приложения к ним в первый раз происходит обращение. Ди- намическая компиляция не является уникальной особенностью страниц приложений (файлов .aspx). Эта технология применяется и к Web-сервисам .NET (файлам .asmx), пользовательским элементам управления Web (файлам .ascx), обработчикам HTTP (файлам .ashx), а также к некоторым другим файлам приложений ASP.NET, в том числе к globaLasax. Конвейер модулей исполняющей среды принимает входящий пакет HTTP и из простого сообщения в формате, определяемом протоколом, превращает его в серверный объект HTTP — экземпляр класса, производного от системного клас- са Page. Исполняющая среда HTTP ASP.NET вначале формирует объект страницы, а затем велит ему сгенерировать разметку, вставляемую в ответ браузеру. Процесс активизации объекта-страницы и генерирования им ответа, называемый жизненным циклом страницы, сопровождается рядом событий, которые могут обрабатываться пользовательским кодом. Из этой главы вы узнаете, как происходит превращение HTTP-запроса ресурса .aspx в объект страницы, изучите программный интерфейс класса Page и выясните, как можно контролировать генерирование разметки путем обработки событий жиз- ненного цикла страницы. Активизация страницы Начнем мы с исследования процесса преобразования страницы .aspx в класс и его по- следующей компиляции в сборку. Генерирование сборки для определенного ресурса .aspx осуществляется в два этапа. Вначале анализируется исходный код файла ресурса и создается соответствующий класс, наследующий либо класс Page, либо производный от него. Затем этот динамически сгенерированный класс компилируется в сборку и кэшируется во временной папке ASP.NET. Откомпилированная страница используется до тех пор, пока в ее исходный файл .aspx не будут внесены изменения или пока все приложение не будет перезапущено. Любые изменения в исходном файле делают соответствующую сборку недействитель- ной и заставляют исполняющую среду HTTP сгенерировать для следующего запроса на получение этой страницы новую сборку. Примечание Редактирование таких файлов, как web.config и globaLasax, вызывает перезапуск всего приложения. В этом случае все страницы перекомпилируются по мере обращения к ним. То же происходит и в ситуации, когда в папку Bin приложения по- мещается новая сборка, заменяющая собой одну из существующих сборок или просто добавляемая в приложение. Особенности функционирования исполняющей среды Ресурсы, которые предоставляет Web-сервер, работающий под управлением Internet Information Server (IIS), делятся на разные категории в соответствии с расширениями
76 Часть I Разработка страниц ASP.NET файлов. Полученный сервером запрос передается исполняющему модулю, который отвечает за обработку ресурсов данной категории. Такими модулями являются расши- рения Internet Server Application Programming Interface (ISAPI), то есть обыкновенные библиотеки динамической компоновки (DLL)Win32, каждая из которых содержит набор API-функций (нечто вроде интерфейса) с предопределенными именами и про- тотипами. IIS и расширения ISAPI используют эти точки входа DLL как внутренний коммуникационный протокол. Когда IIS для выполнения определенной задачи требу- ется расширение ISAPI, он загружает данную DLL и вызывает соответствующую ее функцию. Хотя в документации ISAPI интерфейс расширений ISAPI не упоминается, именно таковыми они являются на практике — модулями, реализующими общеиз- вестный программный интерфейс. При поступлении запроса ресурса IIS вначале определяет его тип. Статические ресурсы, такие как изображения, текстовые файлы, страницы HTML и ASP-страницы, не содержащие сценариев, IIS предоставляет сам, не прибегая к помощи внешних мо- дулей. Он считывает файл, расположенный на локальном сервере, и записывает его со- держимое в выходной поток, отправляемый браузеру. Ресурсы, требующие серверной обработки, передаются соответствующему зарегистрированному модулю. Например, страницы ASP обрабатываются ISAPI-расширением asp.dll. А файлы с расширением aspx ассоциированы с ISAPI-расширением aspnet isapi.dll (рис. 3-1). DirectX Mappngs ,options {Debugging The TVTan»wwk\v2.0.50215\aspnet_lsapi.dl Executable: Pn'"*> Extension: GET.HEAD.POST,DEBUG Caned 0E. □й ft 7. 0 Script engine йи s i • ft “ •, .□jHfy that He exists Verbs J'W C:\WINDOWS\sy3bBm32\inetsrv\asp.dll "^jELHEA. c.foHndowsVniCTQsoft.nettframewprk., GETJCA.. c:\tMrdowsVnicrosoft.net\fr “rework... GET.HEA.. Add/Edit Application Extension Mapping Pro A pH•’ '*!• Apphcatiou :ur Hen Рис. 3-1. Соответствие между приложением IIS и ресурсами с расширением .aspx Информация о соответствии между расширениями IIS и типами предоставляемых им ресурсов хранится в метабазе IIS. Во время инсталляции ASP.NET в метабазу вносится запись о том, что ресурсы ASP.NET должно обрабатывать расширение aspnet_isapi.dll. Важнейшие типы этих ресурсов перечислены в табл. 3-1. Кроме того, ISAPI-расширение aspnet_isapi.dll обрабатывает запросы ресурсов с другими типичными для Microsoft Visual Studio -NET расширениями имен файлов, в том числе .cs, .csproj, .vb, .vbproj, .config и .resx.
Анатомия страницы ASP.NET Глава 3 77 Как упоминалось в главе 1, поведение ISAPI-расширения ASP.NET зависит от модели процесса, выбранной для приложения. Табл. 3-1. Ресурсы IIS, связанные с ISAPI-расширением aspnet_isapi.dll Расширение Тип ресурса .asax Файл приложения ASP.NET (типичным примером может служить globaLasax) .ascx Файл пользовательского элемента управления ASP.NET .ashx Обработчик HTTP (именованный модуль, который взаимодействует с низко- уровневыми сервисами запроса и ответа IIS) .asmx Файл, реализующий Web-сервис .NET .aspx Файл, представляющий страницу ASP-NET .axd Внутренний обработчик HTTP, используемый для реализации системной функции, такой как трассировка уровня приложения (trace.axd) или вставка сценария (webresource.axd) Модель процесса IIS 5.0 Когда ваше приложение работает под управлением одной из версий Windows, пред- шествующих Windows 2003 Server, единственным выбором для него является модель процесса IIS 5.0. Согласно этой модели ISAPI-расширение aspnet_isapi.dll не обраба- тывает файл .aspx само, а лишь играет роль диспетчера. Оно собирает всю доступную информацию о запрошенном URL и связанном с ним ресурсе, а затем передает запрос рабочему процессу ASP.NET с именем aspnet_wp.exe. Взаимодействие между расшире- нием ISAPI и рабочим процессом осуществляется посредством именованных каналов. Общая схема выполнения запроса в такой конфигурации представлена на рис. 3-2. Единственная копия рабочего процесса выполняется непрерывно и служит хост- средой для всех активных Web-приложений. Исключение составляют системы, в ко- торых Web-сервер функционирует на многопроцессорном компьютере. В них можно сконфигурировать исполняющую среду ASP.NET таким образом, чтобы на каждом центральном процессоре выполнялся отдельный рабочий процесс. Данная возмож- ность предусмотрена, например, для случаев, когда необходимо организовать вы- полнение кода, который поддерживает многопоточность, но не настолько хорошо, чтобы успешно работать в мультипроцессорной системе. Модель, в которой несколько процессов функционируют на разных ЦПУ одного сервера, называется Web-садом, и при ее использовании поведением системы управляют атрибуты, заданные в разделе <processModel> файла machine.config. Тот факт, что единственный рабочий процесс используется всеми ЦПУ и управляет всеми Web-приложениями, отнюдь не означает отсутствия изоляции приложений друг от друга. Каждое Web-приложение идентифицируется собственным виртуальным каталогом и принадлежит к отдельному домену приложения, часто называемому App- Domain. Домен приложения создается в рабочем процессе ASP.NET, когда происходит первое клиентское обращение к виртуальному каталогу этого приложения. Создав новый AppDomain, виртуальная среда ASP.NET загружает туда все необходимые сборки и передает управление конвейеру HTTP для обслуживания запроса. Когда клиент запрашивает страницу уже активного Web-приложения, исполняющая среда ASP.NET просто передает запрос существующему AppDomain, связанному с данным виртуальным каталогом. Если в этот AppDomain еще не загружена необходимая для обработки страницы сборка, она будет создана на лету; если же такая сборка уже загружена (для обработки одного из предыдущих обращений к этой странице), то именно она и будет использоваться.
78 Часть I Разработка страниц ASP.NET Браузер HTTP HTML HTML Рис. 3*2. Схема исполняющей среды ASP.NET во время использования модели процесса IIS 5.0 Модель процесса IIS 6.0 В том случае, когда операционной системой Web-сервера является Windows 2003 Server, по умолчанию используется модель процесса IIS 6.0, для функционирования которой, естественно, необходима такая же версия IIS. Однако на компьютере с Win- dows 2003 Server и IIS 6.0 может действовать и модель процесса IIS 5.0. Для этого она должна быть явно активизирована в разделе <processModel> файла machine.config: <processModel enable="true"> Имейте, однако, в виду, что не рекомендуется переключаться на старую модель процесса IIS 5.0 без необходимости, хотя это вполне допустимо. Дело в том, что в IIS 6.0 обработка запросов выполняется другим конвейером внутренних модулей, и воспроизводить поведение своей предыдущей версии этот Web-сервер может только в режиме эмуляции. Центральным элементом конвейера IIS 6.0 является рабочий процесс w3wp.exe. Его копия совместно используется всеми Web-приложениями, назначенными тому же пулу приложений. В IIS 6.0 пулом приложений называется группа Web-приложений, совместно использующих одну и ту же копию рабочего про- цесса. IIS 6.0 позволяет настраивать пулы приложений для достижения необходимой степени изоляции отдельных приложений.
Анатомия страницы ASP.NET Глава 3 79 Рабочий процесс w3wp.exe загружает aspnet_isapi.dll, а это расширение ISAPI, в свою очередь, загружает общеязыковую среду (CLR) и запускает для обработки за- проса исполняющий конвейер ASP.NET. При использовании модели процесса IIS 6.0 встроенный рабочий процесс ASP.NET отключен. Примечание Все преимущества модели процесса IIS 6.0 впервые были использованы в ASP.NET версии 1.1. Если установить ASP.NET 1.0 на компьютере, работающем под управлением Windows 2003 Server, по умолчанию будет включена модель процесса IIS 5.0. Это происходит потому, что лишь версия aspnet_isapi.dll из ASP.NET 1.1 достаточно ин- теллектуальна, чтобы распознать хост и при необходимости загрузить CLR. Та версия aspnetjsapi.dll, что входит в состав ASP.NET 1.0, перенаправляет запросы рабочему про- цессу ASP.NET и никогда не загружает CLR. На рис. 3-3 показано, как приложения ASP.NET и другие Web-приложения функ- ционируют в IIS 6.0. Браузер HTTP Режим ядра http.sys Слушает и маршрутизирует Очередь запросов пула прложений Очередь запросов пула прложений Пользовательский режим Рабочие процессы извлекают запросы из очереди приложений IIS 6.0 Рабочий процесс IIS (w3wp.exe) Управляет жизненным циклом и повторным использованием рабочих процессов Рабочий процесс IIS (w3wp.exe) Этот процесс загружает aspnetjsapi.dll для обработки страниц .aspx. В свою очередь, aspnetjsapi.dll загружает CLR Этот процесс загружает asp.dll для обработки страниц .asp Web Administration Service (WAS) WAS читает метабазу WAS инициализирует http.sys Метабаза Рис. 3-3. Функционирование приложений ASP.NET и других Web-приложений в IIS 6.0 В IIS 6.0 процесс-слушатель HTTP реализован как модуль уровня ядра — драйвер http.sys. Поэтому все входящие запросы сначала обрабатывает этот драйвер, никакой
80 Часть I Разработка страниц ASP.NET код сторонних производителей с ним не взаимодействует и никакие сбои програм- много обеспечения, функционирующего в пользовательском режиме, не отражаются на стабильности IIS. Драйвер http.sys получает входящие запросы и помещает их в очереди соответствующих приложений. Модуль под названием Web Administration Service (WAS) считывает из метабазы IIS информацию о том, сколько пулов при- ложений в ней зарегистрировано, и дает указание драйверу http.sys создать такое же количество очередей запросов. Когда действует модель процесса I IS 6.0, ASP.NET функционирует даже быстрее, поскольку отсутствует межпроцессное взаимодействие между inetinfo.exe (исполня- емый файл IIS) и рабочим процессом. Запрос HTTP сразу доставляется рабочему процессу, который служит хостом CLR. Более того, рабочий процесс ASP.NET яв- ляется не каким-то особым процессом, а лишь копией рабочего процесса IIS. Таким образом, заботы о повторном использовании процессов, кэшировании вывода страниц и контроле состояния процессов перекладываются на IIS. В модели процесса IIS 6.0 система ASP.NET игнорирует большую часть содер- жимого раздела <processModel> файла machine.config и считывает из него только установки, которые касаются потоков и взаимоблокировок. Все прочие установки данного раздела хранятся в метабазе и могут конфигурироваться только с использо- ванием IIS Manager. (Остальная информация по-прежнему считывается из файлов .config.) Представление запрошенной страницы Каждый поступающий запрос ресурса .aspx сопоставляется классу, производному от Page, и обрабатывается этим классом. Исполняющая среда HTTP ASP.NET первым делом определяет имя класса, который будет использоваться для выполнения запро- са. При этом она следует специальному соглашению об именах, определяющему, как на основании URL вычисляется имя класса. Если, к примеру, запрошена страница default.aspx, соответствующим классом будет ASP.defaultaspx. Если класса с таким именем нет ни в одной из загруженных в AppDomain сборок, исполняющая среда HTTP отдает команду о его создании и компиляции. Исходный код данного класса создается путем разбора исходного кода ресурса .aspx и сохраняется во временной папке ASP.NET. Затем класс компилируется и загружается в память для обслужива- ния запроса. При поступлении нового запроса той же страницы данный класс уже готов к использованию, и поэтому его компиляция не производится. (Класс повторно создается и компилируется только в случае изменения штампа времени исходного файла aspx.) Класс ASP.default aspx является производным от Page или класса, наследующего Page. В большинстве случаев базовый класс для ASP.default aspx будет соединением класса отделенного кода, то есть частичного класса, созданного в Visual Studio .NET, и еще одного частичного класса, динамически сгенерированного исполняющей средой HTTP ASP.NET. На рис. 3-4 в графической форме показано, как происходит динами- ческое формирование исходного кода класса страницы. Частичные классы -> замечательное нововведение нынешнего поколения компи- ляторов .NET. При использовании этой технологии исходный код класса делится на две части, хранящиеся в разных файлах. Каждый из них содержит самое обыкновен- ное определение класса, снабженное новым ключевым словом partial. Оно говорит компилятору, что данное определение является неполным. Для получения полного исходного кода компилятор должен обратиться к другим файлам, указанным в ко- мандной строке.
Анатомия страницы ASP NET Глава 3 81 Написано вами в default.aspx Сгенерировано ASP.NET при компиляции Рис. 3-4. Обработка страницы конвейером HTTP Мастичные классы в проектах ASP.NET Технология частичных классов идеально подходит для командной разработки, упро- щает кодирование, а также избавляет от необходимости синхронизировать вручную файлы, одновременно содержащие программный код, определенный пользователем и сгенерированный инструментально. Вы хотели бы увидеть пример? Что ж, за ним не нужно далеко ходить: такие файлы вы найдете в проектах, разрабатываемых в Visual Studio .NET. Частичные классы — это технология компилятора, а не новый синтаксический элемент языка программирования. Она разработана для разрешения проблемы уяз- вимости инструментально генерируемого кода, присутствующего во многих проектах Visual Studio .NET, включая и проекты ASP.NET. Введение частичных классов позво- лило удалить из файлов отделенного кода автоматически генерируемый полускры- тый код, который Visual Studio .NET 2003 вставляет туда для поддержки дизайнеров страниц. Хотя технология частичных классов не является объектно-ориентированной, ее активное применение дает множество преимуществ. Данная технология предоставляет возможность нескольким командам одновременно работать над одним компонентом. Кроме того, это элегантный способ инкрементального наращивания функциональ- ности класса. Именно таким образом работает исполняющая среда ASP.NET.
82 Часть t Разработка страниц ASP.NET Разметка ASPX определяет серверные элементы управления, которые будут обрабатываться кодом класса отделенного кода, Для того чтобы такая модель была дееспособной, класс отделенного кода должен содержать ссылки на эти серверные элементы управления в виде своих внутренних членов, как правило, защищенных. В Vi- sual Studio .NET 2003 объявления таких членов добавлялись интегрированной средой разработки при сохранении разметки и содержались в полускрытых областях кода. В Visual Studio .NET 2005 классы отделенного кода стали создаваться как частичные классы, не содержащие объявлений указанных членов. Недостающие объявления инкрементально добавляются во время разработки из второго частичного класса, создаваемого исполняющей средой HTTP ASP.NET. Выбранный вами компилятор (С#, Microsoft Visual Basic.NET или другой) соединяет два частичных класса и создает итоговый родительский класс динамически генерируемого класса страницы. Обработка запроса Для обработки запроса страницы default.aspx исполняющей среде ASP.NET необхо- димо получить ссылку на класс ASP.default aspx. Если этого класса нет ни в одной из загруженных в данный момент в AppDomain сборок, он создается динамически. Затем исполняющая среда HTTP вызывает этот класс посредством методов стандартного интерфейса IHttpHandler. Указанный интерфейс реализован в базовом классе Page и содержит два члена: метод ProcessRequest и булево свойство IsReusable. После того как исполняющая среда HTTP получает экземпляр класса, представляющего запрошен- ный ресурс, вызовом открытого метода ProcessRequest инициируется заключительный процесс, в ходе которого генерируется ответ браузеру. Этапы и события этого процесса совокупно образуют жизненный цикл страницы. В отличие от страниц ASP страницы ASP.NET не просто подвергаются синтаксиче- скому разбору и предоставляются пользователю. Хотя конечной целью исполняющей среды ASP.NET также является возврат страницы, результирующая разметка генери- руется значительно более сложным способом, чем в ASP, и в этом процессе участвует значительно большее количество объектов. Рабочий процесс ASP.NET, будь то w3wp. ехе или aspnet_wp.exe, передает входящие запросы HTTP так называемому конвейеру HTTP. Последний представляет собой расширяемую цепочку управляемых объектов, действующую согласно классической концепции конвейера. Все эти объекты и об- разуют совокупно то, что мы называем исполняющей средой HTTP ASP.NET. Объект HttpRuntime Запрос страницы проходит через конвейер объектов, обрабатывающих исходное со- держимое HTTP-запроса и генерирующих код разметки для браузера. Точкой входа этого конвейера является класс HttpRuntime. Рабочий процесс ASP.NET активизирует конвейер HTTP путем создания нового экземпляра этого класса и вызова его метода ProcessRequest. Обратите внимание: несмотря на свое имя, метод HttpRuntime.Process- Request не имеет ничего общего с интерфейсом IHttpHandler. Класс HttpRuntime содержит множество закрытых методов и только три открытых статических метода: Close, ProcessRequest и UnloadAppDomain (описаны в табл. 3-2). Следует отметить, что все методы, описанные в табл. 3-2, имеют ограниченное применение в пользовательских приложениях. В частности, предполагается, что вы не будете использовать в своем коде метод ProcessRequest и что метод Close полезен лишь в тех случаях, когда пользовательское приложение является хостом ASP.NET. Из трех перечисленных методов только UnloadAppDomain предназначен для широко- го применения, а именно для случаев, когда с учетом тех или иных условий времени выполнения вы решаете перезапустить приложение. (См. врезку «Чем может быть вызван перезапуск приложения?» далее в этой главе.)
Анатомия страницы ASP.NET Глава 3 83 Табл. 3-2. Открытые методы класса HttpRuntime Метод Назначение Close Удаляет из кэша ASP.NET все элементы и завершает работу Web-при- ложения. Этот метод следует использовать только в том случае, когда ваш код реализует собственную хост-среду для ASP.NET. Нет необхо- димости вызывать его в ходе обычной обработки запроса системой ProcessRequest Инициирует обработку запроса ASP.NET UnloadAppDomain Завершает работу текущего Web-приложения. Приложение перезапу- скается при получении следующего обращенного к нему запроса После своего создания объект HttpRuntime инициализирует множество внутренних объектов, которые помогают обрабатывать запрос страницы. К числу таких вспомога- тельных объектов относятся менеджер кэша и монитор файловой системы, использу- емый для отслеживания изменений в файлах, составляющих приложение. При вызове метода ProcessRequest объект HttpRuntime начинает работы по подготовке страницы для браузера. Он создает для запроса новый контекст и инициализирует специальный объект записи текста (text writer), в котором будет накапливаться код разметки. Контекст представлен экземпляром класса HttpContext, который инкапсулирует всю специфическую для HTTP информацию о запросе. После этого, используя информацию контекста, объект HttpRuntime находит либо создает объект Web-приложения, способный обработать запрос. Поиск объекта Web- приложения осуществляется с использованием информации о виртуальном каталоге, содержащейся в URL. Объектом, используемым для поиска или создания нового объекта Web-приложения, является HttpApplicationFactory — предназначенный для внутреннего использования объект, отвечающий за возврат допустимого объекта, способного обработать запрос. Прежде чем вы ближе познакомитесь с компонентами конвейера HTTP, предлагаю рассмотреть рис. 3-5, на котором изображена его схема. | default.aspx |-----------1 Рабочий процесс ASP.NET — AppDomain HttpRuntime вызывает метод ProcessRequest объекта ASP.default_aspx Рис. 3-5. Конвейер HTTP, обрабатывающий запрос страницы
84 Часть I Разработка страниц ASP.NET Фабрика обработчиков В течение жизненного цикла приложения объект HttpApplicationFactory поддер- живает пул объектов HttpApplication, предназначенных для обслуживания входя- щих запросов HTTP. Будучи вызванным, объект фабрики обработчиков выясня - ет, существует ли для указанной в запросе виртуальной папки Арр Domain. Если приложение уже выполняется, то есть AppDomain существует, фабрика извлекает объект HttpApplication из пула доступных объектов и передает ему запрос. Если же ни одного доступного объекта HttpApplication не оказывается в наличии, фабрика создает новый такой объект. Если к данной виртуальной папке еще ни разу не производилось обращение, для нее создается новый AppDomain. В этом случае создание объекта HttpAppli- cation сопровождается компиляцией файла global.asax, если таковой имеется, и созданием сборки, которая представляет запрошенную страницу. Данное событие эквивалентно запуску приложения. Объект HttpApplication в каждый конкретный момент времени обрабатывает один запрос страницы. Для того чтобы одновременно можно было обрабатывать несколько запросов страниц, используется несколько объектов HttpApplication. Объект HttpApplication Работающее приложение ASP.NET представлено динамически создаваемым классом, наследующим базовый класс HttpApplication Исходный код этого динамически соз- даваемого класса формируется путем разбора содержимого файла global.asax, если таковой имеется. Когда файл global.asax существует, класс приложения создается на его основе и получает имя ASP.global asax. В противном случае используется базовый класс HttpApplication. Экземпляр класса, производного от HttpApplication, отвечает за управление жиз- ненным циклом запроса, которому он назначен. По окончании выполнения запроса этот экземпляр может использоваться повторно. Объект HttpApplication поддержи- вает список объектов, представляющих модули HTTP, которые могут фильтровать и даже модифицировать содержимое запроса. Зарегистрированные модули вызываются в разные моменты прохождения запросом конвейера HTTP. Объект HttpApplication определяет тип объекта, представляющего запрошенный ресурс. Чаще всего таким ресурсом является страница ASP.NET, Web-сервис или пользовательский элемент управления. Затем, используя соответствующую фабрику обработчиков HTTP, HttpApplication получает объект, представляющий запрошенный ресурс. Указанная фабрика либо сразу создает экземпляр класса запрошенного ре- сурса, если этот класс содержится в одной из существующих сборок, либо вначале динамически создает необходимую сборку. Фабрика обработчиков HTTP — это класс, реализующий интерфейс IHttpHandlerFactory и отвечающий за возврат экземпляра управляемого класса, способного обработать HTTP-запрос, то есть обработчика HTTP. Страница же ASP.NET является просто объектом обработчиком HTTP — экземпляром класса, представляющим интерфейс IHttpHandler. Фабрика страниц Объект HttpApplication определяет тип объекта, который должен обрабатывать запрос, и делегирует специальной фабрике обработчиков, соответствующей указанному типу, задачу создания экземпляра этого типа. Давайте посмотрим, что происходит, когда запрошенным ресурсом является страница.
Анатомия страницы ASP.NE1 Глава 3 85 Как только объект HttpApplication выясняет, какой обработчик необходим для вы- полнения полученного запроса, он создает экземпляр объекта-фабрики обработчиков. Для запроса страницы подходящей фабрикой является класс PageHandlerFactory. Во время поиска подходящего обработчика HttpApplication использует информацию из раздела <httpHandlers> конфигурационного файла. Краткий список важнейших зарегистрированных фабрик обработчиков приведен в табл. 3-3. Табл. 3-3. Классы-фабрики .NET Framework Фабрика обработчиков Тип ресурсов Описание HttpRemotingHandlerFactory *.rem; *.soap Создает объект, который возьмет на себя заботу о запросе .NET Remoting, осуществляемом через IIS. Создает объект типа HttpRemotingHandler PageHandlerFactory *.aspx Компилирует тип, представляющий страницу, и создает его экземпляр. Исходный код этого класса создается при разборе содержимого файла .aspx. Класс создаваемого объекта является про- изводным от Page SimpleHandlerFactory *.ashx Компилирует заданный обработчик HTTP, соз- данный на основе исходного кода из файла .ashx, и создает его экземпляр. Созданный объект реа- лизует интерфейс IHttpHandler WebServiceHandlerFactory *.asmx Компилирует исходный код Web-сервиса и пре- образует содержимое запроса SOAP в вызов метода. Создает экземпляр объекта, тип которого задан в файле Web-сервиса Имейте в виду, что объекты фабрик обработчиков не компилируют исходный файл запрошенного ресурса при каждом их вызове. Откомпилированный код хранится во временной папке ASP.NET на Web-сервере и используется до тех пор, пока в соот- ветствующий исходный файл не будут внесены изменения. Таким образом, фабрика обработчиков страниц создает экземпляр объекта, который представляет конкретную запрошенную страницу. Как уже упоминалось, класс этого объекта наследует класс Sys- tem.Web.ULPage, реализующий интерфейс IHttpHandler. Объект страницы возвращается фабрике приложений, которая передает его объекту HttpRuntime. Последним действи- ем исполняющей среды ASP.NET является вызов принадлежащего объекту страницы метода ProcessRequest из интерфейса IHttpHandler. Этот вызов заставляет страницу выполнить определенный пользователем код и сгенерировать разметку для браузера. В главе 12 мы вернемся к теме инициализации приложения ASP.NET, поговорим о содержимом файла globaLasax, а также об информации, составляющей контекст HTTP — контейнерный объект, который создается классом HttpRuntime, заполняется и передается по конвейеру, а в конце связывается с обработчиком страницы. Чем может быть вызван перезапуск приложения? Существует несколько причин, по которым осуществляется перезапуск приложения ASP.NET. Как правило, это происходит, когда система возвращает себе ресурсы серве- ра, очищает рабочий набор или пытается предотвратить долговременные последствия латентных ошибок, из-за чего приложение перестает отвечать. Еще одной причиной является внесение в страницы ASPX слишком большого количества динамических изменений, вследствие чего в памяти оказывается слишком много сборок. Приложение,
86 Часть I Разработка страниц ASP.NET исчерпавшее выделенную ему виртуальную память, останавливается и перезагужается. Исполняющая среда ASP.NET производит множество проверок и автоматически перезапускает приложение при наличии одной из следующих предпосылок: достигнут лимит количества перекомпиляций страницы; изменился физический путь к Web-приложению или переименован один из его подкаталогов; внесены изменения в файл global.asax, machine.config или web.config, находящийся в корневой папке приложения, папке Bin или в одной из ее подпапок; внесены изменения в файл политики безопасности доступа к коду, если такой файл имеется; в папках контента изменилось слишком много файлов (обычно так бывает, если запрашиваемые клиентом файлы генерируются на лету); изменились установки, управляющие перезапуском/завершением рабочего про- цесса ASP.NET (если не используется Windows 2003 Server и модель процесса IIS 6.0, эти установки считываются из файла machine.config; если же модель про- цесса IIS 6.0 используется, приложение перезапускается, когда вы модифицируете свойства узла Application Pools в IIS Manager). Кроме того, в ASP.NET версии 1.1 и выше приложение может быть перезапущено программно, путем вызова метода HttpRuntime.UnloadAppDomain. Директивы обработки страницы Директивы страницы служат для конфигурирования среды ее выполнения. ASP.NET позволяет располагать эти директивы в любом месте страницы, хотя по общему со- глашению их помещают в начало файла. Имя директивы чувствительно к регистру, а значения ее атрибутов не обязательно заключать в кавычки. Самой важной и часто используемой директивой является ©Page. Полный список директив ASP.NET при- веден в табл. 3-4. Табл. 3-4. Директивы страниц, поддерживаемые ASP.NET Директива Описание @ Assembly Связывает сборку с текущей страницей или пользовательским элементом управления @ Control Позволяет задать атрибуты, специфические для элементов управления и влияющие на поведение их компилятора @ Implements Указывает, что страница или пользовательский элемент управления реа- лизует заданный интерфейс .NET Framework © Import Задает пространство имен, которое должно быть импортировано в стра- ницу или пользовательский элемент управления @ Master Идентифицирует эталонную страницу ASP.NET (см. главу 6). В ASPNET 1.x эта директива не поддерживается @ OutputCache Определяет политики кэширования вывода страницы (см. главу 14) ©Page Позволяет задать специфические для страниц атрибуты, управляющие поведением синтаксического анализатора и компилятора @ Reference Связывает страницу или пользовательский элемент управления с теку- щей страницей или пользовательским элементом управления @ Register Определяет для страницы или элемента управления пользовательский тэг Этот новый тэг (идентифицируемый префиксом и именем) связыва- ется с пространством имен и кодом пользовательского или специализиро- ванного элемента управления
Анатомия страницы ASP.NET Глава 3 87 Все директивы, за исключением ©Page, ©Master и ©Control, могут использовать- ся как в файлах страниц, так и в файлах элементов управления. Директивы ©Page и ©Control являются взаимоисключающими: первая может использоваться только в файлах .aspx (страниц), а вторая — только в файлах .ascx (элементов управления). Что касается директивы ©Master, то она идентифицирует страницу особого вида, уже упоминавшегося в этой книге, — эталонную страницу. Синтаксис директив обработки уникален и для всех поддерживаемых их типов одинаков. Атрибуты, когда их несколько, разделяются пробелами, а вот знак равен- ства (-) пробелами с обеих сторон не отделяется: <%@ Имя_директивы атрибута"значение" [атрибут=“значение"...] %> Каждая директива имеет собственный набор типизированных атрибутов. При- своение атрибуту значения неверного типа или использование в директиве не под- держиваемого ею атрибута приводит к ошибке компиляции. О Внимание! Содержимое атрибутов директив всегда задается в текстовом виде. Од- нако предполагается, что атрибуты содержат значения, которые могут быть приведены к определенным типам .NET Framework. При разборе страницы ASP.NET все атрибуты директив извлекаются и сохраняются в словаре. Имена и количество атрибутов должны соответствовать схеме директивы. Строка, представляющая значение атрибута, считается допустимой при условии, что ее можно преобразовать к требуемому типу. Например, если атрибут должен содержать значение булева типа, то допустимыми являются только строки 'true" и "false". Директива @Раде Директива ©Page может присутствовать только в файлах страниц .aspx, и при ее использовании в файлах других типов, таких как файлы элементов управления и Web-сервисов, генерируется ошибка компиляции. Каждый файл .aspx может содер- жать только одну директиву ©Page, и хотя ее наличие не является обязательным, на практике она требуется любой более или менее сложной странице. У директивы ©Page около 30 атрибутов, которые могут быть логически разделены на три категории: связанные с компиляцией (табл. 3-5), общим поведением страницы (табл. 3-6) и ее выводом (табл. 3-7). Любая страница ASP.NET компилируется при первом к ней обращении, и отправляемый браузеру HTML-код генерируется методами динамически создаваемого класса. Атрибуты, перечисленные в табл. 3-5, позволяют настраивать параметры компилятора и задавать используемый язык. Табл. 3-5. Атрибуты директивы @Раде, связанные с компиляцией страницы Атрибут Описание ClassName Не содержащее ссылку на пространство имен имя класса, который будет динамически компилироваться по запросу страницы CodeFile Путь к файлу отделенного кода текущей страницы. Файл исходного кода должен быть развернут на Web-сервере. В ASP.NET 1jc данный атрибут не поддерживается CodeBehind Путь к файлу отделенного кода текущей страницы, подлежащему компиляции в развертываемую сборку. Данный атрибут используется Windows Server 2003 CodeFileBaseClass Родительский класс родительского класса страницы. Иными словами, в этом атрибуте (при желании) задается имя класса, от которого дол- жен быть производным класс отделенного кода страницы. В ASP.NET 1.x данный атрибут не поддерживается см. след. стр.
88 Часть I Разработка страниц ASP.NET Табл. 3-5. (окончание) Атрибут Описание CompilationMode Одно из трех значений, указывающих, должна ли страница компилиро- ваться во время выполнения: Never, Auto или Always (по умолчанию). В ASP NET 1х данный атрибут не поддерживается CompilerOptions Последовательность ключей команды запуска компилятора для данной страницы Debug Булево значение, указывающее, должна ли страница компилироваться с отладочными символами Explicit Булево значение, указывающее, должна ли страница, языком которой является Visual Basic, компилироваться с установкой On параметра Option Explicit (данная установка требует, чтобы программист явно объявил все переменные). Если языком страницы является не Visual Basic, значение данного атрибута игнорируется Inherits Базовый класс, наследуемый страницей. Им может быть любой класс производный от Page Language Язык, который должен использоваться при компиляции встроенных блоков кода (<% ...% >) и кода, содержащегося в разделе <script> страницы. Поддерживаются языки Visual Basic .NET, С#, JScript .NET и J#. По умолчанию используется язык Visual Basic .NET MasterPageFile Эталонная страница текущей страницы. В ASP.NET 1 х данный атрибут не поддерживается Src Файл исходного кода, содержащий реализацию базового класса, задан- ного в атрибуте Inherits. Атрибут Src не используется Visual Studio .NET и другими средствами быстрой разработки приложений Strict Булево значение, указывающее, должна ли страница, языком которой является Visual Basic, компилироваться с установкой On параметра Op- tion Strict (данная установка запрещает неявное преобразование типов, при котором возможна потеря данных). Если языком страницы являет- ся не Visual Basic, значение данного атрибута игнорируется Trace Булево значение, указывающее, включена ли трассировка страницы. Если она включена, в вывод страницы добавляется трассировочная информация. По умолчанию данный атрибут имеет значение false TraceMode Указывает, как при включенной трассировке страницы должны отобра- жаться трассировочные сообщения. Допустимыми значениями данного атрибута являются SortByTime и SortByCategory. По умолчанию при включенной трассировке используется значение SortByTime WamingLevel Определяет степень важности предупреждения компилятора, достаточ- ную для прекращения им компиляции страницы. Допустимы значения от 0 до 4 Заметьте, что значения атрибутов Explicit и Strict по умолчанию извлекаются из набора конфигурационных установок приложения (речь идет о том итоговом наборе, который получается путем слияния содержимого файлов, действующих на уровне компьютера, приложения и отдельных его папок). А это означает, что задаваемые по умолчанию значения атрибутов Explicit и Strict можно контролировать. Если вы не вносили изменений в файлы .config, созданные при установке .NET Framework, оба эти атрибута имеют значение true. Если же удалить соответствующие установки из кон- фигурационных файлов, данные атрибуты по умолчанию получат значение false. Атрибуты, перечисленные в табл. 3-6, обеспечивают вам некоторый контроль над общим поведением страницы и поддерживаемыми возможностями. Например, с их помощью можно задать пользовательскую страницу сообщения об ошибке, отключить поддержку состояния сеанса, управлять транзакционным поведением страницы.
Анатомия страницы ASP.NET Глава 3 89 Табл. 3-6. Атрибуты директивы @Раде, определяющие общее поведение страницы Атрибут Описание AspCompat Когда этот атрибут имеет значение true, разрешено выполнение страницы в однопоточном апартаментном режиме (Single-Threaded Apartment, STA). Данная установка позволяет странице вызывать компоненты СОМ+ 1.0 и компоненты, разработанные с использо- ванием Microsoft Visual Basic 6.0, которым требуется доступ к не- управляемым встроенным объектам ASP.NET (эту тему я раскрою в главе 12) Async Когда этот атрибут имеет значение true, генерируемый класс страницы наследует не IHttpHandler, a IHttpAsyncHandler, включаю- щий функции поддержки асинхронного выполнения страницы. В ASPNET 1.x данный атрибут не поддерживается AutoEventWireup Булев атрибут, указывающий, должны ли события страницы под- держиваться автоматически. По умолчанию данный атрибут имеет значение true. У страниц, разработанных с использованием Visual Studio .NET, он установлен в false, так что события связываются с обработчиками в индивидуальном порядке Buffer Булев атрибут, определяющий, включена ли буферизация ответов HTTP По умолчанию данный атрибут имеет значение true Description Текстовое описание страницы. Синтаксический анализатор страниц ASPNET игнорирует этот атрибут, предназначенный исключитель- но для целей документирования EnableSessionState Способ работы страницы с данными состояния сеанса. Если этот атрибут имеет значение true, состояние сеанса считывается и за- писывается, при значении false оно недоступно приложению, а при значении Readonly состояние сеанса может считываться, но не изменяться Enable View State Булев атрибут, указывающий, сохраняется ли состояние представ- ления страницы между ее запросами. Состоянием представления страницы называется контекст ее вызова — набор значений, кото- рые составляют состояние страницы и передаются между сервером и клиентом (эта тема подробно рассматривается в главе 13) Enable ViewStateMac Булев атрибут, указывающий, должна ли ASP.NET вычислять ма- шинно-зависимый аутентификационный код и применять его к состоянию представления страницы (в дополнение к кодированию методом Base64). Суффикс Мас в имени атрибута расшифровыва ется как machine authentication check — машинный аутентификаци- онный контроль. Когда данный атрибут имеет значение true, после возврата страницы ASP.NET проверяет аутентификационный код состояния представления, чтобы убедиться, что оно не было под- менено на клиенте ErrorPage Определяет целевой URL, по которому пользователь будет автома- тически перенаправлен в случае, если при выполнении страницы произойдет исключение, которое останется необработанным SmaitNavigation Булев атрибут, указывающий, какие навигационные функции под- держивает страница: те, что были реализованы в Microsoft Internet Explorer 5 или разработанные позднее функции интеллектуальной навигации, позволяющие обновлять страницу, не теряя позицию прокрутки и фокус элемента Theme. StylesheetTheme Имя темы (или темы таблицы стилей), выбранной для страницы. В ASP.NET данный атрибут не поддерживается см. след. стр.
90 Часть I Разработка страниц ASP.NET Табл. 3-6. (окончание) Атрибут Описание Transaction Указывает, поддерживает ли страница транзакции. Допустимыми значениями являются: Disabled, NotSupported, Supported, Required и RequiresNew. По умолчанию поддержка транзакций отключена ValidateRequest Булев атрибут, указывающий, должна ли производиться проверка запроса. Если данный атрибут имеет значение true, ASP.NET сверя- ет все вводимые данные с жестко закодированным списком потен- циально опасных значений, что позволяет уменьшить риск межсай- товых сценарных атак; по умолчанию он имеет значение true. В ASP.NET 1.0 данный атрибут не поддерживается Атрибуты, перечисленные в табл. 3-7, позволяют управлять форматом генериру- емого страницей вывода. Например, вы можете задать тип контента страницы или локализовать вывод. Табл. 3-7. Атрибуты директивы @Раде, влияющее на формирование выводимой страницы Атрибут Описание ClientTarget Целевой браузер, для которого должен быть адаптирован выводи- мый серверными элементами управления контент CodePage Номер кодовой страницы для ответа. Этот атрибут следует уста- навливать только в том случае, если вы создаете Web-страницу с использованием кодовой страницы, отличной от заданной по умол- чанию для того Web-сервера, на котором будет выполняться данная страница. В таком случае присвойте этому атрибуту номер кодовой страницы, заданной на том компьютере, где ведется разработка. Кодовая страница — это набор символов, включающий буквы, циф- ры, знаки препинания и другие знаки. У каждого языка имеется своя кодовая страница ContentType Тип контента ответа (один из стандартных MIME-типов). Поддер- живается любой допустимый идентификатор MIME-типа Culture Культура страницы. От этой установки зависят такие параметры, как система записи и сортировки текста, календарь, форматы даты и времени. Имя культуры не должно быть нейтральным, иными словами, оно должно включать как идентификатор языка, так и идентификатор страны. Например, допустимым является иденти- фикатор enUS, в отличие от идентификатора еп, нейтрального по отношению к стране LCID 32-разрядный идентификатор культуры страницы. По умолчанию используется идентификатор культуры Web-сервера ResponseEncoding Идентификатор кодировки символов страницы. Это значение ис пользуется для установки атрибута CharSet в заголовке HTTP Content-Type UlCulture Имя задаваемой по умолчанию культуры, используемое менедже- ром ресурсов для поиска культуро-зависимых ресурсов во время выполнения приложения Как видите, многие из перечисленных в табл. 3-7 атрибутов имеют отношение к локализации страницы. ASP.NET в частности и .NET Framework в целом очень упрощают задачу сознания многоязыковых интернациональных приложений. Директива @ Assembly Директива @Assembly связывает с текущей страницей сборку, классы и интерфейсы которой должны быть доступны этой странице. Компилируя страницу, ASP.NET
Анатомия страницы ASP.NET Глава 3 91 автоматически связывает с ней несколько сборок, определенных как используемые по умолчанию, поэтому директива ©Assembly потребуется вам лишь в том случае, если вы захотите связать со страницей какие-либо еще сборки. В табл. 3-8 перечислены сборки .NET, автоматически связываемые со страницей. Табл. 3-8. Сборки, связываемые со страницей по умолчанию Имя файла сборки Содержимое Mscorlib.dll Базовые функциональные элементы .NET Framework, в том числе типы, домены приложений и сервисы исполняющей среды System.dll Еще одна группа системных сервисов, в которую входят средства поддержки регулярных выражений, компилятор, встроенные методы, подсистемы файлового ввода-вывода и сетевого обмена System.Configuration.dll Классы для чтения и записи конфигурационной информа- ции. В ASP.NET 1х данная сборка отсутствует System. Data.dll Контейнер данных и классы доступа к данным, включая всю ADO.NET System.Drawing.dll Функции GDI+ System. EnterpriseServices.dll Классы, обеспечивающие возможность взаимодействия сервисных компонентов и СОМ+ System.Web.dll Базовые сервисы, элементы управления и классы ASP.NET System.Web.Mobile.dll Сервисы, элементы управления и классы ASP.NET, предна- значенные для разработки мобильных приложений. В ASP.NET 1.0 данная сборка отсутствует System. Web. Services.dll Код, необходимый для выполнения Web-сервисов System.Xml.dll XML-функции .NET Framework Помимо этих сборок исполняющая среда ASP NET автоматически связывает со страницей все те сборки, которые она находит в папке Bin приложения. У вас имеется возможность модифицировать список подключаемых по умолчанию сборок, в том чис- ле удалять из него элементы и добавлять новые. Это делается путем редактирования конфигурационных установок в файле machine.config или web.config. В первом случае действие внесенных вами изменений будет распространяться на все приложения ASP.NET, функционирующие на данном Web-сервере, а во втором — только на одно приложение, в чьем корневом каталоге расположен файл web.config. Если вы не хотите, чтобы со страницей автоматически связывались сборки, найденные в папке Bin, удалите из конфигурационного файла строку <add assembly="*" /> ©Внимание! Полный набор конфигурационных установок приложения задается на уровне компьютера в файле machine.config, поэтому первоначально для всех приложений дей- ствуют одинаковые установки. Однако установки отдельных приложений разрешается пе реопределять в их файлах web.config. У каждого приложения может быть основной файл web.config, хранящийся в его корневой папке, и некоторое количество одноименных файлов во вложенных папках. Набор установок, применяемых к конкретной странице, формиру- ется как сумма установок, хранящихся в конфигурационных файлах, начиная от уровня компьютера и заканчивая уровнем данной страницы. В ASP.NET 1.x файл machine.config содержит все дерево установок, задаваемых по умолчанию. В ASP.NET 2.0 те конфигура- ционные данные, которые относятся к Web-приложениям, перемещены в файл web.config, устанавливаемый в ту же системную папку, что и machine.config. Она имеет имя CONFIG и располагается в инсталляционном разделе ASP.NET, то есть в папке %WINDOWS%\ Microsoft. Net\Framework\[eepcHfl],
92 Часть I Разработка страниц ASP.NET Для того чтобы связать сборку со страницей, включите в файл последней одну из двух следующих директив: <%@ Assembly Мате="имя_сборки" %> <%@ Assembly Sгс="код_сборки.cs" %> У директивы ©Assembly есть два взаимоисключающих атрибута: Name и Src. Атри- бут Name определяет имя сборки, которая должна быть связана со страницей (это имя не содержит пути и расширения). В атрибуте Src указывается путь к исходному файлу, подлежащему динамической компиляции и последующему связыванию со страницей. В теле страницы директива ©Assembly может встречаться несколько раз. В част- ности, отдельная такая директива вам потребуется для каждой подключаемой сборки. Атрибуты Name и Src не могут использоваться в одной директиве, но если страница содержит несколько директив ©Assembly, в них можно задавать разные атрибуты, СТ Примечание С точки зрения производительности разница между атрибутами Name и Src минимальна, хотя Name и указывает на существующую и готовую к загрузке сборку. Дело в том, что исходный файл, на который указывает атрибут Src, компилируется лишь во время первого запроса. Исполняющая среда ASP.NET сохраняет динамически отком- пилированную сборку до тех пор, пока ее исходный файл не изменится Это означает, что после первого вызова страницы, содержащей ссылку на определенную сборку, уже не имеет значения, какой из атрибутов используется для идентификации этой сборки в файлах других страниц — она будет связываться с ними одинаково быстро. Директива ©Import Директива ©Import связывает со страницей пространство имен, чтобы все определен- ные в нем типы были доступны странице без указания их полных имен. Например, если вы хотите создать в файле страницы новый экземпляр класса ADO.NET DataSet, можно либо импортировать пространство имен SystemData, что даст вам возможность выполнить такой оператор: DataSet ds = new DataSetO; либо каждый раз задавать уточненное имя класса: System.Data.DataSet ds = new System.Data.DataSetO; Синтаксис директивы ©Import так прост, что не нуждается в пояснениях <%@ Import аатеарасе-'пространство_инен" %> Данная директива может встречаться в теле страницы несколько раз. Она явля- ется аналогом оператора using языка C# и оператора Imports языка Visual Basic.NET, а также директивы ttinclude неуправляемого C/C++. ©Внимание! Следует заметить, что директива ©Import лишь помогает компилятору разре- шать имена классов; она не осуществляет динамического связывания сборок. Применение данной директивы дает возможность использовать короткие имена классов, однако если сборка, содержащая код класса, не связана со страницей должным образом, компилятор генерирует ошибку типа. В этом случае и указание полного имени класса не решает проблемы, поскольку у компилятора нет определения типа. Вы могли заметить, что имена сборки и пространства имен, как правило, совпадают. Однако имейте в виду, что сборка и пространство имен — принципиально различные сущности, а потому для работы с ними используются разные директивы.
Анатомия страницы ASP.NET Глава 3 93 Например, для того чтобы подключиться к базе данных SQL Server и извлечь из нее определенные данные, необходимо импортировать два пространства имен: <%@ Import namespace="System.Data" %> <%@ Import namespace=”System.Data SqlClient" %> Пространство имен System.Data требуется для работы с классами DataSet и Data- Table, a System.Data.SqlClient — для подготовки и выдачи SQL-команды. В данном слу- чае нет нужды в дополнительных директивах ©Assembly, так как сборка System.Data.dll, где содержатся все необходимые классы, связывается со страницей по умолчанию. Директива ©Implements Директива ©Implements позволяет указать, что текущая страница реализует опреде- ленный интерфейс .NET Framework. Напомню, что интерфейсом называется набор сигнатур логически связанных между собой функций. Это род контракта, по которому компонент обязуется экспортировать группу функций. В отличие от абстрактного класса интерфейс не содержит исполняемого кода, связанного с той или иной функ- циональностью. Реализуя интерфейс в странице ASP.NET, вы объявляете его методы и свойства в разделе <script>. Синтаксис директивы ©Implements таков: <%© Implements interface^имя_интерфейса %> Данная директива может встречаться в теле страницы несколько раз, если страница должна реализовать несколько интерфейсов. Однако учтите, что в случае, когда вся логика страницы реализуется в отдельном файле класса, директиву ©Implements ис- пользовать нельзя. Вы просто реализуете требуемые интерфейсы в этом файле. Директива ©Reference Директива ©Reference позволяет установить динамическую связь между текущей страницей и заданной страницей или пользовательским элементом управления. Ее введение открывает новый способ межстраничного взаимодействия и дает возмож- ность создавать строго типизированные экземпляры элементов управления. Директива ©Reference может встречаться в теле страницы несколько раз и имеет два взаимоисключающих атрибута, Page и Control, в каждом из которых задается путь к некоторому исходному файлу: <%@ Reference раде="страница" %> <%@ Reference control="пользовательский_элемент_управления" %> Атрибут Page указывает на исходный файл .aspx, то есть на файл страницы, а атри- бут Control — на исходный файл .ascx, то есть на файл пользовательского элемента управления. В обоих случаях заданный исходный файл динамически компилируется в сборку и определенные в нем классы становятся программно доступными странице, содержащей директиву ©Reference. Как вы уже знаете, во время выполнения страница ASP.NET является экземпля- ром класса .NET Framework с определенным интерфейсом (включающим свойства и методы). Когда выполняется страница, содержащая директиву ©Reference, та стра- ница, которая в ней указана, становится классом. Вы можете программным способом создать экземпляр этого класса и программно же им управлять, вызывая его свойства и методы. Обе страницы, вызывающая и вызываемая, должны принадлежать к одному домену, межсайтовые вызовы не допускаются. В каждом из атрибутов, Page и Control, задается относительный виртуальный путь. СЗ Примечание В ASP.NET 2.0 взаимодействие между страницами лучше осуществлять методом межстраничного постинга.
94 Часть I Разработка страниц ASP.NET Класс Раде В .NET Framework класс Page реализует базовое поведение всех объектов приложе- ния ASP.NET, создаваемых на основе файлов .aspx. Определенный в пространстве имен System.Weh.UI, этот класс является производным от TemplateControl и реализует интерфейс IHttpHandler. public class Page : TemplateControl, IHttpHandler TemplateControl — это абстрактный класс, лежащий в основе страниц и пользова- тельских элементов управления ASP.NET и реализующий их общую функциональ- ность. На верхнем уровне иерархии расположен класс Control, определяющий свой- ства, методы и события, общие для всех серверных элементов ASP.NET — страниц, ' элементов управления и пользовательских элементов управления. Класс Page, который является производным от TemplateControl, служит контейнером имен всех элементов управления, входящих в состав страницы. В .NET Framework кон- тейнером имен элемента управления является ближайший его родительский элемент, реализующий интерфейс INamingContainer. Для любого класса, который реализует данный интерфейс, ASP.NET создает новое виртуальное пространство имен, гарантиру- ющее всем дочерним элементам управления уникальность имен в пределах всего дерева элементов управления. (Эта функция очень важна для элементов управления, связан- ных с данными, таких как DataGrid, и для пользовательских элементов управления.) Класс Page реализует также методы интерфейса IHttpHandler и, следовательно, является обработчиком определенного типа запросов HTTP, а именно запросов файлов .aspx. Ключевым элементом интерфейса IHttpHandler является метод ProcessRequest, вызываемый исполняющей средой ASP.NET для запуска процесса выполнения запроса и формирования его результата. Примечание INamingContainer— маркерный интерфейс, методов он не имеет. Само его присутствие заставляет исполняющую среду ASP.NET создать дополнительное про- странство имен для дочерних элементов управления страницы (элемента управления), которая его реализует. Класс Раде является контейнером имен всех элементов управления страницы, за исключением тех, которые сами реализуют интерфейс INamingContainer (а также их дочерних элементов). Свойства класса Раде Свойства класса Page можно разделить на три группы: внутренние объекты, рабочие свойства и свойства, специфические для страницы. Все они описаны далее. Внутренние объекты В табл. 3-9 приведен список свойств объекта Page, возвращающих внутренние объекты страницы. Эти объекты являются важными элементами инфраструктуры, обеспечи- вающей выполнение страницы. Табл. 3-9. Внутренние объекты ASP.NET в составе класса Раде Свойство Описание Application Экземпляр класса Http Applicationstate, представляющий состо- яние приложения. Он является функциональным эквивалентом внутреннего объекта ASP Application Cache Экземпляр класса Cache, реализующего кэш приложения ASP.NET. Более мощный и эффективный, чем Application, этот класс поддерживает приоритетность и устаревание элементов
Анатомия страницы ASP.NET Глава 3 95 Табл. 3-9. (окончание) Свойство Описание Request Экземпляр класса HttpRequest, представляющий текущий запрос HTTP. Он является функциональным эквивалентом внутренне- го объекта ASP Request Response Экземпляр класса HttpResponse, осуществляющий отправку ответа клиенту. Он является функциональным эквивалентом внутреннего объекта ASP Response Server Экземпляр класса HttpServerUtility, предоставляющего вспомо- гательные методы для обработки Web-запросов. Он является функциональным эквивалентом внутреннего объекта ASP Server Session Экземпляр класса HttpSessionState, который управляет данными, связанными с определенным пользователем. Он является функ- циональным эквивалентом внутреннего объекта ASP Session Trace Экземпляр класса I'race Context, осуществляющий трассировку выполнения страницы User Объект IPrincipal, представляющий пользователя, от которого поступил запрос О классах Request, Response и Server мы поговорим в главе 12, о классах Application и Session — в главе 13; класс Cache будет рассмотрен в главе 14, а класс User и защита приложений станут темой главы 15. Рабочие свойства В табл. 3-10 перечислены свойства объекта Page, содержащие важную информацию и используемые при реализации определенной функциональности. Едва ли можно создать реальную страницу, не обращаясь к этим свойствам. Табл. 3-10. Рабочие свойства класса Раде Свойство Описание ClientScript Возвращает объект ClientScriptManager, содержащий клиентский сценарий, используемый в составе страницы. В ASP.NET 1х данное свойство не поддерживается Controls Возвращает коллекцию дочерних элементов управления текущей страницы ErrorPage Возвращает и позволяет задать страницу сообщения об ошиб- ке, куда в случае обнаружения необработанного исключения страницы будет перенаправлен браузер Form Возвращает текущий объект HtmlForm страницы. В ASP.NET 1х данное свойство не поддерживается Header Возвращает ссылку на объект, который представляет заголовок страницы. Этот объект реализует интерфейс IPageHeader. В ASPNET 1х данное свойство не поддерживается IsAsync Указывает, вызвана ли страница асинхронным обработчиком. В ASPNET 1 jc данное свойство не поддерживается IsCallback Указывает, загружена ли страница в ответ на обратный вызов кли- ентского сценария. В ASPNET 1х данное свойство не поддерживается IsCrossPagePostBack Указывает, загружена ли страница в ответ на возврат формы, вы- полненный другой страницей. В ASPNET 1х данное свойство не поддерживается IsPostBack Указывает, загружена страница в ответ на возврат формы клиен- том или это ее первая загрузка см. след, стр
96 Часть I Разработка страниц ASP.NET Табл. 3-10. (окончание) Свойство Описание IsValid Указывает, успешно ли прошла валидация страницы Master Экземпляр класса MasterPage, представляющий эталонную стра- ницу, которая определяет внешний вид текущей страницы. В ASP.NET 1л данное свойство не поддерживается MasterPageFile Возвращает и позволяет задать имя файла эталонной страницы, связанной с текущей страницей. В ASP.NET 1л данное свойство не поддерживается NamingContainer Возвращает значение null Page Возвращает текущий объект Page PageAdapter Возвращает адаптерный объект текущего объекта Page Parent Возвращает значение null PreviousPage В случае межстраничного возврата формы возвращает ссылку на вызывающую страницу. В ASP.NET 1л данное свойство не поддер- живается TemplateSourceDirectory Возвращает имя виртуального каталога страницы Validators Возвращает коллекцию имеющихся в составе страницы валидаци- онных элементов управления View State UserKey Строковое свойство, представляющее пользовательский иденти- фикатор, который предназначен для хэширования данных состоя- ния представления. Он используется для защиты от атак типа one-click (когда злоумышленник отправляет серверу поддельное состояние представления страницы). В ASP.NET 1.0 данное свой- ство не поддерживается В контексте приложения ASP.NET корнем иерархии объектов страницы является объект Page. Поэтому унаследованные свойства, такие как NamingContatner и Parent, всегда возвращают null, а свойство Page возвращает тот же объект (this в C# и Me в Visual Basic .NET). Особого упоминания требует свойство ViewStateUserKey, появившееся в .NET Frame- work 1.1. Содержащийся в нем ключ пользователя применяется для хэширования содер- жимого состояния представления — совместно с другими данными он в качестве специ- фической для пользователя информации используется для формирования хэш-значения (см. главу 13). Обычно значением свойства ViewStateUserKey является имя аутентифи- цированного пользователя или идентификатор пользовательского сеанса. Описанный прием служит для повышения степени защиты данных состояния представления и уменьшения вероятности атаки. Когда применяется специфический для пользователя ключ, атакующий не может сформировать допустимое состояние представления, если только сам не войдет в систему под вашим именем. В такой конфигурации создается дополнительный барьер для атак типа one-click. Но для Web-сайтов, допускающих ано- нимный доступ, такая технология может быть неэффективной — разве что вы найдете другой источник уникальной информации отслеживания пользователей. Если вы планируете установить свойство ViewStateUserKey, то учтите, что делать это нужно в обработчике события Page Init. Если попытаться установить его позднее, скажем, в ответ на событие PageJLoad, будет выброшено исключение. Контекстные свойства В табл. 3-11 перечислены свойства класса Page, представляющие визуальные и не- визуальные атрибуты страницы, такие как URL строки запроса, целевой клиент, заголовок и таблица стилей.
Анатомия страницы ASP.NET Глава 3 97 Табл. 3-11. Свойства класса Раде, специфические для страницы Свойство Описание ClientID ClientQueryString Всегда возвращает пустую строку Возвращает URL из строки запроса. В ASP.NET 1х данное свойство не поддерживается ClientTarget По умолчанию содержит пустую строку; позволяет за- •1 дать тип целевого браузера, для которого должна быть адаптирована результирующая разметка. При установ- ке этого свойства отключается функция автоматиче- ского определения возможностей браузера Enable View State Указывает, должна ли страница управлять данными состояния представления. При желании данную функ- цию можно включать и отключать декларативно, ис- пользуя атрибут EnableViewState директивы ©Page Enable ViewStateMac Указывает, должна ли ASP.NET вычислять машинно- зависимый аутентификационный код и добавлять его к состоянию представления страницы EnableTheming Указывает, применяются ли к странице темы. В ASP.NET 1х данное свойство не поддерживается ID MaintainScrollPositionOnPostback Всегда возвращает пустую строку Указывает, следует ли после возврата формы восста- навливать позицию просмотра страницы в браузере. В ASP.NET 1х данное свойство не поддерживается SmartNavigation Указывает, осуществляется ли интеллектуальная на- вигация. (Напомню, что средствами интеллектуаль- ной навигации называется группа функций браузера, делающих работу пользователя со страницей более удобной. Они поддерживаются Internet Explorer вер- сии 5.5 и выше) StyleSheetTheme Возвращает и позволяет задать имя таблицы стилей, которая будет применена к странице. В ASP.NET 1х данное свойство не поддерживается Theme Возвращает и позволяет задать имя темы страницы. Заметьте, что тему можно задать только в обработчике события Prelnit. В ASP.NET 1х данное свойство не под- держивается Title Возвращает и позволяет задать заголовок страницы. В ASP.NET 1 х данное свойство не поддерживается TraceEnabled Позволяет включить или отключить трассировку страницы. В ASP.NET 1х данное свойство не поддержи- вается TraceMode Value Возвращает и позволяет задать режим трассировки страницы. В ASP.NET 1х данное свойство не поддержи- вается UniquelD ViewStateEncryptionMode Всегда возвращает пустую строку Указывает, должно ли быть зашифровано состояние страницы и, если должно, то как именно Visible Указывает, должна ли ASP.NET осуществлять рен- деринг страницы. Если присвоить этому свойству значение false, ASP.NET не будет генерировать для страницы HTML-код и клиент получит только тот текст, который вы явно запишете в выходной поток с помощью метода Response. Write
98 Часть I Разработка страниц ASP.NET Три свойства-идентификатора (ID, ClientID и UniquelD) объекта Page всегда возвра- щают пустую строку. Они предназначены лишь для серверных элементов управления. Методы класса Раде Все методы класса Page в соответствии с их назначением можно условно разделить на три категории. Одни из них связаны с генерированием разметки страницы, другие используются как вспомогательные при формировании страницы и управлении со- ставляющими ее элементами управления, третьи имеют отношение к работе с кли- ентскими сценариями. Методы рендеринга В табл. 3-12 перечислены методы, которые так или иначе связаны с генерированием кода разметки страницы. Табл. 3-12. Методы, связанные с генерированием кода разметки Метод Описание DataBind Все имеющиеся в составе страницы элементы управления, связанные с данными, этот метод связывает с их источни- ками данных. Сам метод DataBind не генерирует кода, но осуществляет необходимую подготовку к последующему рендерингу RenderControl Выводит HTML-код, в том числе информацию трассиров- ки, если данная функция включена Verify RenderinglnServerForm Элементы управления вызывают этот метод непосредствен- но перед рендерингом, проверяя включены ли они в тело серверной формы. Данный метод не возвращает значения, но в случае ошибки выбрасывает исключение В странице ASP.NET элементы управления не могут размещаться вне тэга <form> с атрибутом runat==server. Метод VerifyRenderinglnServerForm используется элементами управления Web и HTML для обеспечения правильного рендеринга. Теоретически специализированные элементы управления должны вызывать данный метод на этапе рендеринга. Но поскольку во многих случаях они включают в себя существующие элементы управления Web и HTML или являются от них производными, последние осуществляют необходимую проверку сами. Не входит в состав класса Page, хотя и тесно с ним связан, метод GetWebResourceUrl класса ClientScriptManager ASP.NET 2.0. Он реализует функцию, о которой давно мечтали разработчики элементов управления. Создавая собственный элемент управ- ления, вы обычно включаете в его состав те ли иные ресурсы, скажем, изображения или клиентские сценарии. Их файлы можно загружать по отдельности, и хотя это решение не является таким уж неэффективным, изящным его тоже не назовешь. Начиная с версии 2003 Visual Studio .NET позволяет встраивать ресурсы в сборку элемента управления, но как программным способом извлекать их оттуда и связывать с элементом управления? Например, для того чтобы связать содержащееся в сборке изображение с тэгом <IMG>, вам потребуется URL этого изображения. Вот тут-то и придет на помощь метод GetWebResourceUrl, возвращающий URL заданного ресурса. Этот URL указывает на новый сервис Web Resource (webresource.axd), который из- влекает из сборки и возвращает запрошенный ресурс. // Связываем тэг <IMG> с заданным изображением типа GIF, // содержащимся в сборке элемента управления img.ImageUrl = Page.GetWebResourceUrl (typeof(Tr.eControl), GifName));
Анатомия страницы ASP.NET Глава 3 99 Методу GetWebResourceUrl необходим объект Туре, который он использует для поиска сборки, содержащей ресурс. Она идентифицируется как сборка из текущего » домена AppDomain, содержащая определение указанного типа. Если вы пишете спе- циализированный элемент управления, то его тип и нужно здесь указать. Во втором аргументе методу GetWebResourceUrl передается имя встроенного ресурса. Возвраща- емый методом URL имеет форму WebResou rce.axd? а=сборка&г=имя_ресурса&1=штамп_времени В качестве штампа времени задается текущий штамп времени сборки, чтобы бра- узер мог повторно загрузить ресурс в случае ее модификации. Методы, связанные с элементами управления В табл. 3-13 перечислены вспомогательные методы класса Page, предназначенные для управления дочерними элементами управления, выполнения их валидации и раз- решения URL. Табл. 3-13. Вспомогательные методы класса Раде Метод Описание Designerlnitialize Инициализирует экземпляр класса Page во время разработки, когда страница отображается в дизайнере Visual Studio или другого RAD-средства FindControl Принимает идентификатор элемента управления и ищет его в контейнере именования страницы. Поиск не производится среди дочерних элементов управления, которые сами явля- ются контейнерами имен GetTypeHashCode Извлекает хэш-код, сгенерированный объектом страницы ASPjcxxaspx во время выполнения. В базовом классе Page реализация этого метода просто возвращает О', значения, име- ющие определенный смысл, возвращают классы реальных страниц GetValidators Возвращает коллекцию валидаторов элементов управления для определенной группы валидации. В ASP.NET 1х данный метод не поддерживается HasControls Определяет, содержит ли страница дочерние элементы управления LoadControl Компилирует пользовательский элемент управления из файла .asex, загружает его и возвращает объект Control. Если пользовательский элемент управления поддерживает кэши- рование, возвращаемый объект относится к классу Partial- CachingControl LoadTemplate Компилирует пользовательский элемент управления из фай- ла .asex, загружает его и возвращает заключенным внутрь экземпляра класса SimpleTemplate, реализующего интерфейс ITemplate MapPath Извлекает полный физический путь, соответствующий задан- ному абсолютному или относительному виртуальному пути ParseControl Выполняет разбор заданной строки, содержащей допустимый код разметки, и возвращает экземпляр соответствующего эле- мента управления. Если строка содержит несколько элемен- тов управления, возвращается только первый. Атрибут runat может быть опущен. Метод возвращает объект типа Control, который должен быть приведен к более конкретному типу см. след. стр.
100 Часть I Разработка страниц ASP.NET Табл. 3-13. (окончание) Метод Описание RegisterRequiresControlState Регистрирует заданный элемент управления как требующий сохранения состояния. В ASP.NET 1jc данный метод не под- держивается RegisterRequiresPostBack Регистрирует заданный элемент управления как получатель уведомления об обработке события обратного вызова, даже если его идентификатор не соответствует ни одному из иден- тификаторов в коллекции возвращенных данных. Элемент управления должен реализовать интерфейс IPostBackData- Handler RegisterRequiresRaiseEvent Регистрирует заданный элемент управления как обработчик события возврата формы. Э гот элемент управления должен реализовать интерфейс IPostBackEventHandler RegisterViewStateHandler Предназначенный главным образом для внутреннего приме- нения, этот метод устанавливает внутренний флаг, который сигнализирует о необходимости сохранить состояние пред- ставления страницы. Если не вызвать этот метод на этапе предрендеринга, состояние представления никогда не будет сохранено. Обычно данный метод вызывается только сервер- ным элементом управления HtmlForm. Из пользовательских приложений вызывать его ни к чему ResolveUrl По заданному относительному URL возвращает абсолютный, основываясь на значении свойства TemplateSourceDirectory Validate Указывает валидационным элементам управления страницы на необходимость проверить введенную информацию. ASP.NET 2.0 поддерживает группы валидации Методы LoadControl и LoadTemplate используют общую кодовую инфраструктуру, но возвращают разные объекты, что демонстрирует следующий псевдокод: public Control LoadControl(string virtualPath) { Control ascx = GetCompiledllserControlType(virtualPath); ascx. InitializeAsUserControlO; return ascx; } public ITemplate LoadTemplate(string virtualPath) { Control ascx = GetCompiledUserControlType(virtualPath); return new SimpleTemplate(ascx); } Эти методы отличаются от метода ParseControl тем, что последний никогда не ини- циирует компиляцию, а просто осуществляет разбор строки и получает информацию об элементе управления. Эта информация затем используется для создания и иници- ализации нового экземпляра элемента управления. Как уже упоминалось, в данном контексте атрибут runat не является обязательным. В ASP.NET он играет ключевую роль при обработке страницы, но эта его роль сводится к тому, что данный атрибут просто помечает элемент разметки как требующий синтаксического разбора и созда- ния экземпляра элемента управления. Другой информации, полезной для создания экземпляра элемента управления, атрибут runat не несет и поэтому в строках, которые передаются непосредственно методу ParseControl, задавать его не обязательно. Методы, связанные со сценариями В табл. 3-14 перечислены методы класса Page, имеющие отношение к HTML и коду сценариев, вставляемых в клиентскую страницу.
Анатомия страницы ASP.NET Глава 3 101 Табл. 3-14. Методы класса Раде, связанные со сценариями Метод Описание GetCallbackEventReference Получает ссылку на клиентскую функцию, осуществляю- щую обратный вызов серверного кода. В ASP.NET 1jc данное свойство не поддерживается GetPostBackClientEvent GetPostBackClientHyperlink Вызывает метод GetCallbackEventReference Добавляет строку javascript: в начало строки, полученной от метода GetPostBackEventReference, как показано ниже: javascript. doPostBack( 'CtllD") GetPostBackEventReference Возвращает прототип функции клиентского сценария, осуществляющей обратный вызов серверного кода; метод принимает объект Control и аргумент функции и возвращает строку, подобную следующей: _doPostBack('CtlID") IsClientScriptBlockRegistered Определяет зарегистрирован ли для страницы заданный кли ентский сценарий. Данный метод помечен как устаревший IsStartupScriptRegistered Определяет, зарегистрирован ли для страницы заданный клиентский startup-сценарий. Данный метод помечен как устаревший RegisterArrayDeclaration Используется для добавления в клиентскую страницу мас- сива ECMAScript. Данный метод принимает имя массива и строку, которая будет использоваться в том виде, в каком вы ее задали, в качестве i ела массива. Например, если вызвать метод с аргументами "theArray "и "'аЪ то в результате получится следующий код на языке JavaScript: var theArray - new Array ('a', Ъ); Данный метод помечен как устаревший RegisterChentScnptBlock Вставляет в клиентскую страницу блок клиентского сце- нария после открывающегося HTML-тэга <form>. Данный метод помечен как устаревший RegisterHiddenField Регистрирует разметку для скрытого поля. Данный метод помечен как устаревший RegisterOnSubmitStatement Выводит клиентский сценарий, ассоциированный с событи- ем OnSubmit формы. Этим сценарием должен быть вызов за- регистрированной клиентской функции JavaScript. Данный метод помечен как устаревший RegisterStartupScript Выводит клиентский сценарий непосредственно перед за- крывающимся тэгом </form>. Данный метод помечен как устаревший SetFocus Дает указание браузеру присвоить фокус ввода заданному элементу управления. В ASP.NET 1-х данный метод не под- держивается Как видите, многие из перечисленных в табл. 3-14 методов были определены и ис- пользовались в ASP.NET 1.x, однако теперь помечены как устаревшие. В приложениях ASP.NET 2.0 следует избегать их вызова и пользоваться одноименными методами объекта, возвращаемого свойством ClientScript (см. табл. 3-10). И Не делайте так в ASP NET 2.0: Page RegisterArrayDeclaration(... // В ASP.NET 2.0 следует использовать вызов: Page.Clientscript.RegisterArrayOeclaration(...);
102 Часть I Разработка страниц ASP.NET Методы, перечисленные в табл. 3-14, позволяют вставлять в клиентскую стра- ницу сценарии на языке JavaScript или VBScript. Вызывая один из этих методов, вы на самом деле указываете странице, что на этапе рендеринга она должна будет вставить заданный сценарий в код генерируемой клиентской страницы. Поэтому при вызове данных методов связанная со сценариями информация просто кэшируется во внутренних структурах страницы и используется ею позднее, когда приходит время генерировать разметку. | ^"| Примечание Язык сценариев JavaScript поддерживается всеми современными браузе- рами, поэтому некоторые методы из табл. 3-14 по умолчанию используют именно его. Однако ничто не мешает вам писать регистрируемые сценарии на языке VBScript — не забудьте только задать его в атрибуте language тэга <script>. Учтите, что методы Re- gisterOnSubmitStatement и RegisterArrayDeclaration могут использоваться лишь с кодом JavaScript. События класса Раде В течение жизненного цикла страницы класс Page генерирует серию событий. Как видно из табл. 3-15, некоторые из них ортогональны типичному жизненному ци- клу страницы (который составляют этапы инициализации, обработки возвращенных данных формы, рендеринга) и генерируются в случаях, когда в процессе принимает участие дополнительная страница. Мы вкратце рассмотрим все события, а затем перейдем к подробному изучению жизненного цикла страницы. Табл. 3-15. События, которые может генерировать страница Событие Когда происходит AbortTransaction Отменена автоматическая транзакция, в которой участвует данная страница ASP.NET CommitTransaction Завершена автоматическая транзакция, в которой участвует данная страница ASP.NET DataBinding Для страницы вызван метод DataBind, связывающий все дочерние элементы управления с их источниками данных Disposed Страница удаляется из памяти — это последний этап ее жизненного цикла Error Выброшено необработанное исключение Init Страница инициализируется — это первый этап ее жизненного цикла InitComplete Все дочерние элементы управления и сама страница инициализирова- ны. В ASP.NET 1х данное событие не поддерживается Load Только что инициализированная страница загружается LoadComplete Загрузка страницы завершена, сгенерированы серверные события. В ASP.NET 1х данное событие не поддерживается Prelnit Непосредственно перед началом этапа инициализации страницы. В ASP.NET 1х данное событие не поддерживается PreLoad Непосредственно перед началом этапа загрузки страницы. В ASP.NET 1х данное событие не поддерживается PreRender Страница готова к рендерингу PreRenderComplete Непосредственно перед началом этапа предрендеринга страницы. В ASP.NET 1х данное событие не поддерживается SaveStateComplete Состояние представления страницы сохранено в постоянной памяти. В ASP.NET 1х данное событие не поддерживается Unload Страница выгружается из памяти, но еще не уничтожена
Анатомия страницы ASP.NET Глава 3 103 Модель событий ASP.NET Класс страницы и входящие в ее состав серверные элементы управления отвечают за выполнение запроса этой страницы и рендеринг HTML-кода для клиента. Взаи- модействие между клиентом и сервером осуществляется без сохранения состояния, и после передачи каждого пакета соединение разрывается, поскольку такова природа протокола HTTP. Однако реальным приложениям необходимо, чтобы определенные данные состояния сохранялись между последовательными вызовами одной и той же страницы. В ASP и других серверных платформах разработки, например в Java Server Pages и LAMP, за сохранение состояния отвечает программист. В ASP.NET, напротив, имеется встроенная инфраструктура, которая сохраняет и восстанавливает состояние страницы прозрачным для приложения способом. Таким образом, несмотря на при- роду базового протокола, пользователю кажется, что он участвует в непрерывном процессе. Но это лишь иллюзия. Состояние представления Иллюзия непрерывности создается подсистемой ASP.NET, отвечающей за управле- ние состоянием представления страниц, на основе определенных предположений об устройстве и работе страниц. Важную роль в этом играют серверные элементы управ- ления. Если говорить коротко, то перед рендерингом своего контента в HTML-форме страница кодирует и сохраняет, обычно в скрытом поле, всю информацию состояния представления, в сохранении которой нуждаются она сама и ее элементы управления. Когда происходит возврат формы, состояние представления извлекается из скрытого поля, десериализуется и используется для инициализации экземпляров серверных элементов управления, объявленных в разметке серверной страницы. Состояние представления уникально для каждого экземпляра страницы, поскольку оно встроено в HTML-код. Благодаря этому при возврате формы элементы управле- ния страницы инициализируются теми же значениями, какие они имели в тот момент, когда в последний раз формировалось состояние представления, то есть когда в по- следний раз осуществлялся рендеринг страницы для клиента. На дополнительном этапе жизненного цикла страницы происходит объединение сохраненного состояния представления с изменениями, внесенными в результате действий клиента. Таким образом, после возврата формы страница выполняется в актуальном контексте, как будто используется непрерывное соединение «точка-точка». Данная схема выполнения страницы основана на двух важных предположениях: во-первых, что страница всегда выполняет возврат формы самой себе и передает между клиентом и сервером состояние представления, а во-вторых, что серверные элементы управления объявлены с атрибутом runat^server, благодаря чему после возврата формы они «возвращаются к жизни». Модель с одной формой Для программистов, имеющих опыт работы с ASP, реализованная в ASP.NET модель страницы с одной формой непривычна. Они часто спрашивают в форумах и группах новостей: «Куда подевалось свойство формы Action?» и «Почему я не могу перена- править возврат формы определенной странице?». Страницы ASP.NET могут содержать только один серверный тэг <form>, поэтому форма должна включать все элементы управления, с которыми вы хотите взаимодей- ствовать на сервере. И форму, и элементы управления необходимо помечать атрибутом runat; в противном случае они будут рассматриваться как чистый текст, выводимый в исходном виде. Серверная форма является экземпляром класса HtmlForm. У этого класса нет экспортируемого свойства, эквивалентного свойству Action HTML-тэга
104 Часть I Разработка страниц ASP.NET <form>, поскольку страница ASP.NET всегда осуществляет возврат формы самой себе. В отличие от свойства Action другие часто используемые свойства формы, такие как Method и Target, полностью поддерживаются. Страница ASP.NET может одновременно содержать и серверную форму, и фор- му HTML, однако в ней не должно быть более одного тэга <form> с атрибутом runat^server. Форма HTML работает, как обычно, и может быть возвращена любой странице приложения, однако при этом автоматического восстановления ее состояния не происходит. Иными словами, модель Web Forms ASP.NET работает то. хько при ис- пользовании единственного серверного элемента управления <form>. Мы вернемся к этой теме в главе 5. । Примечание Запросы страниц ASP.NET обслуживаются обработчиком HTTP, которым является экземпляр класса Раде. Для обработки каждого запроса выделяется поток из пула потоков ASP.NET, освобождаемый лишь по завершении выполнения запроса. Что будет, если часто запрашиваемая страница инициирует выполнение внешней и достаточно длительной операции? Процесс страницы в течение всего времени выполнения этой опе- рации будет бездействовать, но оставаться занятым, и в результате может оказаться, что при поступлении очередного запроса все потоки будут заняты. Дело в том, что обработ- чики HTTP, и в частности классы страниц, работают синхронно. Для решения описанной проблемы в ASP.NET, начиная с версии 1.0, были введены асинхронные обработчики, которые реализуют интерфейс IHttpAsyncHandler. В ASP.NET 2.0 создание асинхронной страницы выполняется проще, чем раньше, благодаря улучшенной поддержке со сторо- ны рабочей среды. Данная тема подробно рассматривается в моей книге «Programming Microsoft ASP.NET 2.0 Applications: Advanced Topics» (Microsoft Press, 2005). Жизненный цикл страницы Когда клиент в очередной раз запрашивает страницу, экземпляр ее класса создает- ся заново, после чего исполняющая среда вызывает метод ProcessRequest объекта страницы и начинается ее выполнение, то есть ее жизненный цикл. Весь жизненный цикл страницы условно разделяют на этапы, или стадии, которые, в свою очередь, подразделяются наименьшие этапы, или стадии, и наименьшие из них называются шагами. Одними этапами можно управлять с помощью обработчиков событий, другие требуют переопределения методов. Некоторые этапы (или, скорее, их составляющие) недоступны контролю разработчика, их я упоминаю здесь для полноты изложения. В жизненном цикле страницы можно выделить три базовых этапа: подготови- тельный, этап обработки информации, связанной с возвратом формы, и заверша- ющий. Каждый из них делится на несколько меньших этапов и шагов, на которых генерируются события. Описание жизненного цикла страницы, которое я здесь при- веду, охватывает все возможные пути ее выполнения. Однако следует понимать, что описанный процесс может изменяться в зависимости от текущей ситуации: страница выполняется в первый раз, имеет место межстраничный возврат формы, обратный вызов сценария или обычный возврат формы. Подготовка страницы к выполнению Когда исполняющая среда HTTP создает экземпляр класса страницы для обслу- живания текущего запроса, конструктор страницы формирует дерево ее элементов управления. Это дерево связывается с классом страницы, созданным после анализа исходного файла .aspx. Перед началом процесса обработки запроса подготавливают- ся и инициализируются все дочерние элементы управления и внутренние объекты страницы, такие как контекст HTTP и объекты запроса и ответа. В процессе обработки запроса первым делом страница выясняет причину своего запуска. Этой причиной может быть получение обычного запроса, возврат формы,
Анатомия страницы ASP.NET Глава 3 105 межстраничный возврат формы или обратный вызов сценария. Объект страницы конфигурирует свое внутреннее состояние с учетом этой причины и подготавливает коллекцию возвращенных клиентской страницей значений (если таковые имеются) с учетом метода запроса — GET или POST. После выполнения этого первого шага страница готова генерировать события для пользовательского кода. Событие Prelnit Это событие, введенное в ASP.NET 2.0, является точкой входа жизненного цикла страницы. Когда оно генерируется, ни эталонная страница, ни тема еще не связаны с текущей страницей. Однако позиция прокрутки страницы уже восстановлена, доступны возвращенные клиентом данные, экземпляры всех элементов управления страницы созданы и инициированы значениями свойств по умолчанию, которые определяются в исходном файле .aspx. (Но элементы управления пока что не име- ют идентификаторов, если только они не заданы явно в исходном файле aspx.) Это единственный момент, когда можно программным способом задать для теку- щей страницы эталонную страницу и тему. Данное событие доступно только для страницы. Свойства IsCallback, IsCrossPagePostback и IsPostback к этому моменту уже установлены. Событие /п/t Теперь эталонная страница и тема уже связаны с текущей страницей, и изменить их невозможно. Процессор страницы, то есть метод ProcessRequest класса Page, про- должает свою работу и перебирает в цикле все дочерние элементы управления, давая каждому из них шанс инициализировать свое состояние с учетом текущего контекста. У каждого дочернего элемента управления есть метод Onlnit, который и вызывается процессором. Каждому дочернему элементу управления присваивается контейнер имен и идентификатор, если они не указаны в исходном файле страницы. Событие Init вначале Достигает дочерних элементов управления, а уж потом са- мой страницы. На этом этапе страница и ее элементы управления обычно начинают загрузку определенных составляющих своего состояния. Состояние представления пока еще не восстановлено. Событие InitComplete Данное событие (оно также впервые появилось в ASP.NET 2.0) сигнализирует об окончании стадии инициализации. Между событиями Init и InitComplete страница производит только одну операцию: включает режим отслеживания изменений в со- стояний представления Функция отслеживания изменений в состоянии представления обеспечивает элементам управления возможность сохранить те значения, которые программно добавляются в коллекцию ViewState. Если для какого-то из элементов управления данная функция отключена, любые добавленные им в коллекцию ViewState значения между возвратами формы утрачиваются. Все элементы управления включают режим отслеживания состояния представления сразу после того, как будет сгенерировано событие Init, и страница — не исключение. (Ведь по сути она тоже является элемен- том управления.) ©Внимание! В свете только что сказанного становится очевидным, что любые значения, записанные в коллекцию ViewState до события InitComplete, при следующем возврате формы оказываются недоступными. В ASP.NET 1.x, для того чтобы надежно сохранить данные состояния представления страницы или элемента управления, приходилось ждать наступления события Load.
106 Часть I Разработка страниц ASP.NET Восстановление состояния представления Если страница обрабатывается по причине возврата формы, то есть ее свойство IsPost- Back содержит значение true, восстанавливается содержимое скрытого поля_VIEW- STATE клиентской страницы. В этом поле хранится состояние представления всех элементов управления, записанное туда в конце выполнения предыдущего запроса. Общее состояние представления страницы является своего рода контекстом вызова и содержит состояние всех ее элементов управления, в последний раз переданное браузеру. На данном этапе каждый элемент управления получает возможность обновить свое текущее состояние, сделав его таким, каким оно было во время выполнения предыдущего запроса. События тут не генерируются, а если необходимо выполнить ту или иную обработку, можно переопределить метод LoadViewState, определенный как защищенный и виртуальный в классе Control. Обработка данных, полученных в результате возврата формы На этом этапе обрабатываются все клиентские данные, содержащиеся в запросе HTTP, то есть содержимое всех полей ввода, определенных в тэге <form>. Обычно данные пересылаются в такой форме: TextBox1=text&DropDownList1=selectedItem&Button1=Submit Это разделенная символами & строка пар имя—значение, которые загружаются в спе- циальную коллекцию, предназначенную для внутреннего использования. Процессор страницы ищет соответствие между именами из полученной коллекции и идентифи- каторами элементов управления страницы. Найдя его, он проверяет, реализует ли соответствующий серверный элемент управления интерфейс IPostBackDataHandler. Если да, вызываются методы этого интерфейса, чтобы дать элементу управления возможность обновить свое состояние с использованием полученных данных. В част- ности, процессор страницы вызывает метод LoadPostData. Если этот метод возвращает true, то есть состояние элемента управления обновлено, последний добавляется в от- дельную коллекцию для дальнейшей обработки. Если имя полученного элемента данных не принадлежит ни одному из серверных элементов управления, он игнорируется и временно помещается в отдельную коллек- цию для повторной попытки идентификации, которая будет предпринята позднее. Событие PreLoad Данное событие (также было введено в ASP.NET 2.0) указывает, что страница за- вершила этап инициализации системного уровня и переходит к тому этапу, на ко- тором пользовательский код страницы имеет возможность сконфигурировать ее для дальнейшего выполнения и рендеринга. Это событие генерируется только для страниц. Событие Load Данное событие генерируется сначала для страницы, а потом рекурсивно для всех ее дочерних элементов управления. К этому моменту элементы управления, состав- ляющие дерево страницы, уже созданы и их состояние полностью восстановлено с учетом данных, полученных от клиента. Страница готова к выполнению инициа- лизационного кода, связанного с ее логикой и поведением. На этом этапе доступ к свойствам элемента управления и состоянию представления можно осуществлять без каких-либо опасений.
Анатомия страницы ASP.NET Глава 3 107 Обработка динамически созданных элементов управления Когда все элементы управления страницы получили возможность завершить перед отображением свою инициализацию, процессор страницы предпринимает вторую попытку идентифицировать полученные от клиента значения, для которых не на- шлось соответствий среди существующих элементов управления. Для этого он по- вторяет действия, описанные выше, в подразделе «Обработка данных, полученных в результате возврата формы». Такое его странное поведение объясняется очень просто: процессор рассчитывает на то, что недостающие элементы управления могли быть созданы динамически. Динамическое добавление элемента управления в дерево страницы может осу- ществляться, например, в ответ на определенное действие пользователя. Как уже упоминалось, после каждого возврата формы дерево страницы строится заново, так что информация о динамически созданных ранее элементах управления утрачивается. Однако на клиенте в момент возврата формы все динамически созданные элементы управления в ней присутствуют и заполнены данными, которые и получит сервер. Очевидно, что первоначально, то есть сразу после создания дерева страницы, полу- ченным от клиента значениям динамических элементов управления не соответствуют никакие идентификаторы серверных элементов, но ASP.NET знает, что элементы управления могут создаваться в обработчике события Load. Вот потому-то и произ- водится вторая попытка идентификации полученных значений как принадлежащих динамическим элементам управления. Если на этот раз соответствие будет найдено, элемент управления сможет обновить свое состояние, используя полученные данные. Обработка возврата формы Механизм возврата формы является главной движущей силой любого приложения ASP.NET. Суть операции возврата формы заключается в том, что данные формы кли- ентской страницы передаются серверной странице — той самой, которая эту клиент- скую страницу сгенерировала, и серверная страница восстанавливает контекст вызова, используя сохраненное ранее состояние представления и текущие данные формы. При этом серверная страница сначала возвращается к тому состоянию, в котором она находилась, когда сгенерировала страницу, выполнившую возврат формы (для чего используется сохраненное тогда состояние представления), а потом корректирует свое состояние с учетом текущих введенных пользователем данных. После того как страница выполнила инициализацию и обработала полученные от клиента значения, приходит время двух групп серверных событий: события первой группы сигнализируют об изменении состояния определенных серверных элементов управления, а события второй группы генерируются в ответ на действие клиента, вызвавшее возврат формы. Обнаружение изменений в состоянии элементов управления Система ASP.NET действует, исходя из предположения о наличии взаимнооднознач- ного соответствия между HTML-тэгами ввода, используемыми в браузере, и элемен- тами управления ASP.NET, функционирующими на сервере. Примером может слу- жить соответствие между тэгом <input type~ "text"> и элементом управления TextBox. Технически связь между этими двумя элементами устанавливается посредством их идентификаторов, которые должны быть одинаковыми. О том, как устанавливается эта связь, рассказывалось в разделе «Обработка данных, полученных в результате возврата формы».
108 Часть I Разработка страниц ASP.NET Для всех элементов управления, вернувших из метода LoadPostData значение true, теперь пришло время выполнить второй метод интерфейса IPostBackDataHand- ler — RaisePostDataChangedEvent. Его вызов сигнализирует элементу управления, что пора уведомить приложение ASP.NET об изменении своего состояния. Реализац] [я данного метода оставляется на усмотрение разработчика элемента управления, однако большинство элементов делают в нем одно и то же: генерируют серверное событие и предоставляют разработчику страницы возможность включиться в игру и выполнить код, обрабатывающий данное событие. Например, если после возврата формы содер- жимое свойства Text элемента управления TextBox оказалось измененным, элемент управления TextBox генерирует событие TextChanged. Обработка серверного события возврата формы Операция возврата формы начинается с того, что на клиенте осуществляется некото- рое действие, требующее реакции сервера. Например, пользователь щелкает кнопку, предназначенную для отправки содержи мого формы серверу и получения от него новой или обновленной страницы. Такая клиентская кнопка, обычно реализованная как гиперссылка или кнопка $м&т?и£-типа, связана с серверным элементом управления, реализующим интерфейс IPostBackEventHandler. Процессор страницы просматривает полученные от клиента данные и определяет, какой элемент управления инициировал возврат формы. Если этот элемент реализует интерфейс IPostBackEventHandler, процессор вызывает его метод RaisePostBackEvent. Реализация данного метода оставлена на усмотрение разработчика элемента управ- ления и, по крайней мере теоретически, может у разных элементов несколько раз- личаться. Однако на практике все элементы управления генерируют в нем серверное событие, позволяющее автору страницы программно отреагировать на возврат формы. Например, элемент управления Button генерирует событие onclick. Страница может осуществить возврат формы двумя способами, с использованием кнопки submit-типа (то есть элемента <input type"' "submit ">) или посредством сце- нария. Разметка кнопки submit-типа генерируется серверным элементом управления Button. Вместо того чтобы использовать LinkButton или другие элементы управления, можно вставить в клиентскую страницу сценарий, связывающий событие HTML (на- пример, onclick) с методом submit формы из объектной модели браузера. Мы вернемся к этой теме в следующей главе. Примечание В ASP.NET 2.0 у класса Button появилось новое свойство, UseSubmitBeha- vior, позволяющее разработчику страницы управлять клиентским поведением генерируе- мого HTML-элемента, связанным с возвратом формы. В ASP.NET 1 .х элемент управления Button всегда выводит <input type='submit>. В ASP.NET 2.0 можно установить свойство UseSubmitBehavior в false, чтобы этот элемент выводил <input type="button'l>. В таком случае возврат формы будет происходить с использованием сценария. Событие LoadComplete $ Введенное в ASP.NET 2.0 и поддерживаемое только для страниц, событие LoadCom- plete сигнализирует об окончании этапа подготовки страницы. Обратите внимание, что дочерние элементы управления этого события не получают. Сгенерировав событие LoadComplete, страница вступает в фазу рендеринга. Завершающий этап выполнения страницы После обработки события возврата формы страница готова сгенерировать вывод для браузера. Этап рендеринга делится на две стадии: предрендеринг и генерир, >вание
Анатомия страницы ASP.NET Глава 3 109 разметки. Стадия предрендеринга, в свою очередь, также делится на две части, кото- рым соответствуют события предобработки и постобработки. Событие PreRender Обрабатывая событие PreRender, страница и элементы управления могут выполнять изменения, которые необходимо внести до того, как начнется рендеринг страницы. Данное событие сначала достигает страницы, а затем рекурсивно всех ее элементов управления. Заметьте, что страница уже обеспечила создание всех дочерних элементов. Этот этап особенно важен для составных элементов управления Событие PreRenderComplete Поскольку событие PreRender рекурсивно генерируется для всех дочерних элементов управления страницы, ее автор не знает, когда завершится фаза предрендеринга. По- этому в ASPNET 2.0 введено новое событие, PreRenderComplete, генерируемое только для страницы и уведомляющее об этом моменте. Событие SaveStateComplete Следующим шагом, предшествующим генерированию разметки клиентской страницы, является сохранение ее текущего состояния. Важно отметить, что каждое действие, выполненное после этого момента и связанное с модификацией состояния представ- ления, может отразиться на рендеринге, но внесенные при этом изменения состояния представления уже не сохранятся и при следующем возврате формы будут утрачены. Сохранение состояния страницы — рекурсивный процесс, при выполнении которо- го процессор страницы проходит по всему ее дереву, вызывая метод SaveViewState элементов управления и самой страницы. SaveViewState — это защищенный и вирту- альный (то есть переопределяемый) метод, отвечающий за сохранение содержимого словаря ViewState для текущего элемента управления. (О том, что такое словарь ViewState, я расскажу в главе 13.) В ASP.NET 2.0 помимо состояния представления с элементами управления свя- зано еще и так называемое состояние элемента — род приватного состояния пред- ставления, не являющегося предметом управления со стороны приложения. Иными словами, управление состоянием элемента нельзя программно отключить, как это можно сделать для состояния представления. Так вот, состояние элемента также со- храняется на данном этапе. Событие SaveStateComplete, введенное в ASP.NET 2.0, генерируется после полного сохранения состояния элементов управления страницы. [ J Примечание Данные состояния представления страницы и отдельных ее элементов управления накапливаются в специальной структуре памяти и затем сохраняются — по умолчанию в скрытом поле_______VIEWSTATE. Сериализация и десериализация этих данных производится парой переопределяемых методов класса Page-. SavePageStateToPersistence- Medium и LoadPageStateFromPersistenceMedium. Переопределив оба метода, вы можете, например, сохранять состояние представления в серверной базе данных или в состоянии сеанса, кардинально уменьшив размер отправляемой пользователю страницы. (Мы по- говорим об этом подробнее в главе 13.) Генерирование разметки Генерирование разметки для браузера осуществляется путем вызова каждого элемента управления страницы, с тем чтобы он сгенерировал собственную разметку и вывел ее в буфер, где накапливается код формируемой клиентской страницы. Несколько переопределяемых методов позволяют разработчику вмешиваться в процесс на разных
110 Часть! Разработка страниц ASP.NET этапах генерирования разметки: когда выводится начальный тэг, тело и конечный тэг. Пользовательские события с этапом рендеринга не связаны. Событие Unload По окончании этапа рендеринга для каждого элемента управления, а затем и для самой страницы генерируется событие Unload. Это событие позволяет элементам выполнить перед освобождением объекта страницы заключительные операции, такие как закрытие файлов и закрытие подключений к базам данных. Заметьте, что уведомление о выгрузке страницы или элемента управления из па- мяти поступает непосредственно перед выполнением этой операции, поэтому, пока происходит его обработка, объект еще не удален. Для освобождения памяти, занимае- мой объектом страницы, ее процессор вызывает метод Dispose. Это происходит сразу после того, как завершится выполнение всех рекурсивно вызванных обработчиков события Unload. Переопределение метода Dispose класса Page или, что сделать еще проще, обработка события Disposed страницы, дает вам последнюю возможность вы- полнить для нее действия по освобождению ресурсов, пока она сама не будет удалена из памяти. Заключение ASP.NET — сложная технология, функционирующая поверх относительно простой и, к счастью, очень стабильной инфраструктуры Web. Для обеспечения высокой про- изводительности и предоставления разработчикам богатых возможностей программи- рования ASP.NET формирует абстрактную модель функционирования приложения, приближенную к модели настольных приложений, но на нижнем уровне эта модель по-прежнему основывается на HTTP и HTML. В модели Web Forms можно выделить две важные составляющие: модель процесса, включающую модель процесса Web-сервера, и объектную модель страницы. Каждый запрос, URL которого оканчивается расширением .aspx, назначается объекту прило- жения, функционирующему в общеязыковой среде, хост-процессом которой является рабочий процесс IIS. Для обработки запроса динамически создается и компилируется класс страницы, а потом создается экземпляр этого класса. Базовым классом всех страниц ASP.NET является Page. Сам он в большинстве случаев не используется для представления страниц — эта роль отводится производным от него классам, которые могут содержать обработчики событий и вспомогательные методы. Они называются классами отделенного кода. Класс, который представляет страницу в действии, реализует модель событий ASP.NET, основанную на модели реентерабельной страницы с одной формой и использовании серверных элементов управления. Жизненный цикл страницы, ко- торый был подробно описан в этой главе, делится на ряд этапов, последовательно проходимых страницей и ее элементами управления на пути к генерированию разметки для браузера. Глубокое понимание жизненного цикла страницы является непременным условием правильной диагностики возникающих в процессе раз- работки проблем, а также быстрой и эффективной реализации наиболее сложных функций приложения. В этой главе неоднократно упоминались элементы управления — компоненты, которые получают ввод от пользователя, обрабатывают его и выводят результат в виде HTML. В следующей главе мы рассмотрим различные серверные элементы управления трех категорий: Web, HTML и валидационные.
Анатомия страницы ASP.NET Глава 3 111 Только факты Конвейер исполняющих модулей получает от IIS входящий пакет HTTP и пре- вращает его в экземпляр класса, производного от класса Page. Класс страницы, необходимый для обслуживания определенного запроса, ком- пилируется, когда он в первый раз требуется в контексте выполняющегося Web- приложения (это называется динамической компиляцией по требованию). Класс страницы компилируется в сборку которая используется до тех пор, пока в исходный файл .aspx не будут внесены изменения или пока все приложение не будет перезапущено. Каждый класс страницы является обработчиком HTTP, то есть компонентом, который исполняющая среда использует для обслуживания запросов определен- ного типа. В модели отделенного кода ASP.NET 2.0 используются частичные классы, со- держащие генерируемые инструментальным средством объявления защищенных членов, представляющих серверные элементы управления. Visual Studio .NET 2003 генерировала аналогичный код, но помещала его в полускрытые области файла отделенного кода. Страница ASP.NET всегда выполняет возврат формы самой себе и использует со- стояние представления для восстановления того состояния элементов управления, в котором они находились, когда страница в последний раз генерировалась на сервере. Благодаря тому, что ASP.NET прозрачно осуществляет сохранение состояния представления страницы, создается иллюзия классической модели программиро- вания с сохранением состояния в среде, которая по своей природе не сохраняет состояния. Обработка страницы на сервере сопровождается серией событий, отмечающих этапы ее жизненного цикла. Глубокое понимание жизненного цикла страницы является обязательным условием правильной диагностики возникающих про- блем, а также быстрой и эффективной реализации сложных функций приложения.
Глава 4 Базовые серверные элементы управления ASP.NET Страницы ASP.NET состоят из кода, тэгов разметки, литерального текста и серверных элементов управления. Основываясь на запросе, серверные элементы управления генерируют код разметки. Исполняющая среда ASP.NET объединяет вывод всех эле- ментов управления и предоставляет клиенту страницу для отображения в браузере. Программное богатство ASP.NET обеспечивается наличием обширной библиотеки серверных элементов управления, охватывающих все базовые задачи HTML-взаимо- действия, например ввод текста посредством тэгов input, и более сложную функцио- нальность, в частности, отображение интерактивного календаря, меню, древовидных и табличных представлений данных. Ключевую роль в программировании элементов управления ASP.NET играет атри- бут runat. Если тэг в файле .aspx не имеет этого атрибута, он рассматривается как чистый текст и выводится в выходной поток в исходном виде. В противном случае тэг интерпретируется как серверный элемент управления и принимает участие в жизненном цикле страницы. В главе 1 мы идентифицировали две основные группы серверных элементов управления: HTML и Web. Элементы управления HTML соот- ветствуют тэгам HTML и реализуются как серверные классы, программный интерфейс которых в точности представляет стандартный набор атрибутов соответствующего тэга HTML. Что касается элементов управления Web, то они являются более абстрактными компонентами и их API не воспроизводят синтаксис HTML. Кроме того, среда раз- работки Web-элементов богаче, у них большие наборы методов, свойств и событий и они активнее участвуют в жизненном цикле страницы. В то же время у элементов управления Web и HTML много общего, и в определенном смысле элементы Web можно считать надмножеством элементов HTML. Ближе познакомившись с серверными элементами управления ASP.NET, вы обна- ружите, что среди них можно выделить более двух семейств. В реальных приложениях используются элементы управления следующих функциональных категорий: HTML, базовые Web, валидационные, связанные с данными, пользовательские, мобильные и специализированные. Валидационные элементы управления являются особым под- множеством элементов Web. Элементы управления, связанные с данными, не образуют отдельной категории, не пересекающейся с остальными, и сами по себе не являются чем-то отличным от элементов управления HTML и Web. Название этого вида эле- ментов связано с их способностью подключаться к данным посредством одного из свойств. Такие элементы управления имеются в каждой из остальных категорий, но они используются столь часто и обладают столь характерными чертами, что заслужи- вают описания в отдельном разделе. Пользовательский элемент управления является просто совокупностью существующих элементов управления Web и HTML, исполь- зуемой как отдельный инкапсулированный программируемый элемент. Мобильные элементы управления предназначены для создания Web-приложений для мобильных устройств. Ну а специализированные элементы создаются программистом как про- изводные от того или иного базового класса элементов управления.
Базовые серверные элементы управления ASP.NET Глава 4 113 В данной главе мы рассмотрим элементы управления HTML, Web и валидацион- ные. Об элементах управления, связанных с данными, речь пойдет в главе 9. Пользова- тельские, мобильные и специализированные элементы управления описаны в другой моей книге, «Programming Microsoft ASP.NET 2.0 Applications: Advanced Topics» (Mi- crosoft Press, 2005), предназначенной для продвинутых программистов и являющейся дополнением к настоящему изданию. Серверные элементы управления ASP.NET Все серверные элементы управления, включая элементы Web и HTML, а также соз- даваемые или загружаемые вами специализированные элементы управления, являют- ся производными от класса Control. Он определен в пространстве имен System.Web.UI и, как рассказывалось в главе 3, является основой всех страниц ASP.NET. Вот как выглядит объявление этого класса: public class Control : IComponent, IDisposable, IParserAccessor, lUrlReso]utionService, IDataBindingsAccessor, IControlBuilderAccessor, IControlDesignerAccessor, lExpr essionsAccessor Интерфейс IComponent определяет способ взаимодействия элемента управления с другими компонентами, функционирующими в общеязыковой среде, тогда как интерфейс IDisposable реализует общую схему детерминированного освобождения ресурсов, занимаемых управляемыми объектами. Роль остальных интерфейсов, реа- лизованных в классе Control, указана в табл. 4-1. Табл. 4-1. Интерфейсы, реализуемые классом Control Интерфейс Назначение IControlBuilderAccessor Для внутреннего применения. К его членам обращается анали- затор страниц, когда строит элемент управления и содержащие- ся в нем дочерние элементы управления. В ASP.NET 1jc данный интерфейс не поддерживается IControlDesignerAccessor Для внутреннего применения. Посредством его членов элемент управления взаимодействует с дизайнером. В ASP.NET 1.x дан- ный интерфейс не поддерживается IDataBindingsAccessor Во время разработки обеспечивает элементам управления под- держку выражений связывания с данными lExpressionsAccessor Для внутреннего применения.. Необходим для обеспечения под- держки классом коллекций выражений. В ASP.NET 1х данный интерфейс не поддерживается IParserAccessor Позволяет элементу управления играть роль контейнера дочер- них элементов управления и получать уведомление о разборе блока дочерней разметки lUrlResolutionService Содержит члены, с помощью которых осуществляется разре- шение относительных URL как во время выполнения, так и во время разработки. В ASP.NET 1.x данный интерфейс не поддер- живается Интерфейс IDataBindingsAccessor определяет доступное только для чтения свой- ство-коллекцию DataBindings, где содержатся все связи с данными. Оно использу- ется дизайнерами, входящими в состав RAD-средств, и в частности Microsoft Visual Studio .NET. Заметьте, что данная коллекция существует только во время разработки и будет вам полезна лишь в том случае, если придется самостоятельно писать RAD- дизайнер элемента управления.
114 Часть! Разработка страниц ASP.NET Примечание В ASP.NET 2.0 в класс Control был добавлен ряд новых интерфейсов. Однако только один из них, а именно lExpressionsAccessor, представляет действитель- но новую функциональность. Он содержит члены, необходимые для извлечения и об- работки пользовательских выражений связывания с данными. (Выражения связывания с данными — одно из важных нововведений ASP.NET 2.0. Я вкратце расскажу о них в главе 9, а более подробные сведения об этом виде выражений излагаются в моей кни- ге «Programming Microsoft ASP.NET 2.0 Applications: Advanced Topics» (Microsoft Press, 2005).). Остальные же интерфейсы являются результатом рефакторинга и оптимизации функциональности элемента управления, существовавшей и в ASP.NET 1.x Свойства класса Control Свойства класса Control не связаны с пользовательским интерфейсом элементов управ- ления. Ведь этот класс представляет лишь минимальную, базовую, функциональность серверных элементов управления. Список его свойств приведен в табл. 4-2. Табл. 4-2. Свойства, общие для всех серверных элементов управления Свойство Описание BindingContainer Возвращает элемент управления, который является логическим родителем текущего элемента управления в контексте связыва- ния с данными. В ASP.NET 1jc данное свойство не поддерживается ClientID Возвращает идентификатор, который будет присвоен элементу управления в HTML-странице. Это строковое значение является измененной версией значения свойства UniquelD: Если последнее может содержать знак доллара ($), то в ClientID данный символ не поддерживается и заменяется символом подчеркивания (_) Controls Возвращает коллекцию ссылок на все дочерние элементы управ- ления EnableTheming Указывает, применяются ли к элементу управления темы В ASPNET 1jc данное свойство не поддерживается Enable ViewState Указывает и позволяет определить, должен ли элемент управле- ния сохранять между запросами состояние представления (свое, а также своих дочерних элементов) в определенном хранилище: скрытом поле HTML, состоянии сеанса, серверной базе данных или файле ID Возвращает и позволяет задать имя, которое будет использовать- ся для программной идентификации элемента управления в коде страницы NamingContainer Возвращает ссылку на контейнер именования элемента управле- ния. Контейнером именования элемента управления, входящего в состав страницы, является его родительский элемент (соглас- но иерархии элементов страницы), реализующий интерфейс INamingContainer. Если такого элемента не существует, контейне- ром именования является родительская страница Page Возвращает ссылку на экземпляр класса ASM, содержащий эле- мент управления Parent Возвращает ссылку на родительский элемент данного элемента управления согласно иерархии элементов страницы Site Возвращает информацию о контейнере, в котором содержится текущий элемент управления, когда он выводится на рабочей поверхности дизайнера. Это свойство можно использовать для доступа к дизайнеру Visual Studio .NET 2005 из элемента управ- ления, размещенного в Web-форме см. след. стр.
Базовые серверные элементы управления ASP.NET Глава 4 115 Табл. 4-2. (окончание) Свойство Описание SkinlD Возвращает и позволяет задать имя обложки, применяемой к эле- менту управления. Обложкой называется подмножество атрибу- тов темы. В ASP.NET 1.x данное свойство не поддерживается TemplateControl Возвращает ссылку на шаблон, содержащий текущий элемент управления. В ASP.NET 1jc данное свойство не поддерживается TemplateSourceDirectory UniquelD Возвращает имя виртуального каталога хост-страницы Возвращает иерархически уточненный идентификатор элемента управления Visible Указывает и позволяет определить, должна ли ASP.NET осущест- влять рендеринг элемента управления Класс Control является идеальным базовым классом для создания новых элементов управления, не имеющих пользовательского интерфейса и не нуждающихся в стиле- вой информации. Идентификация серверного элемента управления Клиентский идентификатор (ClientID) элемента управления генерируется на основе значения его свойства UniquelD — настоящего серверного идентификатора, который ASP.NET генерирует для каждого элемента управления. Содержимое свойства ClientID отличается от содержимого свойства UniquelD лишь тем, что вместо знаков доллара (если таковые имеются) в нем используются символы подчеркивания. Использова- ние знака доллара в UniquelD возможно только в случае, когда элемент управления принадлежит контейнеру именования, отличному от страницы. ASP.NET генерирует значение свойства UniquelD на основе значения свойства ID, задаваемого программистом. Если оно не задано, имя элемента управления генерирует- ся автоматически по шаблону _ctlX, где X — О-базированный индекс. Если контейнером именования элемента управления является хост-страница, UniquelD просто повторяет ID. В противном случае к значению ID добавляется префикс в виде строки, представ- ляющей контейнер именования, и результат присваивается свойству UniquelD. Контейнеры именования Контейнером именования называется элемент управления, который действует как контейнер других элементов. Он образует нечто вроде виртуального пространства имен, имя которого ASP.NET добавляет в начало идентификаторов дочерних элемен- тов управления. Чтобы вы лучше поняли роль и значение контейнеров именования, рассмотрим пример. Представьте, что у вас имеется составной элемент управления, содержащий до- черний элемент, скажем, кнопку. Полностью заключенная внутрь внешнего элемента, эта кнопка недоступна коду страницы, и ей не может быть присвоен отдельный иден- тификатор экземпляра. Идентификатор кнопки в таком случае жестко кодируется в том элементе управления, который ее содержит. Но что произойдет, если разместить на странице несколько экземпляров составного элемента управления? Окажется ли в ее составе несколько экземпляров кнопки с одинаковыми идентификаторами? Именно так и будет, если вы не сконфигурируете составной элемент управления как контейнер именования. Однако роль контейнеров именования этим не ограничивается. Вообразите, что у вас имеются два экземпляра составного элемента управления, с именами Controll и Control2, и что его встроенная кнопка имеет имя Trigger. Полные имена экземпля- ров этой кнопки, дочерних по отношению к элементам управления, в данном случае
116 Часть! Разработка страниц ASP.NET таковы: Control1$Triggern Control2$Trigger. Предположим, вы щелкнули первую Кнопку и это вызвало возврат формы. Если имя элемента управления, который инициировал такое действие, содержит символ $, исполняющая среда ASP.NET распознает два разделенных указанным символом элемента и правильно определит элемент управ- ления, вызвавший возврат формы, какую бы глубину ни имело дерево элементов страницы. В то же время, если кнопка содержится в элементе управления, не помеченном как контейнер именования, идентификатор того ее экземпляра, на котором вы щелкнули, не имеет префикса — это просто Trigger. Исполняющая среда ASP.NET будет искать его как дочерний элемент формы, но не найдет, поскольку кнопка является дочерней по отношению к элементу управления высшего уровня. В результате событие возврата формы останется незамеченным. г Примечание ASP.NET 2 0 для разделения компонентов идентификатора элемента управ- ления, содержащегося в контейнере именования использует знак доллара ($). Отметим, что в ASP.NET 1 .х той же цели служил знак двоеточия (:). Контейнеры связывания В ASP.NET 2.0 был введен новый род контейнеров — контейнеры связывания. Контей- нером связывания является элемент управления, который получает от хоста (обычно страницы) связанные данные и передает их дочерним элементам управления Появи- лось и новое свойство элементов управления, BindingContainer, которое определяет, какой элемент управления в иерархии страницы является родительским по отноше- нию к данному элементу управления в контексте связывания с данными. Возможно, вы заметили, что контейнер именования и контейнер связывания элемен- та управления обычно совпадают. Единственное исключение составляет случай, когда элемент управления является частью шаблона. Тогда в свойстве NamingContainer за- дается физический родитель элемента управления, то есть элемент из шаблона, а свой- ство BindingContainer указывает на элемент управления, который определяет шаблон. Видимость серверного элемента управления Если установить свойство Visible элемента управления в false, ASP.NET не будет генерировать для данного элемента код разметки. Однако это еще не означает, что элемент управления не сможет ничего вывести. Ведь он остается активным объектом, имеющим методы и обрабатывающим события. Если метод или обработчик события направит текст в выходной поток с помощью вызова Response. Write, этот текст будет включен в состав выводимой страницы. Элемент управления, свойство Visible кото- рого установлено в false, все равно является частью страницы и сохраняет свое место в дереве ее элементов. Методы класса Control В табл. 4-3 перечислены и описаны методы класса Control. Табл. 4-3. Методы серверного элемента управления Метод__________________Описание_________________________________________________ ApplyStyleSheetSkin Применяет к элементу управления свойства, определенные в таб- лице стилей страницы. Какая именно обложка используется, за- висит от значения свойства SkinlD В ASP.NET 1.x данный метод не поддерживается DataBind Генерирует событие OnDataBinding, после чего вызывает для всех дочерних элементов управления метод DataBind
Базовые серверные элементы управления ASP.NET Глава 4 117 Табл. 4-3. (окончание) Метод Описание Dispose Предоставляет элементу управления возможность выполнить задачи очистки, прежде чем тот будет удален из памяти Focus Присваивает элементу управления фокус ввода. В ASP.NET 1.x данный метод не поддерживается FindControl Ищет заданный элемент управления в коллекции дочерних эле- ментов. Те элементы, которые не входят в коллекцию Controls, то есть не являются непосредственно дочерними, метод не воз- вращает HasControls RenderControl ResolveChentUrl Указывает, имеет ли элемент управления дочерние элементы Генерирует для элемента управления вывод HTML Служит для получения URL, который может использоваться клиентом для доступа к ресурсам Web-серверэ таким как файлы изображений, дополнительные страницы и т. д. Может вернуть относительный путь. Метод является скрытым, и переопреде- лить его в дочерних классах нельзя ResolveUrl Разрешает относительный URL, возвращая абсолютный на осно- ве значения, переданного свойству TemplateSourceDirectory SetRenderMethadDelegate Предназначен для внутреннего применения. Назначает делегат для рендеринга элемента управления и его дочернего контента в составе родительского элемента управления Каждый элемент управления может иметь дочерние элементы управления. Все они хранятся в его коллекции Controls — объекте типа Controlcollection. У данного класса-коллекции имеется несколько особенностей. В частности, он выполняет пост- обработку элементов управления, которые добавляются в коллекцию или из нее удаляются. При добавлении элемента восстанавливается, если нужно, его состояние представления и включается отслеживание состояния. Когда же происходит удаление элемента, генерируется событие Unload. События класса Control Класс Control определяет набор базовых событий, поддерживаемых всеми серверными элементами управления .NET Framework. Эти события описаны в табл. 4_4. Табл. 4-4. События серверного элемента управления Событие Когда производится DataBinding Для элемента управления вызван метод DataBind и производит- ся связывание этого элемента с источником данных Disposed Элемент управления удаляется из памяти — это последний этап его жизненного цикла Init Элемент управления инициализируется — это первый этап его жизненного цикла Load Элемент управления загружается в память. Данное событие про- исходит после события Init PreRender Unload Элемент управления готов к рендерингу своего содержимого Элемент управления выгружается из памяти Все серверные элементы управления выводят HTML-код с помощью метода Ren- derControl, и когда это происходит, генерируется событие PreRender.
118 Часть! Разработка страниц ASP.NET Нововведения ASP.NET 2.0 При переходе от ASP.NET 1.x к ASP.NET 2.0 серверные элементы управления при- обрели новые функции, в большей мере связанные с архитектурой, нежели с про- граммированием. Эти новые функции порождены изменениями, произошедшими во внутреннем устройстве и функционировании элементов управления Адаптивный рендеринг Адаптивным рендерингом называется процесс генерирования разметки, производимый с учетом индивидуальных особенностей целевого браузера. Для его поддержки зада- ча генерирования разметки была делегирована внешнему по отношению к элементу управления компоненту — адаптеру. Выбор адаптера определяется возможностями целевого браузера, сведения о кото- рых извлекаются из базы данных браузеров ASP.NET. Если запись об определенном браузере содержит имя класса адаптера элемента управления, создается и использу- ется экземпляр этого класса. В противном случае адаптером элемента управления становится экземпляр класса ControlAdapter. Данный класс является универсальным адаптером и просто генерирует разметку элемента управления, вызывая его методы рендеринга. Примечание База данных, в которой содержится информация о браузерах, является " не настоящей базой данных. Это набор текстовых файлов с расширением .browser, хра- нящийся в инсталляционной папке ASP.NET на Web-сервере. Путь к этой папке таков: %WINDOWS%\Microsoft.NET\Framework\[eepcMH]\CONFIG\Browsers. Хранящиеся здесь дан- ные используются для получения информации о возможностях браузера. Элемент управления хранит ссылку на экземпляр своего адаптера в защищенном свойстве Adapter. Адаптер ассоциирован с каждым элементом управления, кроме тех, которые являются составными и предоставляют заботу о рендеринге дочерним элементам управления. Рендеринг для конкретного браузера В ASP.NET 2.0 можно декларативно присвоить всем свойствам элемента управления значения, специфические для конкретных браузеров. Вот небольшой пример: <asp:Button ID="Button1" runat="server” Text='T’m a Button" ie:Text="IE Button” mozilla:Text="FireFox Button" /> Свойство Text кнопки содержит значение "IE Button ", когда страница выводит- ся в Microsoft Internet Explorer, и значение "FireFox Button ", когда она выводится в Firefox. Если страницу запрашивает другой браузер, используется атрибут Text без префикса. Все свойства, которые разрешается задавать в составе тэга, можно по- мечать идентификатором браузера. Каждый поддерживаемый браузер имеет свой уникальный идентификатор. Как следует из приведенного выше кода, идентификатор ie соответствует браузеру Internet Explorer, а идентификатор mozilla — браузеру Fire- fox. Уникальные идентификаторы есть и у других версий браузера Netscape, а также у браузеров мобильных устройств. Фильтрация с учетом браузера поддерживается и для эталонных страниц. Мы вернемся к данной функции в главе 6, где приводится перечень наиболее часто ис- пользуемых идентификаторов браузера. Эти идентификаторы разбросаны по файлам .browser, которые вы найдете по следующему пути: %WINDOWS%\Microsoft NET\Framework\[sepcnfll\CONFIG\Browsers
Базовые серверные элементы управления ASP.NET Глава 4 119 Совместимость с XHTML XHTML — это стандарт World Wide Web Consortium (W3C), определяющий Web- страницы как документы XML. Данный подход гарантирует, что элементы страниц имеют правильную структуру и будут совместимы с будущими браузерами. По умол- чанию разметка, генерируемая элементами управления ASP.NET 2.0, за небольшими исключениями соответствует стандарту XHTML. Для обеспечения такого соответ- ствия в итоговую разметку, направляемую браузеру, внесено множество изменений. Например, каждый элемент или содержит явный закрывающий тэг, или является самозакрывающимся (то есть оканчивается символами />) и всегда заключен внутрь контейнерного элемента. Так, скрытое поле состояния представления теперь заклю- чено внутрь тэга <div>, а из элемента <form> удален атрибут пате: <form method="post" action="default.aspx" id="MainForm"> <div> <input type="hidden" name="_VIEWSIATE” id="_VIEWSTATE” value="...” /> </div> </form> Кроме того, каждый тэг сценария, выводимый в составе страницы, включает со- ответствующий атрибут type и выводится в элементах CDATA. Очевидно, что некоторые изменения могут нарушить работу созданных ранее страниц. Что если у вас имеется страница, работа которой зависит от атрибута пате формы? Для облегчения переноса страниц ASP.NET 1.x на платформу ASP.NET 2.0 вы можете добавить в файл web.config следующую установку, чтобы рендеринг элементов управления осуществлялся так, как это делалось в ASP.NET 1.x. <system.web> <XHTML11Conformance enableObsoleteRendering="true” /> </system.web> Возможность отключить рендеринг в формате XHTML предоставляется главным образом для того, чтобы облегчить вам перенос существующих страниц на новую платформу. Данной возможностью не стоит злоупотреблять, поскольку в будущих версиях ASP.NET она, быть может, уже не будет поддерживаться. Более того, вам все равно рано или поздно придется переходить на XHTML, так не лучше ли сделать это прямо сейчас, раз уж указанный формат стал в ASP.NET 2.0 основным? Примечание Генерирование вывода, совместимого с XHTML, гарантировано для преоб- ладающего большинства серверных элементов управления ASP NET Однако элементы управления HyperLink, BulletedList и AdRotator независимо от выбранных вами установок генерируют разметку, не совместимую с XHTML. Элементы управления GridView и Tree- View также оказываются «в зоне риска», если содержат компоненты HyperLinkColumn и TreeNode. Поэтому их не рекомендуется использовать в составе страниц, к которым предъявляется требование соответствия стандарту XHTML. Если вы приобретаете эле- менты управления, всегда интересуйтесь у производителя, соответствует ли генерируемая ими разметка данному стандарту. Учтите также, что ASP.NET не способна исправлять ошибки XHTML, допущенные в литеральных частях страниц. Если ваша страница содержит статический текст или элементы HTTP, ответственность за обеспечение их соответствия стандарту XHTML целиком лежит на вас. Как же обеспечить соответствие определенной страницы или специализирован- ного элемента управления стандарту XHTML? Для этого можно воспользоваться сервисом, который выполнит страницу и проверит ее вывод. Примером подобного сервиса является W3C Markup Validation Service, который вы найдете по адресу
120 Часть! Разработка страниц ASP.NET http://validator.w3.org. Этот валидационный сервис можно использовать двумя спо- собами: ввести URL страницы, чтобы он сам запросил ее и проверил, либо загрузить данную страницу на его сайт. Элементы управления, оформляемые с использованием тем В ASP.NET 2.0 темой называется набор установок свойств, применяемый к элементам управления, которые должны иметь согласованный вид. Тема может быть применена ко всему Web-сайту, к отдельной странице и ее элементам и даже к отдельному элемен- ту управления. Она идентифицируется именем и состоит из файлов каскадных таблиц стилей (CSS), изображений и обложек элементов управления. Обложкой элемента управления называется текстовый файл, содержащим предопределенные значения некоторых свойств данного элемента. Вместе эти установки определяют внешний вид и поведение элемента, а их применение позволяет придать сайту профессиональный и согласованный вид. Еще одно важное достоинство тем и обложек элементов управления заключается в том, что их можно без труда переносить с одного сайта на другой, достигая таким образом согласованности интерфейса нескольких приложений. Если, к примеру, раз- работчик разместит на странице элемент управления DataGrid, то он будет выводиться с тем заданным по умолчанию интерфейсом, который определен в выбранной для сайта теме. Поддержку тем можно включать и отключать динамически для каждого серверного элемента управления в отдельности, для этой цели в ASP.NET 2.0 предусмотрено бу- лево свойство EnableTheming, по умолчанию равное true. Как правило, тема определяет только те свойства элементов управления, которые связаны с их внешним видом. Свойства же, определяющие поведение элемента, не желательно делать определяемы- ми темой. Разрабатывая элемент управления, вы сами решаете, для каких его свойств темы будут поддерживаться, а для каких — не будут, и соответствующим образом устанавливаете атрибут Themeable, играющий роль директивы компилятору. Подроб- нее о темах я расскажу в главе 6, а о том, как осуществляется разработка специали- зированных элементов управления, вы можете почитать в моей книге «Programming Microsoft ASP.NET 2.0 Applications: Advanced Topics» (Microsoft Press, 2005). Состояние элемента управления Для функционирования некоторых элементов управления ASP.NET необходимо, что- бы их состояние сохранялось между запросами. Примерами информации состояния, которую важно сохранять, могут служить идентификатор текущей страницы много- страничного элемента управления и идентификатор текущего порядка сортировки данных, выведенных в табличном элементе управления. В ASP.NET 1.x есть только один контейнер, подходящий для хранения такой информации, — состояние пред- ставления. Однако эта структура предназначена для хранения установок, заданных приложением, и. что особенно важно, функция ее поддержки может быть отключена. Что же будет в таком случае с состоянием элемента управления? Очевидно, необходим иной контейнер, который бы лучше подходил для хранения такого рода информации. Для решения этой проблемы в ASP.NET 2.0 введено понятие состояния элемента управления как отдельной от состояния представления и жизненно важной части инфраструктуры элементов управления. Состоянием элемента управления называется набор данных состояния представ- ления, имеющих критическое значение для его функционирования. Эти данные хра- нятся в отдельных переменных-членах (не тех, которые служат для хранения состо- яния представления), и отключение состояния представления на них не отражается.
Базовые серверные элементы управления ASP.NET Глава 4 121 Каждый элемент управления сохраняет и загружает свое состояние, используя пару переопределяемых методов, как показано ниже: protected override object SaveControlStateO protected override void LoadControlState(object state) Состояние элемента управления используется подобно состоянию представления, сохраняется и загружается на тех же этапах, что и состояние представления, и со- держится в том же самом скрытом поле. Фокус ввода Полезной функцией, которой недостает ASP.NET 1.x, является возможность быстро присвоить фокус ввода определенному элементу управления при выводе страницы. Как рассказывалось в главе 3, у класса Page в ASP.NET 2.0 есть метод SetFocus, по- зволяющий присвоить фокус ввода любому элементу управления страницы. Следу- ющий пример показывает, как активизировать элемент управления TextBox с именем txtLastName'. void Page_Load(object sender, EventArgs e) { if (!IsPostBack) SetFocus("txtLastName"); } Метод SetFocus кэширует идентификатор элемента управления и заставляет класс Page сгенерировать во время рендеринга страницы специальный код сценария. Кроме того, любой элемент управления может попросить фокус ввода себе, вызвав новый метод Focus. Элементы управления HTML В коде страницы серверные элементы управления HTML похожи на тэги HTML и отличаются от последних только наличием атрибута runat-server. Однако именно этот скромный атрибут вносит кардинальные изменения в их интерпретацию и функцио- нирование. Я уже говорил, что стоит снабдить тэг атрибутом tunat, как из статического элемента он превращается в действующий экземпляр серверного компонента, который можно программно конфигурировать, используя приемы объектно-орисшированного программирования. У элемента управления HTML имеется набор свойств и методов, детально отражающих синтаксис HTML. Например, для задания текста, выводимого в текстовом поле по умолчанию, предназначено свойство с именем Value, а не Text, хотя последнее было бы более информативным. Имя экземпляра серверного элемента управления определяется значением атрибута ID. Для примера ниже показано, как определить серверный тэг input с именем lastName". cinput runat="server" id="lastName" type="text" /> Данный тэг не содержит явно и статически заданного значения атрибута Value, но его можно определить программно: void Page_Load(object sender. EventArgs e) { lastName.Value = "Esposito": } После обработки исполняющей средой ASP.NET приведенный выше тэг превра- тится в тэг HTML: <input name="lastName" id="lastName” type="text" value="Esposito” />
122 Часть! Разработка страниц ASP.NET Заметьте, что серверный атрибут ID соответствует двум атрибутам HTML: Name и ID. Сделано это ради совместимости с браузерами и отнюдь не означает, что на сервере атрибуты Name и ID можно использовать как взаимозаменяемые для имено- вания экземпляра серверного элемента управления. Его имя задается свойством ID, а если задать в серверном тэге оба атрибута, то значение, присвоенное Name, будет автоматически переопределено. Общая информация об элементах управления HTML В NET Framework серверные элементы управления определены для большинства ши- роко используемых элементов HTML, таких как <form>, <input> и <select>, а также для таблиц, изображений и гиперссылок. Все они наследуют один и тот же базовый класс — HtmlControl. Кроме того, каждый серверный элемент управления имеет свой набор уникальных свойств и событий. Свойства элемента управления по большей части предназначены для программного манипулирования атрибутами HTML из серверного кода. Элементы управления HTML хорошо интегрированы с технологиями связывания с данными и управления состоянием, поддерживают события возврата формы и клиентские сценарии. Напри- мер, вы можете связать с кнопкой код на языке JavaScript, который будет выполняться на клиенте в ответ на событие onclick, а также другой код, обрабатывающий указанное событие на сервере после произошедшего в результате этого возврата формы. Отметим, что элементы управления HTML определены в пространстве имен Sys- tem.Web.UI.HtmlControls. Большинство тэгов HTML (хотя и не все) имеют соответствия среди классов .NET Framework. Элементы HTML, для которых нет специальных классов, выводятся с использованием класса HtmlGenericControl, и их атрибуты уста- навливаются не посредством свойств, а через универсальную коллекцию. К числу тэгов, обрабатываемых таким образом, относятся <iframe>, <hr>, <font> и <body>. Вообще же вам следует помнить, что всякий элемент, который может использоваться в составе страницы HTML, можно пометить атрибутом runat = "server", после чего он станет доступным для программирования и определения стилей на сервере. Базовый класс HtmlControl Класс HtmlControl является производным от Control и определяет методы, свойства и события, общие для всех элементов управления HTML. Многие его свойства, все методы и события унаследованы от базового класса. Те свойства класса HtmlControl, которые являются его уникальной принадлежностью, описаны в табл. 4-5. Табл. 4-5. Собственные свойства класса HtmlControl Свойство Описание Attributes Возвращает коллекцию, представляющую все атрибуты элемента управле- ния и их значения Disabled Возвращает и позволяет задать булево значение, указывающее, отключен ли элемент управления Style Возвращает коллекцию, представляющую все CSS-свойства элемента управ- ления TagName Возвращает имя HTML-тэга элемента управления Отключенный серверный элемент управления HTML видим и всегда генерирует код HTML. Однако когда свойство Disabled элемента управления установлено в true, в вывод этого элемента добавляется HTML-атрибут disabled. Для того чтобы элемент
Базовые серверные элементы управления ASP.NET Глава 4 123 управления вовсе не генерировал вывода, его свойство Visible нужно установить в false, о чем уже упоминалось ранее. Работа с атрибутами HTML У каждого элемента управления HTML гораздо больше свойств, чем указано в табл. 4-5. Все эти свойства соответствуют атрибутам HTML, а присваиваемые им значения пере- носятся в HTML-вывод. Для установки атрибутов тех элементов управления, которым не соответствуют специализированные серверные элементы управления, использу- ется коллекция Attributes. Ее же можно использовать и для установки тех свойств, что не имеют соответствий в интерфейсе элемента управления, и, если нужно, для определения пользовательских HTML-атрибутов. Содержимое элемента коллекции Attributes всегда интерпретируется как строка. Давайте на примере следующего фрагмента HTML-кода посмотрим, как осущест- вляется программная установка атрибутов тэга <body>: <script> function Init() { alert("Hello"); } </script> <script runat=server language="C#"> void Page_Load(object sender, EventArgs e) { theBody. Attributes[ "onload"] = "InitO"; } </script> <html> <body runat="server" id="theBody"> </body> </html> Вы связываете сценарий на языке JavaScript с атрибутом onload тэга <body>. Ре- зультирующий HTML-код, выводимый для браузера, получается таким: <script> function InitO { alert("Hello"); } </sc ript> <html> <body id="theBody" onload="Init()"> </body> </html> Рендеринг свойства Attributes осуществляется посредством особого класса Attribute- Collection. Несмотря на его имя, содержимое этого класса нельзя перебирать в цикле for...each, поскольку он не поддерживает интерфейса lEnumerable. У класса Attribute- Collection имеются специальные методы для рендеринга атрибутов с помощью объекта записи текста, а также для добавления и удаления элементов коллекции. Интересно, что этот класс достаточно интеллектуален, чтобы в случае добавления в коллекцию атрибута с именем Style перенаправить содержимое последнего в коллекцию Style.
124 Часть I Разработка страниц ASP.NET Иерархия элементов управления HTML Большинство элементов управления HTML можно разделить на две категории: контей- нерные и предназначенные для ввода данных. Однако элементы Htmllmage, HtmlLink, HtmlMeta и HtmlTitle, которые соответствуют тэгам <img>, <link>, <meta> и можно отнести к обеим категориям. На рис. 4-1 показано дерево элементов управления HTML. Те элементы, которые выделены в нем полужирным шрифтом, поддержива- ются только ASP.NET 2.0. Рис. 4-1. Классификация элементов управления в соответствии с их базовыми классами К числу элементов управления, предназначенных для ввода данных, относятся все разновидности тэга <input>, от кнопок типа submit до флажков и от текстовых полей до переключателей. Контейнерными элементами управления являются якори, табли- цы, формы и вообще все HTML-тэги, которые могут содержать дочерние элементы Контейнерные элементы управления HTML Базовым классом контейнерных элементов управления HTML является HtmlContai- nerControl, непосредственно наследующий класс HtmlControl. Он представляет все элементы, которые должны иметь закрывающийся тэг: формы, блоки выбора, таблицы, а также якори и текстовые области. По сравнению с классом HtmlControl контейнер- ный элемент управления имеет два дополнительных строковых свойства — InnerHtml и InnerText. Оба свойства управляют чтением и записью литерального контента, расположен- ного между открывающимся и закрывающимся тэгами элемента. Учтите, что в случае, когда внутренний контент элемента управления включает серверные элементы управ- ления, получить его невозможно. Свойства InnerHtml и InnerText используются только с литеральным Контентом. Сам тэг не учитывается при выводе. В отличие от InnerText свойство InnerHtml позволяет работать С HTML-форматированным текстом, но не выполняет ни автоматического кодирования, ни декодирования этого текста. Иными словами, InnerText извлекает и устанавливает контент тэга как чистый текст, a Inner- Html извлекает и устанавливает его как текст в формате HTML.
Базовые серверные элементы управления ASP NET Глава 4 125 Контейнерные элементы управления HTML, определенные в ASP.NET, перечис- лены в табл. 4-6. Табл. 4-6. Контейнерные элементы управления HTML Класс Что представляет HtmlAnchor Якорь HTML, именно тэг <а> HtmlButton HTML-тэг <button>. Элемент <button> определен в спецификации 4.0 и поддерживается только в Internet Explorer версии 4.0 и выше языка HTML HtmlForm Тэг <form>. Может использоваться только в качестве контейнера интерактивных серверных элементов управления но не пригоден для создания HTML-форм, программируемых на сервере HtmlGenericControl HTML-тэг, для которого в .NET Framework не определен специальный класс. Примерами являются тэги <font>, <hr>, <iframe>. Их програм- мируют с использованием коллекции Attributes и неявно устанавлива- ют атрибуты HtmlHead Тэг <head>. Позволяет программно управлять метатэгами, таблицами стилей и заголовком страницы. В ASP.NET 1.x данный класс не поддер- живается HtmlSelect Тэг <select>, то есть группу вариантов выбора HtmlTable Таблицу HTML — тэг <table> HtmlTableCell Тэг <td>, то есть ячейку таблицы HtmlTableRow Тэг <tr>, то есть строку таблицы HtmlTextArea Многострочное текстовое поле — тэг <textarea > Заметьте, что элемент управления HtmlButton отличается от HtmllnputButton. пред- ставляющего «кнопочную» разновидность тэга <input>. Элемент HtmlButton соответ ствует тэту <button>, определяемому спецификацией HTML 4.0. Мы вернемся к вопросу о кнопках в следующем разделе, когда будем обсуждать элементы управления Web. Серверные формы играют ключевую роль в Web-приложениях, поскольку именно они являются средством реализации возврата формы и сохранения состояния пред- ставления. Поэтому элемент управления HtmlForm — не просто форма, которую можно программировать на сервере. Этот элемент управления скрывает свойство Action и не может использоваться для возврата данных странице, отличной от текущей. Теме HTML-форм посвящена глава 5. Управление заголовком Для страницы, содержащей тэг <head> с атрибутом runat= server, автоматически создается элемент управления HtmlHead. Заметьте, что эта установка задается по умолчанию, когда в Web-проект Visual Studio .NET 2005 добавляется новая страница: <head runat="server"> <title>Untitled Page</title> </head> Заголовок страницы представлен новым свойством Header класса Page. Если тэг <head> отсутствует или не имеет атрибута runat, данное свойство содержит null. Элемент управления HtmlHead реализует интерфейс IPageHeader, состоящий из свойств-коллекций Metadata LinkedStylesheet, Stylesheet и строкового свойства Title. Свойство Metadata представляет собой словарь, содержащий все необходимые до- черние тэги <meta> заголовка: Headeг.Metadata.Add("CODE-LANGUAGE", "Сй");
126 Часть I Разработка страниц ASP.NET Результатом выполнения этого кода является следующая разметка: <meta name="CODE-LANGUAGE" content="C#" /> Для того чтобы представить другие типичные метаданные, скажем, Http-Equiv, можно обратиться к новому элементу управления HtmlMetcr. void Page_Init(object sender, EventArgs e) { HtmlMeta meta = new HtmlMeta(); meta.HttpEquiv = "refresh"; meta.Content = Int32.Parse(Content.Text).ToString(); ((Cont rol)Heade r).Cont rols.Add(meta); } Этот код на этапе инициализации динамически генерирует тэг <meta> и добавляет его в раздел <head> страницы. Существующими тэгами <meta>, если они помечены атрибутом runat, можно манипулировать программно. Совет В Internet Explorer тэг <meta> может использоваться для «сглаживания» перехода v от одной страницы к другой и возврата к предыдущей. Обычно при переходе между страницами в браузере текущая страница внезапно исчезает и на ее месте появляется новая. Однако, используя следующие два метатэга, можно сделать так, чтобы смена страниц осуществлялась более плавно: <meta http-equiv="Page-Enter" content="progid DXImageTransform.Microsoft.Fade(duration=.5)" /> <meta http-equiv="Page-Exit" content="progid:DXImageT ransfо rm.Mic rosoft.Fade(du ration=.5)" /> Думаю, нет нужды говорить, что в ASP NET 2.0 метатэги можно создавать и изменять программно. Примечание В ASP.NET 1 .х, для того чтобы программным способом включить тэг <meta> в состав страницы, приходилось прибегать к особому трюку создавать строку как лите- ральный элемент управления и добавлять его в коллекцию Controls заголовка: string meta = "<meta http-equiv="refresh" content="3" />"; Literalcontrol equiv = new LiteralControl(meta); ((Cont rol)Header).Controls.Add(equiv); Здесь Header представляет серверный тэг <head>, который вы модифицируете. Для того чтобы связать страницу с файлом таблицы стилей, нужно выполнить такой код: Header.LinkedStyleSheets Add("MyStyles.css ); В качестве альтернативы можно обратиться к элементу управления HtmlLink, кото- рый представляет тэг <link>. В отличие от тэга <а> тэг <link> может присутствовать только в разделе <head> документа, хотя здесь разрешается использовать его любое количество раз. Кроме того, элемент управления HtmlHead имеет свойство Title, посредством ко- торого можно извлекать и задавать заголовок страницы: Header.Title = "This is the title"; Заметьте, что это свойство возвращает верный заголовок страницы только при условии, что тэг <title> заключен внутрь тэга <head>. Правда, некоторые браузеры не требуют соблюдения этого правила и позволяют разработчикам определять заголовок вне шапки. Чтобы получить возможность манипулировать тэгом <title> независимо
Базовые серверные элементы управления ASP.NET Глава 4 127 от тэга <head>, нужно воспользоваться элементом управления HtmlTitle и пометить тэг <title> атрибутом runat. [у] Примечание В ASP.NET 2.0 список HTML-тэгов, которым соответствуют специальные элементы управления HTML, увеличился — теперь в него входят тэги <head>, <title>, <Нпк> и <meta>. Также имеют соответствия среди элементов управления HTML некоторые конфигурации тэга <input>, а именно кнопки типа submit и reset и поля для ввода пароля. Переход по URL Программный способ доступа к тэгу <а> и его конфигурирования предоставляет класс HtmlAnchor. Данный класс имеет несколько свойств, которых нет у других кон- тейнерных элементов управления, в том числе у HRef, Name, Target и Title. Свойство HRef служит для установки целевого URL гиперссылки. Свойство Name определяет имя раздела страницы ASP.NET, к которому можно перейти из любого места той же страницы, используя HRef с префиксом #. Приведем пример якоря-закладки с име- нем Morelnfo’. <а name="MoreInfo" /> Перейти к этому якорю можно с помощью такой гиперссылки: <а href="#MoreInfo">Get More Info</a> Свойство Target идентифицирует целевое окно или фрейм, куда будет загружен контент, находящийся по целевому URL Типичными значениями этого свойства являются _self, _top, _blank и _parent, но может также использоваться любое имя, идентифицирующее фрейм, который входит в состав страницы. Такие имена лучше всегда набирать в нижнем регистре, поскольку не все браузеры допускают исполь- зование здесь символов верхнего регистра. Наконец, свойство Title содержит текст, который практически все браузеры выводят в виде всплывающей подсказки при на- ведении указателя мыши на область якоря. Обработка событий на сервере Якорный элемент управления и элемент управления HtmlButton могут использоваться не только для перехода к другой странице (что является их основным назначением), но и для осуществления возврата формы. Ключом к выполнению этой операции является событие ServerClick. Вы можете определить имена методов, которые будут обрабатывать на сервере и клиенте событие, вызванное тем, что пользователь щел- кнул элемент управления. Ниже продемонстрировано объявление якоря, в котором событию-щелчку назначены и клиентский, и серверный обработчики: <а runat=server onclick="Run()" onserverclick=”DoSomething">Click</a> Атрибутом onclick определяется клиентский обработчик, написанный на JavaScript, а атрибутом onserverclick — серверный обработчик, код которого будет выполнен после возврата формы. Очевидно, что если заданы оба обработчика, первым выполняется клиентский. Элемент управления HtmlSelect Элемент управления HtmlSelect представляет список опций, из которых можно вы- брать одну или несколько. Его поведением управляют свойства Size и Multiple’, первое из них определяет количество строк элемента управления, а второе указывает, раз- решен ли выбор нескольких элементов. Сами элементы хранятся в коллекции Items, и каждый из них представлен объектом Listitem. Интересно, что пространством имен класса Listitem является WebControls, а не HtmlControls. Чтобы задать текст элементов,
128 Часть! Разработка страниц ASP.NET можно либо установить свойство Text каждого из объектов Listitem, либо разместить между открывающимся и закрывающимся тэгами <select> группу тэгов <6ption>. По умолчанию элемент управления HtmlSelect выводится в виде раскрывающего- ся списка. Однако если поддерживается множественное выделение или если высота этого элемента больше одной строки, он выводится как обычный список. Когда мно- жественный выбор запрещен, индекс выделенного элемента содержится в свойстве Selectedlndex, в противном случае вы просто перебираете в цикле элементы коллекции Items и проверяете свойство Selected каждого из них. Элемент управления HtmlSelect поддерживает связывание с данными, для чего у него есть несколько дополнительных свойств. В свойстве DataSource задается ис- точник данных, которым может быть любой объект .NET, реализующий интерфейс lEnumerable. Если источник данных содержит несколько связываемых таблиц (как объект DataSet), используя свойство DataMember, можно выбрать одну из них. На- конец, свойства DataTextField и DataValueField служат для связывания свойств Text и Value элемента списка со столбцами источника данных. (О связывании с данными мы подробно поговорим в главе 9.) Таблицы HTML В ASP.NET простейшую таблицу HTML можно вывести с помощью элемента управле- ния HtmlTable. В большинстве случаев использовать этот элемент нет необходимости, поскольку существуют списочные и табличные элементы управления с более богатой функциональностью, позволяющие выводить как таблицы, так и отдельные записи. Обычная таблица HTML потребуется лишь при условии, что вы захотите задать фиксированное размещение графических элементов на странице, но в этом случае серверный элемент управления не понадобится вовсе. Серверные таблицы не столь мощны, как обычные таблицы HTML, создаваемые с использованием тэга <table>. Главное их ограничение состоит в том, что класс HtmlTa- ble не поддерживает HTML-элементов <caption>, <col>, <colgroup>, <tbody>, <thead> и <tfoot>. Если использовать их в коде ASP.NET, не будет сообщения ни об ошибке компиляции, ни об исключении времени выполнения — эти элементы просто исчезнут из HTML-кода. Возьмем такой пример: <table runat=”server"> <theadxth>Name</thxth>Last Name</thX/thead> < t rxtd> Joe</tdXtd>Use rs</tdx/t r> < t rxtd>Bob</tdxtd>Whosthisguy</tdx/t r> </table> В результате обработки этой разметки генерируется следующий вывод HTML, который, как видите, не содержит элемента <thead>\ <table> < t r><td>Joe</tdxtd>Use rs</tdx/t r> <trxtd>Bob</td><td>Whosthisguy</td></tr> </table> Дочерними элементами HtmlTable по определению могут быть только объекты класса HtmlTableRow. Любая попытка программно добавить другие элементы таблицы, скажем, <thead> или <tfoot>, приведет к выбросу исключения. В ASP.NET 2.0 поведение элемента управления HtmlTable не изменилось. Однако в этой новой версии системы серверная поддержка таблиц все же стала более осно- вательной: новая функциональность реализована в виде элемента управления Table,
Базовые серверные элементы управления ASP.NET Глава 4 129 который является аналогом HtmlTable в пространстве имен Web Controls. Каждый объект TableRow можно связать с определенным разделом таблицы, будь-то заголо- вок, тело или подвал. Когда элемент управления Table осуществляет свой рендеринг, ор проверяет, к какому разделу относится каждая строка, и выводит в соответствии с результатами такой проверки тэг <tbody>, <thead> или <tfoot>. Элемент управления HtmITextArea Элемент управления HtmITextArea, соответствующий тэгу <textarea>, позволяет программно создавать и конфигурировать многострочные текстовые поля. У класса HtmITextArea имеются свойства Rows и Cols, с помощью которых задается количество строк и столбцов поля, а свойство Value может использоваться для определения вы- водимого в нем текста. При возврате формы класс HtmITextArea генерирует событие SeruerChange, которое позволяет вам проверить на сервере данные, содержащиеся в элементе управления. Однако этот элемент управления не инициирует возврат формы. Он просто вмеши- вается в последовательность серверных событий уже после того, как страница осу- ществит возврат формы в ответ на щелчок ссылки или кнопки, и дает программисту возможность выполнить некоторый код, если между двумя последовательными воз- вратами формы содержимое элемента изменилось. Все элементы управления ASP.NET, которые, подобно HtmITextArea, реализуют интерфейс IPostBackDataHandler, могут в случае изменения своего внутреннего со- стояния вызывать определенный пользователем код. Как рассказывалось в главе 3, элементы управления могут генерировать пользовательские события путем пере- определения метода RaisePostDataChangedEvent упомянутого интерфейса. Следующий псевдокод показывает, что происходит в реализации данного метода, принадлежащей классу HtmITextArea'. void System.Web.UI.IPostBackDataHandler.RaisePostDataChangedEvent() { this. O.nServerChange(EventArgs. Empty); } Заметьте, что элемент управления генерирует событие только в том случае, если его состояние между последовательными возвратами формы изменилось. Узнать, произо- шло ли это, можно лишь при условии, что элемент управления сохраняет где-то свое предшествующее состояние, а хранить его можно только в состоянии представления Конечно, событие ServerChange не будет сгенерировано, если вы отключите поддержку состояния представления для хост-страницы элемента управления. Элементы управления /npuf-типа В языке HTML элемент <input> имеет несколько разновидностей и может исполь- зоваться для вывода кнопки типа submit, флажка или текстового поля. Каждой из таких разновидностей элемента соответствует свой класс ASP.NET. Все эти классы являются производными от HtmllnputControl — абстрактного класса, определяющего их общий программный интерфейс. Данный класс, в свою очередь, наследует класс HtmlControl, добавляя к нему свойства Name, Туре и Value. Свойство Name возвращает имя, присвоенное элементу управления. В ASP.NET оно на практике доступно только для чтения, хотя помечено как доступное и для за- писи. Аксессор (get) этого свойства возвращает значение свойства UniquelD элемента управления, а мутатор (set) представляет собой функцию типа void с пустым телом. Поэтому, какое бы значение вы ни присвоили данному свойству программно или декларативно, оно просто игнорируется.
130 Часть! Разработка страниц ASP.NET Свойство Туре, соответствующее атрибуту type HTML-элемента, доступно только для чтения. Что касается свойства Value, представляющего содержимое поля ввода, то его значение можно и считывать, и записывать. В табл. 4-7 перечислены элементы управления ASP.NET, соответствующие раз- личным разновидностям тэга <input>. Табл. 4-7. Элементы управления HTML, соответствующие разновидностям тэга <input> Класс Элемент HTML, представленный данным классом HtmllnputButton Различные виды командных кнопок, поддерживаемые HTML. Допустимыми значениями атрибута Туре являются button, submit и reset HtmllnputCheckBox Флажок HTML, то есть тэг <input> типа checkbox HtmllnputFile Загрузчик файлов — тэг <input> типа file HtmllnputHidden Скрытый буфер текстовых данных — тэг <input> типа hidden Htmllnputlmage Графическая кнопка — тэг <input> типа image. Заметьте, что этот тэг поддерживается всеми браузерами HtmllnputPassword Защищенное текстовое поле — тэг <input> типа password- В ASP.NET 1х данный класс не поддерживается HtmllnputRadioButton Переключатель — тэг <input> типа radio HtmllnputReset Командная кнопка типа reset. В ASP.NET 1х данный класс не под- держивается HtmllnputSubmit Командная кнопка типа submit. В ASP.NET 1х данный класс не под- держивается HtmllnputText Текстовое поле — тэг <input> типа password или text Скрытый {HtmllnputHidden) и текстовый {HtmllnputText) элементы управления inpwt-типа почти идентичны, и содержимое обоих передается серверу при возврате формы. Различаются они лишь тем, что скрытые поля не отображаются в браузере и потому не имеют свойств, связанных с пользовательским интерфейсом, таких как MaxLength или Size. Командные кнопки Класс HtmllnputButton является самым гибким из «кнопочных» классов .NET Frame- work. Он отличается от HtmlButton тем, что выводится в виде тэга <input>, а не под- держиваемого лишь Internet Explorer тэга <button>. Тем самым гарантируется под- держка элемента управления всеми браузерами. Элемент управления HtmllnputButton генерирует событие ServerClick, позволяя задать код, который будет выполнен на сервере после щелчка кнопки. Заметьте, что когда для кнопки указан тип Button и задан обработчик события ServerClick, элемент управления автоматически добавляет HTML-атрибут onclick с кодом сценария. Таким образом обеспечивается возврат формы и выполнение серверного кода. Рассмотрим следующий фрагмент страницы ASP.NET: <input runat="server" type="button" id="btn" value="Click" onserverclick="buttonClicked" /> Ему соответствует такой код HTML: <input language="javascript" onclick=”_doPostBack(‘btn’,'’)" name="btn" type="button" value="Click" />
Базовые серверные элементы управления ASP.NET Глава 4 131 Вызов клиентской функции doPostBack — это стандартный код, генерируемый ASP.NET для реализации возврата формы. Если задать для кнопки тип submit, сооб- щив тем самым браузеру, что она должна инициировать возврат формы, клиентский сценарий не потребуется вовсе, и атрибут onclick устанавливаться не будет. В ASP.NET 2.0 добавлены более специализированные элементы управления для рендеринга кнопок типа submit и reset — HtmllnputSubmit и HtmllnputReset. s Примечание Элемент управления Htmllnputlmage поддерживает практически идентичную схему обработки серверных событий и валидации, но имеет несколько дополнительных свойств, связанных с выводом изображения. В частности, он позволяет задать альтерна- тивный текст (выводимый, когда изображение недоступно), рамку и способ выравнивания по отношению к остальной части страницы. Обработчик события ServerClick имеет не- много другую форму и выглядит так: void ImageClickEventHandler(object sender, ImageClickEventArgs e); По щелчку графической кнопки серверу передаются координаты указателя мыши, которые можно прочитать из свойств X и Y структуры данных ImageClickEventArgs Управление валидацией Класс HtmllnputButton, как и класс HtmlButton, имеет булево свойство CausesValidation. Оно указывает, должно ли после щелчка кнопки проверяться содержимое полей ввода. По умолчанию свойство установлено в true, то есть валидация осуществляется. Мы еще вернемся к этой теме в настоящей главе, а пока достаточно сказать, что этап вали- дации можно программно включать и отключать с помощью свойства CausesValidation. Обычно валидацию отключают в том случае, если кнопка, которую щелкнул поль- зователь, не выполняет никаких действий, кроме очистки элементов пользователь- ского интерфейса или отмены текущей операции. По определению серверная валида- ция осуществляется перед выполнением обработчика события ServerClick. Установка свойства CausesValidation в false является единственным способом предотвращения ненужной валидации. Как определить, что состояние элемента управления изменилось Ранее в этой главе, рассказывая об элементе управления HtmlTextArea, я упоминал о событии ServerChange как о средстве обнаружения изменений в состоянии элемента управления между последовательными возвратами формы. Это событие не является принадлежностью только элемента управления HtmlTextArea — оно поддерживается и другими элементам и, в частности HtmllnputCheckBox, HtmllnputRadioButton, HtmllnputHidden и HtmllnputText. Для примера рассмотрим страницу, содержащую группу флажков и кнопку, с по- мощью которой, завершив установку флажков, пользователь будет отправлять стра- ницу на сервер. С помощью события ServerChange нам нужно выяснить, какой из этих флажков пользователь установил после того, как страница последний раз побывала на сервере. Заметьте, что ни элемент управления HtmllnputCheckBox, ни какой-либо другой in- put-элемент, за исключением кнопок, не выполняет возврат формы, когда пользователь щелкает его мышью. Поэтому на Web-странице и размещают дополнительный элемент управления, специально предназначенный для данной цели, например HtmlButton или HtmllnputButton. Приведенный ниже код создает страницу, показанную на рис. 4-2: <%© Page Language="C#" %> <html> <script runat=”server">
132 Часть I Разработка страниц ASP.NET public void DetectChange(object sender, EventArgs e) { HtmllnputCheckBox cb = (HtmllnputCheckBox) sender; Response.Write("Control <b>” + cb.UniquelD + ”</b> changed<br>”); } </script> <body> <form runat=”server"> <input runat="server" type="checkbox" id="one" OnServerChange="DetectChange">One<br> <input runat="server" type=”checkbox" id="two" OnServerChange="DetectChange">Two<br> <input runat="server" type="checkbox" id="three" OnServerChange=”DetectChange">Three<br> <input runat="server" type="submit" value="Submit" /> </form> </body> </html> Рис. 4-2. Первый снимок экрана сделан после того, как пользователь установил флажок и щелкнул кнопку Submit, а второй — после повторного щелчка этой же кнопки Как видно из рисунка, событие ServerChange генерируется в том случае, если состояние элемента управления изменилось между двумя последовательными воз- вратами формы. Поэтому на первом снимке сообщение об изменении, выводимое обработчиком данного события, присутствует, а на втором — нет. Как уже упоминалось в главе 3, реализуя интерфейс IPostBackDataHandler, серверный элемент управления получает возможность обновить свое состояние с учетом данных, поступивших от клиента. Этот интерфейс подробно описан в моей книге «Programming Microsoft ASP.NET 2.0 Applications Advanced Topics» (Microsoft Press, 2005). Загрузка файлов на сервер Элемент управления HtmllnputFile является HTML-средством загрузки файлов из браузера на Web-сервер. Для выполнения такой загрузки необходим Internet Explorer версии 3.0 или выше. Пользуясь элементом управления HtmllnputFile, нужно убедить- ся, что свойству Enctype серверной формы присвоено значение multipart/form-data. Заметьте, что в ASP.NET 2.0 правильное значение Enctype устанавливается автома- тически перед выводом разметки элемента. <form runat="server" enctype=”multipart/form-data"> <input runat= server" type="file” id=”upLoader” > <input runat="server" type?"Submit" value="Upload..." /> </form>
Базовые серверные элементы управления ASP.NET Глава 4 133 Способ рендеринга элемента управления HtmllnputFile зависит от конкретного браузера, но обычно для этого элемента генерируется текстовое поле и кнопка с над- писью «Browse». Щелкнув ее, пользователь выбирает файл на локальном компьютере, после чего с помощью другой кнопки инициирует возврат формы, а браузер при этом передает выбранный файл на сервер (рис. 4-3). Э Pro ASP.NET ((_ h04) Microsoft Internet Explorer (Me View FivorFes Tods H<lp Q Back ’ JJ'Seardi Favorites ф Med Addr^ss]^ http://tocaf ost/test20/inputfilc.aspx МНММмймКм Select a picture to upload: Picture to upload___ I ' |[ Browse.,, j [ Upload ] Рис. 4-3. Новый файл загружен на Web-сервер и помещен в папку назначения Примечание До появления ASP.NET для обработки совмещенного возврата формы и данных необходимо было, чтобы в фоновом режиме выполнялся специальный серверный процесс — акцептор постинга. Теперь он больше не нужен, так как его задачу выполняет сама ASP.NET. На сервере файл упаковывается в объект типа HttpPostedFile и остается там до тех пор, пока не будет явно обработан, например сохранен на диске или в базе данных. Объект HttpPostedFile имеет свойства и методы, с помощью которых можно получить информацию о файле, извлечь его и сохранить. Следующий код показывает, как со- хранить полученный файл в папке на диске: <%@ Раде 1апдиаде="С#" %> <%@ Import Namespace="System.IO" %> <script runat="server"> void UploadButton_Click(object sender, EventArgs e) { // *** СЧИТАЕМ, ЧТО ПУТЬ СУЩЕСТВУЕТ *** string savePath = @"c:\temp\Pictures\"; if (!Directory.Exists(savePath)) { string msg = "<h1>The upload path doesn't exist:{0}x/h1>"; Response.Write(String.Format(msg,. savePath)); Response End(); // Проверяем, получен ли файл if (Filellploadl. PostedFile != null) { // Сохраняем загруженный файл по заданному пути string fileName = Path.GetFileName(FileUpload1.Value); savePath += fileName; FileUploadl.PostedFile.SaveAs(savePath);
134 Часть I Разработка страниц ASP.NET // Уведомляем пользователя, под каким именем сохранен файл Uploadstatus.InnerText = "File saved as: " + savePath; } else { // Уведомляем пользователя, что файл не был загружен Uploadstatus.InnerText = "No file specified."; } } </script> <html> <head runat="sefver”> <title>Pro ASP.NET (Ch04)</title> </head> <body> <form runat="server"> <h3>Select a picture to upload:</h3> <hr /> <b>Picture to upload</b><br /> <input type="file’ id='FileUploadl" runat="server" /> <br><br> <input runat="server" id="UploadButton" type="submit" value="Upload" onserverclick="UploadButton_Click" /> <hr /> <span runat="server" id="UploadStatusLabel" /> </form> </body> </html> Для того чтобы прочитать полученные сервером данные, прежде чем они будут обработаны или сохранены, можно воспользоваться свойством InputStream объекта HttpPostedFile. Кроме того, элемент управления HttpInputFile позволяет ограничить перечень типов файлов, которые разрешено загружать на сервер. Для этого нужно присвоить свойству Accept разделенный запятыми список MIME-типов. О Внимание! Пользуясь методом SaveAs, не забывайте указывать полный путь к резуль- тирующему файлу. Если указан относительный путь, ASP.NET пытается записать файл в системную папку, результатом чего обычно становится отказ в доступе. Убедитесь также, что учетной записи ASP.NET предоставлено разрешение на запись в ту папку, куда вы хотите сохранить файл. ASP.NET позволяет вам в некоторой степени контролировать количество загру- жаемых данных. Максимально допустимый размер файла (по умолчанию — 4 Мбайт) можно задать в атрибуте maxRequestLength раздела <httpRuntime> конфигурационного файла. Если размер заданного пользователем файла окажется больше, браузер выве- дет сообщение об ошибке. Загрузка слишком большого файла может также привести к ошибке времени выполнения, обусловленной превышением лимита выделенной приложению памяти. Элемент управления Htmllmage Класс Htmllmage представляет в ASP.NET тэг <img>, и с его помощью можно скон- фигурировать на сервере способ вывода изображения. В частности, имеется возмож- ность задать размер изображения, его рамку и заменяющий текст. Экземпляр класса Htmllmage создается для тэга <img> лишь в том случае, если он снабжен атрибутом
Базовые серверные элементы управления ASP.NET Глава 4 135 runat. Когда нужно лишь поместить изображение на страницу, но не требуется ни определять, ни конфигурировать его динамически, экземпляр класса Htmllmage соз- давать ни к чему — это будет напрасная трата времени и ресурсов сервера. Следующий код показывает, как происходит конфигурирование серверного тэга <img>, предназначенного для вывода изображения, имя которого определяется ди- намически во время выполнения: thelmg.Width = 100; thelmg.Height = 100; thelmg.Src = GetImageUrl(Request); Элемент управления Htmllmage следует использовать для программного манипу- лирования изображением с целью изменения его исходного файла, высоты, ширины или способа выравнивания относительно других элементов страницы. Большинство свойств данного элемента реализовано в виде строк; к числу таких свойств относят- ся Src, в котором задается URL изображения, а также Align. Допустимые значения свойства Align представлены небольшой группой слов: left, right, top и т. д. На мой взгляд, эти слова стоило бы объединить в пользовательский перечислимый тип, как того требуют правила программирования со строгой типизацией. Но если вы тоже так думаете, тогда этот пример поможет вам понять суть различия между серверными элементами управления Web и HTML! Элементы управления HTML являются всего лишь отражением тэгов HTML, тогда как Web-элементы реализуют продуманный и эффективный интерфейс программирования, позволяющий использовать все пре- имущества .NET Framework. Литеральные элементы управления Литеральными элементами управления называются серверные элементы управ- ления особого типа, создаваемые и используемые ASP.NET, когда она встречает в исходном файле страницы чистый текст, не требующий серверной обработки. В сущности, все, что содержится в файле страницы, интерпретируется как эле- менты управления. Встретив тэг, помеченный атрибутом runat= "server", ASPNET создает экземпляр определенного класса; если же для тэга не задан атрибут runat, он компилируется в объект LiteralControl — литеральный элемент управления. Так же ASP.NET поступает с текстом, который содержится в теле страницы сам по себе, не заключенный внутрь того или иного тэга. Литеральный элемент управле- ния — это просто текстовый контейнер. Для включения таких элементов в состав страницы и их удаления используется тот же интерфейс программирования, что и для других серверных элементов. Заметьте, что литеральный элемент управления создается для каждой после- довательности символов, расположенной между двумя серверными элементами управления, включая и символы возврата каретки. Если вам захочется разделить серверные элементы управления пустыми строками, чтобы сделать удобочитае- мым исходный код страницы, подумайте о том, что тем самым вы увеличите ко- личество серверных элементов управления, создаваемых при обработке страницы. Когда весь ее код записан в одну строку, количество генерируемых серверных элементов управления минимально. Элементы управления Web В элементах управления Web, определенных в пространстве имен System. Web. VI WebC- ontrols, реализован альтернативный подход к созданию серверных элементов управле- ния. Подобно элементам HTML, Web-элементы являются серверными компонентами,
136 Часть I Разработка страниц ASP.NET создаваемыми при наличии атрибута runat- "server". Однако в отличие от них, эле- менты Web предоставляют интерфейс программирования с иным, переработанным и значительно оптимизированным набором атрибутов и событий. Таким образом, Web-элементы обладают более согласованным и абстрактным API и более богатой функциональностью, но, как и элементы HTML, в конце своего жизненного цикла они генерируют разметку для браузера. В теле страницы .aspx Web-элементы можно отличить по префиксу пространства имен asp. Множества элементов управления Web и HTML в значительной мере пересекаются в том смысле, что многие йх пары генерируют практически идентичную разметку, но интерфейс программирования при этом имеют разный. Например, в пространстве имен Web-элементов определен элемент управления TextBox, создаваемый при обра- ботке тэга <asp:textbox>t а в пространстве имен элементов HTML определен элемент управления HtmllnputText, объявляемый при помощи тэга <input>. Какой из двух вы- брать — преимущественно дело вкуса; лишь в некоторых случаях функциональность этих элементов слегка отличается. Общая информация об элементах управления Web Все элементы управления Web наследуют общий базовый класс Web Control. Он яв- ляется производным от класса Control и определяет ряд собственных свойств и ме- тодов. Однако не в каждом производном элементе управления реализованы все эти дополнительные члены. Большинство свойств и методов класса WebControl связаны с внешним видом и поведением элементов управления (это шрифты, стиль, цвета, CSS) и зависят от версий браузера и HTML. Например, хотя все элементы управле- ния Web позволяют определить рамку, эта возможность поддерживается не каждым тэгом HTML. Свойства элементов управления Web Свойства класса WebControl перечислены и описаны в табл. 4-8. Табл. 4-8. Свойства, характерные для элементов управления Web Свойство Описание AccessKey Возвращает и позволяет задать букву клавиши, которая совместно с клавишей Alt будет использоваться для быстрого перехода к элемен- ту управления в Web-форме. Поддерживается Internet Explorer версии 4.0 и выше Attributes Возвращает коллекцию атрибутов, не имеющих соответствия среди свойств элемента управления. Атрибуты, задаваемые через эту коллек- цию, выводятся в составе результирующей страницы как атрибуты HTML BackColor Возвращает и позволяет задать цвет фона элемента управления Web BorderColor Возвращает и позволяет задать цвет рамки элемента управления Web BorderStyle Возвращает и позволяет задать стиль рамки элемента управления Web BorderWidth Возвращает и позволяет задать ширину рамки элемента управления Web* Controlstyle Возвращает и позволяет задать стиль элемента управления Web. Стиль задается как объект типа Style ControlStyleCreated Возвращает значение, указывающее, создан ли для свойства Control- Style объект Style CssClass Возвращает и позволяет задать класс каскадной таблицы стилей (CSS), связанной с элементом управления Enabled Указывает и позволяет определить, активен ли элемент управления
Базовые серверные элементы управления ASP.NET Глава 4 137 Табл. 4-8. (окончание) Свойство Описание Font Возвращает свойства шрифта элемента управления Web ForeColor Возвращает и позволяет задать цвет фона элемента управления Web; используется главным образом при выводе текста Height Возвращает и позволяет задать высоту элемента управления Web. Вы- сота задается как значение типа Unit Style Возвращает коллекцию CssStyleCollection, составленную из атрибутов, которые присвоены выводимому тэгу элемента управления Tabindex Возвращает и позволяет задать индекс вкладки элемента управления ToolTip Возвращает и позволяет задать текст всплывающей подсказки, которая выводится при наведении на элемент управления указателя мыши Width Возвращает и позволяет задать ширину элемента управления, выра- женную в виде значения типа Unit Свойства Controlstyle и ControlStyleCreated используются главным образом разра- ботчиками элементов управления, тогда как свойство Style чаще других используют разработчики приложений, устанавливая с его помощью CSS-атрибуты выводимого тэга элемента управления. Последнее свойство реализовано в виде экземпляра клас- са CssStyleCollection — простой коллекции строк, подобной той, что присваивается HTML-атрибуту style. Стили элементов управления Web Свойство Controlstyle возвращает объект типа Style класса, который инкапсулиру- ет свойства элемента управления, определяющие его внешний вид. В состав класса Style входят некоторые из свойств, перечисленных в табл. 4-8, а сам он действует как репозитарий графических и «косметических» атрибутов, характерных для всех эле- ментов управления Web. Его свойствами являются: BackColor, BorderColor, BorderStyle, BorderWidth, CssClass, Font, ForeColor, Height и Width. Все они строго типизированы. Указанные свойства сохраняются в состоянии представления не по отдельности, а с использованием механизма сериализации класса Style. Теперь вам, думаю, ясно, что класс Style очень отличается от одноименного свой- ства, типом которого является CssStyleCollection. Заметьте, что составляющие сти- ля, задаваемые посредством свойства Style, не переносятся автоматически в строго типизированный объект Style. Так, вы можете задать CSS-атрибут border-style с по- мощью свойства Style, но это значение не будет отражено в свойстве BorderStyle'. // Установка цвета рамки посредством CSS-атрибута MyCpntrol.Style["border-color"]- = "blue"; Ц Установка цвета рамки посредством стилевого свойства ASP.NET MyControl.BorderColor = Color.Red; Что же произойдет, если выполнить приведенный выше код? Какая из установок возобладает? Когда приходит время рендеринга элемента управления, содержимое свойств Controlstyle и Style выводится в виде составляющих HTML-атрибута style: style="border-color:Red;border-color:blue; ..." Определение стиля Web-элементов Стилевыми свойствами элементов управления Web можно манипулировать програм- мно. Например, с помощью метода CopyFrom вы можете сдублировать объект класса Style, а с помощью метода MergeStyle объединить два стилевых объекта: currentstyle MergeStyle(newStyle);
138 Часть I Разработка страниц ASP.NET Метод MergeStyle объединяет свойства двух объектов; при этом он не заменяет значения тех свойств, которые уже установлены в базовом объекте, и определяет лишь неинициализированные свойства. Есть еще метод Reset, который очищает все свойства стилевого объекта. Методы элементов управления Web Класс WebControl определяет несколько дополнительных методов, отсутствующих у базового класса Control. Все они перечислены в табл. 4-9. Табл. 4-9. Собственные методы класса WebControl Свойство Описание ApplyStyle Копирует в элемент управления непустые элементы заданного стиле- вого объекта. Существующие стилевые свойства переопределяются CopyBaseAttributes Импортирует из заданного элемента управления Web свойства Access- Key, Enabled. ToolTip. Tabindex и Attributes Иными словами, он копиру- ет все свойства, не инкапсулированные объектом Style MergeStyle Подобно методу ApplyStyle копирует в элемент управления непустые элементы заданного стиля. Существующие стилевые свойства не переопределяются RenderBegin Tag Осуществляет рендеринг открывающегося HTML-тэга элемента управления в заданный объект записи текста. Вызывается непосред- ственно перед методом RenderControl RenderEndTag Осуществляет рендеринг закрывающегося HTML-тэга элемента управления в заданный объект записи текста. Вызывается сразу после метода RenderControl Разработчики приложений редко пользуются перечисленными методами, посколь- ку они предназначены главным образом для тех программистов, которые создают элементы управления. Базовые элементы управления Web Весь набор элементов управления Web можно разделить на несколько категорий в соответствии с выполняемыми ими функциями: элементы управления input и but- ton, валидаторы, элементы управления, связанные с данными и защитой, табличные элементы управления и представления, а также элементы, которые можно было бы отнести к категории «разное», — календарь, рекламный баннер со сменяющимися изображениями и т. п. В этой главе мы сконцентрируемся на наиболее популярных и важных элементах управления Web, таких как элементы для ввода данных от пользователя и их проверки, а также элементы для возврата этих данных серверу. В главах 9-11 будут рассмотрены разные виды элементов управления, связанных с данными, а в главе 15 — элемен- ты, связанные с защитой. В табл. 4-10 перечислены ключевые серверные элементы управления Web. Табл. 4-10. Ключевые элементы управления Web Элемент управления Что представляет Button Кнопку, реализованную в виде тэга <input> CheckBox Флажок, реализованный в виде тэга <input>
Базовые серверные элементы управления ASP.NET Глава 4 139 Табл. 4-10. (окончание) Элемент управления Что представляет FileUpload Элемент интерфейса, дающий возможность пользователю выбрать файл для загрузки на сервер. В ASP.NET 1х данный элемент управле- ния не поддерживается HiddenField Скрытое поле. В ASP NET 1х данный элемент управления не поддер- живается HyperLink Якорный тэг <л>; позволяет указать либо адрес для перехода, либо сценарий для выполнения Image ImageButton Изображение, реализованное в виде тэга <img> Изображение, отвечающее на щелчки мыши подобно настоящей кнопке ImageMap Изображение с необязательно имеющейся в нем областью, которую можно щелкать мышью В ASP.NET 1х данный элемент управления не поддерживается Label Обыкновенный статический текст, не реагирующий на щелчки. Реа- лизован в виде тэга <span> LinkButton Якорный тэг, обеспечивающий возврат формы с использованием со- ответствующего механизма ASP.NET. Это гиперссылка особого рода, для которой программист не может задать целевой URL MultiView Элемент управления, действующий как контейнер группы дочерних элементов типа View. В ASP.NET 1х данный элемент управления не поддерживается Panel HTML-контейнер, реализованный с использованием блочного эле- мента <div>. В ASP.NET 2.0 этот контейнер поддерживает скрол- линг. Заметьте, что для старых браузеров данный элемент управле- ния выводится как <table> RadioButton Одну кнопку переключателя, реализованную в виде тэга <input> Table Внешний табличный контейнер; эквивалентен HTML-элементу <table> TableCell Ячейку таблицы; эквивалентен HTML-элементу <td> TableRow Строку таблицы; эквивалентен HTML-элементу <tr> TextBox Текстовое поле, реализованное в виде тэга <input> или <textarea>, что зависит от запрошенного типа текста. Может работать в одно- или многострочном режиме либо в режиме ввода пароля View Контейнер группы элементов управления. Элемент управления View всегда должен содержаться в элементе управления MultiView. В ASP.NET 1х данный элемент управления не поддерживается Большинство из перечисленных в этой таблице элементов управления выгля- дят как элементы управления HTML, но их программная модель богаче и более аб- страктная. Однако следует понимать, что в конечном счете все они генерируют са- мую обыкновенную HTML-разметку. Если определенную функциональность нельзя реализовать, используя чистый HTML, то никакой серверный элемент управления, будь то Web-элемент, пользовательский или специализированный, вам ее не предо- ставит. Какой бы сложной ни была программная модель, каким бы ни был целевой
140 Часть I Разработка страниц ASP.NET браузер, задача элемента управления всегда заключается в генерировании стандартной HTML-разметки. Кнопочные элементы управления В ASPNET 2.0 у элементов управления, генерирующих кнопки, появился новый ин- терфейс — IButtonControl. Его реализуют элементы Button, ImageButton и LinkButton, а в общем случае — любой специализированный элемент управления, который должен действовать как кнопка. Интерфейс IButtonControl является хорошим примером рефакторинга, которому подверглась вся .NET Framework при переходе от версии 1.x к версии 2.0. Этот интер- фейс состоит из нескольких свойств, которые в ASP.NET 1.x поддерживались боль- шинством кнопочных элементов управления (включая и некоторые HTML-кнопки). Кроме того, в его состав входят несколько новых свойств, необходимых для поддержки новой функциональности, например PostBackUrl и Validation Group. В табл. 4-11 пере- числены все члены интерфейса IButtonControl. Табл. 4-11. Интерфейс IButtonControl Член интерфейса Описание Causes Validation Значение булева типа, указывающее, должна ли по щелчку элемента управления выполняться валидация формы CommandArgument Возвращает и позволяет задать значение необязательного параметра, передаваемого обработчику события Command кнопки вместе со свя- занным с этой кнопкой значением CommandName CommandName Возвращает и позволяет задать имя связанной с кнопкой команды, передаваемое обработчику события Command PostBackUrl Определяет URL страницы, которая будет обрабатывать возврат фор- мы, вызванный щелчком кнопки. Данная функция, специфическая для ASP.NET, называется межстраничным возвратом формы (Мы подробнее рассмотрим ее в главе 5) Text Возвращает и позволяет задать надпись на кнопке ValidationGroup Возвращает и позволяет задать имя валидационной группы, в состав которой входит кнопка Visible Значение булева типа, указывающее, должен ли осуществляться рен- деринг данного элемента управления В дополнение к свойствам, определяемым интерфейсом IButtonControl, класс Button получил в ASP.NET 2.0 два новых свойства: OnClientClick и UseSubmitBehavior Первое стандартизирует типичную практику большинства разработчиков, использующих ASP.NET 1.x, — позволяет определить имя функции JavaScript, которая будет выпол- няться на клиенте в ответ на событие onclick. Так, совершенно законны и полностью эквивалентны следующие два оператора: Ц Введен в ASP.NET 2.0 Buttonl OnClientClick = "ShowMessageO"; // Эквивалент из ASP.NET 1.x Buttonl.Attributes["onclick’] = "ShowMessageO”; Свойство OnClientClick имеется также у элементов управления LinkButton и Image- Button.
Базовые серверные элементы управления ASP.NET Глава 4 141 ,, По умолчанию элемент управления Button осуществляет свой рендеринг в виде тэга <input type—subrnit>. Иными словами, он использует для возврата формы встроенный механизм браузера. Свойство UseSubmitBehavior позволяет изменить это стандартное поведение. Если установить данное свойство в false, элемент управления будет выво- диться в виде тэга <input type~button>. Однако и в этом случае элемент управления останется кнопкой возврата формы. Когда свойство UseSubmitBehavior имеет значе- ние false, клиентским обработчиком события onclick данного элемента управления является код на языке JavaScript, а именно функция_doPostBack, которая реализу- ет механизм возврата формы ASP.NET подобно тому, как это делается у элементов управления LinkButton и ImageButton. ©Внимание! Кнопки — не единственные элементы управления, которые могут вызывать возврат формы. Аналогичной способностью обладают текстовые поля и флажки (а также некоторые связанные с данными списочные элементы управления, о которых мы по- говорим в главе 9), когда их свойство AutoPostBack установлено в true. (Заметьте, что по умолчанию это свойство имеет значение false.) В таком случае элемент управления генерирует клиентское событие (у текстового поля оно называется onchange, а у флаж- ка — onclick) и инициирует операцию возврата формы посредством сценария. Гиперссылки Элемент управления HyperLink создает ссылку на другую Web-страницу и обычно выводится в виде текста, задаваемого в свойстве Text. В качестве альтернативы ги- перссылка может быть представлена изображением, и тогда URL этого изображения задается в свойстве ImageUrl. Когда установлены оба свойства, преимущество имеет ImageUrl, а содержимое свойства Text выводится в виде всплывающей подсказки, когда на изображении задерживается указатель мыши Свойство NavigateUrl определяет URL, на который указывает гиперссылка. А в свойстве Target задается имя окна или фрейма, где будет выводиться контент, расположенный по целевому URL. Статические изображения и графические кнопки Элемент управления Image выводит на Web-странице обычное статическое изобра- жение, путь к которому задается в свойстве ImageUrl. URL изображений могут быть абсолютными или относительными, причем большинство программистов отдают предпочтение относительным URL, позволяющим без труда перемещать сайт. При желании в свойстве AltemateText можно задать альтернативный текст, который будет выводиться в случае, когда изображение недоступно или когда браузер по какой-то причине блокирует изображения. Способ выравнивания изображения относительно других элементов страницы указывается в свойстве ImageAlign, допустимыми значе- ниями которого являются члены одноименного перечисления. Щелкать на элементе управления Image нельзя — это обыкновенное статичное изо- бражение, не связанное ни с какими действиями. Если нужно перехватывать щелчки изображения, воспользуйтесь вместо элемента управления Image элементом ImageBut- ton. Класс ImageButton является производным от Image и расширяет его парой собы- тий, Click и Command, генерируемых в ответ на щелчок. Обработчик первого из них, OnClick, получает структуру данных ImageChckEventArgs, которая содержит инфор- мацию о координатах точки элемента управления, которую щелкнул пользователь. Наличие обработчика события OnCommand превращает элемент управления в ко- мандную кнопку. Со всякой командной кнопкой связано имя команды, задаваемое в свойстве CommandName. Если на странице размещено несколько командных кно- пок, по этому имени всегда можно определить, какую из них щелкнул пользователь.
142 Часть! Разработка страниц ASP.NET Свойство CommandArgument используется для передачи дополнительной информации о команде и элементе управления. Еще одним нововведением ASP.NET 2.0 стал элемент управления ImageMap. В своей простейшей и наиболее часто используемой форме этот элемент выводит на странице изображение. Однако когда для него определена так называемая горячая область, элемент управления инициирует возврат формы или переход по заданному URL. Горячей областью называется часть изображения, щелчок которой вызывает определенное действие. Она реализуется в виде класса, наследующего класс HotSpot. Существует три предопределенных типа горячих областей: многоугольники, круги и прямоугольники. Флажки и переключатели Флажки и переключатели реализуются с помощью тэга <input>, атрибут type которого имеет соответственно значение checkbox или radio. Web-версии флажков и переключа- телей, в отличие от аналогичных элементов управления HTML, позволяют задавать связанный с элементом управления текст в виде свойства. У тэгов HTML и элемен- тов управления HTML нет текстового атрибута, содержимое которого выводилось бы рядом с флажком или переключателем. Для этой цели приходится использовать отдельный тэг <label> с атрибутом for. <input type="checkbox" id="ctl" /> <label for="ctl">Check me</label> Таким образом, элементы управления HtmllnputCheckBox и HtmllnputRadioButton не выводят надписей — эта задача полностью на вашей ответственности. А вот ана- логичные элементы управления Web не привязаны к синтаксису HTML, у них есть свойство Text, значение которого автоматически выводится в виде тэга <1аЬеГ>. Возь- мем, к примеру, код ASP.NET <asp:checkbox runat="server" id="ctl" text="Check me” /> При его обработке генерируется такой код HTML: <input type="checkbox” id="ctl" /> clabel for="ctl">Check me</label> Панели с прокруткой Элемент управления Panel служит для группировки других элементов управления с использованием тэга <div>. Он позволяет разработчикам добавлять и удалять эле- менты управления и поддерживает оформление с помощью стилей. В ASP.NET 2.0 панели могут иметь вертикальные и горизонтальные полосы прокрутки, реализо- ванные с использованием CSS-стиля overflow. Следующий пример показывает, как создается прокручиваемая панель: <asp:Panel ID="Panel1" runat=”server" Height="80px" Width="420px” Sc rol1Ba rs="Auto" BorderStyle="Solid"> <h2>Choose a technology</h2> <asp:CheckBox ID="ChkBox1" runat=”sen/er Text="ASP.NET" /><br /> <asp:CheckBox ID="ChkBox2” runat='server" Text="ADO.NET" /><br /> <asp:CheckBox ID="ChkBox3" runat= server" Text="Web Services /><br /> <asp:CheckBox ID= ChkBox4" runat=”server" Text="XML” /><br /> <asp:CheckBox ID="ChkBox5" runat="server" Text="SQL Server" /><br /> <asp:CheckBox ID="ChkBox6" runat="server" Text="CLR" /><br /> </asp:Panel> Эта панель представлена на рис. 4-4.
Базовые серверные элементы управления ASP.NET Глава 4 143 Рис. 4-4. Страница с прокручиваемой панелью Текстовые элементы управления Наиболее быстрым способом размещения текста на Web-странице является его ли- теральное задание, то есть вставка статического текста прямо в исходный файл .aspx. Такой текст будет откомпилирован в элемент управления, но количество динамически создаваемых при этом литеральных элементов управления получится минимальным, поскольку вся последовательность символов этого текста будет представлена как один литерал. Если вам потребуется возможность программно манипулировать стро- ками текста, воспользуйтесь элементом управления Literal, а еще лучше, элементом управления Label, имеющим более богатый интерфейс. Чтобы текст можно было модифицировать, обратитесь к элементу управления TextBox. В перечисленные элементы управления в ASP.NET 2.0 были внесены небольшие изменения. Во-первых, для логической группировки их функций введены два новых интерфейса. Один из них, ITextControl, состоит лишь из свойства Text и реализован в элементах Literal, Label, TextBox, а также в списочных элементах управления. Дру- гой интерфейс, lEditableTextControl, определяет событие TextChanged. Он реализован в списочных элементах управления и элементе TextBox. Отдельного упоминания заслуживает новая составляющая элемента управления Label — свойство AssociatedControlID. В этом свойстве содержится идентификатор элемента управления страницы, который вы хотите связать с надписью. Свойство AssociatedControlID изменяет способ рендеринга элемента управления Label. Если оно не содержит значения, элемент выводится как <span>, в противном случае — как <label>. Рассмотрим такой пример: <asp:Label ID="Label1” runat="server" Text="Sample text” /> <asp:TextBox ID="TextBox1 runat="server" /> Для этого кода генерируется следующая разметка: <span id="Label1">Sample text</span> cinput name="TextBox1" type="text" id=”TextBox1" /> Однако если присвоить свойству AssociatedControlID значение TextBoxI, разметка изменится: <label for=”TextBoxT' id=”Label1">Sample text</label> cinput name="TextBox1" type="text” id="TextBox1” />
144 Часть I Разработка страниц ASP NET Поведение элемента управления в браузере также немного изменится — теперь по щелчку будет выделяться не только он сам, но и связанный с ним элемент управле- ния. Например, стоит пользователю щелкнуть надпись, и фокус ввода переместится в текстовое поле либо будет установлен или снят флажок. Примечание Свойство AssociatedControllD введено с целью сделать работу пользова- ' теля со страницей более удобной. В Visual Studio .NET 2005 страницу можно проверить на соответствие стандартам удобства доступа (WCAG и Section 508) — для этого нужно воспользоваться командой Tools\Check Accessibility. Скрытые поля и загрузка файлов на сервер Если вам понадобился удобный программный интерфейс для создания скрытых по- лей и загрузки файлов с клиента на сервер, возможно, новые элементы управления ASP.NET 2.0 HiddenField и FileUpload окажутся именно тем, что вы ищете. С их по- явлением никаких функциональных новшеств в ASP.NET не прибавилось, но они упрощают решение двух стандартных задач. Например, скрытое поле можно создать еще двумя способами, которые поддерживает и ASP.NET 1.x. В частности, вы можете воспользоваться методом RegisterHiddenField класса Page: // Работает в ASP.NET 1.x, но в ASP.NET 2.0 определен как устаревший RegisterHiddenField(”HiddenField1", "Great book!"); или написать обычную HTML-разметку, дополнив ее атрибутом runat, если значение предполагается задавать программно: <input runat="server" id="HiddenField1" type="hidden" value="..." /> Аналогична и ситуация с элементом управления FileUpload, который обладает той же функциональностью, что и рассмотренный нами ранее элемент управления HtmllnputFile. Однако его программный интерфейс несколько иной, пожалуй, более интуитивный. Свойство HasFile и метод SaveAs скрывают ссылку на объект, пред ставляющий загруженный на сервер файл, а свойство FileName содержит имя этого файла. Код загрузки файла, приведенный ранее в этой главе, можно было бы пере- писать следующим образом: if (FileUploadl.HasFile) { // Получаем имя загруженного файла string fileName = FileUploadl.FileName; string targetPath = GetSavePath(fileName); a FileUploadl.SaveAs(targetPath); } Выбор между элементами управления FileUpload и HtmllnputFile зависит исклю- чительно от ваших предпочтений. Создание таблиц Как вы, вероятно, заметили, существует три способа размещения таблицы на странице HTML: с использованием статических тэгов HTML, элемента управления HtmlTable и элемента управления Table. Какой из них лучше? Применение статических тэгов обеспечивает максимальную гибкость при опреде- лении структуры таблицы. Если вы хотите использовать все табличные тэги HTML, то данный метод — для вас. Элемент управления HtmlTable, напротив, предельно ограничивает ваши возможности. Фактически, он поддерживает только тэги строк и удаляет все остальное.
Базовые серверные элементы управления ASP.NET Глава 4 145 Что касается элемента управления Table, то в ASP.NET 2.0 он поддерживает строки трех разных типов: обычные, заголовочные и подвальные. Тип конкретной строки можно задавать программным способом с использованием свойства TableSection класса Table Row. Элемент управления Table позволяет динамически манипулировать строка- ми и ячейками, но и ему недостает декларативной поддержки всех табличных тэгов и атрибутов HTML. В частности, им не поддерживаются тэги COLGROUP и TBODY. Разные элементы управления Web В пространстве имен WebControls определено несколько элементов управления с по- лезной функциональностью, типичной для Web-приложений. В частности, мы про- анализируем элементы управления AdRotator, который может использоваться для создания рекламных баннеров, и Calendar — гибкий интерактивный элемент, ис- пользуемый для ввода даты. Элемент управления AdRotator Если говорить абстрактно, то элемент управления AdRotator выводит кнопку с изо- бражением, размер которого автоматически подгоняется под размер кнопки, и об- новляет изображение, а также URL при каждом обновлении страницы. Выводимое изображение и другая необходимая элементу управления информация считываются из XML-файла, содержимое которого соответствует определенной схеме. А если го- ворить более конкретно, элемент управления используется для создания рекламных баннеров на страницах Web Forms. Этот элемент включает в состав страницы изобра- жение, служащее гиперссылкой на рекламную страницу. Изображение подгоняется под размеры элемента управления AdRotator, каким бы ни был его собственный размер. Ниже приведено содержимое типичного XML-файла рекламы: <Advertisements> <Ad> <ImageUrl>6225. gif </Imagell r 1> <NavigateU rl>www.microsoft.com/MSPr ess/books/6235.asp</NavigateU rl> <AlternateText>Applied XML Programming with .NET</AlternateText> <Impressions>50</Impressions> </Ad> <Ad> <Imagel)rl>5727 gif</ImageUrl> <NavigateUrl>www.microsoft com/MSPress/books/5727.asp</NavigateUrl> <AlternateText>Building Web Solutions with ASP.NET</AlternateText> <Impressions>50</Impressions> </Ad> </Advertisements> Корневой раздел <Advertisements> содержит несколько элементов <Ad>, по одно- му на каждое выводимое изображение. Файл рекламы должен принадлежать тому же приложению, что и элемент управления AdRotator. Вот синтаксис этого элемента управления: <%@ Page Language="C#" %> <html> <head><title>Pro ASP.NET (Ch04)</title></head> <body> <form runat="server"> <h1>Dino Esposito's Books</h1> <asp:AdRotator runat= "server” id="bookRotator” Adve rt isement File="MyBooks.xml" />
146 Часть I Разработка страниц ASP.NET </form> </body> </html> В XML-файле рекламы узел <ImageUrl> служит для задания загружаемого изо- бражения, а узел <NavigateUrl> указывает, куда должен осуществляться переход, когда пользователь щелкает изображение. Альтернативный текст, который будет выведен вместо изображения, если оно окажется недоступным, задается в узле <AltemateText>, а в узле <Impressions> указывается, как часто должно выводиться данное изображение по сравнению с другими изображениями из файла рекламы. С каждым изображением посредством узла <Keyword> может быть ассоциировано ключевое слово. Среди всех перечисленных элементов управления только <ImageUrl> является обязательным. Элемент управления AdRotator генерирует серверное событие AdCreated, которое происходит перед рендерингом страницы. Его обработчик получает аргумент типа AdCreatedEcentArgs, содержащий информацию об изображении, навигационный URL, альтернативный текст и пользовательские свойства, связанные с рекламой. Событие AdCreated может использоваться для программного выбора выводимого изображения. XML-схема рекламного файла не фиксирована, и ее можно расширять пользователь- скими элементами. Все нестандартные элементы, связанные с выбранным рекламным баннером, будут переданы обработчику события AdCreated упакованными в словарный объект AdProperties, который является членом класса AdCreatedEcentArgs. Примечание В ASP.NET 2.0 элемент управления AdRotator претерпел существенное из- менение. Ранее, в ASP.NET 1.x, он был производным от WebControl, а теперь стал произ- водным от DataBoundControl. Кроме всего прочего это означает, что теперь информация для вывода рекламы может извлекаться не только из XML-файла, но и из реляционного источника данных. Изображения и навигационные URL, равно как и альтернативный текст, могут считываться из полей источника данных. При этом элемент управления не бывает связанным с несколькими источниками данных одновременно. Если установлено более одного из соответствующих его свойств (AdvertisementFile, DataSourcelD, DataSource), генерируется исключение. Элемент управления Calendar Элемент управления Calendar, показанный на рис. 4-5, выводит календарь на месяц, позволяет выбирать этот месяц и указывать дату. Он гибко настраивается как в от- ношении внешнего вида, так и в функциональном отношении. Например, свойство SelectionMode позволяет указать, что сможет выбрать пользователь: день, неделю или месяц. <asp:calendar runat="server" id="hireDate" SelectedDate="2005-05-04" VisibleDate="2005-05-04" /> В свойстве VisibleDate задается дата, которая должна отображаться в календаре, а в свойстве SelectedDate — дата, которая в нем будет выделена. Элемент управления генерирует три собственных события: DayRender, SelectionChanged и VisibleMonth- Changed. Событие Day Render сигнализирует о том, что элемент управления только что создал новую ячейку дня; его перехватывают, если хотят изменить способ вывода содержимого этой ячейки. Когда выбранная дата изменяется, генерируется событие SelectionChanged, а когда пользователь выбирает другой месяц с помощью специальных кнопок элемента управления — событие VisibleMonthChanged. Элемент управления Calendar осуществляет возврат формы каждый раз, когда вы что-либо в нем выбираете. Хотя он привлекателен и довольно мощен, его слабым местом является производительность, и если это для вас имеет значение, лучше огра- ничиться обыкновенным текстовым полем для ввода даты.
Базовые серверные элементы управления ASP.NET Глава 4 147 Рис. 4-5. Элемент управления Calendar Элемент управления Xml Элемент управления Xml, определяемый тэгом <asp:Xml>, используется для вывода на странице ASP.NET контента XML-документа. Содержимое XML-файла может вы- водиться в исходном виде или с применением к нему XSL трансформации (XSLT). Данный элемент управления является декларативным аналогом класса XslTransform и может использовать этот класс для своих целей. С помощью элемента управления Xml очень удобно создавать блоки используемых клиентом XML-данных, задавая документы и применяемые к ним трансформации. XML-документ можно задать разными способами: используя объектную модель XML, в виде строки или указав имя файла. Трансформация XSLT определяется как заранее сконфигурированный экземпляр класса XslTransform из .NET Framework или путем указания имени файла: <asp:xml runat="server" documentsource="document.xml” transformsource="transform.xsl" /> Если вы намерены применять к XML-данным ту или иную трансформацию, ее нужно задать здесь, между открывающимся и закрывающимся тэгами элемента управления. Элемент управления Xml упрощает решение одной типичной задачи ASP.NET: при- менения зависимой от браузера трансформации к частям страницы, описанным сред- ствами метаязыка XML. Предположим, что страница содержит такой фрагмент: <asp:xml runat="server" id="theXml" documentsource="document.xml" /> Здесь задан только XML-файл, трансформация же задается с использованием про- граммного интерфейса элемента управления Xml. В обработчике события Page Load вы определяете возможности браузера и решаете, какую именно трансформацию следует применить: void Page_Load(object sender, EventArgs e) { if (IsInternetExplorer(Request.Browser))
148 Часть I Разработка страниц ASP NET theXml.TransformSource = "ie5.xsl"; else theXml.TransformSource = "downlevel.xsl"; } Элемент управления Placeholder PlaceHolder — один из немногих элементов управления пространства имен Web Cont- rols, не являющихся производными от класса WebControl. Он наследует класс Control и используется только как контейнер для других элементов управления с границы. Элемент управления PlaceHolder не генерирует собственного вывода, и его функции ограничены отображением дочерних элементов управления, динамически добавля- емых в его коллекцию Controls. Приведенный ниже код показывает, как включить данный элемент управления в состав страницы: <asp:placeholder runat="server" id="theToolbar" /> Такой элемент управления не сообщает странице новой функциональности, а про- сто помогает сгруппировать связанные между собой элементы управления и облегчает их идентификацию. Вот как производится создание новой кнопки и добавление ее в существующий элемент управления PlaceHolder. Button btn = new Button(); btn.Text = "Click me"; theToolba r.Cont rols.Add(btn); Элемент управления резервирует место в дереве элементов, и с его помощью очень удобно программно идентифицировать области страницы, которые необходимо сконфигурировать или дополнить новыми элементами управления. О Внимание! Заметьте, что элементы управления, динамически добавляемые в коллекцию Controls родительского элемента управления, после возврата формы не восстанавли- ваются. Если элемент управления генерирует для клиента элементы /nptrf-типа, клиент возвращает их данные при возврате формы, но не существует серверных элементов управления, которые могли бы эти данные обработать. Поэтому не забывайте, какие элементы управления вы создаете динамически, и вовремя воссоздавайте их при загрузке страницы, осуществляемой вследствие возврата формы. Для сохранения информации о динамически добавленных элементах управления в состав состояния представления можно включить пользовательский элемент или воспользоваться скрытым полем. Элементы управления View и MultiView В ASP.NET 2.0 введены два новых элемента управления, предназначенных для созда- ния группы сменяющих одна другую панелей дочерних элементов. Элемент управле- ния MultiView определяет группу представлений, создаваемых экземплярами класса View. В каждый конкретный момент времени только одно из них активно и выводится для клиента. Элемент управления View предназначен для использования в составе элемента управления MultiView, а сам по себе он использовагься не может. Вот как определяется элемент управления MultiView'. <asp:MultiView runat="server" id="Tables"> <asp:View runat="server" id="Employees"> </asp:View> <asp:View runat="seiver" id="Products"> </asp:View> <asp:View runat-"servdr ' id="Customers">
Базовые серверные элементы управления ASP.NET Глава 4 149 </asp:View> </asp:MultiView> Вы выбираете активное представление, используя событие возврата формы, ко- торое происходит, когда пользователь щелкает кнопку или ссылку, выводимую в со- ставе текущего представления. Чтобы указать, какое представление будет следующим, можно либо установить свойство Active Viewindex, либо передать объект-представление методу Set Active View. Демонстрационная страница, включающая элемент управления MultiView, показана на рис. 4 6. Пользователь выбирает элемент в раскрывающемся списке, и представ- ление обновляется: void Page Load(object sender, EventArgs e) { // Views - это раскрывающийся список, И автоматически осуществляющий возврат формы Tables.ActiveViewIndex = Views.Selectedlndex; Рис. 4-6. Элемент управления MultiView в действии Сочетание элементов управления View и MultiView очень удобно использовать для реализации мастеров. Фактически, именно на его основе функционирует новый элемент управления Wizard из ASP.NET, который мы рассмотрим в главе 6. Валидационные элементы управления Важным правилом написания защищенных приложений является получение только корректных данных, для чего вся вводимая информация перед использованием должна проверяться. Иными словами, обработке любого ввода извне должен предшествовать шаг валидации. В ASP.NET для этой цели имеются специальные валидационные эле- менты управления, которые реализуют удобный механизм выполнения типичных за- дач по проверке данных, включая проверку на допустимость типов, на принадлежность значений заданному диапазону, а также на заполнение обязательных полей.
150 Часть! Разработка страниц ASP.NET Валидационные элементы управления наследуют класс BaseValidator, который, в свою очередь, является производным от Label. Все входящие в состав страницы валидаторы автоматически объединяются в коллекцию Validators класса Page. Вы можете активизировать их все вместе, используя метод Validate класса Page, либо по отдельности, посредством метода Validate каждого из них. Метод Validate устанав- ливает значение свойства IsValid страницы и отдельного валидатора. Это свойство указывает, соответствует ли пользовательский ввод требованиям валидатора. Но даже если метод Validate не вызван явно, весь пользовательский ввод автоматически проверяется при возврате формы. [~^~i Примечание В ASP.NET 2.0 члены элемента управления, связанные с проверкой поль- зовательского ввода, объединены в интерфейс /Validator, реализованный в классе BaseVa- lidator. В состав этого интерфейса входят метод Validate, свойства IsValid и ErrorMessage. .NET Framework предоставляет также полную клиентскую реализацию валидаци- онных элементов управления средствами динамического HTML. Поддерживающие DHTML браузеры, например Internet Explorer версии 4.0 и выше, могут выполнять валидацию на клиенте, как только пользователь заполнит определенные поля. Общая информация о валидационных элементах управления Каждый валидационный элемент управления содержит ссылку на тот или иной рас- положенный на странице элемент, предназначенный для ввода данных от пользовате- ля. Когда приходит время отправлять страницу на сервер, содержимое проверяемых элементов управления передается для обработки валидаторам. Каждый валидатор выполняет проверку определенного типа. Поддерживаемые .NET Framework ти- пы валидаторов перечислены в табл. 4-12. Табл. 4-12. Валидационные элементы управления в .NET Framework Валидатор Описание Compare Validator Сравнивает пользовательский ввод с фиксированным значе- нием. используя операторы сравнения (больше, меньше, равно и т. д.). Может выполнять сравнение со значением свойства другого элемента управления той же страницы Custom Validator Применяет для проверки допустимости ввода пользователь- скую валидационную логику, определенную программным способом. Используется, когда другие валидаторы не могут выполнить нужную проверку и поэтому для ее осуществления необходим пользовательский код Range Validator Проверяет, попадает ли введенное Пользователем значение в определенный диапазон. Нижняя и верхняя границы этого диапазона могут быть представлены числами, строками или датами RegularExpression Validator Проверяет, соответствует ли пользовательский ввод опреде- ленному шаблону, заданному как регулярное выражение RequiredFieldValidator Проверяет, задал ли пользователь обязательное значение С одним элементом управления, осуществляющим ввод данных от пользователя, может быть связано несколько валидационных элементов управления. Предположим, на странице имеется текстовое поле для ввода адреса электронной почты. Вы можете убедиться, во-первых, что пользователь не пропустил это поле (RequiredFieldValidator), а во-вторых, что введенное им значение соответствует стандартному формату адресов электронной почты (RegularExpression Validator).
Базовые серверные элементы управления ASP.NET Глава 4 151 В табл. 4-12 не упоминается элемент управления ValidationSummary. Сам по себе он не выполняет валидацию, а лишь выводит надпись с сообщением обо всех ошиб ках, обнаруженных валидаторами на данной странице. Мы подробнее обсудим этот элемент далее в настоящей главе. Класс BaseValidator В табл. 4-13 перечислены собственные свойства валидационных элементов управле- ния. Некоторые из этих свойств, например ForeColor, Enabled и Text, являются пере- определенными версиями одноименных свойств базового класса. Табл. 4-13. Основные свойства валидаторов Свойство Описание ControlTo Validate Возвращает и позволяет задать проверяемый элемент управления. По- следний определяется именем, то есть значением атрибута ID Display Если поддерживается и используется клиентская валидация, возвра- щает и позволяет задать способ выделения места для сообщения об ошибке: Static (статически) или Dynamic (динамически). При сервер- ной валидации значение этого свойства игнорируется. Значение Static может использоваться только с браузерами, которые поддерживают CSS-стиль display, по умолчанию используется значение Dynamic EnableClientScript Возвращает и позволяет задать булево значение, которое указывает, осуществляется ли клиентская валидация; по умолчанию имеет значе- ние true Enabled Возвращает и позволяет задать булево значение, указывающее, акти- вен ли валидационный элемент управления ErrorMessage Возвращает и позволяет задать текст сообщения об ошибке ForeColor Возвращает и позволяет задать цвет сообщения об ошибке IsValid Возвращает и позволяет задать булево значение, указывающее, прошел ли валидацию связанный элемент управления, предназначенный для ввода данных SetFocusOnError Указывает, должен ли элемент управления, который не прошел вали- дацию, получать фокус ввода. В ASP.NET 1х данное свойство не под- держивается Text Возвращает и позволяет задать описание, которое выводится при от- сутствии сообщения об ошибке. Заметьте, что этот текст не заменяет в сводке ошибок содержимое свойства ErrorMessage ValidationGroup Возвращает и позволяет задать имя валидационной группы, к которой принадлежит элемент управления. В ASP.NET 1х данное свойство не поддерживается Все валидационные элементы управления наследуют класс BaseValidator, исклю- чение составляют валидаторы, выполняющие типизированное сравнение, — они на- следуют производный класс BaseCompareValidator. Для определения типа данных, к ко- торому перед сравнением должно быть приведено значение, предназначено свойство Туре. Статический метод CanConvert указывает, может ли пользовательский ввод быть приведен к этому типу. К числу поддерживаемых типов относятся: строка, целое число, целое число двойной точности, дата и денежный тип. Классами, которые мы называем сравнивающими валидаторами, являются RangeValidator и CompareValidator. Связывание валидаторов с элементами управления, предназначенными для ввода данных Связь между валидатором и проверяемым элементом управления устанавливается через свойство валидатора ControlToValidate, которому присваивается идентификатор проверяемого элемента управления. Если не будет задано значение этого свойства,
152 Часть! Разработка страниц ASP.NET при рендеринге страницы будет выброшено исключение. Валидатор и элемент управ- ления, с которым он связан, должны принадлежать к одному контейнеру, будь то страница, пользовательский элемент управления или шаблон. Не все серверные элементы управления можно связывать с валидаторами, а только те из них, у которых есть метаатрибут [ValidationProperty], в котором задается про- веряемое свойство. Например, проверяемым свойством элемента управления TextBox является Text, о чем говорит следующее определение класса: [ValidationProperty("Text")] public class TextBox : WebControl, ITextControl { } К числу элементов управления, поддерживающих валидацию, относятся TextBox, DropDownList, ListBox, RadioButtonList, FileUpload, а также группа элементов управ- ления HTML, в частности HtmllnputFile, HtmllnputText, HtmllnputPassword, HtmIText- Area и HtmlSelect. Moryi поддерживать валидацию и специализированные элементы управления, если вы пометите определения их классов упомянутым атрибутом [Vali- dationProperty] [Д Примечание Если проверяемое свойство элемента управления останется пустым, все валидаторы подтвердят правильность его значения и только RequiredFieldValidator поступит противоположным образом — ведь он для того и предназначен, чтобы выявлять незаполненные поля. Типы валидационных элементов управления В этом разделе подробно рассматриваются валидационные элементы управления разных типов, используемые на страницах Web Forms ASP.NET. Элемент управления CompareValldator Элемент управления CompareValidator позволяет сравнить введенное пользователем значение с константой или значением другого элемента управления, относящегося к тому же контейнеру именования. Поведение элемента Compare Validator характеризу- ется следующими дополнительными свойствами: ControlToCompare Представляет идентификатор элемента управления, значение которого будет сравниваться со значением, введенным пользователем в текущий элемент управления. Свойства ControlToCompare и Value То Compare не следует ис- пользовать одновременно — они являются взаимоисключающими, и если задать их оба, преимущество получит ControlToCompare. Operator Определяет операцию сравнения. Список его допустимых значений содер- жит перечисление ValidationCompareOperator. По умолчанию используется оператор Equal, но могут также использоваться операторы LessThan, GreaterThan и их раз- новидности. Особый оператор DataTypeCheck полезен для проверки того, может ли введенное значение быть приведено к определенному типу. Когда он задан, свойства ControlToCompare и ValueToCompare игнорируются и проверяется только тип вве- денных данных. Проверка считается успешно пройденной, если введенные данные могут быть приведены к заданному типу. Поддерживаются типы, идентифицируемые ключевыми словами String, Integer, Double, Date и Currency (десятичный) ValueToCompare Указывает значение, с которым нужно сравнить пользовательский ввод. Если установлено свойство Туре, значение свойства Value То Compare должно соответствовать заданному в нем типу.
Базовые серверные элементы управления ASP.NET Глава 4 153 Ниже приведена типичная разметка элемента управления Compare Validator, пред- назначенного для проверки введенного в текстовое поле целочисленного значения, представляющего возраст человека: <asp:CompareValidator runat="server" id="ageValidator" ControlToValidate="ageTextBox" VaiueToCompa re="18" Ope rato r="G reate rThanEqual" Type="Integer" ErrorMessage="Must specify an age greater than 17." /> Элемент управления CustomValidator CustomValidator — это универсальный валидатор, логика которого полностью опреде- ляется пользователем. Обычно к нему прибегают, когда ни один другой валидатор не подходит или когда в дополнение к стандартным проверкам необходимо выполнить пользовательский проверочный код. Для создания пользовательского валидатора нужно указать в свойстве ClientVali- dationFunction клиентскую функцию. Если клиентская валидация отключена или не поддерживается, просто опустите эту установку. В качестве альтернативы клиентской валидации или в дополнение к ней можно задать управляемый код, который будет выполняться на сервере, — для этого нужно определить обработчик события Server- Validate. Такой код будет выполнен при возврате формы в ответ на щелчок кнопочного элемента управления. Следующий код показывает, как сконфигурировать пользо- вательский валидатор, сверяющий введенное пользователем значение с массивом допустимых данных: <asp:CustomValidator runat="server" id="membershipValidator" ControlToValidate="membership" ClientValidationFunction="CheckMembership" OnServerValidate="ServerValidation” ErrorMessage="Membership can be Normal, Silver, Gold, or Platinum." /> Если задается клиентская валидационная функция, она должна иметь стандарт- ную сигнатуру: function CheckMembership(source, arguments) { } Аргумент source указывает на тэг HTML, представляющий элемент-валидатор, — обычно это тэг <span>. В аргументе arguments задается ссылка на объект с двумя свойствами: свойство Value содержит значение проверяемого элемента управления, а булево свойство IsValid указывает на результат валидации. Элемент управления CustomValidator не связывается с каким-то однйм проверяемым элементом управления, и его свойство ControlToValidate задавать не обязательно. Напри- мер, если валидатор должен проверить содержимое нескольких полей ввода, значение свойства ControlToValidate не задается, а переменная arguments.ValUe возвращает пустую строку. В этом случае валидационная логика программируется таким образом, чтобы все необходимые значения извлекались динамически. В клиентском сценарии это можно сделать путем доступа к членам формы документа, как показано ниже: function CheckMembership(source, arguments) { // Извлекаем текущее значение // элемента с заданным идентификатором var membership = document.forms[0][”membership"].value;
154 Часть I Разработка страниц ASP.NET О Внимание! Ограничившись только клиентским валидационным кодом, вы тем самым оставите брешь в защите приложения, поскольку атакующий сможет обойти валидаци- онную логику и отправить на сервер неверные или даже опасные данные. Написав же серверный обработчик события, вы получите возможность проверить данные, прежде чем выполнять на их основе какие-либо действия в серверной системе. Для определения серверного валидатора нужно написать обработчик события ServerValidate'. void ServerValidation(object source, ServerValidateEventArgs e) { } В состав структуры ServerValidateEventArgs входят два свойства, IsValid и Value, которые имеют то же назначение, что и составляющие второго аргумента клиентской валидационной функции. Если элемент управления не связан ни с каким полем ввода, свойство Value является пустым и вы извлекаете необходимое значение, используя объектную модель ASP.NET. Вот как можно, например, проверить на сервере состо- яние флажка: void Servervalidation (object source, ServerValidateEventArgs e) { e.IsValid = (CheckBoxl.Checked == true); } Элемент управления Custom Validator является единственным средством проверки значений элементов управления, не помеченных атрибутом [ValidationProperty], на- пример календаря или флажка. Элемент управления RegularExpressionValidator Применение регулярных выражений — эффективный способ убедиться в том, что пользовательский ввод содержит определенную последовательность символов. Напри- мер, используя регулярные выражения, можно проверять формат почтовых индексов, идентификаторов налогоплательщиков, адресов электронной почты, телефонных номеров и т. д. Элемент управления RegularExpressionValidator позволяет проверить формат пользовательского ввода, задав необходимое регулярное выражение в свой- стве ValidationExpression. Ниже демонстрируется валидатор, проверяющий, ввел ли пользователь адрес электронной почты: <asp:RegularExpressionValidator runat="server" id="emailValidator" ControlToValidate="email" ValidationExpression="[a-zA-Z_0-9.-]+\@[a-zA-Z_0-9.-]+\ \w+" ErrorMessage=”Must be a valid email address." /> В приведенном выше регулярном выражении указано, что допустимые значе- ния адреса образуются двумя непустыми последовательностями знаков: букв, цифр, символов подчеркивания, дефисов и точек, разделенными символом @, за которыми следует точка и строка букв. (Нельзя сказать, что это совершенно точное определение адреса электронной почты, но оно годится для большинства адресов.) Примечание Синтаксис валидации регулярных выражений на клиенте и на сервере не- сколько различается. Элемент управления RegularExpressionValidator на клиенте использует регулярные выражения языка Microsoft JScript, а на сервере — объект Regex .NET Frame- work. Синтаксис регулярных выражений JScript является подмножеством синтаксиса модели Regex. Поэтому по возможности старайтесь придерживаться синтаксиса JScript, и тогда ваши выражения будут одинаково интерпретироваться и на клиенте, и на сервере.
Базовые серверные элементы управления ASP.NET Глава 4 155 Элемент управления RangeValidator Элемент управления RangeValidator позволяет убедиться, что введенное пользователем значение попадает в определенный диапазон. Тип значений, которыми система опе- рирует при проверке, определяется динамически, а перечень поддерживаемых типов ограничен строками, числами и датами. Следующий код показывает, как пользоваться элементом управления RangeValidator. <asp:RangeValidator runat=''server" id="hiredDateValidator" ControlToValidate=' hired" MinimumValue="2000-1-41 MaximumValue="9999-12-31" Type="Date" ErrorMessage="Must be a date after <b>Jan 1, 1999</b>." /> Ключевыми свойствами этого элемента управления являются Minimum Value и Maxi- mumValue — нижняя и верхняя границы интервала. Если строки, присвоенные данным свойствам, не могут быть приведены к типу, заданному в свойстве Туре, выбрасывается исключение. Если задан тип Date, но для приложения не указана определенная культура, даты следует задавать в культуро-независимом формате гггг-ММ-дд. Иначе значения могут быть интерпретированы неправильно. | Примечание Элемент управления RangeValidator расширяет возможности более простого элемента CompareValidator, проверяя значение на принадлежность заданному интерва- лу. Поэтому он может выбросить исключение, если вы не укажете значение свойства MinimumValue или Maximumvalue. Будет ли исключение выброшено в действительности, зависит от заданного типа данных, точнее от того, как для него интерпретируется пустая строка. Например, для типа Date пустая строка недопустима, и потому выбрасывается исключение. Если вы хотите задать открытый интервал, без верхней или нижней границы, воспользуйтесь оператором GreaterThan (или LessThan) элемента управления CompareVa- hdator либо просто укажите условно-бесконечное значение, примером которого для даты может быть 9999-12-31. Элемент управления RequiredFieldValidator Если некоторое поле формы является обязательным и важно, чтобы пользователь его не пропустил, воспользуйтесь элементом управления RequiredFieldValidator с со- ответствующим сообщением об ошибке: <asp:RequiredFieldValidator runat="server" id="lnameValidator" ControlToValidate="lname" ErrorMessage="Last name is mandatory" /> Если вы имеете дело с современным браузером и для каждого валидатора ис- пользуется клиентский сценарий (а таково поведение валидаторов по умолчанию), то в случае, когда ввод оказывается недопустимым, пользователь видит сообщение об ошибке и возврат формы не выполняется. О Внимание! Если пользователь просто переходит между элементами управления с по- мощью клавиши Tab, сообщение об ошибке не выводится — валидатор активизируется только в том случае, когда пользователь вводит пробелы или когда поле оказывается пустым при возврате формы. Как определить, является ли поле пустым? Во многих случаях достаточно, чтобы в нем содержалась пустая строка, но это не универсальное правило. Свойство Initial- Value определяет начальное значение элемента управления. Если после потери фокуса ввода значение элемента управления останется равным начальному, проверка будет
156 Часть! Разработка страниц ASP.NET считаться не пройденной. По умолчанию свойство InitialValue инициализируется пустой строкой. Дополнительные возможности Главная цель размещения валидационных элементов управления в Web-форме за- ключается в обнаружении в пользовательском вводе ошибок и несогласованных данных. Но как выводить сообщения об ошибках? Требуется ли вашему приложе- нию клиентская валидация, и если да, то как ее организовать? Наконец, что если вы захотите по щелчку определенной кнопки проверять только заданное подмножество элементов управления? Для всего этого предусмотрены специальные возможности валидационных элементов управления. Вывод информации об ошибках В свойстве ErrorMessage задается статическое сообщение, которое валидационный элемент управления будет выводить в случае ошибки. Важно знать, что если одно- временно с ним установлено свойство Text, оно получит преимущество перед ErrorMes- sage. Значение свойства Text выводится в том месте, где расположен валидационный элемент управления, а значение свойства ErrorMessage — в специально отведенном месте, предназначенном для отображения результатов валидации всех элементов управления. (Стратегии использования свойств Text и ErrorMessage описаны в сле- дующем разделе, посвященном элементу управления ValidationSummary.) Поскольку все валидационные элементы управления являются надписями, никакой другой под- держки или вспомогательных элементов управления для вывода их сообщений не требуется. Сообщение просто выводится в теле валидационого элемента управления в том месте, где он размещен. Оно отображается в HTML-формате, а значит, может содержать атрибуты форматирования. Валидаторы, работающие в клиентском режиме, могут динамически или статически создавать для сообщения тэг <span>. Управлять способом его создания вы можете посредством свойства Display валидатора. Если задан режим отображения Static (зна- чение по умолчанию), элемент управления <span> имеет такой стиль: style="color:Red;visibility:hidden:" Когда CSS-атрибут visibility установлен в Hidden, браузер не выводит элемент, а просто резервирует для него место. Если же задан режим отображения Dynamic, стиль элемента управления таков: style=”color:Red;display:none;" CSS-атрибут visibility установлен здесь в попе, то есть элемент не просто скрыт, а отсутствует и, следовательно, не занимает места на странице. Значение свойства Display становится особенно важным, если с одним элементом управления связано несколько валидаторов (рис. 4-7). Как видите, в данном примере во время проверки текстового поля Hire Date, которое представляет дату найма, в первую очередь активизируется валидатор, проверяющий, содержит ли поле дату, а затем — еще один, удостоверяющий, что эта дата больше 1-1-1999. Если свойство Display первого валидатора установлено в Static и дата выходит за границу допустимого диапазона, вывод получается таким, как на рис. 4-8. Полный исходный код этой страницы доступен в Web по адресу http://wwio.micro- soft.com/mspress/companion/0-7356-2176-4.
Базовые серверные эле! генты управления ASP.NET Глава 4 157 Рис. 4-7. Значения полей ввода проверяются на клиенте Рис. 4-8. Статическое сообщение об ошибке занимает место, даже когда не отображается Примечание С одним элементом управления можно связать несколько валидаторов. Они будут срабатывать по очереди, и каждый из них выведет свое сообщение об ошибке. Содержимое элемента управления считается допустимым, когда все валидаторы вернули true. Если значение поля может соответствовать любому из нескольких разных шаблонов, например, содержать номер мобильного или обычного городского телефона, проверяйте его с помощью пользовательского кода либо регулярных выражений.
158 Часть I Разработка страниц ASP.NET Элемент управления Validationsummary ValidationSummary — это надпись, в которой после возврата формы выводятся со- общения обо всех ошибках, обнаруженных при валидации. Эта надпись по вашему усмотрению может быть отформатирована как список, маркированный список или обычный текст — конкретный способ задается в свойстве DisplayMode. Допустимые значения данного свойства определяются перечислением ValidationSummaryDisplay - Mode. По умолчанию выводится маркированный список. Каким бы ни был формат сводного сообщения об ошибках, оно выводится в виде текста на странице, в диалоговом окне либо там и там. Выбор между этими вариантами осуществляется с помощью свойств ShowSummary и ShowMessageBox. Вывод элемента управления ValidationSummary не отображается до тех пор, пока не будет выполнен возврат формы, каким бы ни было значение свойства EnableClientScript. Свойство HeaderText определяет текст, который выводится в заголовке сводного сообщения. Рассмотрим пример: <asp:ValidationSummary runat="server" ShowMessageBox="t rue" ShowSumma ry="true" HeaderText="The following errors occurred:" DisplayMode="BulletList" /> Результат обработки этого кода представлен на рис. 4-9. После возврата формы надпись со сводным сообщением об ошибках на странице обновляется и выводится диалоговое окно, содержащее то же самое сообщение. Рис. 4-9. Страница и диалоговое окно с одним и тем же сводным сообщением
Базовые серверные элементы управления ASP.NET Глава 4 159 Сводное сообщение об ошибках валидации выводится только в том случае, когда имела место хотя бы одна ошибка. Заметьте, что по умолчанию надписи рядом с по- лями ввода обновляются вместе с текстом сводного сообщения. Итак, информацию об ошибках валидации вы можете выводить следующими спо- собами. Отдельные сообщения и сводный блок Таково поведение валидационных элемен- тов управления по умолчанию. Используйте элемент управления ValidationSummary и оставьте стандартные установки валидационных элементов управления. Целе- сообразно уменьшить при этом объем отдельных сообщений, воспользовавшись свойством Text. Когда заданы оба свойства, Text и ErrorMessage, значение первого выводится в отдельном сообщении, а значение второго — в сводном. Например, можно задать в свойстве Text графический символ или восклицательный знак, а подробное сообщение определить в свойстве ErrorMessage. Отдельные сообщения Не используйте элемент управления ValidationSummary и задайте свойство ErrorMessage каждого валидационного элемента управления. Эти сообщения будут выведены после возврата формы. Только сводный блок Используйте элемент управления ValidationSummary и задайте свойство ErrorMessage каждого валидационного элемента управления. Установите свойство Display валидаторов в None, чтобы отдельные сообщения не выводились. Пользовательская информация о сообщениях Не используйте элемент управ- ления ValidationSummary и установите свойство Display валидаторов в None. В от- дельном сценарии соберите сообщения, заданные в свойстве ErrorMessage каждого из валидаторов, и сформируйте собственное сообщение для пользователя. Включение клиентской валидации Как уже упоминалось, валидация обычно производится на сервере — после возврата формы или вызова метода Validate. Если браузер поддерживает динамический HTML, процесс валидации можно также организовать на клиенте, тем самым значительно сократив время отклика страницы. Если быть более точным, то ASP.NET автома- тически разрешает клиентскую валидацию, когда браузер имеет соответствующие возможности. И если в ASP.NET 1.x клиентская поддержка обеспечивалась только для Internet Explorer версии 4.0 и выше, то в ASP.NET 2.0 валидационные элементы управления прекрасно работают и с браузерами Mozilla Firefox, Netscape 6.x, Safari 1.2. На рис. 4-10 показано, как выглядит приведенная выше демонстрационная страница в браузере Mozilla Firefox. Если клиентская валидация включена, страница не сможет выполнить возврат формы до тех пор, пока все ее поля не будут содержать правильные данные. Для обеспечения надежности кода и предотвращения хакерских атак данные можно про- верять и на сервере. Однако учтите, что не каждая проверка выполнима на клиенте. Если, предположим, вам нужно убедиться, что введенное пользователем значение присутствует в базе данных, у вас нет иного выхода, как сделать это на сервере после возврата формы. Клиентскую валидацию можно включать и отключать поэлементно, используя булево свойство EnableCHentScript валидаторов. По умолчанию это свойство содер- жит значение true, то есть клиентская валидация включена, если только браузер ее поддерживает. Код класса Base Validator определяет возможности браузера, обращаясь к свойству Request.Brozeser. Если это современный браузер, клиентская валидация
160 Часть I Разработка страниц ASP.NET включается. Браузеры и клиентские устройства, считающиеся современными, должны поддерживать как минимум следующие стандарты и функции: >'• ECMAScript (включая JScript и JavaScript) версии 1.2; HTML 4.0; Microsoft Document Object Model; каскадные таблицы стилей. Что касается устаревших браузеров, то они поддерживают только HTML 3.2. Рис. 4-10. Клиентская валидация осуществляется и в браузере Mozilla Firefox Клиентской валидацией можно управлять и на уровне страницы, используя атри- бут ClientTarget директивы @Page. Следующий код отключает клиентскую валидацию, указывая, что вывод страницы должен быть ориентирован на устаревший браузер: < % @Page ClientTarget="DownLevel" %> Атрибут ClientTarget позволяет переопределить целевой браузер, для которого ASP.NET будет готовить страницу. Когда он установлен, ASP.NET не определяет возможности браузера сама, а просто загружает информацию о них из базы данных браузеров. Валидационные группы В ASP.NET 1.x валидация элемента управления происходит по принципу «все или ни- чего». Например, если в форме имеется набор полей и валидаторов, а также две кнопки, какую бы из них ни щелкнул пользователь, всегда будут проверяться все элементы управ- ления. Иными словами, нельзя проверить одни элементы управления с помощью одной кнопки, а другие — с помощью другой. Воспользовавшись свойством CausesValidation кнопки, можно вовсе отключить для нее валидацию, но это не решает проблемы.
Базовые серверные элементы управления ASP.NET Глава 4 161 Однако теперь решение есть: можно обратиться к свойству ValidationGroup, вве- денному в ASP.NET 2.0. Оно имеется у валидаторов, элементов управления триг-типа и кнопок. Пользоваться этим свойством очень просто: достаточно задать один и тот же идентификатор группы валидации для валидаторов всех элементов управления, которые вы хотите объединить в одну группу, и этот же идентификатор присвоить свойству ValidationGroup кнопки, с помощью которой будет выполняться активизация указанных валидаторов. Вот пример: <asp-textbox runat="server" id=”TextBox1" /> <asp RequiredFieldValidator runat="server" ValidationGroup="Group1" ControlToValidate="TextBox1” ErrorMessage="TextBox1 is mandatory" /> <asp textbox runat=”server" id="TextBox2" /> <asp'RequiredFieldValidator runat="server” ValidationGroup="Group2" Cont rolT oValidate="TextBox2" ErrorMessage="TextBox2 is mandatory" /> <asp:Button runat="server" Text="Check Groupl" ValidationGroup=’Group!" /> <asp:Button runat="server" Text="Check Group2" ValidationGroup="Group2" /> Здесь два элемента управления RequiredFieldValidator принадлежат к разным ва- лидационным группам — Groupl и Group2 Первая кнопка служит для проверки эле- ментов управления из группы Groupl, а вторая - для проверки элементов из группы Group2. Действуя таким же образом, валидационный процесс можно сделать сколь угодно гранулярным. ©Внимание! Свойство ValidationGroup можно задавать и для элементов управления, ис- пользуемых для ввода данных. Но это необходимо делать только в том случае, если используется элемент управления CustomValidator, которому требуется проверить при- надлежность элемента управления к заданной группе валидации. Возможность объединения элементов управления в группы валидации особенно полезна, когда она используется в сочетании с межстраничным возвратом формы — функцией ASP.NET 2.0, которую мы рассмотрим в следующей главе. Данная функция позволяет определить кнопку, с помощью которой содержимое формы отправляется другой странице, что является способом обхода действующей в ASP.NET модели страницы с одной формой. Представьте, что у вас есть поле для ввода искомого текста и вы хотите отправить его содержимое странице поиска, не проходя обычную про- цедуру возврата формы с дополнительной переадресацией. Создав валидационную группу, вы сможете проверить только содержимое поля ввода искомого текста и от- править его странице поиска. Кроме того, валидационные группы используются на сервере. Метод Validate класса Page имеет теперь перегруженную версию, позволяющую указать группу валидации. Заключение Серверные элементы управления являются важнейшей частью страницы ASP.NET. После их появления программная модель этой системы из простейшей фабрики HTML-строк превратилась в современную и эффективную компонентную модель. Сегодня ASP.NET предлагает нам огромное количество классов элементов управле- ния. Глядя на названия их пространств имен, можно заключить, что все они делятся на две категории: HTML и Web. Элементы управления из первой группы отражают синтаксис тэгов HTML — у каждого такого элемента имеется столько же свойств, сколько атрибутов имеет соответствующий тэг HTML. Эти свойства названы по
162 Часть! Разработка страниц ASP.NET именам атрибутов, и их действие, по мере возможности, отражает действие атрибутов. Главной целью разработчиков элементов управления HTML было облегчение перехода от ASP к ASP.NET — для переноса страницы на новую платформу создателю этой страницы достаточно добавить к декларативным объявлениям элементов управления атрибут runat= "server" и обновить страницу. Структура элементов управления Web более абстрактна и значительно меньше привязана к синтаксису HTML. В общем случае не существует взаимнооднозначного соответствия между элементами управления Web и тэгами HTML. Однако возмож- ности элементов управления Web и HTML пересекаются. Все серверные элементы управления ASP.NET выводят HTML-разметку, и у Web-элементов она лишь более сложная, чем у элементов HTML. В семействе ключевых элементов управления Web мы выделили интересные и весь- ма мощные группы элементов, к числу которых относятся и валидаторы. Их приме- нение позволяет поместить элементы управления, используемые для ввода данных, в некие декларативные рамки, чтобы пользовательский ввод фильтровался на клиенте и на сервере. Одного только этого недостаточно для обеспечения надежной защиты приложения, но это, безусловно, шаг в нужном направлении. Только факты В ASP.NET имеется два больших семейства элементов управления: HTML и Web. В состав первого входят элементы, один-к-одному соответствующие тэгам HTML, а в состав второго — элементы с более абстрактной программной моделью и богатой функциональностью, не привязанные к синтаксису HTML. Если элемент управления ASP.NET сделан невидимым, для него не генерируется разметка, но он все равно активизируется и обрабатывается. Адаптивным рендерингом называется процесс генерирования разметки, подходя- щей для конкретного целевого браузера. Элементы управления ASP.NET 2.0 позволяют декларативно присваивать свойствам значения, специфические для конкретных браузеров. Например, в Internet Explorer вы можете использовать один стиль элементов, а в Mozilla Firefox — другой. Подавляющее большинство элементов управления ASP.NET 2.0 может генериро- вать разметку, совместимую со стандартом XHTML. Однако в целях обеспечения обратной совместимости поддерживается и традиционный HTML-режим. < Новые элементы управления позволяют программно манипулировать тэгом <head> страницы. Все, что вы помещаете в файл страницы, обрабатывается как элементы управления, включая и литерально заданный текст, пробелы и символы возврата каретки. Не- прерывная последовательность символов интерпретируется как один экземпляр литерального элемента управления. Валидационные элементы управления позволяют проверять допустимость введен- ных пользователем данных, в частности принадлежность значения к определенному типу или к определенному диапазону, соответствие заданному в виде регулярного выражения шаблону, а также само наличие значения для обязательного поля. В ASP.NET 2.0 элементы управления страницы можно разделять на валидационные группы, которые будут проверяться по отдельности.
Глава 5 Работа со страницей Хотя страницы без форм вполне могут существовать и обрабатываются без ошибок, обычно страница ASP.NET содержит тэг <form>, помеченный атрибутом runat=server. При обработке этот тэг превращается в экземпляр класса HtmlForm. Данный класс действует как самый внешний контейнер всех серверных элементов управления, а при рендеринге страницы генерирует HTML-тэг <form>, внутрь которого заключаются все остальные элементы. Результирующая страница отправляется клиенту и оттуда осуществляет возврат формы той же самой странице ASP.NET — поэтому страница и содержащаяся на ней форма называются реентерабельными. По умолчанию для воз- врата данных серверу используется HTTP-метод POST, но возможно использование и метода GET. В большинстве случаев серверная форма является самым внешним тэгом страницы и содержится непосредственно в тэге <body>. Вообще говоря, серверный тэг <form> может быть дочерним тэгом любого серверного контейнерного элемента управле- ния, например <table>, <div> или <body>. (Об элементах управления HTML и Web подробно рассказывалось в главе 4.) Если вне тэга <form> обнаруживается элемент управления, не являющийся контейнерным (скажем, TextBox), выбрасывается ис- ключение. Заметьте, однако, что во время компиляции соответствующая проверка не производится. Неконтейнерные элементы управления Web сами проверяют, не выводятся ли они вне серверной формы и, если это так, выбрасывают исключение. Данная задача выполняется путем вызова метода VerifyRenderinglnSeruerForm класса Page. (Помните об этом, когда будете писать код специализированных элементов управления.) В данной главе мы проанализируем некоторые аспекты программирования на базе форм, включая размещение на одной странице нескольких форм и возврат данных другой странице. Кроме того, мы коснемся вопроса персонализации — новой и столь долгожданной функции ASP.NET 2.0, а в завершение обсудим средства и эффективные технологии отладки и трассировки приложений, а также обработки ошибок. Программирование форм Одно из наибольших затруднений, с которыми сталкиваются разработчики приложе- ний ASP при переходе на ASP.NET, обусловлено тем, что Web-приложения ASP.NET строятся по модели SFI (Single-Form Interface — интерфейс с одной формой). 22 Примечание Если вы никогда не встречали аббревиатуру SFI, не удивляйтесь — я ее придумал сам, по аналогии с двумя другими популярными аббревиатурами, которые, правда, используются в ином контексте, но описывают похожую программную модель. Речь идет об SDI (Single-Document Interface — однодокументный интерфейс) и ее антониме MDI (Multiple-Document Interface — многодокументный интерфейс). Согласно модели SFI страница всегда выполняет возврат формы самой себе, и у разработчика нет возможности задать целевую страницу, которая получит воз- вращенные данные. В HTML и ASP такая возможность была — этой цели служил
164 Часть I Разработка страниц ASP.NET атрибут Action формы, но в классе HtmlForm аналог данного атрибута отсутствует. Модель SFI глубоко интегрирована в платформу ASP.NET, и у вас имеются лищь две возможности: принять этот факт или пользоваться старой технологией ASP, не под- держивающей серверные формы. Однако в ASP.NET 2.0 возможность вернуть данные другой странице появилась снова — только ее реализация теперь совсем иная, она основана на новых возможностях кнопочных элементов управления. Что касается собственно форм, то они в ASP.NET 2.0 работают точно так же, как работали в ASP. NET 1.x, и по-прежнему не имеют аналога атрибута Action. В отличие от целевого URL, метод HTTP и целевой фрейм возврата формы можно задавать программно, используя специальные свойства класса HtmlForm, а именно Method и Target. Класс HtmlForm Класс HtmlForm является производным от HtmlContainerControl — класса, которому форма обязана своей способностью содержать дочерние элементы. Такой способностью обладают и другие элементы управления HTML (например, HtmlTable), HTML-пред- ставления которых характеризуются наличием дочерних и закрывающегося тэгов. Свойства класса HtmlForm Класс HtmlForm обеспечивает разработчику программный доступ к HTML-элементу <form>, осуществляемый посредством свойств, перечисленных в табл. 5-1. Заметьте, что в этой таблице упоминается несколько свойств, унаследованных классом HtmlForm от корневого класса Control. Табл. 5-1. Свойства формы Свойство Описание Attributes Унаследовано от Control, возвращает коллекцию имен и значений атрибутов тэга ClientID Унаследовано от Control, возвращает значение свойства UniquelD Controls Унаследовано от Control, возвращает объект-коллекцию дочерних элементов формы DefaultButton Строковое свойство, возвращает и позволяет задать кнопочный эле- мент управления, который будет используемым по умолчанию эле- ментом формы. В ASP.NET 1.x данное свойство не поддерживается DefaultFocus Строковое свойство, возвращает и позволяет задать кнопочный элемент управления, который получит фокус ввода при отображе- нии формы. В ASP.NET 1х данное свойство не поддерживается Disabled Возвращает и позволяет задать булево значение, указывающее, от- ключена ли форма. Соответствует HTML-атрибуту disabled EncType Возвращает и позволяет задать тип кодировки. Соответствует HTML-атрибуту enctype ID Унаследовано от Control, возвращает и позволяет задать програм- мный идентификатор формы InnerHtml Унаследовано от HtmlContainerControl. возвращает и позволяет задать контент разметки, содержащийся между открывающимся и закрывающимся тэгами формы InnerText Унаследовано от HtmlContainerControl, возвращает и позволяет задать текст, содержащийся между открывающимся и закрываю- щимся тэгами формы
Работа со страницей Глава 5 165 Табл. 5-1. (окончание) Свойство Описание Method Возвращает и позволяет задать значение, указывающее, как бра- узер возвращает данные формы серверу. По умолчанию это свой- ство имеет значение POST, при желании ему можно присвоить значение GET Name Возвращает значение свойства UniquelD Style Возвращает коллекцию CSS-свойств формы SubmitDisabledControls Указывает, следует ли дать отключенным на клиенте элементам управления указание предоставить свои значения для включения в состав возвращаемых данных, чтобы эти значения сохранились после возврата формы. По умолчанию имеет значение false. В ASP.NET 1jc данное свойство не поддерживается TagName Возвращает значение "form" Target Возвращает и позволяет задать имя фрейма или окна для ренде- ринга сгенерированного для страницы HTML-кода UniquelD Унаследовано от Control, возвращает уникальное, полностью уточ- ненное имя формы Visible Возвращает и позволяет задать значение, указывающее, должен ли осуществляться рендеринг формы. Если свойство имеет значение false, рендеринг формы с выводом результирующего HTML-кода не производится Форма всегда должна иметь уникальное имя. Если программист никак ее не назвал, ASP.NET сама генерирует имя по встроенному алгоритму. Идентификатор формы можно задать в свойстве ID либо в свойстве Name. Когда установлены оба эти свой- ства, предпочтение отдается первому. (Заметьте, однако, что использование атрибута Name противоречит правилам XHTML.) Родительским объектом формы является внешний контейнерный элемент с атри- бутом runat. Если такого элемента не существует, роль родительского выполняет объект страницы. Типичными контейнерами серверных форм являются тэги <table> и <div>, но при условии, что они помечены как серверные объекты. По умолчанию свойству Method присвоено значение POST, но его можно изменить программно. Если данные формы возвращаются с использованием метода GET, все они передаются в строке запроса. При этом следует учитывать, что размер строки запроса ограничен 2 Кбайт, — иначе вы рискуете, что данные будут попросту обрезаны, а это может привести к нежелательным для приложения последствиям. Методы класса HtmIForm В табл. 5-2 перечислены те методы класса HtmIForm, которые вы будете использовать наиболее часто. Все они унаследованы от базового класса System. Web. UI. Control. Табл. 5-2. Методы формы Метод Описание ApplyStyleSheetSkin Применяет стилевые свойства, определенные в таблице стилей страницы. В ASP.NET 1.x данный метод не поддерживается DataBind Вызывает для всех дочерних элементов управления метод DataBind FindControl Возвращает элемент управления с заданным идентификатором см. след. стр.
166 Часть! Разработка страниц ASP.NET Табл. 5-2. (окончание) Метод Описание Focus Присваивает элементу управления фокус ввода. В ASP.NET 1 х данный метод не поддерживается HasControls Указывает, содержит ли форма дочерние элементы управления RenderControl Выводит HTML-код формы. Если используется трассировка, кэ- ширует трассировочную информацию, которая будет выведена позднее, в конце страницы Отмечу, что метод FindControl ищет заданный вами элемент управления только среди дочерних элементов управления первого уровня. Если элемент управления от- носится к входящему в состав формы контейнеру именования или является дочерним элементом дочернего элемента управления формы, метод его не вернет. Страница с несколькими формами Как уже упоминалось, по умолчанию в ASP.NET используется модель SFI, играющая ключевую роль в работе описанного в главе 3 механизма автоматического управле- ния состоянием страницы и ее элементов. Нельзя сказать, что данная модель так уж ограничивает возможности программиста, а отсутствие поддержки нескольких форм является большим недостатком. Но все же существуют такие страницы, для которых более естественным было бы иметь несколько логических форм, то есть логически взаимосвязанных групп элементов управления, используемых для ввода данных. Примером может служить страница, которая предоставляет пользователю некоторую информацию и при этом содержит форму для ввода данных, например поле поиска или поля для ввода имени и пароля пользователя. Упомянутые блоки элементов управления, используемые для выполнения поиска и входа в систему, можно реализовать в виде специализированных классов, вызываемых из страницы, но удобно ли это — большой вопрос. Если вы портируете к ASP.NET унаследованный код, то, возможно, предпочтете изолировать код поиска или входа в систему, создав для него отдельную страницу. Но как вы передадите этой странице входные данные, введенные через форму? Использование HTML-форм Выше уже было сказано, что ASP.NET не позволяет включать в состав страницы несколько тэгов <form>, помеченных атрибутом runat. Однако ничто не мешает раз- местить в теле одной страницы Web Forms единственный серверный и несколько клиентских тэгов <form>. Например: <body> <tablextrxtd> <form id="form1" runat="server"> <h2>0rdinary contents for an ASP.NET page</h2> </form> </td> <td> <form method="post" action="search.aspx"> <table><tr> <td>Keyword</td> <td><input type="text" id="Keyword" name="Keyword" /x/td> </trxtr> <tdxinput type="submit" id="Go” value="Search" /x/td> </trx/table>
Работа со страницей Глава 5 167 </form> </td> </tr></table> </body> Эта страница содержит две формы, одна из которых является классической HTML- формой и не имеет атрибута runat, а потому совершенно игнорируется ASP.NET. Формируемая для браузера разметка просто содержит два элемента <form> с разными URL возврата. Приведенный код нормально работает, но имеет один важный недостаток: для извлечения данных, возвращенных клиентской формой с атрибутом action, не может использоваться программная модель ASP.NET. Создавая страницу search.aspx, нель- зя рассчитывать на то, что возвращенные значения будут доступны через состояние представления. Чтобы узнать, что вернул клиент, придется обратиться к устаревшей, но все еще действующей модели ASP: public partial class Search : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { // Для извлечения полученных данных используйте объект Request string textToSearch = Request.Form["Keyword"].ToStringO; // Пользовательский интерфейс страницы можно формировать // с помощью программной модели ASP.NET KeywordBeingUsed.Text = textToSearch; } } Для извлечения присланных клиентом данных обратитесь к одной из двух кол- лекций объекта Request'. Form, если использовался метод POST, или QueryString, если использовался метод GET. Элементы интерфейса, с помощью которых осуществлялся ввод, можно идентифицировать по атрибуту пате. Описанный подход не является рекомендуемым, но отлично работает. Как выглядит страница из нашего примера, показано на рис. 5-1. Рис. 5-1. Серверный и клиентский элементы управления <form> используются совместно Когда пользователь щелкает кнопку поиска, вызывается страница search.aspx. Она получает только данные, возвращенные HTML-формой, и использует их для выполнения своей задачи.
168 Часть! Разработка страниц ASP.NET Несколько тэгов <form> в одной странице Приведенный выше код работает благодаря тому, что в составе страницы имеется лишь одна серверная форма. Если объявить несколько таких форм, будет выброшено исключение. Немногим известен тот факт, что страница Web Forms может содержать любое количество серверных форм при условии, что только одна из них будет видима в каждый конкретный момент времени. Например, страница может содержать три тэга <form runat=server>, но осуществлять рендеринг только для одного из них. При динамическом рендеринге страницы в случае, когда более одного элемента управления HtmlForm пытаются осуществить свой рендеринг, выбрасывается исключение. Вклю- чая и отключая свойство Visible класса HtmlForm, на протяжении жизненного цикла страницы активной можно делать то одну, то другую форму. Этот трюк не решает проблемы нескольких активных форм, но кое в чем может вам помочь. Рассмотрим следующую страницу ASP.NET: <body> <form id="stepO" runat="server visible="true"> <h1>Welcome</h1> <asp:textbox runat="server" id= "Textboxl” /> <asp:button ID="Button1" runat="server“ text="Step #1” OnClick="Button1_Click" /> </form> <form id="step1" runat="server visible='Talse > <h1>Step #1</h1> <asp:textbox runat="server" id="Textbox2" /> <asp:button ID="Button2" runat="server" text='Previous step" OnClick="Button2_Click" /> <asp:button ID="Button3” runat="server" text="Step #2" OnClick="Button3_Click" /> </form> „г * <form id=”step2" runat=”server'' visible="false"> <h1>Finalizing</h1> <asp:button ID="Button4" runat="server" text=”Finish" 0nClick=”Button4_Click" /> </form> </body> Как видите, все тэги <form> помечены атрибутом runat, но только первый из них является видимым. Использование взаимоисключающих форм — прекрасное решение для создания мастеров ASP.NET 1.x. Переключая видимость форм в обработчиках событий кнопок, можно реализовать «мастероподобное» поведение — оно продемон- стрировано на рис. 5-2. public partial class MultipleForms : System.Web,UI.Page protected void Page_Load(object sender, EventArgs e) { Title = "Welcome': } protected void Button1_Click(object sender, EventArgs e) { Title = "Step 1”; stepO.Visible = false; stepl.Visible = true;
Работа со страницей Глава 5 169 } protected void Button2_Click(object sender, EventArgs-e) stepO.Visible = true; stepi.Visible = false; .} protected void Button3_Click(object sender, EventArgs e) { Title = ’Finalizing"; stepi.Visible » false; step2.Visible = true; } protected void Button4_Click(object sender, EventArgs e) { Title = "Done"; step2.Visible = false; Response.Write("<h1>Successfully done.</h1>"); } 3 Welcome Microsoft Internet Ixptorer I - l№l О» &* rgtr- Trtf- fchb G Иск - 0 J fl Д “ АЙН* ffl httpif/loceno^ Ъ2Ьd6f₽raAjp4et20/Smple*fCh1B1 - ЯЙ ’u fc ’ Welcome — Г' ' X j 1 2 Step 1 Microsoft Internet T^plurer Previous step Step #2 |Hello.ASP.NEf"" Step#! Local intranet — — * — ------------• “1 —*hmi Local n mac Рис. 5-2. Взаимоисключающие формы, используемые в ASP.NET 1 .х для реализации мастеров Множественные представления и мастера в ASP.NET 2.0 Если вы создаете приложение ASP.NET 2.0, то вам незачем прибегать к описанному выше трюку, ведь теперь есть два новых элемента управления, которые сделают за вас всю работу. Речь идет об элементах Multi View и Wizard. Элемент управления MultiView инкапсулирует логику, практически идентичную описанной выше логике использования взаимоисключающих форм, с тем отличием, что вместо полноценных форм он выводит панели. Иными словами, данный элемент
170 Часть I Разработка страниц ASP.NET позволяет определить несколько взаимоисключающих панелей и предоставляет API для переключения их видимости, гарантирующий, что только одна из этих панелей будет отображаться в каждый конкретный момент времени. У элемента управления MultiView нет собственного пользовательского интерфейса. Зато он есть у элемента управления Wizard, в котором объединены элемент MultiView и несколько стандартных элементов интерфейса мастеров. Мы более подробно поговорим о нем в следующей главе. Межстраничный постинг В ASPNET 2.0 имеется встроенный механизм, позволяющий изменить стандартный цикл обработки страницы и тем самым помешать ей вернуть данные самой себе. Возврат формы осуществляется одним из двух способов: с использованием submit- кнопки или посредством сценария. Клиентский браузер обычно берет на себя заботу о постинге, осуществляемом с помощью szz/jttzzV-кнопки, и автоматически возвращает данные формы той странице, которая указана в атрибуте action. Использование сце- нария — более гибкий метод. В ASP.NET 2.0 определенные элементы управления страницы, а именно те, которые реализуют интерфейс IButtonControl, можно сконфи- гурировать таким образом, чтобы они осуществляли возврат формы другой странице. Это и называется межстраничным постингом. Возврат данных другой странице Добиться того, чтобы Web-страница осуществляла возврат данных другой странице, совсем несложно. Выберите элементы управления, которые будут инициировать воз- врат формы, и задайте необходимый URL в их свойстве PostBackUrl Страница может содержать несколько кнопок, в том числе и более одной кнопки типа submit. Под кнопкой в данном случае подразумевается любой серверный элемент управления, реализующий интерфейс IButtonControl (он был подробно описан в главе 4). Приведенный ниже код показывает, как вам нужно действовать: <form runat="server"> <asp textbox runat="server” id=”Data' /> <asp:button runat='server" id="buttonPost" Text=”Click" PostBackUrl="target.aspx" /> </form> Когда свойство PostBackUrl установлено, исполняющая среда ASP.NET связывает представляющий кнопку HTML-элемент с функцией JavaScript. Но это не старая и хорошо вам известная doPostback, а новая функция WebForm DoPostBackWithOp- tions. Таким образом, при рендеринге кнопки получается следующая разметка: <input type="submit" name="buttonPost" id="buttonPost" value="Click onclick=”javascript:WebForm_DoPostBackWithOptions( new WebForm_PostBackOptions("buttonPost”, "", false, "target.aspx", false, false))" /> В результате, когда пользователь щелкает кнопку, текущая форма возвращает свое содержимое заданной целевой странице. А как же состояние представления? Если страница содержит элемент управления, осуществляющий межстраничный постинг, для нее создается новое скрытое поле с именем _PREVIOUSPAGE. Здесь хранится информация состояния представления, которая будет использоваться при обработке запроса. Эта информация совершенно прозрачно подменяет собой исходное состояние представления страницы, получившей данные.
Работа со страницей Глава 5 171 Для ссылки на страницу, выполнившую постинг, и доступа ко всем ее элементам управления используется свойство PreviousPage. Вот код демонстрационной целевой страницы, извлекающий содержимое текстового поля формы: protected void Page_Load(object sender, EventArgs e) // Извлекаем полученные данные TextBox txt = (TextBox) PreviousPage.FindControl("TextBox1"); } Используя свойство PreviousPage класса Page, можно обращаться к любым элемен- там управления страницы, выполнившей постинг. Доступ к элементам управления, служащим для ввода данных, осуществляется слабо типизированным и косвенным способом, через посредство метода FindControl. Дело в том, что целевой странице не- известен тип исходной. PreviousPage объявлено как свойство типа Page, а потому оно не может предоставить доступ к странице производного класса. Обратите внимание: метод FindControl находит только те элементы, которые со- держатся непосредственно в текущем контейнере именования. Если нужный элемент управления находится внутри другого элемента управления, скажем, шаблона, вам, прежде чем искать в нем требуемый элемент, нужно получить ссылку на этот контейнер. Чтобы не пользоваться методом FindControl, можно применить другой подход. Директива @ PreviousPageType Итак, посмотрим, что же это за подход. Для извлечения данных страницы, выполнив- шей постинг, вовсе не обязательно заранее знать, какая это страница. Однако когда межстраничный постинг осуществляется в рамках одного приложения, весьма веро- ятно, что вам совершенно точно известно, кто и когда будет вызывать страницу. В та- ком случае можно воспользоваться преимуществами директивы ©PreviousPageType и указать, какой конкретно тип должно иметь свойство PreviousPage класса страницы. Добавьте в файл целевой страницы такую строку: <%@ PreviousPageType VirtualPath="crosspostpage.aspx" %> Эта директива имеет два взаимоисключающих атрибута, VirtualPath и TypeName. В первом задается URL страницы, выполняющей постинг, а во втором — имя ее класса. Так, в приведенном примере указано, что свойство PreviousPage класса целевой страницы должно иметь тот же тип, что и страница, расположенная по заданному пути. Одного этого, однако, недостаточно для того, чтобы можно было непосредственно обратиться к элементам управления, служащим для ввода данных. В главах 2 и 3 упо- миналось, что в составе класса каждой страницы есть защищенные члены, представ- ляющие дочерние элементы управления; к сожалению, защищенные члены класса из внешнего по отношению к нему класса вызывать нельзя. (Обращаться к защищенным членам класса могут только производные от него классы.) Чтобы обойти это ограничение, придется включить в состав класса вызывающей страницы открытые члены, предоставляющие доступ к информации, которая необ- ходима целевой странице. Предположим, у вас имеется страница crosspostpage.aspx с элементом управления TextBox, который имеет имя _textBox1. Для того чтобы он был доступен целевой странице, нужно добавить в класс отделенного кода страницы crosspostpage.aspx вот такое свойство: public TextBox TextBoxI { get { return _textBox1; } }
172 Часть I Разработка страниц ASP.NET Оно служит оболочкой для внутреннего элемента управления TextBox и открывает к нему доступ. Теперь целевая страница может выполнять такой код: Response.Write(PreviousPage.TextBoxI.Text); i Выявление факта межстраничного постинга Тот факт, что страница является потенциальным объектом межстраничного перехода, не делает ее какой-то особенной. Не исключено, что она была вызвана сама по себе, а следовательно, должна обрабатываться самым обычным образом. В этом случае свой- ство PreviousPage возвращает null, а другие свойства, связанные с возвратом формы (например, IsPostBack), содержат обычные значения. Если у вас имеется такая страница, используемая двумя разными способами, не- обходимо вставить дополнительный код, меняющий ее поведение. Ниже приведен код страницы, которая может быть вызвана только из другой страницы if (PreviousPage == null) { . Response.Write("Sorry, that’s the wrong way to invoke me."): Response.End(); return: } Свойство IsCrossPagePostBack класса Page заслуживает особого внимания. Оно возвращает true, если текущая страница вызвала другую страницу ASP NET. Разуме- ется, для целевой страницы оно всегда возвращает false. Поэтому следующий код не эквивалентен приведенному выше: if (!IsCrossPagePostBack) { } Чтобы узнать, была ли текущая страница вызвана из другой страницы, нужно проверить значение свойства IsCrossPagePostBack объекта страницы, возвращенного свойством PreviousPage: // В случае обычного запроса PreviousPage возвращает null if (!PreviousPage.IsCrossPagePostBack) { } Однако этот код неизбежно выбросит исключение, если страница была вызвана обычным способом (по гиперссылке или с использованием адресной строки). В ко- нечном счете, самым простым и эффективным способом узнать, была ли страница вызвана путем межстраничного постинга, является проверка свойства PreviousPage на равенство значению null. Валидация при межстраничном постинге А что будет, если исходная страница содержит валидаторы? Представьте себе страни- цу с текстовым полем, значение которого должно быть отправлено другой странице. Если вы не хотите, чтобы отправка происходила при пустом поле, в состав страницы можно включить элемент управления RequiredFieldValidator и связать его с текстовым полем: <asp:TextBox ID="TextBox1” runat=”server"></asp:TextBox> <asp:RequiredFieldValidator ID=”Validator1" runat="server"
Работа со страницей Глава 5 173 ControlToValidate="TextBox1" Text="*" /> <asp:Button ID="Button1 runat="server” Text="AppLy request..." OnClick="Button1_Click" PostBackUrl="targetpage.aspx" /> / Как и ожидалось, если текстовое поле пусто, щелчок кнопки не вызовет возврата^ формы; вместо этого будет выведена красная звездочка, сигнализирующая об ошиб- ке, и, возможно, дополнительное сообщение. По умолчанию кнопочные элементы управления инициируют валидацию элементов управления, используемых для ввода данных, и только потом выполняют возврат формы. В большинстве случаев элемент управления RequiredFieldValidator использует клиентские возможности браузера. Это значит, что в случае, когда текстовое поле оказывается пустым, кнопка даже не пытается выполнить возврат формы. А вот как быть с,элементом управления CustomValidator, которому для проверки условия не- обходимо выполнить серверный код? Представляете себе ситуацию? Вы находитесь на странице post.aspx и пытаетесь перейти на страницу target.aspx; однако для того, чтобы этот переход был выполнен только при определенных условиях, вам сначала нужно зайти на страницу post.aspx и произвести там необходимую проверку. Включите в состав страницы следующий элемент управления, напишите серверный валидаци- онный обработчик и поместите в него точку останова: <asp:CustomValidator ID="CustomValidator1" runat="server" Text="*" ControlToValidate="TextBox1" OnServerValidate="ServerValidate" /> При отладке этой демонстрационной страницы вы обнаружите, что возврат формы другой странице выполняется в два этапа. Сначала происходит классический возврат формы для выполнения серверного кода исходной страницы (например, серверного валидационного кода или кода, связанного со щелчком кнопки). И лишь затем вы- полняется межстраничный вызов для перехода к целевой странице: void ServerValidate(object source, ServerValidateEventArgs args) { . args.IsValid = false; if (String.Equals(args.Value,. "Dino")) args.IsValid = true; } Данный код устанавливает свойство IsValid страницы в true, только если текстовое поле содержит что-то отличное от "Dino". Но одного только невыполнения этого усло- вия недостаточно для предотвращения перехода к целевой странице. Иными словами, вполне возможно, что целевой странице будут переданы неверные входные данные. Эта проблема, впрочем, имеет очень простое решение: if ('PreviousPage.IsValid) { Response.Write("Sorry, the original page contains invalid input."); Response.End(); return; } ,B целевой странице вы проверяете свойство IsValid объекта, возвращаемого свой- ством PreviousPage, и прекращаете обработку запроса, если результат проверки ока- зывается отрицательным. Перенаправление пользователя на другую страницу В дополнение к свойству PostBackUrl кнопочных элементов управления в ASP.NET предусмотрен еще один механизм передачи данных элементов управления от одной странице другой — метод Server.Transfer.
174 Часть I Разработка страниц ASP.NET При его использовании URL новой страницы не отражается в строке адреса брау- зера, поскольку переход осуществляется на сервере. Следующий код показывает, как с помощью данного метода перенаправить пользователя к другой странице: protected void Button1_Click(object sender, EventArgs e) { Server.Transfer("targetpage aspx"); } Заметьте, что если за вызовом метода Transfer следует еще какой-либо код, он никогда не выполняется. Хотя метод Transfer всего лишь выполняет переключение страницы, он очень эф- фективен. Ведь при его вызове не выполняется переход на клиент и обратно, как в случае использования метода Response.Redirect. Да и к тому же объект HttpApplication, который обслуживал исходный запрос, повторно используется для обработки ново- го запроса, благодаря чему снижаются затраты ресурсов инфраструктуры ASP.NET. В ASP.NET 1.x целевая страница может обратиться к вызывающей с помощью свойства Handler контекста HTTP: Page caller = (Page) Context.Handler; Поскольку это свойство возвращает действующий экземпляр класса страницы, це- левая страница может обращаться к его свойствам и методам. Однако непосредствен- ного доступа к элементам управления она не имеет из-за ограничений защиты. Хотя данная программная модель работает и в ASP.NET 2.0, необходимости в ее использовании больше нет, и свойство Handler вам теперь ни к чему. Взамен в вашем распоряжении имеется более удобная программная модель межстраничного постинга, которая прекрасно подходит и для случая серверного переключения страниц: восполь- зовавшись директивой @PreviousPageType и убедившись, что свойство PreviousPage не содержит null, вы можете непосредственно обращаться к полям ввода. Как страница может определить, вызвана она путем серверного переключения страниц или методом межстраничного постинга? В обоих случаях свойство Previ- ousPage возвращает непустой объект, но свойство IsCrossPagePostBack этого объекта содержит значение true при межстраничном постинге и значение false, если речь идет о серверном переключении страниц. (Эта и все остальные методики и технологии, связанные с возвратом формы, подробно иллюстрируются демонстрационным кодом, который прилагается к данной главе.) ©Внимание! Передать значения от одной страницы другой можно разными способами: методом межстраничного постинга, серверного переключения страниц, с помощью HTML- форм и в строке запроса. Какой из них наиболее эффективен? Технологии межстра- ничного постинга и серверного переключения страниц ASP.NET 2.0 хороши тем, что вы можете использовать знакомую модель программирования, но при этом велика вероят- ность передачи большого количества данных через поле _PREVIOUSPAGE. Насколько нужна вам эта информация, зависит от целевой страницы. Нередко целевая страница нуждается всего в нескольких параметрах, и в таком случае использование клиентской HTML-формы будет более эффективным решением с точки зрения передачи данных. Однако при работе с HTML-формой приходится действовать по модели ASP. Обработка ошибок страниц Для перехвата и обработки ошибок приложения ASP.NET, как и любые другие при- ложения .NET, могут использовать исключения. Выброс исключения — это особый способ реакции на нестандартное событие в жизни приложения или на нарушение
Работа со страницей Глава 5 175 определенных условий, которые должны были бы выполняться. Обычно все ошибки, которые могут возникнуть в ходе функционирования приложения, перехватывают с помощью блоков обработки исключений. Очевидно, что заключение фрагментов кода внутрь блоков try...catch — простой и удобный метод централизованного «от- лова* ошибок. Однако злоупотребление этим методом приводит к значительному снижению производительности. Исключения предназначены для обработки особых событий, которые невозможно предсказать заранее, и не стоит использовать их для контроля нормального потока выполнения программы. Если существует иной способ обнаружения тех или иных несоответствий, его и нужно использовать, рассматривая применение блока обработки исключений как крайнюю меру. Иными словами, исключения не являются официальным средством обработки ошибок в приложениях .NET. Они не отличаются легковесностью, и их не следует использовать слишком интенсивно. В то же время это надежное, современное и эф- фективное средство перехвата ошибок и восстановления приложения. Когда в приложении ASP.NET происходит исключение, общеязыковая среда ищет блок кода, который выполнит его обработку. Исключение перемещается вверх по стеку, пока не достигнет корневого модуля приложения. Если подходящий обработ- чик к этому моменту не обнаружен, исключение получает статус необработанного и общеязыковая среда выбрасывает исключение системного уровня. Пользователь видит стандартную страницу с сообщением об ошибке, которую разработчики иногда называют желтым экраном смерти (Yellow Screen of Death) — по аналогии с печально известным синим экраном смерти (Blue Screen of Death), многие годы оповещавшем пользователей о сбое в операционной системе Microsoft Windows. Необработанное исключение — это ошибка, и оно приводит к остановке прило- жения. Какие же меры против появления необработанных исключений может пред- принять разработчик приложения ASP.NET? Основы обработки ошибок В любом приложении ASP.NET могут происходить самые разнообразные ошибки. Это могут быть ошибки конфигурации, вызванные нарушением синтаксиса или структуры одного из файлов web.config приложения, или ошибки анализатора, генерируемые при нарушении синтаксиса страницы. Кроме того, возможны ошибки компилятора, когда что-то неправильно в классе отделенного кода. И наконец, существуют ошибки времени выполнения, которые проявляются в процессе выполнения страницы. Страницы с сообщениями об ошибках, выводимые по умолчанию Когда на странице ASP.NET происходит ошибка, пользователь получает страницу с со- общением, более или менее вразумительно информирующем его о произошедшем. Эту страницу (рис. 5-3) выводит ASP.NET, перехватившая необработанное исключение. Локальные и удаленные пользователи приложения получают разные страницы ошибок. По умолчанию локальные пользователи, то есть те, кто обращается к прило- жению с локального хоста, получают страницу, которую вы видите на рис. 5-3. На ней отображается содержимое стека вызова (цепочка методов, вызовы которых привели к исключению) и краткое описание ошибки. Если страница выполняется в режиме отладки, выводится дополнительная информация с фрагментом исходного кода. Из соображений безопасности удаленным пользователям столь подробная информация не предлагается, и они получают страницу, подобную показанной на рис. 5-4. Обработка исключений — мощный механизм отслеживания аномалий выполнения кода и восстановления либо корректного завершения работы приложения. Однако для его применения нужно точно знать, в каком месте кода может произойти то или
176 Часть! Разработка страниц ASP.NET иное исключение (либо цепь таковых). Если исключение произойдет вне обозначен- ного вами блока, оно останется необработанным и пользователь неминуемо увидит желтый «экран смерти». Рис. 5-3. Страница с сообщением о необработанном исключении Рис. 5-4. Хотя на этой странице нет подробной информации об ошибке, дружественной к пользователю ее тоже не назовешь
Работа со страницей Глава 5 177 ASP.NET предлагает разработчикам две глобальные точки перехвата и програм- мной обработки исключений, происходящих на уровне страницы и приложения. Как упоминалось в главе 3, в базовом классе Page определено событие Error, которое мож- но обрабатывать в коде страницы, перехватывая тем самым любые необработанные исключения, выбрасываемые в ходе выполнения страницы. Одноименное событие имеется и у класса HttpApplication, оно служит для перехвата необработанных ис- ключений на уровне приложения. Обработка исключений на уровне страницы Для того чтобы перехватывать необработанные исключения определенной страницы, нужно определить для нее обработчик события Error. protected void Page_Error(object sender, EventArgs e) { // Перехватываем ошибку Exception ex = Server.GetLastErrorO; // Выбираем в зависимости от ее типа страницу // с сообщением об ошибке и переключаемся на нее if (ex is NotlmplementedException) Server.TransferC'/errorpages/notimplementedexception.aspx”); else Se rve r.T ransfe r("/e r ro rpages/appe r ror.aspx"); // Удаляем ошибку ; Server. ClearErrorO; К- <л '; ’ Объект, представляющий исключение, можно получить с помощью метода Get- LastError объекта Server. В обработчике события Error вы можете передать управление определенной странице и таким образом вывести персонализированное сообщение, индивидуальное для конкретной ошибки. При этом URL в адресной строке браузера не изменится, поскольку переключение страниц будет выполнено на сервере. Благо- даря использованию метода Server.Transfer информация об исключении сохранится, и страница с сообщением об ошибке сама сможет вызвать метод GetLastError, чтобы вывести для пользователя максимально подробные сведения. После того как ис- ключение будет полностью обработано, необходимо удалить объект-ошибку, вызвав метод ClearError. ©Внимание! При выводе сообщений об ошибках следите за тем, чтобы в них не содер- жались важные сведения, которые могут использоваться злонамеренным пользователем для атаки на вашу систему. К числу подобных сведений относятся имена пользователей, пути в файловой системе, строки подключения и информация, связанная с паролями. Страницу с сообщением об ошибке можно сделать достаточно интеллектуальной. В таком случае она будет определять, является пользователь удаленным или локальным, имеется ли пользовательский заголовок, и при необходимости выводить более подробные данные, помогающие идентифицировать ошибку: if (Request.UserHostAddress == "127.0.0.1”) { } Можно также воспользоваться коллекцией Request.Headers, которая будет проверять на- личие пользовательских заголовков, добавляемых только определенным Web-сервером. ’ Пользовательский заголовок задается с помощью управляющей консоли IIS, на вкладке HTTP Headers диалогового окна свойств виртуальной папки приложения.
178 Часть I Разработка страниц ASP.NET Глобальная обработка ошибок Обработчик события Error страницы перехватывает лишь ошибки, происходящие на этой странице. Соответственно, каждая страница, на которой будет производиться обработка ошибок, должна либо вызывать некий общий для всех код, либо иметь собственный уникальный обработчик. Если вы решили, что для всех страниц прило- жения будет использоваться один и тот же обработчик ошибок, то лучше всего создать глобальный обработчик ошибок на уровне приложения. Он будет перехватывать все необработанные исключения и перенаправлять их определенной странице ошибок. Такой обработчик реализуется в точности так же, как обработчик ошибок страницы. Добавьте в приложение файл global.asax и заполните кодом предопределенную за- глушку ApplicationError. void Application_Error(Object sender, EventArgs e) { 1 В Microsoft Visual Studio .NET 2005 для создания файла global.asax нужно вос- пользоваться командой Add New Item и выбрать элемент Global Application Class. Примечание Отметим, что у каждого приложения ASP NET может быть только один файл global.asax. В Visual Studio .NET 2003 пустой файл с таким именем генерируется при создании проекта Web-приложения. В Visual Studio .NET 2005 в том случае, если проект Web-сайта уже содержит файл global.asax, при вызове команды Add New Item соответствующий элемент удаляется из списка предлагаемых для выбора. В данном обработчике ошибок можно, например, отправить по электронной почте сообщение администратору сайта или внести в журнал событий Windows запись с со- общением об ошибке страницы. Для этого в ASP.NET 2.0 определен набор классов из пространства имен System.Net.Mail. (Такой набор классов существовал и в ASP.NET 1.x, но там он относился к пространству имен System.Web.Mail, которое теперь более не используется.) MailMessage mail = new MailMessage(); mail.From = new MailAddress("automated@contoso.com"); mail.To.Add(new MailAdd ress("administ rator@contoso.com")); mail.Subject = "Site Error at " + DateTime Now; mail.Body = "Error Description: " + ex.Message; SmtpClient server = new SmtpClientO; server.Host = outgoingMailServerHost; server.Send(mail); Аналогичный код для ASP.NET 1.x будет несколько иным, в нем следует исполь- зовать класс SmtpMail и не нужно явно указывать хост. Принципы надежной обработки ошибок Хорошая стратегия надежной и эффективной обработки ошибок должна основываться на следующих трех основных правилах. 1. Предупреждайте проблемы, заключая весь код, выполнение которого может завер- шиться неудачей, в блоки try...catch.../inally. Этим вы, конечно, не воспрепятствуете возникновению ошибок, но, по крайней мере, правильно обработаете наиболее типичные из них. 2. Не оставляйте исключения необработанными. Даже если вы уверены, что никаких проблем не возникнет, все равно позаботьтесь о том, чтобы пользователи никогда не увидели стандартную страницу с сообщением об ошибке. Исключения можно обрабатывать как на уровне страницы, так и на уровне приложения.
Работа со страницей Глава 5 179 3. Убедитесь, что страницы с сообщениями об ошибках не содержат важной для жизнедеятельности системы информации. Если нужно, идентифицируйте поль- зователей как локальных или удаленных и выведите для них разные сообщения. Локальный пользователь — это тот, кто обращается к приложению с сервера, на котором оно функционирует. Таким образом, вся обработка ошибок сводится к написанию нужного кода в нуж- ном месте. Однако в дополнение к этому ASP.NET предоставляет разработчикам встроенный механизм автоматического перенаправления пользователей к страницам ошибок. Указанный механизм полностью декларативен и управляется через файлы web.config. ©Внимание! По мере изучения ASP.NET мы будем постоянно обращаться к информации, хранящейся в конфигурационных файлах. О них упоминалось в главе 1, но специально посвященной им главы вы в этой книге не найдете. Зато такая глава есть в моей книге «Programming Microsoft ASP.NET 2.0 Applications: Advanced Topics» (Microsoft Press, 2005). Определение соответствия между ошибками и страницами с сообщениями об ошибках Когда необработанное исключение достигает основания стека, ASP.NET выводит используемую по умолчанию страницу ошибок — тот самый желтый экран смер- ти. Данный аспект работы ASP.NET является в значительной мере настраиваемым, а соответствующие установки задаются в разделе <customErrors> файла web.config приложения. Раздел <customErrors> Вывод пользовательских сообщений об ошибках включается декларативно, с помощью установки, задаваемой в разделе <customErrors>: <configuration> <system.web> <customErrors mode="RernoteOnly’’ /> </system.web> </configuration> Атрибут mode определяет, будет ли вывод пользовательских сообщений об ошибках включен, отключен или включен только для удаленных клиентов. Данный атрибут является обязательным. По умолчанию он имеет значение Remote Only, при котором удаленные пользователи видят стандартную страницу с минимально информативным сообщением об ошибке (см. рис. 5-4), не говорящим ни о чем, кроме того, что на сер- вере что-то не так. Локальные пользователи при этом получают сообщения ASP.NET с детальными описаниями ошибок (см. рис. 5-3). Политику обработки ошибок можно изменять по своему усмотрению. В частности, вы можете дать ASP.NET указание выводить для всех локальных и удаленных поль- зователей страницы с подробными сведениями об ошибках. Для активизации такого режима нужно присвоить атрибуту mode значение Off. По очевидным соображениям, связанным с безопасностью, это значение не следует использовать в среде эксплуа- тации приложения, иначе потенциальные злоумышленники получат информацию, имеющую критическое значение для защиты системы. Пользовательские страницы с сообщениями об ошибках Какое бы значение вы ни выбрали для атрибута mode, стандартные страницы ASP.NET с сообщениями об ошибках не порадуют информативностью. Для того чтобы выводить
180 Часть! Разработка страниц ASP.NET более профессиональные и дружественные пользователю сообщения с вежливым из- винением за неудобство, которые были бы согласованы с общим интерфейсом сайта (рис. 5-5), необходимо включить в файл web.config такие установки: <configuration> <system.web> <customErrors mode="0n" defaultRedirect="/GenericError.aspx" /> </system.web> </configuration> Рис. 5-5. Дружественная пользователю страница с сообщением об ошибке После этого, какой бы ни была ошибка, ASP.NET станет переадресовывать пользо- вателя на страницу GenericError.aspx, содержимое и структура которой всецело опре- деляются вами Это происходит благодаря необязательному атрибуту defaultRedirect, в котором задается страница с сообщением об ошибке. Если атрибут mode установлен в On, и локальные, и удаленные пользователи перенаправляются на стандартную страницу с сообщением об ошибке. Если же этот атрибут установлен в RemoteOnly, удаленные пользователи перенаправляются на указанную вами страницу, а локальные (которыми обычно являются разработчики) — на выводимую по умолчанию. Как правило, пользовательская страница с сообщением об ошибке состоит из чи- стого HTML, так что больше никакая ошибка произойти не может. Однако если при обработке страницы с сообщением об ошибке все же произойдет еще одна ошибка, ASP.NET выведет свою стандартную страницу. gД Примечание Когда используется стандартная страница с сообщением об ошибке, брау- зер получает HTTP-код состояния 302, и ему предлагается снова запросить страницу, на которой произошла ошибка. При этом любая информация об исходной ошибке теряется и метод GetLastError, будучи вызванным из пользовательской страницы с сообщением об ошибке, возвращает null. Обработка типичных ошибок HTTP Универсальная страница с сообщением об ошибке едва ли может быть контекстно- зависимой, особенно если принять во внимание, что у автора страницы нет доступа к исходному исключению. Мы вернемся к этому вопросу немного погодя. Однако перенаправление пользователей к единой для всех ошибок странице — не единственная возможность, которую предоставляет ASPNET; эта система позволяет задать отдельную страницу для каждой из ошибок HTTP. Соответствие между стра- ницами с сообщениями об ошибках и кодами состояния HTTP определяется в файле
Работа со страницей Глава 5 181 web.config. Для раздела <customErrors> поддерживается внутренний тэг <еггог>, кото- рый можно использовать для связывания кодов состояния HTTP с пользовательскими страницами ошибок. <configuration> <system.web> <customErrors mode="RemoteOnly" defaultRedirect="/GenericError.aspx"> <error statusCode="404" redirect=”/ErrorPages/Error404.aspx" /> <error statusCode="500" redirect=”/ErrorPages/Error500.aspx" /> </customErrors> </system.web> </configuration> В атрибуте statusCode этого тэга задается код ошибки HTTP, а в атрибуте redirect — страница, куда в случае возникновения такой ошибки должен быть перенаправлен пользователь. Для примера на рис. 5-6 показано, что увидит пользователь, если он неверно введет URL, из-за чего будет сгенерирована ошибка HTTP 404 («Resource not found»). Рис. 5-6. Пользовательская страница с сообщением о распространенной ошибке HTTP 404 Страница с сообщением об ошибке, вызванная инфраструктурой ASP.NET, получает от нее URL, ставший причиной ошибки. Ниже приведено определение класса отделен- ного кода для демонстрационной страницы с сообщением об ошибке HTTP 404: public partial class Error404 : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { string errPath = "<i>No error path information is available.</i>"; object о = Request.QueryString["AspxErrorPath"]; if (o != null) errPath = (string) o; // Обновляем пользовательский интерфейс ' > ErrorPath.InnerHtml = errPath; } }
182 Часть I Разработка страниц ASP.NET Получение информации об исключении Как уже упоминалось, сконфигурировав ASP.NET таким образом, чтобы она пере- направляла пользователя на определенные страницы с сообщениями об ошибках, вы теряете информацию о том внутреннем исключении, которое привело к ошибке. Очевидно, что ошибки HTTP 404 и HTTP 302 никак не связаны с внутренними ис- ключениями приложения. Зато необработанные исключения являются типичной причиной ошибки HTTP 500. Можно ли сделать так, чтобы по крайней мере для локальных пользователей страница с сообщением об ошибке выводила контекстно- зависимую информацию? Доступ к объекту-исключению имеют обработчики события Error уровня страницы и приложения. Поэтому указанную выше задачу можно решить как минимум одним способом: написать обработчик исключений уровня страницы, перехватить исключе- ние и сохранить его (или те его свойства, которые вас интересуют) в состоянии сеанса. Тогда в коде страницы, на которую будет перенаправлен пользователь, вы сможете извлечь из состояния сеанса необходимую контекстную информацию. protected void Page_Error(object sender, EventArgs e) // Перехватываем исключение и сохраняем связанную с ним информацию Exception ex = Server.GetLastЕrror(); // Идентифицируем пользователя как локального или удаленного if (Request.UserHostAddress == "127.0.0.1") Session["LastErrorMessage'] = ex Message; else SessionfLastErrorMessage"] = "Internal error."; // Удаляем ошибку Server. ClearErrorO; } Этот код проверяет адрес хоста и, если оказывается, что страницу запросил локаль- ный пользователь, сохраняет связанную с исключением информацию (для простоты мы сохраняем только сообщение об ошибке). Следующий код необходимо включить в тело метода Page_Load той страницы, которая обрабатывает ошибку HTTP 500: string msg = "No additional information available.”; object extraInfo = Session["LastErrorMessage"]; if (extraInfo != null) msg = (string) extraInfo; ExtraInfo.InnerHtml = msg; Session["LastErrorMessage"] = null; Результат всей этой работы показан на рис. 5-7. Итак, для поддержки контекстно-зависимых сообщений об ошибках необходим обработчик события Error уровня страницы, кэширующий исходное исключение. А это означает, что такой обработчик вам придется написать для каждой страницы, для которой вы хотели бы выводить контекстно-зависимые сообщения об ошибках. Чтобы упросить себе задачу, можно либо ограничиться глобальным обработчиком ошибок, либо написать новый производный от Page класс с обработчиком события Error и сделать производными от этого класса все страницы, которым потребуется реализованная в нем функциональность.
Работа со страницей Глава 5 183 Рис. 5-7. Контекстно-зависимая страница с сообщением об ошибке HTTP 500 Примечание Какой из обработчиков ошибок получит приоритет, если их окажется три: уровня приложения, уровня страницы и страница с сообщением об ошибке, на которую бу- дет перенаправлять пользователя ASP.NET? Предпочтение отдается первому Код уровня страницы будет выполняться позже, а после него — код, обрабатывающий внутреннюю ошибку HTTP 500. Заметьте, что ошибки, отличные от HTTP 500, не перехватываются обработчиками события Error уровня страницы и приложения; они обрабатываются Web- сервером и не проходят через описанный здесь механизм обработки ошибок ASP.NET. Возможности отладки Отладка страницы ASP.NET возможна только при условии, что эта страница была откомпилирована в режиме отладки. В таком случае ее сборка содержит дополнительную информацию, необходимую отладчику для пошагового выпол- нения кода. Режим отладки можно активизировать для отдельных страниц и для всего приложения. Соответствующие установки задаются в разделе <compilation> файла web.config. В частности, вы устанавливаете атрибут Debug в true, когда хо- тите включить режим отладки для всех страниц приложения; по умолчанию он имеет значение false. Если нужно включить режим отладки для одной страницы, добавьте атрибут Debug в ее директиву ©Page'. <% ©Page Debug=”true" %> Как известно, прежде чем выполнить какой-нибудь .aspx-pecypc, ASP.NET ком- пилирует его содержимое. Сначала оно подвергается синтаксическому разбору, по результатам которого создается файл класса C# (или Visual Basic.NET), а затем этот файл передается компилятору этого языка программирования. Если страница помечена атрибутом Debug, ASP.NET не удаляет временный файл класса, использо- вавшийся для генерирования сборки страницы. Он сохраняется в расположенной на Web-сервере папке %SYSTEMROOT%\Microsoft.NET\Framework\[eepcufl]\ Temporary ASP.NET Files, так что вы можете открыть его и исследовать содержимое. Режим отладки используется для тестирования приложений и диагностиро- вания связанных с ними проблем. Однако учтите, что при запуске приложений в этом режиме сильно страдает их производительность. Поэтому, принимая решение о развертывании приложения на эксплуатационном сервере, следует убедиться, что данный режим отключен.
184 Часть t Разработка страниц ASP.NET Трассировка приложений ASP.NET Важную роль в обеспечении стабильной работы приложений и корректного ее вос- становления после возникновения исключительных ситуаций играют средства обра- ботки ошибок и отладки. Для этой же цели используются и инструменты трассировки. Трассиро&кой приложения называется процесс вывода сообщений, предназначенных для отслеживания хода его выполнения. Данная функция позволяет выявлять несо- гласованные и неверные данные, следить за потоком выполнения, проверять утверж- дения и даже собирать профилировочную информацию. В .NET Framework предусмотрены полнофункциональные встроенные средства трассировки приложений. В частности, в пространстве имен Sy stems.Diagnostics определены два класса с именами Trace и Debug, методы которых можно использо- вать для трассировки кода. Эти классы почти идентичны и работают поверх более специализированных модулей, называемых слушателями. Слушатель действует по- добно драйверу, собирая трассировочные сообщения и записывая их в определенное хранилище, например в журнал событий Windows или текстовый файл, либо просто выводя их в окно приложения. Каждое приложение может иметь собственный набор слушателей, получающих генерируемые сообщения. В ASP.NET используются другие средства трассировки, действующие, по крайней мере в ASP.NET 1.x, независимо от средств .NET Framework В состав ASP.NET входит специализированная подсистема, предоставляющая при- ложениям диагностическую информацию по их запросу. Трассировочная подсистема является частью данной инфраструктуры, и все, что приложению или странице нужно сделать для ее использования, это ее активизировать. Трассировка потока выполнения Подсистема трассировки позволяет разработчикам вставлять в код приложений отладочные операторы, а затем включать и отключать их выполнение с помощью специального атрибута. Заметьте, что их включение возможно даже после того, как приложение будет развернуто на эксплуатационном сервере. Когда трассировка приложения включена, ASP.NET добавляет диагностическую информацию в вывод страницы или отправляет ее в другое приложение, специально предназначенное для просмотра такого рода сведений. Обычно трассировочная информация имеет фик- сированную структуру, но автор страницы или элемента управления может вносить в нее некоторые изменения. Включение трассировки страницы Хотя в файле web.config имеется раздел <trace>, позволяющий конфигурировать трассировку на уровне приложения, чаще это делают на уровне страницы. Для боль- ших проектов включение и отключение трассировки можно выполнять с помощью следующего кода, помещаемого в файл web.config приложения: <configuration> <system.web> <trace enabled="true" pageOutput="true" /> </system.web> </configuration> Атрибут enabled включает трассировку приложения, & page Output разрешает вывод трассировочной информации на страницах. Если атрибут pageOutput имеет значение false (значение по умолчанию), трассировочная информация автоматически перена- правляется специальному средству ASP.NET — просмотрщику trace.axd. Завершив
Работа со страницей Глава 6 185 работу над проектом, не забудьте удалить из файла web.config элемент <trace> либо установить оба его атрибута в false, иначе вы рискуете случайно оставить трассиро- вочные сообщения в какой-нибудь странице приложения. Атрибут trace директивы ©Page по умолчанию имеет значение false', если установить его в true, внизу страницы будет выводиться трассировочная информация (рис. 5-8). Рис. 5-8. Трассировка выполнения приложения в ASP.NET Трассировочная информация является частью содержимого страницы и потому выводится в любом браузере, который эту страницу запросит. Блок трассировочных данных состоит из нескольких стандартных таблиц и сведений, выведенных стра- ницей. В таблицах представлены подробные сведения о запросе, дерево элементов управления, а также содержимое некоторых важных коллекций, таких как коллекции cookie, заголовков и данных формы, а также значения серверных переменных. Если состояние сеанса и состояние приложения не пусты, выводится также содержимое внутренних свойств Session и Application. Директива @Page поддерживает атрибут TraceMode, с помощью которого вы мо- жете задать порядок вывода информации. Допустимыми значениями этого атрибута являются SortByCategory и SortByTime. По умолчанию трассировочные сообщения выводятся в той последовательности, в которой они генерируются. Если присвоить атрибуту TraceMode значение SortByCategory, строки в разделе Trace Information будут отсортированы по имени категории. Категория, к которой относится трассировочное сообщение, определяется используемым для его вывода методом. Включение трассировки программным способом В состав класса Page входят два свойства, с помощью которых можно программно управлять трассировкой страницы, — они называются TraceModeValue и TraceEnabled
186 Часть! Разработка страниц ASP.NET и, как нетрудно заключить из их имен, являются программными аналогами атрибутов директивы ©Page. Булево свойство TraceEnabled, доступное для чтения и записи, слу- жит для включения и отключения трассировки страницы, а свойство TraceModeValue возвращает и позволяет задать значение из перечисления TraceMode, соответствующее желаемому способу сортировки трассировочной информации: по категориям или по времени. По правде сказать, на этапе эксплуатации функция программного управления трассировкой необходима немногим приложениям, но вот во время их отладки она может очень пригодиться. Генерирование трассировочных сообщений Еще одним свойством класса Page, связанным с трассировкой, является свойство Trace, возвращающее экземпляр класса TraceContext. Данный экземпляр создается при под- готовке страницы к выполнению и делается доступным через свойство Trace класса HttpContext и одноименное свойство класса Page. Класс TraceContext У класса TraceContext простой интерфейс, состоящий из пары свойств IsEnabled и TraceMode и пары методов Write и Wain. Доступное только для чтения булево свойство IsEnabled указывает, включена ли трассировка. Возвращаемое им значение определяется атрибутом trace директивы ©Page, а также атрибутом enabled раздела <trace> файла web.config. Свойство TraceMode возвращает и позволяет задать порядок сортировки выводимых на странице трассировочных сообщений. Оно имеет пере- числимый тип TraceMode, состоящий из значений SortByCategory и SortByTime. Выдача трассировочных сообщений Для того чтобы вывести трассировочное сообщение, можно воспользоваться одним из двух упомянутых выше методов класса TraceContext — Write или Warn. Оба они имеют по три перегруженные версии. Функционально эти два метода практически идентичны и отличаются лишь тем, что Warn выводит сообщения красным цветом. Вот сигнатура метода Write: public void Write(string); public void Write(string, string); public void Write(string, string, Exception); Простейшая из его перегруженных версий выводит в столбце Message заданный текст (см. рис. 5-8). Во второй перегруженной версии первый строковый аргумент содержит имя категории, которую вы хотите присвоить сообщению, а второй — само сообщение. Имена категорий могут использоваться для сортировки трассировочной информации; годится любое имя, имеющее смысл для приложения. Третья перегру- женная версия метода получает дополнительный объект типа Exception, необходимый в том случае, если трассировочное сообщение должно уведомлять об ошибке. J Примечание Хотя передаваемый методам Write и Warn текст предназначен для вывода на HTML-страницах, никакие тэги форматирования, которые вы могли бы в него включить, обработаны не будут. Сообщения всегда выводятся как чистый текст, поэтому если вы, к примеру, захотите выделить какое-то слово полужирным шрифтом, единственным ре- зультатом будет вывод с обеих сторон от него последовательностей символов <Ь> и </Ь>. Трассировка из внешних классов Без полного уточнения имени объект ASP.NET Trace доступен в исходном файле страницы и в файле ее отделенного кода. Входящие в состав страницы специализиро-
Работа со страницей Глава 5 187 ванные элементы управления и их классы также могут непосредственно обращаться к трассировочной подсистеме, однако никакие другие классы такой возможности не имеют. Предположим, ваш класс отделенного кода делегирует выполнение определенной задачи внешнему классу. Как этот вспомогательный класс может принять участие в трассировке выполнения страницы? В контексте данного класса объект Trace не- доступен, если только вы не уточните ссылку на него, как показано ниже. Если из внешнего класса нужно внести запись в журнал трассировки текущего НТТР-запроса, это можно сделать следующим образом: System.Web.HttpContext.Си г rent.Т race.Write(catego ry, msg); ASP.NET 2.0 позволяет сконфигурировать подсистему трассировки так, чтобы она перенаправляла сообщения трассировочной инфраструктуре .NET Framework (слу- шателям, которые зарегистрированы для отображения диагностических сообщений). Для этого нужно добавить в файл web.config такой код: <system.web> ctrace enabled=”true" writeToDiagnosticsTrace=”true" /> </system.web> Введенный в ASP.NET 2.0 атрибут writeToDiagnosticsTrace по умолчанию имеет значение false. Слушатели, которые могут получать любые трассировочные сообщения ASP.NET, перечисляются в подразделе <trace> раздела <system.diagnostics>. Средство просмотра трассировочных сообщений ASP.NET поддерживает трассировку уровня приложения, осуществляемую с помощью специального средства — просмотрщика трассировочных сообщений. После включения трассировки всего приложения этому средству перенаправляются трассировочные данные, генерируемые в ходе обработки любой страницы. Чтобы их просмотреть, нужно запросить ресурс trace.axd, указав в качестве его местоположения корневой каталог приложения. Интерфейс средства просмотра трассировочных сообщений представлен на рис. 5-9. Рис. 5-9. Средство просмотра трассировочных сообщений готово к работе
188 Часть I Разработка страниц ASP.NET Чтобы сделать это средство доступным, необходимо добавить в файл web.config приложения раздел <trace>: <configuration> <system.web> <trace enabled="true" /> </system.web> </configuration> Данный раздел поддерживает несколько атрибутов, в том числе атрибут pageOut- put, указывающий, должна ли трассировочная информация выводиться не только в просмотрщике, но и на страницах приложения. По умолчанию данный атрибут имеет значение false, то есть трассировочная информация на страницах отсутствует. Эту установку можно переопределять для каждой страницы в отдельности, используя атрибут Trace директивы ©Page. Есть еще атрибут requestLimit, в котором можно задать максимальное число запросов HTTP, трассировочная информация которых будет кэшироваться просмотрщиком. По умолчанию кэшируются сообщения десяти запросов. Итак, просмотрщик трассировочных сообщений действует как централизован- ная консоль, где собирается вся трассировочная информация, сгенерированная в ходе обработки страниц заданного приложения. В нем представлены НТТР-запро- сы (рис. 5-10), сведения о которых остаются доступными для просмотра до тех пор, пока кэш просмотрщика не будет очищен. Просмотрщик трассировочных сообщений автоматически отслеживает все запросы к заданному приложению и для каждого из них кэширует полную трассировочную информацию. Когда достигается лцмит за- просов, никакие новые запросы более не кэшируются, пока журнал не будет очищен вручную. Рис. 5-10. Средство просмотра трассировочных сообщений в действии Персонализация страниц Широкие возможности персонализации требуются далеко не каждому приложению ASPNET. Включив в состав приложения персонализационный слой, вы можете сделать
Работа со страницей Глава 5 189 его страницы более дружественными пользователю, удобными и привлекательными. Для одних приложений персонализация — всего лишь дополнительное удобство, без которого вполне можно было бы обойтись, тогда как для других, таких как порталы и электронные торговые центры, это ключевая функция, без которой они не могут функционировать должным образом. ASP.NET 2.0 поддерживает две взаимодополня- ющие формы персонализации: пользовательские профили и темы. Подсистема пользовательских профилей обеспечивает возможность постоянного хранения структурированных данных, а для доступа к ней используется типизирован- ный дружественный программисту API. Приложение определяет собственную модель персонализированных данных, и исполняющая среда ASP.NET делает остальное, выполняя синтаксический разбор определения этой модели и компилируя результи- рующий класс. Каждая переменная-член персонализационного класса соответствует элементу информации, связанной с конкретным пользователем. Загрузка и сохранение персонализированных данных выполняются прозрачно для конечного пользователя и не требуют от автора страницы глубокого знания внутренней архитектуры под- системы персонализации. Тема определяет настраиваемый набор стилей и визуальных атрибутов, назнача- емых элементам сайта. К числу таких элементов относятся таблицы стилей страниц, изображения, шаблоны, а также свойства элементов управления. Тема объединяет все визуальные стили всех настраиваемых элементов страницы, так что ее можно назвать чем-то вроде супер-CSS. Мы подробно рассмотрим вопрос о темах в следу- ющей главе. ©Внимание! В ASP.NET 1.x описанные в настоящей главе средства персонализации от- сутствуют. О том, как создать в этой системе подобную инфраструктуру, я рассказывал в выпуске MSDN Magazine за март 2004 года Вы найдете нужную статью по адресу http://msdn.microsoft.com/msdnmag/issues/04/03/CuttingEdge. Создание пользовательского профиля На высшем уровне абстракции пользовательский профиль — это набор свойств, кото- рые исполняющая среда ASP.NET 2.0 сгруппировала в динамически сгенерированный класс. Данные профиля сохраняются в постоянной памяти для каждого пользователя в отдельности и хранятся там до тех пор, пока не будут удалены кем-либо, имеющим административные привилегии. При выполнении страницы ASP.NET динамически создает объект профиля, содержащий правильно типизированные свойства, опреде- ленные вами в модели данных. Этот объект добавляется затем в текущий объект HttpContext и делается доступным страницам через свойство Profile. Место хранения данных профиля скрыто от пользователя и даже, до некоторой степени, от программиста. Пользователю просто ни к чему знать, как и где хранятся эти данные, а программисту обычно достаточно указать тип провайдера профилей, который он желает использовать. Каждый провайдер использует свое хранилище данных; встроенные провайдеры хранят их в базе данных Microsoft SQL Server, а поль- зовательские могут использовать и другие хранилища. Примечание В ASP.NET 2.0 задействуемый по умолчанию провайдер профилей хранит информацию в базе данных SQL Server Express — облегченной версии SQL Server 2005. Физическим хранилищем данных является файл с именем aspnetdb.mdf, расположенный в папке App_Data приложения. Определение модели данных Для того чтобы воспользоваться API профилей ASP.NET 2.0, нужно первым делом решить, какая структура данных профиля требуется вашему приложению. Эту модель
190 Часть I Разработка страниц ASP.NET данных вы описываете в файле web.config в формате XML, определяя свойства и со- ответствующие им типы .NET Framework. Каждое свойство задается в виде пары имя-значение в подразделе <properties> раздела <profile> конфигурационного файла. Раздел <profile>, содержащий также информацию о провайдере, в свою очередь, является подразделом <system.web>. При- ведем пример определения пользовательского профиля, состоящего из двух строковых свойств, BackColor и ForeColor. <profile> <properties> <add name="BackColor" type="string" /> Odd name=”ForeColor" type="string'" /> </properties> </profile> Все свойства, определенные с использованием тэга <add>, являются членами объек- та динамически созданного класса, доступного как часть HTTP-контекста страницы. В атрибуте type задается тип свойства. Если он не задан, по умолчанию свойству назначается тип System.String. Допускается использование любого типа данных, под- держиваемого общеязыковой средой. В табл. 5-3 перечислены допустимые атрибуты элемента <add>. Обязательным является только атрибут пате. Табл. 5-3. Атрибуты элемента <add> Атрибут Описание allowAnonymous Определяет, разрешено ли сохранение значений свойства для аноним- ных пользователей. По умолчанию данный атрибут имеет значение false defaultValue customProviderData Name Provider Значение свойства по умолчанию Данные для пользовательского провайдера профилей Имя свойства Имя провайдера, используемого для чтения и записи значения свой- ства readonly Определяет, будет ли значение свойства доступным только для чте- ния. По умолчанию данный атрибут имеет значение false serializeAs Указывает, как следует сериализовать значение свойства. Допустимы- ми установками являются Xml, Binary, String и ProviderSpecific Type Тип свойства, определенный в .NET Framework. По умолчанию это объект string Класс пользовательского профиля Разработчику приложения вовсе не обязательно знать, как осуществляется запись информации в хранилище персонализационных данных и как она оттуда считывается. Однако само это хранилище разработчику необходимо создать и сконфигурировать. Здесь пока ничего об этом не говорилось, но всему свое время — немного погодя мы подробно обсудим этот вопрос. А пока взгляните на фрагмент кода, который позво- лит вам составить представление о генерируемом ASP.NET классе ProfileCommon, который реализует модель данных профиля: namespace ASP { public class ProfileCommon : PpofileBase {
Работа со страницей Глава 5 191 public virtual string BackColor { get {(string) GetPropertyValue("BackColor");} set {SetPropertyValue("BackColor", value);} } public virtual string ForeColor { get {(string) GetPropertyValue("ForeColor');} set {SetPropertyValue("ForeColor", value);} } public virtual ProfileCommon GetProfile(string username) { object о = ProfileBase.Create(username); return (ProfileCommon) o; } } } Экземпляр этого класса связывается со свойством Profile класса страницы, а до- ступ к нему осуществляется следующим образом: // Использование свойства BackColor для установки цвета фона страницы theBody.Attributes["bgcolor”] = Profile.BackColor; Несмотря на то, что ASP.NET 2.0 поддерживает и профили анонимных пользова- телей, между учетными записями пользователей и информацией их профилей суще- ствует тесная взаимосвязь. Мы еще поговорим об этом подробнее, а пока я хочу лишь обратить ваше внимание на данный факт. Использование коллекций В приведенном выше примере в качестве элемента данных профиля фигурировало одиночное скалярное значение. Однако механизм персонализации позволяет ис- пользовать и более сложные типы данных, такие как коллекции и пользовательские типы. Например, в следующем фрагменте конфигурационного файла свойство Links определено как коллекция строк: <properties> <add name="Links" type="System.Collections.Specialized.Stringcollection" /> </properties> Чтобы в хранилище можно было записывать значения, не являющиеся скалярны- ми, такие как коллекции или массивы, их необходимо сериализовать. Способ сериа- лизации задается с помощью атрибута serialize As, принимающего значения String, Xml, Binary и Provider Specific. Если атрибут serialize As не задан, используется установка String. Как правило, сериализация коллекций осуществляется путем их преобразова- ния в двоичный формат или в формат XML. Пользовательские типы данных Свойство пользовательского профиля не обязательно должно относиться к одному из встроенных типов данных — тип может быть и пользовательским, нужно только указать, как должна осуществляться его сериализация. Реализуя пользовательский
192 Часть I Разработка страниц ASP.NET тип, вы пишете класс и компилируете его, после чего добавляете имя полученной сборки к информации о типе свойства профиля: <properties> <add name="ShoppingCart" type= My Namespace DataContainer, MyAssem" serializeAs="Binary" /> </properties> Чтобы сборка, содержащая пользовательский тип, была доступна приложению ASP.NET, ее следует поместить в папку Вт этого приложения или зарегистрировать в глобальном кэше сборок (GAC). Группировка свойств Раздел <properties> может содержать элемент <group>, позволяющий сгруппировать несколько связанных между собой свойств, как если бы они были свойствами про- межуточного объекта: <properties> <group name="Font"> <add name="Name" type="string" defaultValue="verdana" /> Odd name="SizeInPoints" type="int" defaultValue="8" /> </group> </properties> В данном случае два свойства, представляющие параметры шрифта, объявлены как принадлежащие к одной группе Font. А это означает, что обращаться к ним теперь нужно так: string fontName = Profile Font.Name; s Примечание Значения свойств по умолчанию не сохраняются вместе с остальными данными профиля. В хранилище профилей свойство попадает только тогда, когда его значение отличается от задаваемого по умолчанию. Работа с пользовательским профилем Включение и отключение функции поддержки профилей осуществляется с помо- щью атрибута enabled раздела <profile> файла web.config. Если этот атрибут имеет значение true (а таковым оно является по умолчанию), функция персонализации включена для всех страниц. Когда данная функция отключена, страницы не имеют свойства Profile. Создание базы данных пользовательских профилей Итак, вы уже знаете, что каждый профиль содержит персонализационные данные, связанные с одним конкретным пользователем, и сохраняется в постоянной памяти. Присвоив атрибуту enabled конфигурационного раздела <profile> значение true, вы активизируете подсистему поддержки пользовательских профилей, но прежде чем ее можно будет использовать, необходимо выполнить еще одну задачу — создать инфраструктуру хранения данных. Это можно сделать с помощью уже знакомого вам административного средства Web Site Administration Tool (WSAT), входящего в состав ASP.NET 2.0 и полностью интегрированного в Visual Studio .NET 2005 (рис. 5-11). Напомню, что оно вызывается командой Website\ASP.NET Configuration.
Работа со страницей Глава 5 193 Рис. 5-11. Выбор провайдера профилей с использованием WSAT С помощью данного средства вы можете создать базу данных SQL Server 2005, ис- пользуемую по умолчанию для хранения данных профилей, — ее файл, aspnetdb.mdf, должен находиться в папке App_Data приложения. Схема этой базы данных (в том числе набор и структура таблиц) фиксирована. Здесь же содержатся таблицы, пред- назначенные для хранения информации о членстве и ролях. Указанная информация необходима, в частности, и подсистеме персонализации, поскольку данные пользо- вательских профилей по определению связаны с пользователями. Идентификаторы пользователей (будь то идентификаторы учетных записей Windows или учетных записей, поддерживаемых конкретным приложением) используются для индексиро- вания данных их профилей. Пользовательские профили не имеют срока давности и хранятся «вечно», то есть до тех пор, пока администратор Web-сайта не удалит их по каким-нибудь соображениям Разумеется, WSAT — не единственное средство, с помощью которого можно сфор- мировать инфраструктуру для хранения данных профилей. Например, если вашему приложению необходим пользовательский провайдер персонализации, то подготовку этой инфраструктуры (будь то таблица SQL Server, база данных Oracle или иное хранилище) должна осуществлять программа его инсталляции. Об установке и кон- фигурировании провайдеров профилей речь пойдет в следующем разделе. Работа с анонимными пользователями Хотя функция пользовательских профилей предназначена прежде всего для сохра- нения информации аутентифицированных пользователей, существует возможность сохранять такую информацию и для анонимных пользователей. Однако в таком случае должно выполняться несколько дополнительных требований. В частности, необ- ходимо включить функцию идентификации анонимных пользователей, которая по умолчанию отключена: <anonymousldentification enabled="true" /> Эта новая функция ASP.NET 2.0 позволяет присваивать уникальные идентифика- торы неаутентифицированным пользователям, чтобы в контексте персонализации их можно было интерпретировать как зарегистрированных пользователей.
194 Часть I Разработка страниц ASP.NET Примечание Идентификатор анонимного пользователя не имеет никакого отношения к учетной записи, от имени которой обрабатывается запрос. Никак не связан он и с за- щитой, а также с аутентификацией пользователей. Идентификация анонимных пользовате- лей —• это лишь способ назначить неаутентифицированному пользователю идентификатор, по которому он будет отслеживаться наравне с зарегистрированными пользователями. Кроме того, в модели данных профиля вам необходимо пометить атрибутом allow- Anonymous те свойства, которые должны поддерживаться для анонимных пользова- телей. Например, в приведенном ниже фрагменте кода анонимным пользователям разрешается задавать цвет фона, но не добавлять новые ссылки. Ononymousldentification enabled="true” /> <profile enabled=”true"> <properties> Odd name="BackColor" type="System.D rawi ng.Colo r" allowAnonymous="true" /> Odd name="Links" type="System.Collections.Specialized.Stringcollection" serializeAs=”Xml" /> </properties> </profile> Доступ к свойствам профиля Перед началом цикла обработки запроса свойству Profile страницы присваивается экземпляр динамически созданного класса, представляющего определенный в файле web.config пользовательский профиль. При первой загрузке страницы свойства про- филя получают значения по умолчанию (если таковые имеются) или инициализи- руются пустыми объектами, но эти свойства никогда не бывают равными null. Если свойство имеет пользовательский тип или является коллекцией, как свойство Links в приведенном выше примере, декларативно задать для него значение по умолчанию вы не можете. (Тем не менее во время выполнения свойство Links будет равным не null, а пустой коллекции.) А как задать значение свойства программным способом? Свойства, для которых значения по умолчанию не заданы декларативно, можно инициализировать в обработчике события PageLoad (разумеется, сначала убедив- шись, что страница не обрабатывает возврат формы): if (!IsPostBack) { // Включаем в состав коллекции Links несколько предопределенных ссылок if (Profile.Links.Count == 0) { Profile.Links.Add("http://www.contoso.com"); Profile.Links.Add("http://www.northwind.com"); } } Теперь представьте себе страницу, подобную показанной на рис. 5-12, где на отдель- ной панели выводится список избранных ссылок. Пользователь может изменять этот список и настраивать несколько визуальных атрибутов, в частности цвета и шрифт. Структура пользовательского профиля, используемого кодом этой страницы, опи- сывается следующей XML-разметкой: <profile enabled="true”> <properties> <add name="BackColor" type="string" /> Odd name="ForeColor" type="string" /> odd name="Links”
Работа со страницей Глава 5 195 type="System.Collections.Specialized.StringCollection"/> <group name="Font"> Odd name="Name" type="string" /> Odd name="SizeInPoints" type="int” defaultValue="12" /> </group> </properties> </profile> Рис. 5-12. Благодаря поддержке профилей одна и та же страница у разных пользователей выглядит по-разному Данные этого профиля используются для адаптации пользовательского интер- фейса страницы: private void ApplyPagePersonalization() { // Устанавливаем цвета панели InfoPanel.ForeColor = ColorTranslator.FromHtml(Profile.ForeColor); InfoPanel.BackColor = ColorTranslator.FromHtml(Profile.BackColor); // Устанавливаем шрифт панели InfoPanel.Font.Name = Profile.Font.Name; InfoPanel.Font.Size = FontUnit.Point(Profile.Font.SizelnPoints); // Создаем ссылки Favorites.Controls.Clear(); if(Profile.Links.Count == 0) Favorites.Controls.Add(new LiteralControl("No links available.")); else foreach (object о in Profile.Links) { HyperLink h = new HyperLink (); h.Text = o.ToString (); h.NavigateUrl = o.ToString (); Favo rites.Cont rols.Add(h); Favorites.Controls.Add(new LiteralControl("<br />")); } } Метод ApplyPagePersonalization вызывается из обработчика события Page_Load\ protected void Page_Load(object sender. EventArgs e) {
196 Часть! Разработка страниц ASP.NET if (’IsPostBack) { // Инициализируем необходимым образом свойства профилей } ApplyPagePersonalization(); 1 Инициализация — важный этап жизненного цикла страницы, но происходящее на этом этапе всецело определяется нуждами конкретного приложения. В нашем примере при первом обращении пользователя к странице цвета не задаются и гиперссылки не определяются. Так как мы перебираем элементы коллекции в цикле, пустая коллекция вполне подходит и никакая иная инициализация для нее не требуется. (Заметьте, что .NET Framework обеспечивает создание экземпляров всех объектов, на которые в йро- грамме имеются ссылки.) А как насчет цветовых свойств? Если значение по умолчанию задано в файле web.config, исполняющая среда использует его для инициализации соответствующего свойства при первом обращении пользователя к странице; иначе необходимо позаботиться об установке свойства в обработчике события Page Load. Когда обработка запроса завершается, содержимое объекта профиля сохраняется в постоянной памяти и извлекается при следующем вызове страницы. Тогда уже все свойства будут инициализированы, если только администратор сайта не удалит какие-либо данные. Заметьте, что если пользователю позволено настраивать внешний вид и поведение страницы, ему должен быть предоставлен для этой цели удобный интерфейс. Напри- мер, в состав страницы, показанной на рис. 5-12, входит элемент управления Multi- View, с помощью которого при щелчке пользователем ссылки Click here to edit или Close происходит переключение между основным режимом использования страницы и режимом редактирования ее настроек (рис. 5-13). Рис. 5-13. В состав страницы входит небольшой редактор, с помощью которого пользователь может ее персонализировать
Работа со странице# Глава 5 197 Примечание К тому моменту, когда происходит событие Pagejnit, персонализационные данные страницы уже готовы. В ASP.NET 2.0 определено также событие Page_Prelnit, но когда оно происходит, такие операции, как загрузка персонализационных данных, для страницы еще не выполнены. События персонализации Как уже упоминалось, персонализационные данные добавляются в НТТР-контекст запроса до начала цикла обработки страницы. Какой системный компонент отвечает за цх загрузку? В ASP.NET 2.0 для этой цели имеется новый HTTP-модуль, называ- емый ProfileModule. Он подключается к двум событиям HTTP и вызывается дважды: после авторизации запроса и перед завершением его выполнения. Если функция персонализации отклю- чена, данный модуль немедленно возвращает управление исполняющей среде; в про- тивном случае он генерирует для приложения событие Personalize, а затем загружает персонализационные данные из профиля текущего пользователя. Когда генерируется событие Personalize, эти данные еще не загружены. Обработчик указанного события должен содержаться в файле global.asax: void Profile_Personaiize(object sender, ProfileEventArgs e) { ProfileCommon profile = null;. // Если пользователь анонимный - выход if (User == null) return; // Определяем профиль с учетом роли пользователя. База данных // профилей содержит по одной записи для каждой роли if (User.IsInRoleC'Administrators")) profile = (ProfileCommon) ProfileBase.Create("Administrator"); else if (User.IsInRole("Users")) profile = (ProfileCommon) P«*ofileBase.Create("User"); else if (User IsInRole("Guests )) profile = (ProfileCommon) ProfileBase.Cre3te("Guest"); // Делаем так, чтобы HTTP-модуль поддержки профилей // использовал объект профиля THIS if (profile != null) e.Profile = profile; } Нужно понимать, что роль слоя персонализации в приложении отнюдь не сводится к предоставлению пользователям приятной возможности настроить его интерфейс по своему вкусу. Это универсальное средство хранения информации, связанной с от- дельными пользователями, и не обязательно даже, чтобы информация была введена самими пользователями. Слой персонализации использует идентификатор текущего пользователя в ка- честве ключа для извлечения необходимого набора данных. А как насчет ролей? Что если у вашего приложения сотни пользователей с различными именами и все они используют всего две-три разные группы установок, в зависимости от роли, ис- полняемой каждым из пользователей? Имеет ли смысл хранить в базе данных сотни почти идентичных записей? Конечно, лучше было бы сохранять наборы установок для ролей, но стандартная подсистема пользовательских профилей не имеет о ролях ни малейшего представления. Вот почему иногда возникает необходимость в обработке события Personalize или написании собственного провайдера профиля.
198 Часть I Разработка страниц ASP.NET В приведенном выше коде процесс создания объекта пользовательского профиля переопределен так, чтобы этот объект создавался с учетом роли пользователя, а не его имени. Статический метод Create класса ProfileBase принимает имя пользователя и создает соответствующий экземпляр объекта профиля. ProfileCommon — имя дина- мически создаваемого класса, представляющего пользовательский профиль. Обработчик события Personalize получает данные профиля посредством класса ProfileEventArgs, у которого имеется доступное для чтения и записи свойство с име- нем Profile. Когда выполнение этого обработчика завершается, модуль HTTP, реа- лизующий функцию поддержки профилей, проверяет содержимое свойства Profile. Если его значением является null, модуль продолжает свою работу обычным образом и создает объект профиля, основываясь на идентификаторе пользователя. В против- ном случае модуль просто передает странице в качестве объекта профиля текущее значение свойства Profile. Перенос данных анонимных пользователей Вы уже знаете, что анонимные пользователи могут сохранять и восстанавливать данные персонализации, вводя свой уникальный идентификатор. Однако если поль- зователь, до сих пор бывший анонимным, решит зарегистрироваться на сайте, с соз- данной для него учетной записью нужно будет связать существующий профиль этого пользователя. Такой перенос установок не происходит автоматически. Когда к приложению подключается зарегистрированный пользователь, модуль персонализации генерирует событие MigrateAnonymous, в обработчике которого можно перенести в его профиль установки, заданные им ранее анонимно. Делается это так: void Profile_MigrateAnonymous(object sender, ProfileMigrateEventArgs e) { // Загрузка профиля анонимного пользователя ProfileCommon anonProfile; anonProfile = Profile.GetProfile(e.AnonymousId); // Перенос свойств в новый профиль Profile.BackColor = anonProfile.BackColor; } Вы получаете профиль анонимного пользователя, извлекаете значения его свойств и копируете их в профиль зарегистрированного пользователя. Провайдеры профилей В ASP.NET 2.0 API профилей состоит из двух независимых компонентов: слоя до- ступа и слоя хранения. Слой доступа реализует строго типизированную модель чтения и записи значений свойств и управления идентификаторами пользователей. Он обеспечивает сохранение и восстановление данных от имени текущего пользователя приложения. Слой хранения с помощью специализированных провайдеров выполняет операции, связанные с сохранением и восстановлением значений. В состав ASP.NET 2.0 входит провайдер профилей, в качестве СУБД использующий SQL Server 2005 Express. Он записывает данные в выбранное хранилище и отвечает за то, какой будет конечная схема этих данных. Провайдер должен быть способен либо сериализовать объект данных (превратив его, например, в текст XML-формата или двоичный объект), либо уметь извлечь из него требуемую информацию. При необходимости провайдер про- филей можно написать самостоятельно.
Работа со страницей Глава 5 199 О Внимание! Как рассказывалось в главе 1, провайдером называется подключаемый ком- понент, который заменяет определенный функциональный элемент ASP.NET 2.0, расширяя или изменяя его функции. Одним из примеров такого компонента является провайдер профилей; кроме него существуют и другие провайдеры, например провайдеры членства и ролей, о которых рассказывается в главе 15. Инфраструктура провайдеров дает разра- ботчику возможность изменять стандартную реализацию некоторых системных функций. В домене приложения существует только по одному экземпляру каждого провайдера. Конфигурирование провайдеров профилей Для каждой функции, реализуемой в соответствии с моделью провайдеров, должен быть определен провайдер, используемый по умолчанию. Обычно он задается в атри- буте defaultProvider того раздела конфигурационного файла, который связан с данной функцией. В том случае, если он не задан, используемым по умолчанию считается первый из перечисленных в этом разделе провайдеров. Стандартный провайдер профилей ASP.NET 2.0 носит имя AspNetSqlProfileProvider. Для хранения информации он использует базу данных SQL Server 2005 Express. Вот запись о его регистрации в подразделе <providers> раздела <profile> конфигураци- онного файла machine.config: <profile> <providers> <add name="AspNetSqlProfileProvider" connections!ringName="LocalSqlServer" applicationName="/" type="System.Web.Profile.SqlProfileProvider” /> </providers> </profile> Каждому зарегистрированному провайдеру соответствует свой узел <add> раздела <providers>. Атрибуты пате и type задаются для всех провайдеров, набор остальных атрибутов зависит от типа конкретного провайдера. При желании в атрибуте descrip- tion можно задать текстовое описание провайдера. Атрибут connectionStringName служит для определения строки подключения к ба- зе данных пользовательских профилей. Он содержит не строку подключения, а ее имя — сама же строка зарегистрирована в другом месте конфигурационного файла. Например, очевидно, что LocalSqlSeruer — это не строка подключения к удаленно- му или локальному экземпляру SQL Server. Элемент с таким именем вы найдете в разделе <connectionStrings>, введенном в ASP.NET 2.0 для того, чтобы все строки подключения можно было хранить централизованно (этот раздел также находится в файле machine.config): <connectionSt rings> <add name="LocalSqlServer" connectionString=”data source=.\SQLEXPRESS; Integrated Security=SSPI; AttachDBFilename=|DataDi rectory|aspnetdb.mdf; User Instances rue" providerName="System.Data.SqlClient” /> </connectionSt rings> Как видите, данная строка подключения содержит ссылку на экземпляр SQL Server с именем SQLEXPRESS и служит для подключения к базе данных aspnetdb.mdf, хра- нящейся в разделе данных приложения — папке App_Data. Структура базы данных aspnetdb.mdf Как разработчику приложения вам нет необходимости изучать структуру таблиц базы данных aspnetdb.mdf и логику операций с этими таблицами. Ваша задача — обеспечить
200 Часть I Разработка страниц ASP .NET создание необходимой инфраструктуры, для чего можно воспользоваться утилитой WSAT, запускаемой из Visual Studio .NET 2005 командой Website\ASP.NET Configura- tion. Структура базы данных aspnetdb.mdf представлена на рис. 5 -14. , > t . Рис. 5-14. Структура базы данных aspnetdb.mdf и таблицы профилей Заметьте, что эта база данных служит не только для хранения данных персона- лизации. В ней содержатся таблицы, используемые разными провайдерами, ,в том числе провайдерами членства и ролей. Структура этих таблиц определяется про- вайдерами. Для просмотра и редактирования содержимого базы данных aspnetdb.mdf (как и других связанных с приложением баз данных) можно использовать Server Ex- plorer — средство, входящее в состав Visual Studio .NET. Вы создаете с его помощью новое подключение к источнику данных, а затем выполняете необходимые операции, пользуясь командами его контекстного меню. В качестве альтернативы можно воспользоваться внешней утилитой Microsoft SQL Server Management Studio Expres, которая доступна по адресу http://msdn.mi- crosoft.com/vstudio/express/sql/download/. Это облегченная версия известных каждому администратору SQL Server утилит Enterprise Manager и Query Analyzer, объединенных вместе. Management Studio Express позволяет подключать к СУБД новые базы данных и работать с ними посредством графического пользовательского интерфейса или SQL-команд. Внимание! Задействуемый по умолчанию провайдер профилей пользуется управляемым ADO.NET-провайдером SQL Server и способен работать не только с Express-версией этой СУБД. Изменив строку подключения, можно сообщить ему, что используется полная версия данной СУБД — SQL Server 2000 или SQL Server 2005. База данных SQL Server Express предназначена для небольших приложений, в том числе Web-приложений, а также для различных экспериментальных целей.
Работа со страницей Глава 5 201 Пользовательские провайдеры профилей Для разработки новых приложений вполне подходит встроенный провайдер про- филей. Однако в случае, если вы переносите на платформу ASP.NET 2.0 старое при- ложение ASP или ASP.NET, данные которого хранятся в нереляционной форме или в СУБД, отличной от SQL Server, этот провайдер вам не подойдет, и придется написать собственный провайдер профилей. Провайдер профилей представляет собой прослойку кода между персонализаци- онной подсистемой и физическим хранилищем данных. Этот слой кода абстрагирует физические характеристики хранилища данных и открывает доступ к его содержимому посредством универсального набора свойств и методов. Пользовательский провайдер профилей реализуется в виде класса, наследующего класс ProfileProvider. Примечание Пользовательский провайдер можно с помощью атрибута provider связать со свойствами пользовательских профилей. <properties> <add name=”BackColor" type="string" provider="MyProvider" /> </properties> В данном примере свойство BackColor профиля считывается и записывается провайде- ром MyProvider. Имя провайдера, заданное в атрибуте provider, должно соответствовать одному из элементов, определенных в разделе <providers>. Заключение В этой главе нами были проанализированы три задачи, с которыми сталкивается разработчик Web-приложения: создание форм, обработка ошибок и персонализация страниц. Программирование форм — ключевая задача разработки Web-приложений, по- скольку формы являются единственным средством взаимодействия пользователя с приложением такого типа. Страница ASP.NET может содержать только одну сер- верную форму, причем значение свойства action этой формы является фиксирован- ным. И хотя значение указанного свойства можно динамически изменять, используя клиентский сценарий, это весьма рискованное действие, которое может привести к разрушению состояния представления, в результате чего произойдет ошибка времени выполнения. Но поскольку в некоторых случаях необходимым или по крайней мере оптимальным решением определенной задачи является передача данных и управления между страницами, в ASP.NET 2.0 введена специальная технология межстраничного постинга. Даже в самой лучшей, тщательно написанной программе возможны ошибки. В мире Web задача обработки ошибок делегирована исполняющей среде приложения. Послед- няя способна выводить два типа страниц с сообщениями об ошибке, но и те и другие не отличаются информативностью и не подходят для профессионально выполненных приложений. Когда пользователь, запустивший приложение локально, производит действие, приводящее к ошибке, по умолчанию ASP.NET возвращает «техническую» страницу, на которой отображается приведшая к ошибке последовательность вызовов и полное описание произошедшего исключения. Удаленный пользователь получает страницу с гораздо менее информативным сообщением, но ничуть не более друже- ственную. К счастью, исполняющая среда ASP.NET достаточно гибка, чтобы позволить вам выводить индивидуальные страницы для каждого типа ошибок, причем с учетом того, каким — локальным или удаленным — является пользователь.
202 Часть I Разработка страниц ASP.NET В ASP.NET 2.0 предусмотрены удобные и мощные средства адаптации страниц приложения к потребностям конкретного пользователя, называемые средствами персонализации. Сведения о предпочтениях пользователей и другая связанная с пользователями информация сохраняются в постоянной памяти, причем процесс их сохранения и восстановления полностью автоматизирован. Разработчику даже не приходится разбираться во внутренней структуре хранилища этих данных. Ему нуж- но лишь обратиться к провайдеру персонализации, используя методы стандартного интерфейса. Функция персонализации была введена в ASP.NET 2.0. Мы продолжим изучение темы создания страниц в следующей главе — обсудим мощные и эффективные технологии, основанные на использовании эталонных страниц и тем, а также технологию создания мастеров. Только факты Типичная страница ASP.NET содержит единственный тэг <form> с атрибутом runat. Если же серверных форм в составе страницы несколько, они не могут быть видимы одновременно. В ASP.NET 2.0 элементы управления, реализующие интерфейс IButtonControl, мо- гут инициировать возврат формы другой странице. Заботу о передаче управления и состояния представления нужной странице берет на себя исполняющая среда. Исключения являются официальным средством .NET, предназначенным для об- работки нестандартных ситуаций. Именно нестандартных, то есть таких, когда нарушены ожидаемые условия выполнения приложения; не стоит использовать исключения просто как средство условного выполнения. Когда при выполнении страницы ASP.NET происходит необработанное исклю- чение, пользователь получает одну из двух разных страниц с сообщениями об ошибке, в зависимости от того, работает он локально или удаленно. Для удален- ных пользователей выводится простейшее сообщение об ошибке, не говорящее ни о чем, кроме того, что она произошла. Как разработчик вы можете программным или декларативным способом опреде- лить, какая страница будет выведена для пользователя. Декларативный метод позволяет связывать пользовательские страницы с кодами состояния HTTP. Страницу ASP.NET 2.0 можно связать с дополнительным свойством контекста запроса, содержащим объект профиля. Этот объект является экземпляром класса, динамически созданного на основе модели данных, определенной в конфигураци- онном файле. Когда функция поддержки пользовательских профилей активна, сохранение персонализационных данных страниц в постоянной памяти и их восстановление осуществляется автоматически. Разработчику даже не нужно знать, как и где это происходит. Персонализационные данные можно сохранить и для анонимных пользователей, причем когда такой пользователь зарегистрируется в приложении, вы сможете связать его профиль с созданной для него учетной записью. Чтобы пользователь мог персонализировать страницу, она должна предоставлять ему для этого необходимый интерфейс. Подсистема персонализации страниц ASP.NET тесно связана с подсистемой управ- ления членством пользователей.
Глава 6 Страницы с богатым интерфейсом В наше время многие Web-сайты состоят из единообразно организованных и офор- мленных страниц с богатым пользовательским интерфейсом. Иными словами, у стра- ниц таких сайтов одинаковые структура и графическое оформление, на них использу- ются одинаковые элементы пользовательского интерфейса, все они имеют одинаковые навигационные меню и формы для поиска информации на сайте. При этом сайты обладают богатой функциональностью, их оформление выполнено на высоком про- фессиональном уровне и, что особенно важно, внешний вид и поведение их страниц отвечают золотому правилу эргономики Web-сайта, которое выражается одним сло- вом: согласованность. Как же создавать такие сайты? Одна из возможностей — заключить единые для всего сайта блоки пользователь- ского интерфейса в пользовательские элементы управления и ссылаться на них на каждой странице. Хотя это очень мощная модель, обеспечивающая модульность струк- туры программного кода, при ее использовании сайт с сотнями страниц становится неуправляемым. В классической ASP и в ASP.NET 1.x существуют обходные пути решения данной проблемы, но ни в одной их этих систем не реализован прямой и оптимальный путь. В ASP.NET 2.0 задача была, наконец, решена на уровне исполня- ющей среды путем введения новой технологии — эталонных страниц. Теперь испол- няющая среда ASP.NET способна выполнять слияние «супершаблонных» страниц, определяющих общее оформление и единую функциональность сайта, со страницами, в которых содержится его наполнение. Еще одна технология определения шаблонов, а именно темы, позволяет обеспе- чить согласованность внешнего вида страниц сайта и легко экспортировать эту часть пользовательского интерфейса из одного приложения в другое. Подобно теме Micro- soft Windows ХР тема ASP.NET определяет набор стилей и визуальных атрибутов настраиваемых элементов сайта. Темы содержат подмножества элементов каскадных таблиц стилей (CSS) и поддерживаются только в ASP.NET 2.0. Типичной задачей Web приложения является ввод пользовательских данных по- средством формы. Когда входных данных много и их легко разделить на группы, обычно используется несколько форм. При этом процедура ввода данных реализуется как последовательность шагов, на каждом из которых пользователь задает некоторое подмножество значений. Компонент приложения, обеспечивающий выполнение по- добной пошаговой процедуры ввода данных, часто называют мастером. В ASP.NET 2.0 введен новый элемент управления Multi View, существенно упрощающий задачу соз- дания мастеров. В целом в последней версии ASP.NET стало гораздо легче создавать страницы с богатым пользовательским интерфейсом, чем это было в предыдущих ее версиях. Весь общий для сайта код, как программный, так и код разметки, можно оформить в виде эталонных страниц, дополнить темами и обложками, с помощью которых будут создаваться вариации интерфейса отдельных компонентов сайта, и таким об- разом обеспечить беспрецедентную согласованность интерфейса сайта. При этом вы
204 Часть I Разработка страниц ASP.NET еще и можете предоставить пользователю возможность вносить в интерфейс сайта определенные изменения, адаптируя его в соответствии с собственным вкусом и функциональными потребностями. А реализовав к тому же отдельные составляющие пользовательского интерфейса сайта в виде мастеров, вы сделаете этот интерфейс исключительно дружественным пользователю Эталонные страницы ASP.NET и Microsoft Visual Studio .NET существенно упростили процесс создания Web-страниц и сайтов, так что эта задача стала под силу разработчикам с самым раз- ным уровнем подготовки. Однако спустя несколько месяцев работы даже новички по- няли, что данную систему пока еще нельзя назвать полнофункциональным средством создания реальных приложений. Простой сайт в ней создать легко, однако разработка сайта, состоящего из сотен страниц со сложным интерфейсом, в ASP.NET вплоть до версии 1.x оставалась весьма трудоемкой задачей. Страницы практически любого сайта имеют единый интерфейс, и это че слу- чайно — таково общепринятое представление об эргономичности Web-приложения. У одних сайтов страницы состоят из заголовка, тела и подвала (либо нижнего ко- лонтитула); у других структура страниц сложнее, они содеожат целый комплекс стандартных элементов, включающий навигационные меню, кнопки и панели, на которых выводится наполнение сайта. Нет нужды говорить, что дублирование кода и HTML-элементов вручную — чересчур трудоемкая и чреватая ошибками процедура чтобы ее можно было всерьез рассматривать как метод профессионального .< издания больших сайтов. Код, безусловно, должен быть повторно используемым, но как реа лизовать это на практике? Разработка страниц с богатым интерфейсом в ASP.NET 1 .х В ASP.NET 1.x наилучшим подходом к разработке страниц с единым интерфейсом является создание пользовательских элементов управления. Внутри такой элемент представляет собой композицию серверных элементов управления ASP.NET, литераль- но заданного текста и кода, а для внешнего мира — это программируемый компонент, который можно использовать в составе страниц. (Я подробно рассказал о пользова- тельских элементах управления в книге «Programming Microsoft ASP.NET 2.0 Ap- plications: Advanced Topics» (Microsoft Press, 2005).) Идея заключается в том, чтобы общие для всего сайта блоки пользовательского интерфейса были реализованы как пользовательские элементы управления и чтобы впоследствии на них можно было ссылаться во всех страницах сайта. Например, страницы, на которых должно разме- щаться навигационное меню, могут содержать ссылки на пользовательский элемент управления, реализующий такое меню, причем каждая из них будет конфигурировать этот элемент по-своему. Чем хороши пользовательские элементы управления Пользовательские элементы управления — это нечто вроде встраиваемых страниц. Вы можете создать обычную страницу ASP.NET, а затем превратить ее в пользовательский элемент управления, внеся лишь несколько изменений. Пользовательский элемент управления легко связать с любой страницей, которой требуется соответствующая функциональность. А когда реализация пользовательского элемента управления ме- няется, это никак не сказывается на хост-страницах, й все, что вам (или исполняющей среде) нужно в таком случае сделать, — это откомпилировать измененную версию элемента в новую сборку.
Страницы с богатым интерфейсом Глава 6 205 Примечание Когда в ASP.NET были введены пользовательские элементы управления, отпала необходимость в использовании традиционной для ASP технологии включения файлов. Типичный включаемый файл ASP содержал либо статический, либо динами- г 4. ческий контент части страницы. Такой подход не является объектно-ориентированным, и его применение при разработке больших сайтов значительно усложняло их структуру и затрудняло сопровождение. Кроме того, нередко случалось, что тэг, открытый в одном включаемом файле, закрывался в другом, что делало практически невозможной под- держку разработки страниц WYSIWYG-средствами. Чем плохи пользовательские элементы управления Если вы изменяете внутреннюю реализацию пользовательского элемента управления, то на странице, в которой он используется, это не отражается. Однако если вы хоть что-нибудь измените в его внешнем интерфейсе (имя класса, сигнатуру свойств, мето- дов и событий), вам придется внести изменения во все страницы, которые содержат ссылки на этот элемент. Кроме того, нужно будет перекомпилировать указанные страницы и произвести повторную инсталляцию полученных сборок. Из-за этого, когда пользователь в следующий раз обратится к странице, содержащей ссылку на измененный элемент, произойдет небольшая задержка. Визуальное наследование Страницы ASP.NET создаются как экземпляры особых классов, генерируемых си- стемой на основе содержимого файлов отделенного кода. А можно ли включить блок стандартного пользовательского интерфейса в некоторый базовый класс и сделать страницы производными от этого класса? Да, можно. Подобным образом действует функция визуального наследования, знакомая разработчикам приложений Windows Forms. Однако в чистом виде визуальное наследование а-ля Windows Forms в ASP.NET применяться не может. Дело в том, что страница ASP.NET состоит из кода и раз- метки, Разметка определяет местоположение элементов управления, а код — логику и функциональность страницы. Встраивание определенных графических шаблонов в базовый класс само по себе не составляет проблемы, но как импортировать стандарт- ные блоки пользовательского интерфейса в производные страницы и как соединить их с элементами управления, локальными для производных страниц? В Windows Forms элементы управления имеют абсолютные координаты, благодаря чему разработчик без труда может вставлять новые элементы в любое место страницы. Однако в Web Forms позиции элементов управления являются относительными, чем и обуславливается стоящий перед разработчиком выбор. Он может либо включить зара- нее определенные именованные блоки пользовательского интерфейса в базовый класс, с тем чтобы производные классы загружали их вместо размещенных в теле страницы заменителей, либо использовать эталонные страницы, введенные в ASP.NET 2.0. Для реализации первого подхода необходимо выполнить действия, указанные ниже. 1. Сделайте страницу производной от базового класса, способного создать необ- ходимые блоки пользовательского интерфейса, например панели инструментов, заголовок и подвал. Каждый такой блок должен иметь уникальное имя. 2. Добавьте в тело производной страницы элемент <asp:placeholder>, идентификатор которого соответствует одному из указанных выше имен. Базовый класс должен содержать код для анализа дерева элементов управления страницы и подстановки предопределенных блоков пользовательского интерфейса на место заменителей. Данный подход основан на технологии наследования, но он не обеспечивает не- обходимой поддержки средств WYSIWYG-разработки и требует создания блоков
206 Часть I Разработка страниц ASP.NET пользовательского интерфейса программным способом, без использования разметки. В сопутствующем коде настоящей главы вы найдете пример реализации данного под- хода, однако его следует рассматривать исключительно как решение для приложений ASP.NET 1.x. В ASP.NET 2.0 есть лучшее решение — реализовать второй их указанных выше подходов, воспользовавшись технологией эталонных страниц. Создание эталонной страницы В ASP.NET 2.0 эталонной страницей называется отдельный файл со статической раз- меткой, ссылки на который задаются на уровне приложения и отдельных страниц. Настраиваемые области, которые в каждой производной странице будут содержать свой контент, в эталонной странице обозначаются специальными элементами управ- ления — заменителями. Производная страница представляет собой набор блоков, которые исполняющая среда подставит на место заменителей эталонной страницы. Настоящее визуальное наследование, какое реализовано в Windows Forms не является целью технологии эталонных страниц ASP.NET 2.0. Контент эталонной и произво- дной страниц просто соединяется, а затем на основе этого объединенного контента генерируется новый класс страницы, который и обслуживает запрос пользователя. Слияние происходит во время компиляции и только один раз. Эталонная страница никоим образом не является базовым классом страницы контента. Что же такое эталонная страница Эталонная страница подобна обычной странице ASP.NET, отличает ее лишь наличие директивы ©Master да еще одного или нескольких серверных элементов управле- ния ContentPlaceHolder. Такой элемент управления определяет область эталонной страницы, которая при слиянии с производной страницей будет заменена индивиду- альным контентом последней. В принципе, эталонная страница может не содержать заменителей, и с технической точки зрения она при этом останется корректной, так что исполняющая среда ASP.NET успешно ее обработает. Однако та цель, ради которой создаются эталонные страницы, то есть определение шаблона для страниц контента, реализуется именно благодаря наличию заменителей. Вот пример простейшей эта- лонной страницы: <%@ Master Language="C#” CodeFile="Simple.master.cs" Inherits="Simple" %> <html> <head runat="server"> <title>Hello, master pages</title> </head> <body> <form id="form1" runat="server"> <asp:Panel ID="HeaderPanel" runat="server" BackImageUrl="Images/SkyBkgnd.png" Width="100%"> <asp:Label ID="TitleBox" runat=”server" Text="Programming ASP.NET 2.0" /> </asp:Panel> <asp:contentplaceholder id="PageBody" runat="server”> <! — Контент для этого заменителя должны определять производные страницы —> </asp: content placeholder <asp:Panel ID="FooterPanel" runat="server" BacklmageU r1="Images/SeaBkgnd.png"> <asp:Label ID="SubTitleBox" runat="server" Text="Dino Esposito" /> </asp:Panel> </form> </body> </html>
Страницы с богатым интерфейсом Глава 6 207 Как видите, эталонная страница выглядит точно так же, как обычная страница ASP.NET. Если не считать директивы ©Master, единственным ее отличием является использование элементов управления ContentPlaceHolder. Страница, связанная с этой эталонной страницей, автоматически наследует весь ее контент (в данном случае верхний и нижний колонтитулы). Элемент управления ContentPlaceHolder полнос- тью идентифицируется свойством id и никаких других атрибутов обычно не требует. Директива ©Master Наличие директивы ©Master отличает эталонные страницы от страниц контента и позволяет исполняющей среде ASP.NET правильно их обрабатывать. Файл эталон- ной страницы компилируется в класс, производный от класса Mas ter Page, который, в свою очередь, является производным от класса UserControl, так что эталонная стра- ница представляет собой просто особую разновидность пользовательского элемента управления ASP.NET. У директивы ©Master довольно много атрибутов, но по большей части они совпа- дают с атрибутами директивы ©Page, описанными в главе 3. В табл. 6-1 перечислены те атрибуты директивы ©Master, которые имеют значение только для эталонных страниц. Табл. 6-1. Атрибуты директивы ©Master Атрибут Описание ClassName Имя класса, который будет создан для рендеринга эталонной страницы. Это может быть любой допустимый для класса идентификатор, не уточ- ненный именем пространства имен. По умолчанию класс страницы с име- нем simple.master называется ASP.simple master CodeFile URL файла, содержащего связанный с эталонной страницей программный код Inherits Класс отделенного кода, наследуемый эталонной страницей. Это может быть любой класс, производный от MasterPage MasterPageFile Имя файла эталонной страницы, на которую ссылается данная эталонная страница. Последняя может ссылаться на другую эталонную страницу, используя тот же механизм, который обычные страницы используют для ссылки на эталонную. Если этот атрибут установлен, образуется иерархия эталонных страниц Эталонная страница связана с файлом кода, содержимое которого выглядит при- мерно так: public partial class Simple : System.Web HI.MasterPage { protected void Page_Load(object sender, EventArgs e) { } } Директива ©Master не переопределяет атрибуты, заданные для страницы в дирек- тиве ©Page. Поэтому можно, например, создать эталонную страницу, языком кото- рой будет Visual Basic .NET, и в одной из производных от нее страниц использовать язык С#. Установка языка, сделанная на уровне эталонной страницы, не отражается на выборе языка, осуществленном на уровне страницы контента. В эталонной стра- нице можно использовать и другие директивы ASP.NET, например ©Import. Однако область действия этих директив ограничена файлом эталонной страницы и не рас- пространяется на дочерние страницы, генерируемые на ее основе.
208 Часть I Разработка страниц ASP.NET Примечание Эталонные страницы можно создавать и программно. Вы пишете собствен- ный класс, производный от класса MasterPage, а затем создаете файлы с расширением .master, указывая в их атрибуте Inherits полностью уточненное имя своего класса. Именно так эталонные страницы создаются RAD-дизайнерами, в частности встроенным дизайне- ром Microsoft Visual Studio .NET 2005. Контейнерный элемент управления ContentPlaceHolder Элемент управления ContentPlaceHolder, называемый заменителем, действует как контейнер, размещенный в теле эталонной страницы. Он указывает место в странице, куда может быть вставлен пользовательский контент. Заменитель уникально иден- тифицируется значением атрибута ID’. <asp:contentplaceholder runat="server" ID="PageBody" /> Страница контента ASP.NET не может содержать ничего, кроме серверных тэгов <asp:Content>. Каждый такой тэг соответствует экземпляру класса Content, предо- ставляющему реальный контент, который подставляется на место определенного заменителя эталонной страницы: <asp:Content runat="server" contentplaceholderID="PageBody"> </asp:Content> Связь между заменителем и контентом устанавливается посредством атрибута ID заменителя. Контент определенного экземпляра серверного элемента управления Content записывается в формируемую суммарную разметку страницы на место за- менителя, значение атрибута ID которого соответствует значению свойства Content- PlaceHolderlD данного элемента управления Content. В эталонной странице вы определяете столько заменителей, сколько настраивае- мых областей должна содержать страница. Странице контента не обязательно запол- нять все эти области, однако ее задачей является именно предоставление контента, подставляемого на место заменителей, поэтому хотя бы один элемент управления Content она содержит всегда. Примечание Один заменитель из эталонной страницы не может быть связан с несколь- кими областями контента производной страницы. Если производная страница содержит несколько серверных тэгов <asp:Content>, каждый из них должен указывать на свой заменитель. Определение контента, используемого по умолчанию Входящему в состав эталонной страницы заменителю может быть назначен контент, который будет отображаться по умолчанию, то есть в том случае, если производная страница не предоставит для него замену. Иными словами, если производная страница не содержит ссылку на некоторый заменитель, используется контент этого заменителя, заданный в эталонной странице. Следующий пример показывает, как определяется контент, используемый по умолчанию: <asp:contentplaceholder runat="server" ID="PageBody"> <!— Содержащаяся здесь разметка будет использоваться при условии, что в производной странице контент для этого заменителя не задан --> </asp:contentplaceholder> Определенный внутри заменителя контент полностью игнорируется, если страница содержит соответствующий данному заменителю элемент управления Content — кон- тент заменителя ни при каких условиях не объединяется с контентом, заданным в производной странице.
Страницы с богатым интерфейсом Глава 6 209 pg Примечание Элемент управления ContentPlaceHolder может использоваться только в эта- J лонных страницах. Для обычных же страниц ASP.NET он не поддерживается, и, встретив его там, анализатор страницы генерирует ошибку. Создание страницы контента Если вам нужно использовать определенный блок пользовательского интерфейса на нескольких страницах или сделать так, чтобы они имели одинаковую структуру, этот блок и эту структуру удобнее всего реализовать в эталонной странице, значительно упростив тем самым управление страницами приложения. После создания эталонной страницы вы можете рассматривать производные от нее страницы как некую дельту, разность между результирующей страницей и эталонной. Эталонная страница опреде- ляет общие части группы страниц и содержит заменители их настраиваемых частей. А страница контента определяет, какой контент будут содержать настраиваемые об- ласти на конкретной странице ASP.NET, выводимой для пользователя. На рис. 6-1 показано, как создается страница контента в Visual Studio .NET 2005. Рис. 6-1. Добавление страницы контента в проект Visual Studio .NET 2005 Элемент управления Content Главную роль в структуре страницы контента играют элементы управления Content, служащие контейнерами для других элементов управления. Элемент управления Content всегда используется в паре с элементом управления ContentPlaceHolder эта- лонной страницы и самостоятельного применения не имеет. В рассмотренном нами выше файле эталонной страницы содержится элемент управления ContentPlaceHolder с именем PageBody. Этот заменитель представляет тело страницы и размещается непосредственно под HTML-панелью, на которой выводится верхний колонтитул страницы. На рис. 6-2 показана демонстрационная страница контента, основанная на данной эталонной странице. Она открыта в режиме дизайнера — наряду с ее собствен- ным контентом отображается затененный контент эталонной страницы, недоступный для редактирования. Ниже приведен исходный код указанной страницы контента: <%@ Page Language="C#" MasterPageFile="Simple.master” CodeFile="HelioMaster.aspx cs" Inherits="HelloMaster" %> <asp:Content ID="Content1" ContentPlaceHolderID="PageBody" Runat="Server">
210 Часть I Разработка страниц ASP.NET <h1>Welcome to this page!</h1> <h3>The rest of the page is kindly offered by our sponsor page-the master!</h3> </asp:Content> Рис. 6-2. Страница контента в режиме дизайнера Страница контента — это ресурс, запрашиваемый пользователем с помощью бра- узера. Когда пользователь вводит URL данной страницы в адресной строке или щел- кает ссылку на нее, расположенную на другой странице, он видит то, что показано на рис. 6-3. 3l Hollo, Mdstei - Microsoft Internet Ixplorei 0e б* • Favorites look Цд|р 0 Back * О ' Э 3 $H,PSwrch ^Fewtes ; * Programming ASP JET 2 Welcome to this page! The rest of the page Is kindly offered by our sponsor page-the master! Dino ESrOSltO < 1рЛ8имxiд ль jural intranet Рис. 6-3. Демонстрационная страница в браузере Заменяемая часть эталонной страницы заполняется содержимым раздела контента, определенного в производной странице. Еще раз подчеркну: страница контента, то
Страницы с богатым интерфейсом Глава 6 211 есть страница, связанная с эталонной, — это страница особого рода, которая может содержать только элементы управления <asp:Content>. Вне указанного тэга серверные элементы управления в ней размещаться не могут. Связывание страниц контента с эталонной страницей В приведенном выше примере страница контента была связана с эталонной с помощью атрибута MasterPageFile директивы ©Page. В этом атрибуте задается строка, представ- ляющая путь к эталонной странице. Однако связывание на уровне страницы — это лишь одна, наиболее часто используемая, возможность связывания страниц контента с эталонной страницей. Такую связь можно определить и на уровне приложения либо папки. Когда связы- вание осуществляется на уровне приложения, это означает, что вы привязываете все страницы приложения к одной и той же эталонной странице. Для этого вам нужно задать атрибут Master элемента <pages> в главном файле web.config приложения: <configuration> <system.web> <pages master="MyApp.master” /> </system.web> </configuration> Если такую же установку задать в дочернем файле web.config, хранящемся в одном из подкаталогов сайта, все содержащиеся в этом подкаталоге страницы ASP.NET будут связаны с указанной вами эталонной страницей. Заметьте, что когда связь между эталонной страницей и страницами контента задается на уровне приложения или папки, все Web-страницы этого приложения (или этой папки) должны содержать элемент управления Content, соответствующий одному или более заменителям из эталонной страницы. Иными словами, связывание на уровне приложения лишает вас возможности использовать в его составе страницы, не являющиеся страницами контента. Если включить в состав такого приложения (или поместить в такую папку) классическую страницу ASP.NET, при обращении к ней будет выброшено исключение. Эталонные страницы, предназначенные для определенного устройства Подобно любым другим страницам и элементам управления ASP.NET эталонные страницы могут идентифицировать возможности вызывающего браузера и адапти- ровать к ним свой вывод. Если вы хотите самостоятельно определить, как некоторая группа страниц сайта будет отображаться в заданном браузере, то можете сделать их производными от одной эталонной страницы, специально разработанной с учетом воз- можностей данного браузера. При этом у вас имеется возможность создать несколько версий одной эталонной страницы, предназначенных для браузеров разных типов. Как же связать нужную версию эталонной страницы с определенным браузером? В странице контента вы, используя атрибут MasterPageFile с разными префиксами, идентифицирующими целевой браузер или целевое устройство, определяете несколь- ко связей с разными эталонными страницами. Предположим, вы хотите обеспечить эф- фективную поддержку приложением браузеров Microsoft Internet Explorer и Netscape, для чего создали для каждого из них свою эталонную страницу. Тогда ссылка на эти две эталонные страницы в файлах страниц контента должна быть такой: <%@ Page masterpagefile=”Base.master” ie:master pagefile=”ieBase.master” netscape6to9:masterpagefile=”nsBase.master” %>
212 Часть I Разработка страниц ASP.NET Файл эталонной страницы ieBase.master будет использоваться для формирования HTML-страницы, отправляемой браузеру Internet Explorer, а файл nsBase.master — для формирования страницы, отправляемой браузеру семейства Netscape версий с 6.x по 9.0. Во всех остальных случаях будет использоваться независимая от устройства эталонная страница Base.master. При обработке запроса страницы исполняющая среда ASP.NET автоматически определяет, от какого браузера (или устройства) он поступил, и выбирает соответствующую эталонную страницу (рис. 6-4). Рис. 6-4. Использование двух разных эталонных страниц с одной страницей контента Префиксы, используемые для обозначения типа браузера, определены в конфи- гурационном файле ASP.NET, содержащем информацию о браузерах. Наиболее рас- пространенные идентификаторы перечислены в табл. 6-2. Табл. 6-2. Идентификаторы распространенных браузеров Идентификатор Браузер IE Netscape3 Netscape4 Netscape6to9 Mozilla Opera Up Любая версия Internet Explorer Netscape Navigator 3.x Netscape Communicator 4.x Любая версия Netscape выше 6.0 Firefox Opera Openwave-устройства Как видите, можно делать различие не только между современными и устарев- шими браузерами, но и между браузерами и другими устройствами, такими как со- товые телефоны и КПК. Отмечу, что в случае использования эталонных страниц, предназначенных для конкретных устройств или браузеров, необходимо также задать эталонную страницу, не связанную с определенным устройством, которая будет ис- пользоваться по умолчанию. ©Внимание! Информация о браузере хранится в ASP.NET 1 .х и ASP.NET 2.0 по-разному. В системе ASP.NET 1.x вы найдете ее в разделе <browserCaps> файла machine.config, а в ASP.NET 2.0 — в текстовом файле с расширением .browser, находящемся в разделе %WINDOWS%\Microsoft.NET\Framework\[BepcMfl]\Config\Browsers. Определение заголовка страницы Страница контента должна состоять только из набора тэгов <asp:Content>, поэтому она не может содержать тэг разметки, определяющий ее заголовок. Можно, конечно,
Страницы с богатым интерфейсом Глава 6 213 включить тэг <title> в эталонную страницу, но она по определению используется в качестве шаблона множества страниц, у каждой из которых должен быть свой за- головок. Поэтому в странице контента заголовок задается с помощью атрибута Title директивы ©Page'. <@>Page MasterPageFile="simple.master" Title='Hello, master" %> Учтите, что установка заголовка страницы возможна только в том случае, когда тэг <title> или <head> эталонной страницы помечен атрибутом runat^ server. Обработка эталонной страницы и страниц контента Когда страницы ASP.NET связаны с эталонными страницами, процесс их анализа и компиляции несколько изменяется. Ведь такая страница состоит не из одного, как обычно, а из двух файлов: .aspx и .master. Если в один из них вносится изменение, сборка страницы создается заново. Компиляция эталонных страниц Когда пользователь запрашивает ресурс .aspx, связанный со страницей контента, которая содержит ссылку на эталонную страницу, исполняющая среда ASP.NET на- чинает свою работу с анализа зависимости между этими двумя страницами Данная информация сохраняется в локальном файле, который ASP.NET создает в своей вре- менной папке. Затем анализируется исходный код эталонной страницы и генерируется класс Visual Basic.NET или С#, в зависимости от того, какой из этих языков задан в эталонной странице. Данный класс является производным от класса MasterPage или класса, определенного в файле кода эталонной страницы. После создания класс компилируется в сборку. Если в одной папке обнаружено несколько файлов с расширением .master, все они обрабатываются одновременно и для каждого из них генерируется динамическая сборка, даже если только один из них используется страницей, которую запросил пользователь. Однако задержка, вызванная компиляцией этой группы файлов, про- исходит только однажды, когда пользователь в первый раз запрашивает страницу контента из данной папки. Если в следующий раз потребуется другая эталонная страница из этой же папки, время отклика будет меньше, поскольку страница уже откомпилирована. Вывод страницы для пользователя Итак, вы уже знаете, что каждая страница ASP.NET, связанная с эталонной, должна иметь определенную структуру: весь ее контент необходимо заключить внутрь тэгов <asp:Content>, вне которых не разрешается размещать ни серверные элементы управ- ления, ни литеральный текст. Связь между таким тэгом и его заменителем в эталонной странице устанавливается посредством свойства ID. Тэг <asp:Content> использует- ся как контейнер элементов управления, подобно элементу управления Panel или HTML-тэгу <div>. Весь текст разметки, содержащийся внутри тэга <asp:Content>, компилируется в шаблонный элемент и связывается с соответствующим свойством заменителя, входящего в состав класса эталонной страницы. Эталонная страница является особого рода пользовательским элементом управ- ления, и не случайно ее родительский класс MasterPage наследует класс UserControl. Создав ее класс — класс пользовательского элемента управления, — исполняющая среда дополняет его шаблонами, которые генерируются на основе разметки, опреде- ленной в странице контента. Затем полученный таким образом элемент управления добавляется в дерево элементов управления текущей страницы. Никаких других элементов управления это дерево не содержит, за исключением тех, которые могут
214 Часть I Разработка страниц ASP.NET быть добавлены в него эталонной страницей. Схема формирования предоставляемой пользователю результирующей страницы представлена на рис. 6-5. Страница контента Contentplaceholder 1 Contentplaceholder 2 Эталонная страница Предоставляется пользователю Рис. 6-5. Структура страницы, полученной в результате слияния эталонной страницы и страницы контента Вложенные эталонные страницы До сих пор мы имели дело с очень простым отношением между эталонной страницей и набором страниц контента. Однако топология этого отношения бывает и гораздо более сложной. При желании можно связать одну эталонную страницу с другой, создав их иерархию. Когда эталонные страницы образуют иерархию, те из них, ко- торые являются дочерними, рассматриваются как страницы контента, дополненные элементами управления ContentPlaceHolder. Иными словами, дочерняя эталонная страница по отношению к родительской является страницей контента, а по отноше- нию к дочерней — эталонной страницей. При этом она состоит из элементов управ- ления <asp:Content> и <asp:ContentPlaceHolder>. Подобно любой другой странице контента дочерняя эталонная страница содержит ссылку на родительскую страницу и предоставляет для ее заменителей блоки контента. А подобно эталонным страницам она содержит заменители для блоков контента дочерних страниц. Примечание Хотя архитектурного ограничения на количество уровней вложенности эталонных страниц сайта не существует, большая глубина их вложенности отрицатель- но сказывается на производительности и масштабируемости приложения. Помните, что отправляемая пользователю конечная страница всегда компилируется по требованию и не модифицируется до тех пор, пока не будут внесены изменения в файлы, от которых она зависит. Давайте расширим приведенный выше пример, добавив в него промежуточную эталонную страницу. Корневая эталонная страница с именем parent.master будет опре- делять верхний и нижний колонтитулы страницы и заменяемую область. За исклю-
Страницы с богатым интерфейсом Глава 6 215 чением имен классов исходный код нашего нового примера идентичен предыдущему. Так что первым делом мы рассмотрим промежуточную эталонную страницу content, master: <%@ Master Language="C#" MasterPageFile="Parent.master" CodeFile="Content.master.cs" Inherits- ContentMaster" %> <asp:Content Runat="Server" ContentPlaceHolderID="ContentOfThePage" > <table width="1OO%"><tr> <td> <h1>Welcome to this page!</h1> <h3>The rest of the page is kindly offered by our sponsor page-the master!</h3> </td> <td align="center"> <h2>Select Your Favorite Chapter</h2> <asp:ContentPlaceHolder runat="server" ID="ChapterMenu" /> </td> </trx/table> </asp:Content> Как видите, данная страница содержит некоторое сочетание тэгов <asp:Content> и <asp:ContentPlaceHolder>. В самом ее верху располагается директива ©Master, имею- щая атрибут MasterPage File, характерный для страниц контента. Ресурс content.master не может быть запрошен непосредственно, поскольку он содержит подстановочные области. Если вы знакомы с терминологией объектно-ори- ентированного программирования, то я могу сказать, что промежуточный эталонный класс подобен виртуальному классу, переопределяющему некоторые методы родитель- ского класса, но содержащему наряду с ними абстрактные методы, предназначенные для реализации в другом производном классе. И точно так же, как нельзя создать экземпляр абстрактного класса, нельзя открыть в браузере эталонную страницу, на каком бы уровне иерархии она ни находилась. В любом случае ресурс content.master, без сомнения, является главным классом, и его файл кода содержит класс, производ- ный от MasterPage. О Внимание! Поскольку Visual Studio .NET 2005 не поддерживает визуальное редактиро- вание вложенных эталонных страниц, промежуточную эталонную страницу можно раз- рабатывать как страницу контента, а потом заменить ее главную директиву директивой ©Master, удалить атрибут Title и в качестве базового класса указать в ее файле кода класс MasterPage. Ниже приведен пример страницы контента, основанной на двух иерархически связанных между собой эталонных страницах: <%@ Page Language="C#" MasterPageFile="Content.master" CodeFile="ViewBook.aspx.cs" Inherits="ViewBook" Title="Book Viewer" %> <asp:Content ContentPlaceHolderID="ChapterMenu” Runat="Server"> <asp:DropDownList runat="server‘> </asp:DropDownListxbr /> <asp:Button runat="server" Text="Read ...” /> </asp:Content> Результат вызова этой страницы показан на рис. 6-6.
216 Часть! Разработка страниц ASP.NET Рис. 6-6. Результат соединения одной страницы контента и двух эталонных страниц Как видите, ничто на рисунке не указывает на существование двух эталонных страниц. Поэтому сообщу, что промежуточная эталонная страница определяет область между верхним и нижним колонтитулами страницы, а размещенные справа раскры- вающийся список и кнопка поиска определены в странице контента. Это означает, что можно без труда создать похожую страницу, предлагающую другой метод поиска главы. Взгляните на приведенный ниже код и рис. 6-7. <%@ Page Language="C#" MasterPageFile="Content.master" CodeFile="SearchBook.aspx.cs" Inherits="SearchBook" %> <asp:Content ContentPlaceHolderID="ChapterMenu" Runat="Server"> <asp:TextBox runat="server" Text=”[Enter keywords]" /> <asp:LinkButton runat="server" Text="Search ..." /> </asp:Content> Рис. 6-7. Страница, созданная с помощью другого кода Если вы будете грамотно применять технологию эталонных страниц, то вскоре придете к заключению, что если одна страница мало отличается от другой, то раз- личия в их коде тоже будут небольшими.
Страницы с богатым интерфейсом Глава 6 217 Программирование эталонной страницы В программном коде страниц контента можно обращаться к методам, свойствам, переменным и элементам управления эталонной страницы. Однако учтите, что все они доступны странице контента только при условии, что в классе эталонной страницы объявлены как открытые. Вы можете обращаться к открытым переменным, областью видимости которых является страница, открытым свойствам и открытым методам, Открытие доступа к свойствам эталонной страницы Для того чтобы присвоить элементу управления эталонной страницы идентификатор, нужно установить его атрибуты runat и ID. Можно ли после этого обращаться к дан- ному элементу управления из страницы контента? Непосредственно — нет. Доступ к объектам эталонной страницы осуществляется с помощью свойства Master страницы контента, содержащего ссылку на объект эталонной страницы. Причем через него до- ступны лишь те свойства и методы класса эталонной страницы, которые определены как открытые. Ниже приведен код, которым необходимо дополнить определение класса нашей эталонной страницы, чтобы ее заголовок стал доступен как открытое свойство: public partial class SimpleWithProp System.Web.UI.MasterPage { protected void Page_Load(object sender, EventArgs e) { } public string TitleBoxText { get { return TitleBox.Text; } set { TitleBox.Text = value; } } } Заголовк страницы, показанной на рис. 6-3, представлен элементом управления Label с именем TitleBox. Уровень защиты этого элемента управления делает его не- доступным для внешнего мира, но открытое свойство TitleBoxText класса страницы создает для свойства Text данного элемента доступную извне оболочку. В результате эталонная страница получает дополнительное открытое свойство, посредством кото- рого разработчики могут устанавливать текст заголовка. Вызов свойств эталонной страницы Свойство Master — единственная точка соприкосновения между эталонной страницей и страницей контента. Однако указанное свойство имеет тип MasterPage, и поэтому ему ничего не известно о собственных свойствах и методах эталонной страницы. Иными словами приведенный ниже код не будет откомпилирован, поскольку в классе MasterPage свойство TitleBoxText не определено: public partial class HelioMaster System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { Master.TitleBoxText = Programming ASP.NET-version 2.0"; } }
218 Часть I Разработка страниц ASP.NET Каков же настоящий тип эталонной страницы? Имя ее класса образуется в соот- ветствии с обычным соглашением об именах страниц ASP.NET: ASP.XXX master, trq XXX — имя файла эталонной страницы. Разработчик может задать для данного класса иное имя, указав его в атрибуте ClassName директивы ©Master. <%@ Master Inherits="SimpleWithProp" ... Classname="MyMaster" %> Для вызова пользовательских свойств или методов эталонной страницы из кода страницы контента нужно сначала привести объект, возвращенный свойством Master, к типу эталонной страницы: ((ASP.MyMaster)Master).TitleBoxText = "Programming ASP.NET-version 2.0"; Интересно, что у Visual Studio .NET 2005 имеются функции, позволяющие иден- тифицировать динамически генерируемый тип еще во время разработки (рис. 6-8). ProAspNet20 Microsoft Visual Studio Efe £dt yiew Refactor Webjfte pebug loots Wndow Community tHp Any CPU '-aJfw 4 . a* i Здфм/СЬОб/. thPropmastet Samptes/Cb06/_a*ter.a»px.c*' JHr oMaster System; System.Data; System.Conf igurat ion; System.Collections; System.Ueb; Systern.Feb.Secur ity; System.Feb. UI; System.Feb.UI.BebControls; System.Feb.UI.BebControls.BebParts; System.Beb.UI.HtmlControls; pusing using using using using using using using using using И£ Page_Load(objert sends Qpublic partial class Hilldla tur : Systein.Qeb.UI.Page protected void Page_Lot*d( object sender, EventArqi ((ASP.^lyMaeter)Raster).TitleBoxText “ "Programming ASP.NET—version 2.0”; i^Gobal_asax , HetoMaste*. aspx Myfteste Рис. 6-8. Visual Studio .NET 2005 выводит всплывающую подсказку с именами классов, которые будут созданы динамически лишь при вызове страницы Пространство имен ASP принадлежит системе, и в него включаются все ди- намически генерируемые системой типы. Функция IntelliSense из Visual Studio. NET 2005 распознает его правильно, чего нельзя сказать о предыдущей версии Visual Studio. Директива @ MasterType Если в состав страницы контента включить директиву ©MasterType, то выполнять указанную выше операцию приведения типа не понадобится. Эта директива сообщает компилятору, каков на самом деле тип свойства Master. <%@ Page Language="C#" MasterPageFile="SimpleWithProp.master" CodeFile="HelloMasterType.aspx.cs” Inherits="HelloMasterType" %> <%@ MasterType VirtualPath="SimpleWithProp.master" %>
Страницы с богатым интерфейсом Глава 6 219 При ее наличии в динамически сгенерированном классе страницы данное свой- ство объявляется правильно и любые обращения к нему становятся строго типизи- рованными. Например, файл страницы контента может в таком случае содержать следующий код: « protected void Page_Load(object sender, EventArgs e) Master.TitleBoxText = "Programming ASP NET-version 2.0"; } Директива ©MasterType поддерживает два взаимоисключающих атрибута: Virtual- Path и TypeName. Оба они предназначены для идентификации класса эталонной страницы, но в первом задается ее URL, а во втором — имя типа. Динамическая смена эталонной страницы Для декларативного связывания страницы контента с эталонной страницей исполь- зуется атрибут MasterPageFile директивы ©Page. Однако у класса Page имеется так- же свойство MasterPageFile, в котором содержится имя файла эталонной страницы. Можно ли с помощью этого свойства выбирать эталонную страницу динамически, с учетом условий времени выполнения? В ASP.NET 2.0 это возможно. Такое решение подходит, например, для приложений, которые позволяют пользователю применять различные обложки. Однако важно пра- вильно выбрать момент для программной установки свойства MasterPageFile, а именно сделать это в обработчике события Prelnit страницы, то есть до того, как исполняющая среда начнет обработку запроса: protected void Page_PreInit(object sender, EventArgs e) { MasterPageFile = "simple2.master"; } Если вы попытаетесь установить свойство MasterPageFile в обработчике события Init или Load, будет сгенерировано исключение. " ' j Примечание Свойство Master представляет текущий экземпляр эталонной страницы, оно доступно только для чтения и не может быть установлено программным способом. Его устанавливает исполняющая среда после загрузки содержимого файла, указанного в свойстве MasterPageFile. Темы В течение продолжительного времени CSS-стили помогали разработчикам сайтов единообразно оформлять страницы. Но хотя выбор таблицы стилей можно осущест- влять программно на сервере, CSS по сути своей является клиентской технологией, изначально предназначавшейся для применения обложек к HTML-элементам. А при разработке страниц ASP.NET вы имеете дело преимущественно с серверными эле- ментами управления. Стили CSS можно использовать для оформления серверных элементов управле- ния, но это не лучшее средство для выполнения данной задачи. Главный недостаток CSS-стилей заключается в том, что у элементов управления ASP.NET могут быть свойства, не имеющие прямого соответствия среди стилевых свойств CSS. Внешний вид элемента управления определяется целой группой ресурсов, среди которых изо- бражения, строки, шаблоны, разметка, сочетание нескольких CSS-стилей. Чтобы правильного применить обложки к элементам управления ASP.NET, одних только CSS-файлов недостаточно. Для этой цели используются темы ASP.NET.
220 Часть I Разработка страниц ASP.NET Темы ASP.NET тесно связаны с темами Windows ХР. Для задания темы достаточно установить единственное свойство, и к выбранному объекту — будь то отдельный элемент управления, страница или даже весь сайт — тут же будут применены все ее установки. ©Внимание! Темы являются нововведением ASP.NET 2.0, а в ASP.NET 1 .х встроенная функция их поддержки отсутствует. О том, как создать в этой системе аналогичную инфраструктуру, я рассказал в июньском выпуске MSDN Magazine. Нужную статью вы найдете по адресу http://msdn.microsoft.com/msdnmag/issues/04/06/CuttingEdge. Понятие темы в ASP.NET Разрабатывая страницу в ASP.NET 1.x, вы не можете позволить себе сконцентриро- ваться только на задачах, которые выполняются определенным набором элементов управления, поскольку необходимо позаботиться и о внешнем виде этих элементов. В простейшем случае внешний вид элемента управления определяется такими атри- бутами, как цвета, шрифт, рамка и изображение. Но чем сложнее элемент управле- ния, тем больше у него подобных атрибутов и тем больше времени вы уделяете его внешнему виду. В ASP.NET 1.1 элемент управления DataGrid— один из самых популярных и наиболее гибко настраиваемых элементов — предлагает на выбор целую галерею предопределенных стилей, для чего используется его функция автоформа! ирова- ния. Встроенные стили элемента управления DataGrid реализованы в виде набора предопределенных установок, которые Visual Studio .NET 2003 применяет к нему во время разработки. Функция автоформатирования существенно экономит время раз- работчика, позволяя выбирать стили визуальным способом. Однако у нее есть два существенных недостатка. Во-первых, визуальные атрибуты по-прежнему содержатся в .aspx-файле, из-за чего у страниц с богатым интерфейсом этот файл становится не- читабельным, а во-вторых, список доступных форматов фиксирован и его нельзя ни изменить, ни расширить. А ведь как хорошо было бы просто выбирать элементы управления на панели элементов, размещать их на странице и связывать между собой, не беспокоясь о том, как они будут выглядеть! В таком случае внешний вид каждого типа элементов управления можно было бы определять в дополнительном файле проекта, предназна- ченном специально для хранения визуальных атрибутов всех элементов. Изменения, вносимые в этот файл, отражались бы на всех элементах управления сайта, и его ин- терфейс всегда оставался бы согласованным. Исходные же файлы .aspx не содержали бы установок визуальных атрибутов. Подумайте, как заманчиво все это звучит! А ведь я рассказал о существующей функции ASP.NET 2.0 — о темах! Так что же такое тема Тема — это набор обложек и связанных с ними файлов, таких как изображения и та- блицы стилей; можно сказать, что это нечто вроде супер-CSS. Когда поддержка тем включена, внешний вид всех элементов управления, связанных с определенной темой, задается централизованно. Рассмотрим такую разметку: <aspiCalendar ID="Calendar1" runat=”server" /> Без использования тем определенный здесь календарь будет вы глядеть по-спар- тански скромно. Но стоит задать для него тему, и он станет красочным и привлека- тельным. Таким образом, между контентом и форматированием страницы в систему ASP.NET 2.0 проведена четкая граница. Взгляните на рис. 6-9. Какой из двух кален- дарей, по-вашему, оформлен без применения темы?
Страницы с богатым интерфейсом Глава 6 221 May 2005 » May 2005 Sun Mon Tue Wed Thu Fri Sat 1 HI h 24 25 26 27 28 29 30 25. 25 2S 1 2 3 4 5 6 7 1 2 3 El 5 8 9 10 11 12 13 14 fi 2 15 ii 12 15 16 17 18 19 20 21 15 15 12 15 12 22 23 24 25 26 27 28 25 24 25 25 29 30 31 1 2 3 4 22 32 31 1 2 £ 2Q Рис. 6-9. Sa Z 14 ?9. Один и тот же элемент управления выведен сам по себе и с применением темы Чтобы вы смогли лучше понять принципы использования тем в ASP.NET 2.0, пред- лагаю ознакомиться с несколькими терминами, приведенными в табл. 6-3. Табл. 6-3. Терминология тем в ASP.NET Термин Определение Обложка (skin) Именованный набор свойств и шаблонов, который можно приме- нить к любому количеству элементов управления страницы. Об- ложка всегда связана с определенным типом элементов управления Таблица стилей (style sheet) Тема таблицы стилей (style sheet theme) CSS или серверная таблица стилей, которая может использоваться страницами сайта Тема, используемая для абстрагирования свойств элементов управ- ления от самих этих элементов. Элемент управления может пере- определить такую тему Настроечная тема (customization theme) Тема, используемая для абстрагирования свойств элементов управ- ления от самих этих элементов. Она переопределяет любые уста- новки, заданные в объявлении элемента управления и в определе- нии темы таблицы стилей Представьте себе, что вы создаете новый Web-сайт и хотели бы изначально сделать его как можно более привлекательным. Вместо того чтобы изучать все доступные стилевые свойства используемых элементов управления и затем их конфигуриро- вать, можно просто применить тему ASP.NET. Сделать это очень легко — достаточно установить одно-единственное свойство страницы. В результате она автоматически приобретет заданное оформление, которое, можно надеяться, будет довольно привле- кательным. И если затем разместить на такой странице, скажем, элемент управления Calendar, он автоматически примет вид, соответствующий ее общему оформлению. Задав тему страниц, вы не окажетесь привязанным к ее установкам. В дизайнере Visual Studio .NET можно просмотреть страницы и при желании вручную подкор- ректировать стили некоторых элементов управления. Примечание В этой книге и другой подобной литературе действует следующее согла- шение: если не указано иное, под словом «тема» понимается настроечная тема, а когда речь идет о теме таблицы стилей, термин приводится полностью. Структура темы Физически тема представляет собой совокупность файлов и папок, хранящихся в одной корневой папке, имя которой является именем этой темы. Темы могут быть глобальными и локальными. Глобальные темы видимы всем приложениям, установ- ленным на серверном компьютере, а локальные — только тому приложению, в котором они определены. Каталоги глобальных тем расположены по адресу: %WINDOWS%\Microsoft.NET\Framework\[0epcnfl]\ASP.NETClientFiles\Themes
222 Часть I Разработка страниц ASP.NET Локальные же темы находятся во вложенных каталогах папки App Themes при- ложения. На рис. 6-10 показана папка темы ProAspNet20 в составе демонстрационного Web-приложения настоящей книги. Рис. 6-10. Тема демонстрационного приложения книги в Visual Studio .NET 2005 Тема, папку которой вы видите на данном рисунке, состоит из файлов .css и .skin. В общем случае тема может содержать следующие ресурсы. Файлы CSS Называемый также таблицей стилей, файл CSS содержит задан- ные в специальном формате определения визуальных стилей, применяемых к элементам HTML-документа. Он располагается в корневой папке темы. Старые Web-браузеры, поддерживающие HTML версии 3.2 и более ранних версий, не при- меняют CSS-стили. Текущую спецификацию стандарта CSS, поддерживаемого и постоянно обновляемого консорциумом W3C, вы найдете на сайте последнего по адресу http://www.w3 org. Файлы обложек Файл обложки содержит специфическую для заданной темы разметку определенного набора элементов управления. Он состоит из последова- тельности определений элементов управления, содержащих значения большинства их визуальных свойств, и шаблонов. Каждая обложка связана с конкретным ти- пом элементов управления и имеет уникальное имя. Для одного типа элементов управления можно определить несколько обложек. Когда к элементу управления применяется обложка, его разметка, содержащаяся в aspx-файле страницы, моди- фицируется с учетом установок обложки. Способ модификации элемента управ- ления зависит от того, какая именно тема к нему применяется — настроечная или тема таблицы стилей. Файлы обложек содержатся в корневой папке темы. Файлы изображений Элементы управления с богатым интерфейсом часто со- держат изображения. Так, в многостраничном элементе управления DataGrid они используются на первой и последней страницах. Изображения, входящие в состав обложки, обычно хранятся в подпапке Images папки ее темы (имя подпапки можно изменить, но оно должно соответствовать имени, заданному в URL графических элементов обложки.) Шаблоны Действие обложки элемента управления может распространяться не только на его визуальные свойства, но и на структуру, при условии, что данный
Страницы с богатым интерфейсом Глава 6 223 элемент управления поддерживает шаблоны. Это позволило бы вам, меняя опреде- ление шаблона в теме, изменять внутреннюю структуру элемента управления, не затрагивая, однако, его программный интерфейс и поведение. Шаблон определя- ется как компонент обложки элемента управления и хранится в файле обложки. Перечисленные типы компонентов темы охватывают большую часть данных, которые вы могли бы пожелать включить в ее состав. Однако этот набор типов не является замкнутым — в папку темы можно добавить дополнительные вложенные папки, поместив в них любые данные, имеющие смысл для элементов управления, к которым будет применяться данная тема. Представьте, например, что у вас имеется специализированный элемент управления, который выводит свой пользовательский интерфейс при помощи внешнего пользовательского элемента управления ASP.NET (.ascx). Составляя обложку этого специализированного элемента управления, вы мо- жете указать в ней URL пользовательского элемента управления — тогда последний станет частью темы и его можно будет хранить в папке данной темы. Где именно, решать вам, но, пожалуй, неплохим выбором будет создание для него подкаталога Controls. Мы еще вернемся к этому примеру, когда будем обсуждать разработку де- монстрационной темы. Настроечные темы и темы таблиц стилей Мы говорили, что существуют два вида тем: настроечные и темы таблиц стилей. Пер- вые применяются для окончательной настройки сайта, и заданные в них установки переопределяют установки свойств элементов управления, содержащиеся в исходных файлах .aspx. Изменив тему страницы, вы полностью меняете ее внешний вид, не кор- ректируя ни строчки в ее исходном файле. Поэтому в случае применения настроечных тем страница может содержать для каждого элемента управления минимум разметки. Темы таблиц стилей подобны обычным каскадным таблицам стилей, но они воз- действуют на свойства элементов управления, а не на стили элементов HTML. Такие темы применяются к элементам управления сразу после их инициализации, до того как будут применены атрибуты, заданные в файле .aspx. Используя тему таблицы стилей, разработчик определяет значения свойств элементов управления по умолчанию, кото- рые затем могут быть переопределены установками, заданными в исходном файле .aspx. ©Внимание! Исходные файлы настроечных тем и тем таблиц стилей совершенно одина- ковы. Разница заключается лишь в том, как исполняющая среда ASP.NET применяет к странице заданные в них установки. Одна и та же тема может быть использована и как настроечная, и как тема таблицы стилей. Настроечные темы всегда имеют приоритет перед темами таблиц стилей. Давайте посмотрим, каким получится элемент управления, если применить к нему одновре- менно темы обоих видов. Предположим, у нас имеется такая разметка: <asp:Calendar ID="Calendar1" runat="server" backcolor="yellow“ /> Если содержащая ее страница связана с настроечной темой, внешний вид кален- даря будет соответствовать установкам этой темы. В частности, его фон станет таким, как указано в файле темы. Но если эта страница связана только с темой таблицы стилей, фон календаря будет желтым, а остальные его свойства исполняющая среда установит в соответствии с определением темы. Применение тем к страницам и элементам управления Темы можно применять на разных уровнях: приложения, папки и отдельной страницы. Кроме того, в рамках одной темы можно выбирать для элементов управления одного типа разные обложки.
224 Часть I Разработка страниц ASP.NET Выбор темы на уровне приложения затрагивает все его страницы и элементы управления. Соответствующая установка задается в файле web.config приложения: <system.web> <pages theme="ProAspNet20" /> </system.web> Атрибут theme определяет настроечную тему, тогда как атрибут styleSheetTheme — тему таблицы стилей. Аналогичным образом задается тема, применяемая ко всем стра- ницам в определенной папке: приведенную выше установку включают в содержащийся в этой папке файл web.config. Наконец, тему можно задать для конкретной страницы, чтобы только к ней применялись входящие в состав этой темы стили и обложки. Установка темы страницы Для того чтобы связать тему с определенной страницей, нужно установить атри- бут Theme или StyleSheetTheme директивы ©Page. Первый из этих атрибутов опреде- ляет настроечную тему, а второй — тему таблицы стилей: <% @Раде 1_апдиаде="С#" Theme=”ProAspNet20" %> <% @Раде Iапдиаде=”С#" StyleSheetTheme="ProAspNet20" %> Имейте в виду, что имя выбранной темы должно соответствовать имени существу- ющего подкаталога папки App_Themes приложения либо имени глобальной темы. Если существуют две темы с заданным именем, локальная и глобальная, преимуще- ство отдается локальной теме. На рис. 6-11 показано, какую помощь в выборе темы предлагает функция IntelliSense. ProAspNet20 Microsoft Visual Studio E View Website ‘Id Debug Tools Window Community • J .# ?. • ’ . ’ • Debut r=* Jnternet Explorer 6.0 • ErrorPage«/Error4O4.a»px* ErrcrPages/Web.Cpnfig -< Client Objects вс Event* ff || <*0 Page TheKie»z,^lzLanguage»"C#" AutoEventEJireu Wl WF[ProAsp\ *20~" <’DOCTYFE ht SmokeAndGlass" IC//DTD XHTML 1.1/ | W' <html xnHng-"httD://ww.w3 ,QFq/1999/xhtKilH > ф <head runat»"server"> Рис. 6-11. Поддержка тем функцией IntelliSense в Visual Studio .NET 2005 Говоря о приоритетности тем, следует отметить их иерархическую природу: темы уровня каталога имеют приоритет перед темами уровня приложения, а темы уровня страницы — перед теми и другими. Это правило действует как в отношении настро- ечных тем, так и в отношении тем таблиц стилей. Примечание Хотя возможна одновременная установка атрибутов Theme и StyleSheet- Theme, поступать так не рекомендуется. Дело в том, что исполняющая среда сначала применит тему таблицы стилей, потом установки, заданные в файле страницы, и лишь после этого настроечную тему. Конечный результат будет зависеть от механизма каска- дирования CSS и определяться CSS-установками каждой темы. • .... г Применение обложек Файл обложки напоминает обычную страницу ASP.NET и содержит объявления эле- ментов управления и директивы импорта. Объявление элемента управления определяет,
Страницы с богатым интерфейсом Глава 6 225 как он будет выглядеть по умолчанию. Для примера рассмотрим еще один фрагмент файла обложки: <!— Это пример обложки элемента управления Button —> <asp:Button runat="server" Bo rde rColo r=" da r kg ray" Font-Bold=' true" BorderWidth="1px" Bo rde rStyle="outset" ForeColor=”DarkSlateGray" BackColor="gainsboro" /> Если применить данную обложку к странице, каждый ее элемент управления Button будет выводиться в соответствии с установками, заданными в приведенной разметке. При этом если обложка входит в состав темы таблицы стилей, разработчик страницы сможет переопределить указанные установки, если же это настроечная тема, именно от них будет зависеть окончательный внешний вид элементов управления. Свойствам, которые в теме не заданы, будут присвоены значения, определяемые элементом управления по умолчанию или заданные в файле .aspx. ©Внимание! Какую бы тему вы ни применили — настроечную или тему таблицы сти- лей — свойства элементов управления всегда можно изменить программным способом в обработчиках событий страницы, в частности событий Init и Load. Тема может содержать несколько обложек для определенного типа элементов управления, и все эти обложки, кроме используемой по умолчанию, должны быть именованными. Уникальное имя обложки задается в ее атрибуте SkinlD. Установка обложки конкретного элемента управления в файле страницы .aspx выполняется с помощью свойства SkinlD этого элемента. Значение данного свойства должно соот- ветствовать имени существующей обложки текущей темы. Если тема страницы не содержит обложки с заданным именем, к элементу управления будет применена об- ложка, используемая по умолчанию. Приведем пример двух именованных обложек кнопки, определенных в составе одной темы: <! — Поместите эти два определения в один файл .skin —> <asp:button skinid="skinClassic” BackColor="gray" /> <asp button skinid='skinTrendy" BackColor="lightcyan" /> Когда для страницы задана тема, все элементы управления страницы оформляются согласно установкам этой темы, за исключением тех элементов и отдельных свойств, для которых поддержка тем явно отключена. га Примечание Автоматическое применение установок темы ко всем элементам управле- ния страницы облегчает ее настройку даже в том случае, когда эта страница, подобно страницам, разработанным ранее для ASP.NET 1.x, ничего не знает об обложках. Управление применением тем Инфраструктура поддержки тем ASP.NET 2.0 предоставляет разработчику булево свойство EnableTheming, с помощью которого можно отключить поддержку обложек элементом управления и его дочерними элементами или поддержку тем страницей. По умолчанию это свойство содержит значение true, то есть тема, если таковая зада- на, используется. Свойство EnableTheming определено в классе Control и наследуется всеми серверными элементами управления и всеми страницами. Если вы хотите от- ключить поддержку тем для всех элементов управления страницы, установите в ее директиве ©Page атрибут EnableTheming в false. Программным способом свойство EnableTheming статических элементов управ- ления (то есть тех, которые объявлены в исходном файле страницы .aspx) может
226 Часть I Разработка страниц ASP.NET быть установлено только в обработчике события PagePrelnit. Для динамически же создаваемых элементов управления данное свойство должно быть установлено до того, как элемент будет добавлен в дерево элементов управления страницы. Это про- исходит в тот момент, когда вы добавляете элемент в коллекцию Controls родитель- ского элемента управления (обычно им является форма или входящий в ее состав контейнерный элемент). Какой же смысл в том, чтобы задать тему страницы или обложку элемента управле- ния и тут же ее отключить? Темы предназначены для придания элементам управления и страницам согласованного внешнего вида, но в то же время они переопределяют ви- зуальные атрибуты элементов управления, для которых в их составе заданы обложки. До некоторой степени вы можете контролировать процесс переопределения, выбирая подходящие таблицы стилей и настроечные темы. Однако иногда бывает необходимо сохранить тот внешний вид страницы или элемента управления, который опреде- лен для него в файле .aspx. В таких случаях и применяется свойство EnableTheming. Заметьте, что отключение темы касается только обложек, но не CSS-стилей. Ког- да в состав темы входит один или более файлов каскадных таблиц стилей, все они связываются с тэгом <head> результирующего HTML-документа и обрабатываются браузером. А браузер знать не знает ни о каких темах ASP.NET! Применение тем к элементам управления Тема может задавать столько свойств элемента управления, сколько он сам позволит. Это значит, что для любого из свойств разработанного вами элемента управления поддержку тем можно отключить, для чего применяется атрибут Themeable'. [Themeable(false)] public virtual bool CausesValidation { get { ... } set { .. } } Встроенные серверные элементы управления не позволяют изменять значение этого атрибута. Что касается специализированных элементов управления, то с ними вы должны использовать атрибут Themeable, чтобы запретить тематическую настройку поведенческих свойств, таких как упоминаемое выше свойство CausesValidation, ведь темы предназначены только для настройки визуальных свойств элементов управления. Наконец, атрибут Themeable можно установить для всего класса пользовательско- го элемента управления — в таком случае не придется беспокоиться о том, что этот элемент управления может быть изменен с помощью тем: [Themeable(false)] public MyControl : Control { } Разработка тем Найти темы, которые подходили бы для вашего приложения, — не проблема. Если только вам не требуется что-то уж очень особенное, то, скорее всего, вы найдете нужные темы, стоящие недорого или даже распространяемые бесплатно. Однако не лишним будет научиться создавать темы самостоятельно. Как уже упоминалось, тема обычно включает несколько файлов, в том числе фай- лы каскадных таблиц стилей и обложек элементов управления, с помощью которых
Страницы с богатым интерфейсом Глава 6 227 оформляются элементы HTML и серверные элементы управления. Кроме того, в ее состав могут входить изображения и другие файлы. Лично я глубоко убежден, что разработка привлекательной, гармоничной и удоб- ной темы не является задачей программиста. Это дело дизайнеров и художников, которые справятся с ним во сто крат лучше и быстрее. Но поскольку в составе темы определяются свойства элементов управления, то можно сказать, что темы относятся также и к сфере деятельности программиста. Иными словами, программист должен объяснить дизайнерам, что от них требуется и какова структура конкретной темы, причем в этом случае дизайнерам нужно от него гораздо больше сведений, чем при разработке каскадных таблиц стилей. Изучение структуры файлов тем мы начнем с рассмотрения ключевых различий между ними и файлами каскадных таблиц стилей. CSS и темы Темы подобны каскадным таблицам стилей в том отношении, что и те и другие опреде- ляют набор применяемых к странице атрибутов. Однако есть между ними и суще- ственные различия. Прежде всего, тема определяет значения свойств элементов управления, a CSS — стили HTML-элементов. Поэтому в состав темы можно включать дополнительные файлы. Также с ее помощью можно, например, задать изображения для элементов управления TreeView или Menu, шаблон страниц для элемента управления DataGrid и раскладку элемента управления Login. Наконец, настроечные темы могут пере- определять локальные значения свойств, чего ни при каких условиях не могут делать каскадные таблицы стилей. Поскольку тема включает определения каскадных таблиц стилей и применяет их вместе с другими установками свойств, нет никакого смысла использовать в прило- жениях ASP.NET 2.0 вместо тем CSS. Создание темы Для того чтобы создать новую тему в рамках решения Visual Studio .NET 2005, нужно первым делом создать в папке AppThemes новую вложенную папку. Проще всего это сделать так: щелкнуть правой кнопкой мыши узел App Themes и выбрать команду Add ASP.NET Folder\Theme. После этого вы сможете добавить в созданную папку нужные файлы. Когда вы завершите работу над темой, то, возможно, захотите пере- местить ее целиком в корневой каталог глобальных тем Web-сервера. Перечень типичных вспомогательных файлов темы представлен на рис. 6-12. Это файлы CSS, обложек, текстовые и XML-файлы, а также файлы расширяемых таблиц стилей (extensible StyLe-sheeT, XSLT). Пустые файлы заданного типа создаются в папке темы, а затем редактируются с помощью более или менее специализированных редакторов в Visual Studio .NET 2005. Файл обложек представляет собой набор блоков разметки элементов управления, именованных посредством атрибута SkinlD или безымянных. Проще всего сформи- ровать содержимое такого файла, скопировав в него разметку элементов управления, размещенных в рабочей странице и сконфигурированных визуальным способом. Если для свойств элементов управления потребуются какие-нибудь ресурсы, можно поме- стить их файлы в папку темы и задать в файле обложки пути к этим ресурсам: <asp:BulletedList runat="server" Font-Names="Verdana" BulletImageURL=*Tmages/sniokeandglass_bullet2. gif" BulletStyle="CustomImage"
228 Часть I Разработка страниц ASP.NET BackColo r="t ranspa rent" ForeColor="#585880" /> Рис. 6-12. Включение в состав темы дополнительных файлов Данная обложка предназначена для оформления элемента управления BulletedList, выводящего на странице маркированный список. Обложка содержит URL изображе- ния маркера, которое хранится в файле smokeandglass_bullet2.gif, расположенном в подпапке Images папки темы. Имя Images выбрано произвольно. Если для обложки потребуются еще какие-либо внешние файлы, вы можете разместить их в других вложенных папках по своему усмотрению. В файле обложек может быть определен внешний вид встроенных и специализиро- ванных серверных элементов управления. Однако чтобы создать обложку специали- зированного элемента управления, нужно включить в файл ссылку на этот элемент: <%@ Register TagPrefix="expo" Namespace=”Expowa re.P roAspNet20.Cont rols" Assembly=‘ProAspNet20.Controls" %> Затем добавляется разметка, которая будет использоваться по умолчанию для эле- ментов управления, определенных в заданной сборке и в заданном пространстве имен. Динамическая загрузка тем Темы можно применять к страницам динамически, но выполнение данной задачи требует аккуратности. Исполняющая среда ASP.NET загружает информацию о теме сразу после события Prelnit. Когда оно генерируется, имя темы, заданное в директиве ©Page, уже известно, и именно оно будет использоваться, если вы не переопределите его в обработчике Page_PreInit. Ниже приведено определение класса из файла отде- ленного кода страницы, где выполняется динамический выбор ее темы. public partial class TestThemes : System.Web.DI.Page { protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) {
Страницы с богатым интерфейсом Глава 6 229 ThemeList. DataSource = GetAvailableThemesO, ThemeList.DataBind(); } } void Page_PreInit(object sender, EventArgs e) { string theme = if (Page.Request.Form.Count > 0) theme = Page.Request["ThemeList"].ToString(); if (theme == "None") theme = this.Theme = theme; } protected Stringcollection GetAvailableThemesO { string path = Request.PhysicalApplicationPath + @"App_Themes"; Directoryinfo dir = new Di rectorylnfo(path); Stringcollection themes - new StringCollection(); foreach (Directoryinfo di in dir.GetDirectoriesO) themes.Add(di.Name); return themes; } } На рис. 6-13 данная страница показана в действии. В раскрывающемся списке выводится перечень тем приложения. Пользователь может выбрать любую из них, после чего она будет применена в обработчике события Prelnit и страница примет соответствующий вид. Рис. 6-13. Динамическое изменение темы страницы
230 Часть I Разработка страниц ASP.NET В данном обработчике состояние страницы еще не восстановлено, поэтому вызов Request.Form является единственным способом доступа к возвращенному страницей значению, каковым является имя выбранной пользователем темы. Мастера Эталонные страницы и темы — прекрасные средства разработки единообразных страниц с богатым интерфейсом, имеющих одинаковое графическое оформление, одинаковое расположение элементов управления и даже одинаково функционирую- щих. Одним из распространенных видов страниц со сложным интерфейсом являются мастера. Чаще всего они используются в настольных приложениях Windows, но не являются редкостью и в Web-приложениях. Мастер — это особый компонент, который проводит пользователя через последовательность шагов, необходимых для получения определенного результата. Он реализуется в виде набора единообразно оформленных экранов (окон или страниц), которые выводятся друг за другом и содержат элементы управления, необходимые для ввода данных. Обязательной принадлежностью всяко- го мастера является пара кнопок или ссылок для перехода к следующему либо пре- дыдущему экрану. Концептуально мастер — очень простой компонент, но реализовать его через HTTP не всегда просто. Поэтому каждый, кому приходилось заниматься разработкой приложений для Web, с радостью встретит нововведение ASP.NET 2.0 — элемент управления Wizard, предназначенный для реализации мастеров. Обзор возможностей элемента управления Wizard Элемент управления Wizard поддерживает как линейную, так и нелинейную нави- гацию. Он позволяет возвращаться назад для изменения введенных ранее значений и пропускать шаги, которые на этот раз не нужны — то ли потому, что пользователь ввел определенные установки на предыдущих шагах, то ли потому, что он просто не хочет заполнять определенные поля. Подобно многим другим элементам управления ASP.NET 1.x, Wizard поддерживает темы, стили и шаблоны. Будучи составным элементом управления, Wizard автоматически генерирует не- сколько стандартных элементов интерфейса мастеров, в частности навигационные кнопки и панели. Как вы увидите чуть позже, он поддерживает несколько шабло- нов, благодаря которым возможна глубокая настройка всего его пользовательского интерфейса. Элемент управления Wizard гарантирует правильное управление состо- янием мастера независимо от того, как пользователь перемещается между его экра- нами — вперед, назад или прямо к определенному экрану. Все шаги мастера должны быть определены в рамках одного экземпляра элемента управления Wizard. Иными словами, мастер должен быть целостным компонентом Структура мастера Как видно на рис. 6-14, мастер состоит из четырех частей: заголовка, области пред- ставления и двух панелей — навигационной и боковой. Заголовок мастера — это текст, задаваемый посредством свойства HeaderText. Для его оформления предусмотрено стилевое свойство; кроме того, можно изменить структуру заголовка, используя соответствующее свойство-шаблон. Если свойство HeaderText оставлено пустым и пользовательский шаблон не задан, мастер не имеет заголовка. В области представления выводится основное содержимое текущего экрана масте- ра. Каждый экран (то есть каждый шаг мастера) должен быть определен с помощью элемента <asp:wizardstep>, который соответствует элементу управления Wizard Step.
Страницы с богатым интерфейсом Глава 6 231 Поддерживаются разные типы экранов мастера, но все они являются производными от базового класса WizardStepBa.se. Заголовок Шаг1 Шаг 2 Шаг л Область представления (здесь выводится основное содержимое экрана мастера) Боковая панель Навигационная панель Finish Рис. 6-14. Четыре составляющие элемента управления Wizard Группа тэгов, представляющих экраны мастера, должна быть заключена внутрь тэга <wizardsteps>: <asp:wizard runat="server" DisplaySideBar="true"> <wizardsteps> <asp:wizardstep runat="server" steptype="auto" id="step1"> First step </asp:wizardstep> <asp:wizardstep runat="server" steptype=”auto" id="step2"> Second step </asp:wizardstep> <asp:wizardstep runat="server" steptype="auto" id=”finish"> Final step </asp:wizardstep> </wizardsteps> </asp:wizard> Навигационная панель состоит из автоматически генерируемых кнопок, с помощью которых пользователь может перейти к следующему, предыдущему или последнему экрану мастера. Внешний вид и состав этой панели можно определить с помощью стилей и шаблонов. Необязательная боковая панель служит для вывода некоторой вспомогательной информации в левой части экрана мастера. Обычно на ней отображается перечень
232 Часть I Разработка страниц ASP NET шагов, которые необходимо осуществить для выполнения задачи, реализуемой с по- мощью мастера. По умолчанию здесь выводится описание каждого шага, а текущий шаг выделен полужирным шрифтом. Эту панель также можно конфигурировать с при- менением стилей и шаблонов. На рис. 6-15 показано, какой интерфейс мастер имеет по умолчанию. Названия шагов определяются идентификаторами представляющих их тэгов <asp:wizardstep>. Рис. 6-15. Мастер со стандартным интерфейсом Стили и шаблоны мастера Оформлению с помощью стилей подлежат все составляющие элемента управления Wizard (включая кнопки), для чего используются свойства, перечисленные в табл. 6-4. Табл. 6-4. Стилевые свойства элемента управления Wizard Свойство Элемент, стиль которого определяет данное свойство CancelButtonStyle Finish CompleteButtonStyle FinishPreviousButtonStyle HeaderStyle NavigationButtonStyle NavigationStyle SideBarButtonStyle SideBarStyle StartStepNextButtonStyle StepNextButtonStyle StepPreviousButtonStyle StepStyle Кнопка Cancel Кнопка Finish Кнопка Previous на последнем шаге Заголовок мастера Навигационные кнопки мастера Навигационная область Кнопки боковой панели Боковая панель Кнопка Next на первом шаге Кнопка Next Кнопки Previous Область представления Содержимое заголовка, боковой и навигационной панелей можно также настра- ивать с помощью шаблонов. Перечень доступных шаблонов приведен в табл. 6-5.
Страницы с богатым интерфейсом Глава 6 233 Табл. 6-5. Шаблонные свойства элемента управления Wizard Свойство Элемент, шаблон которого определяет данное свойство FinishNavigation Template Навигационная панель, отображаемая на последнем шаге масте- ра. По умолчанию она содержит кнопки Previous и Finish HeaderTemplate Строка заголовка мастера SideBarTemplate Левая панель мастера StartNavigationTemplate Навигационная панель, отображаемая на первом шаге мастера. По умолчанию содержит только кнопку Вперед StepNavigation Template Навигационная панель, отображаемая на всех шагах мастера, кроме первого и последнего В дополнение к стилям и шаблонам для управления пользовательским интерфей- сом мастера может применяться еще несколько свойств. Программный интерфейс мастера В табл. 6-6 приведен список всех свойств элемента управления Wizard, за исключением стилевых и шаблонных свойств, а также свойств, определенных в базовых классах. Табл. 6-6. Основные свойства элемента управления Wizard Свойство Описание ActiveStep Возвращает объект, представляющий текущий шаг мастера. Этот объект является экземпляром класса WizardStep ActiveStepIndex Возвращает и позволяет задать О-базированный индекс текущего шага мастера Display CancelButton Включает и отключает видимость кнопки Cancel. По умолчанию имеет значение false DisplaySideBar Включает и отключает видимость боковой панели. По умолча- нию имеет значение false HeaderText Возвращает и позволяет задать заголовок мастера SkipLinkText Возвращает и позволяет задать подсказку, которую элемент управ- ления связывает с невидимым изображением как информацию для программных синтезаторов речи, осуществляющих чтение информа- ции с экрана. Значением свойства по умолчанию является строка Skip Navigation Links, локализуемая согласно текущей локализации сервера WizardSteps Возвращает коллекцию, содержащую все объекты WizardStep, определенные для элемента управления Во время выполнения страницы мастер представлен набором объектов, соот- ветствующих его шагам и кнопкам. Различаются следующие типы кнопок мастера: StartNext, StepNext, StepPrevious, FinishComplete, FinishPrevious и Cancel. Каждая из них имеет свойства, посредством которых задаются URL изображения кнопки, ее надпись, тип и URL, по которому осуществляется переход после щелчка этой кнопки. Именем свойства является имя кнопки, за которым следует определенный суффикс. Перечень поддерживаемых суффиксов приведен в табл. 6-7. Табл. 6-7. Суффиксы имен свойств кнопок Суффикс Описание соответствующего свойства ButtonlmageVrl Возвращает и позволяет задать URL изображения, используемо- го для рендеринга кнопки ButtonText Возвращает и позволяет задать текст кнопки см. след. стр.
234 Часть I Разработка страниц ASP.NET Табл. 6-7. (окончание) Суффикс Описание соответствующего свойства ButtonType Возвращает и позволяет задать тип кнопки: обычная, изображе- ние, ссылка DestinationPageUrl Возвращает и позволяет задать URL для перехода по щелчку кнопки Подчеркну, что в табл. 6-7 приведены суффиксы, а не полные имена свойств. Со- ответствующие им четыре свойства могут иметь кнопки всех типов. Полное же имя свойства состоит из имени кнопки, за которым следует один из суффиксов, например: CancelButtonText, FinishCompleteDestinationPageUrl и т. д. Элемент управления Wizard поддерживает и несколько интересных методов. Од- ним из них является метод GetHistory, определяемый следующим образом: public ICollection GetHistoryQ Он возвращает коллекцию объектов Wizard Step Base, представляющих уже ото- бражавшиеся экраны мастера. Порядок следования элементов коллекции обратен порядку вывода экранов. Первый объект коллекции, то есть объект с индексом О, представляет текущий шаг мастера, второй — его предыдущий шаг и т. д. Еще один метод указанного элемента управления, MoveTo, используется для пере- хода к определенному экрану мастера. Вот его прототип: public void MoveTo(WizardStepBase step) Данному методу нужно передать объект WizardStepBase, но получить его не всегда просто. Однако поскольку сам метод является всего лишь оболочкой для мутатора свойства Active Step Index, то в том случае, если вы хотите перейти к определенному экрану мастера, но не имеете экземпляра соответствующего объекта WizardStep, проще установить свойство ActiveStepIndex. На протяжении своего жизненного цикла элемент управления Wizard генерирует ряд событий; они перечислены в табл. 6-8. Табл. 6-8. События элемента управления Wizard Событие__________________Когда происходит Active ViewChanged Сменился экран CancelButtonClick Пользователь щелкнул кнопку Cancel FinishButton Click Пользователь щелкнул кнопку Finish NextButtonClick Пользователь щелкнул кнопку Next PreviousButtonClick Пользователь щелкнул кнопку Previous SideBarButtonClick Пользователь щелкнул кнопку на боковой панели Как видите, с каждой из кнопок мастера связано свое событие, которое можно обработать желаемым образом. Добавление экранов мастера В объектной иерархии страницы конкретному экрану мастера (а точнее, его области представления) соответствует объекта класса WizardStep. Данный класс является про- изводным от класса View и определяет несколько дополнительных открытых свойств. Объект View всегда имеет родительский элемент управления MultiView (см. главу 4), который мастер и использует для формирования экрана. Однако класс мастера не является производным от класса MultiView.
Страницы с богатым интерфейсом Глава 6 235 В теле страницы объявления всех экземпляров класса WizardStep, то есть всех экранов мастера, группируются внутри тэга < Wizardsteps>. Данный тэг соответствует одноименному свойству-коллекции элемента управления Wizard. <WizardSteps> <asp:WizardStep> </asp:WizardStep> <asp:WizardStep> </asp:WizardStep> </WizardSteps> Каждый экран мастера имеет тип и заголовок. В заголовке (свойство Title) выво- дится краткое описание назначения данного экрана. Эта информация не используется, если вывод боковой панели отключен. Если же боковая панель активна, из заголовков экранов составляется выводимый на ней список шагов мастера. Когда боковая панель активна, но заголовки некоторых экранов не заданы, для заполнения указанного спи- ска используются значения свойства ID объектов WizardStep (см. рис. 6-15). Определяя очередной шаг мастера, можно задать для него свойство AllowRetum, указывающее, может ли пользователь возвращаться к этому шагу, если он уже пере- шел к следующему. По умолчанию данное свойство имеет значение true. Типы экранов мастера Свойство StepType объекта WizardStep указывает, как должен обрабатываться и вы- водиться данный экран мастера. Его допустимыми значениями являются элементы перечисления WizardStepType, описанные в табл. 6-9. Табл. 6-9. Типы экранов мастера Значение Описание Auto Значение по умолчанию; когда оно задано, мастер сам определяет, как должен интерпретироваться экран Complete Последний экран мастера, обычно выводимый по окончании работы с ним пользователя. На этом экране навигационная и боковая панели отсутствуют Finish Последний из экранов мастера, используемых для ввода данных от пользова- теля. На нем нет кнопки Next, но есть кнопки Previous и Finish Start Первый экран мастера, не содержащий кнопки Previous Step Все промежуточные страницы, на которых выводятся кнопки Previous и Next Когда мастер работает в автоматическом режиме (Auto), он определяет тип каж- дого экрана, основываясь на порядке его расположения в исходном файле страницы. Первому экрану он назначает тип Start, последнему — Finish, а экран Complete в таком случае у мастера отсутствует. Если же экранам явно назначены типы, порядок их определения в исходном файле страницы не имеет значения. Создание экрана, предназначенного для ввода данных Ниже приведен пример определения экрана, предназначенного для ввода имени про- вайдера и строки подключения, которая будет использоваться для соединения с базой данных и поиска в ней информации. Содержимое экрана заключено внутрь тэга <div>, определяющего панель фиксированной высоты. Если сконфигурировать таким же образом все остальные экраны мастера, размер и структура страницы при переходе от одного экрана мастера к другому меняться не будут.
236 Часть I Разработка страниц ASP.NET <asp wizardstep ID="Wizardstepl" runat="server" title="Connect”> <div style=”height:200px width:400px;margin:10;"> <table> <t r><td>P rovide r</tdxtd> <asp:textbox runat="server" id="ProviderName" width="250px" text="System.Data.SqlClient" /> </td></tr> <t rx td>Connection St ring</td><td> <asp:textbox runat="server" id="ConnString" width= 250px" text="SERVER=(local);DATABASE=northwind;UID=...;" /> </tdx/tr> ctrxtd height="100px"x/td></tr> </table> </div> </asp:wizardstep> Как выглядит этот экран в браузере, показано на рис. 6-16. Нетрудно догадаться, что ему автоматически назначен тип Start, — именно поэтому он содержит только кнопку Next. Рис. 6-16. Демонстрационный экран типа Start Обычно мастер создается для ввода данных от пользователя, поэтому важно пра- вильно осуществлять их валидацию. Это можно делать двумя способами, не исклю- чающими друг друга: с помощью валидаторов и обработчиков событий перехода. Для реализации первого способа нужно включить в состав экрана мастера вали- дационные элементы управления. Тогда ошибки ввода, такие как незаполненные обя- зательные поля и неверные типы данных, будут мгновенно перехватываться, причем вы, вероятнее всего, захотите, чтобы это происходило на клиенте. <asp:requiredfieldvalidator ID="RequiredField1" runat="server" text="*"
Страницы с богатым интерфейсом Глава 6 237 errormessage="Must indicate a connection string" setfocusonerror="true" controltovalidate="ConnString" /> Если же для проверки введенных данных вам необходимо иметь доступ к сервер- ным ресурсам, следует прибегнуть к обработчикам событий перехода. Под событиями перехода подразумевается события, которые генерируются мастером накануне пере- хода к другому экрану. Например, когда пользователь щелкает кнопку Next, чтобы перейти к следующему шагу мастера, генерируется событие NextButtonClick. Вы мо- жете перехватить его, выполнить валидацию введенных данных и при необходимости отменить переход. Мы продолжим обсуждение этого вопроса чуть позже. Определение боковой панели Как уже было сказано, на боковой панели мастера выводится список кнопок для про- извольного перехода к тому или иному его шагу. Отображение этой панели можно включать и отключать с помощью атрибута DisplaySideBar, а ее содержимое опреде- ляется значением свойства SideBarTemplate. Не все в структуре боковой панели оставлено на ваше усмотрение. В частно- сти, внутри тэга <SideBarTemplate> обязательно должен присутствовать элемент управления DataList со стандартным идентификатором SideBarList. Кроме того, блок <ItemTemplate> должен содержать кнопку с именем SideBarButton. Она может быть представлена любым объектом, реализующим интерфейс IButtonControl. I —~1 Примечание Чтобы мастер выглядел более профессионально, можно явно задать вы- соту и ширину каждого его экрана. Кнопки навигационной панели также будут выглядеть лучше, если всем им назначить одинаковый размер. Для этого нужно установить свойства Width и Height объекта NavigationButtonStyle. Навигация по экранам мастера Когда пользователь щелкает кнопку для перехода к другому шагу мастера, генери- руется событие, в обработчике которого принимается решение о том, допустимы ли введенные пользователем данные и следует ли осуществлять переход. В большинстве случаев серверная валидация выполняется, когда пользователь для завершения работы с мастером щелкает кнопку Finish. В этот момент можно проверить, все ли необходимые данные ввел пользователь и все ли обязательные операции он выполнил. Код, связанный с событием FinishButtonClick, выполняется лишь однажды и именно тогда, когда это необходимо. Что касается обработчиков событий кнопок Next и Previous, то они выполняются при каждом переходе от экрана к экрану. Возврат формы осуществляется при каждом из этих событий. Управление навигацией с помощью событий Серверную валидацию стоит производить в том случае, когда дальнейшие действия мастера зависят от введенных пользователем данных. В подобных случаях обычно определяют обработчик события NextButtonClick-. <asp:wizard runat="server" id=”QueryWizard" OnNextButtonClick="OnNext"> </asp:wizard> Если пользователь возвращается к предыдущему экрану, что он ввел, не имеет значения — валидацию этих данных пока можно не выполнять, предполагая, что он еще вернется и, возможно, их изменит.
238 Часть I Разработка страниц ASP.NET Когда активна боковая панель, пользователь может перемещаться между экранами в произвольном порядке. Если реализуемая вами логика мастера требует, чтобы для перехода к определенному шагу выполнялось некоторое условие, следует написать обработчик события SideBarButtonClick и убедиться в нем, что это условие выпол- няется. Для данного события необходим делегат WizardNavigationEventHandler. public delegate void WizardNavigationEventHandler( object sender, WizardNavigationEventArgs e); Структура WizardNavigationEventArgs имеет два полезных свойства, которые со- держат О-базированные индексы текущей страницы и той, к которой пользователь хочет перейти, — это свойства CurrentStepIndex и NextStepIndex. Заметьте, что оба они доступны только для чтения. Ниже приведен код демонстрационного обработчика события кнопки Next. Этот обработчик подготавливает сводное сообщение, которое будет выведено на последней странице. void OnNext(object sender, WizardNavigationEventArgs e) { // Если выполняется переход к последней странице, собираем введенные данные. // Единицу нужно вычесть потому, что экраны индексируются с нуля, а если // используется страница типа Complete, то следует вычесть еще одну единицу if (е.NextStepIndex == QueryWizard.WizardSteps.Count - 2) PrepareFinalStepO; } void PrepareFinalStepO { string cmdText = DetermineCommandText(); // Выводим заключительное сообщение StringBuilder sb = new StringBuilder(”"); sb AppendFormat("You're about to run: <br><br>{O}<hr>", cmdText); sb.Append("<b><br>Ready to go?</b>”); ReadyMsg Text = sb.ToStnngO; } string DetermineCommandTextO { // Генерируем и возвращаем текст SQL-команды } Каждый экран мастера является чем-то вроде панели, определенной в составе родительского элемента управления, которым является мастер. Это означает, что все дочерние элементы управления каждого из экранов должны иметь уникальные идентификаторы и что к каждому из этих элементов можно обращаться по имени. Например, если одна из страниц содержит текстовое поле с именем ProviderName, к нему можно обратиться в обработчике любого события, используя идентификатор ProviderName. Приведенный выше код взят из файла демонстрационного мастера, который вво- дит данные от пользователя и выполняет запрос к базе данных. На первом его шаге выполняется ввод информации о подключении, а на втором пользователю предлага- ется указать имена таблицы, ее полей и задать необязательное предложение WHERE.
Страницы с богатым интерфейсом Глава 6 239 Составленная мастером команда выводится на странице типа Finish, где пользователю предлагается подтвердить сделанные установки (рис. 6-17). Полный исходный код мастера имеется в составе прилагаемого к книге набора примеров. Рис. 6-17. Два шага демонстрационного мастера: ввод информации для составления запроса и подтверждение сделанных установок Отмена перехода Помимо двух упомянутых выше свойств структура WizardNavigationEventArgs содер- жит также доступное для чтения и записи булево свойство Cancel. Если присвоить ему значение true, переход к указанному пользователем экрану будет отменен. Ниже приведен код, демонстрирующий, как предотвратить переход к следующему экрану мастера, если на первом шаге пользователь ввел в качестве своего идентификатора строку sa\ void OnNext(object sender, WizardNavigationEventArgs e) { if (e.Currentstepindex == 0 && ConnString.Text.IndexOf("UID=sa") > -1) { e.Cancel = true; return: } } Переход можно отменять в обработчике любого события перехода, не только Next- ButtonClick. Так поступают, когда результат серверной валидации говорит о том, что пользователь ввел недопустимые данные. При этом вы сами должны позаботиться о выводе соответствующего сообщения. ] Примечание Отменить переход в обработчике события ActiveViewChanged невозможно. Это событие генерируется после события перехода (такого как NextButtonClick или Previ- ousButtonClick), когда сам переход уже выполнен. Обработчик события ActiveViewChanged не имеет параметров. На завершающем этапе своей работы все мастера выполняют определенный код, реализующий ту операцию, ради которой они создавались. При использовании эле- мента управления Wizard в ASP.NET 2.0 этот код нужно поместить в обработчик со- бытия FinishButtonClick. На рис. 6-18 показан последний экран мастера с сообщением об успешном выполнении его задачи.
240 Часть I Разработка страниц ASP.NET Рис. 6-18. Последний этап работы мастера успешно завершился void OnFinish(object sender, WizardNavigationEventArgs e) string finalMsg = "The operation completed successfully.”; try { // Завершаем работу мастера (составляем и выполняем запрос) string cmd = DetermineCommandTextO; DataTable table = ExecuteCommand(ConnString Text, cmd); grid.DataSouice = table; grid. DataBindO; // Цвет, сигнализирующий об успешном выполнении операции FinalMsg.ForeColor = Color.Blue; } catch (Exception ex) { // Цвет, сигнализирующий о неудачном завершении операции FinalMsg.ForeColor = Color.Red; finalMsg = String.Format("The operation cannot be completed due to:<br>{0}", ex.Message); } finally { FinalMsg.Text = finalMsg; } } string DetermineCommandTextO { // Генерируем и возвращаем текст SQL-команды } DataTable ExecuteCommandQ { // Выполняем запрос к базе данных }
Страницы с богатым интерфейсом Глава 6 241 Если мастер содержит экран типа Complete, этот экран должен отображаться по- сле того, как пользователь щелкнет кнопку Finish и будет выполнена завершающая операция мастера. Если что-то пойдет не так, придется либо отменить переход, что- бы предотвратить вывод CoznpZete-экрана, либо адаптировать его пользовательский интерфейс, чтобы на данном экране выводилось сообщение об ошибке. Какое из двух решений выбрать, зависит от реализуемой вами операции. Если она может за- вершиться как успехом, так и неудачей, стоит предоставить мастеру возможность выполнить переход к заключительной странице и вывести на ней сообщение о не- удачном завершении. Когда же нет сомнений, что эта операция обязательно должна завершится успешно (если только пользователь сам не откажется от ее выполнения), то переход осуществлять не следует. В таком случае имеет смысл сообщить поль- зователю о произошедшем, предоставив ему возможность задать другие установки и повторить операцию. Заключение Ранее, еще со времени появления версии 1.0, в состав ASP.NET входил успешно подо- бранный комплекс низко- и высокоуровневых средств разработки. Используя такие низкоуровневые средства, как события, модули HTTP и обработчики HTTP, можно «внедряться» в конвейер HTTP и воздействовать на процесс обработки запроса на любом его этапе. В то же время для тех разработчиков, которым не требуется контроль над каждым шагом обработки запроса, ASP.NET предлагает богатый набор мощ- ных и гибких компонентов, позволяющих выполнять различные прикладные задачи. Качество и количество прикладных сервисов в ASP.NET 2.0 значительно увеличи- лось, поскольку создатели этой системы стремятся сделать так, чтобы разработчикам приложений приходилось писать как можно меньше кода. Введение сложных эле- ментов управления с богатым интерфейсом, подобных рассмотренному в этой главе элементу Wizard, подтверждает это намерение. Мы изучили технологию использования эталонных страниц, служащих шаблонами для страниц сайта и определяющих как их пользовательский интерфейс, так и общий программный код. Эталонные страницы не поддерживают объектно-ориентированного визуального наследования, подобного тому, что реализовано в Windows Forms; зато они содержат все наполнение, единое для группы страниц сайта, и области, наполне- ние которых определяется в страницах контента. При полной поддержке со стороны Visual Studio .NET 2005 эталонные страницы очень сильно экономят время и усилия разработчика и позволяют сделать приложения более совершенными и внутренне согласованными. Похожую роль играют в приложении ASP.NET темы, позволяющие единообразно оформлять страницы и применять к их элементам управления обложки, выбираемые из заранее определенного набора. Темы ASP.NET подобны темам XML и являются надмножеством каскадных таблиц стилей, поскольку применяются не только к эле- ментам HTML, но и к серверным элементам управления. Темы хорошо использовать совместно с API пользовательских профилей, о котором рассказывалось в главе 5. Используя эти две технологии, вы можете предложить пользователю выбрать тему страницы или приложения и сохранить информацию о его выборе в персонализаци- онном слое хранения. Мы говорили также о мастерах — специальных компонентах, облегчающих поль- зователю выполнение различных пошаговых задач. В ASP.NET 2.0 процесс создания мастеров значительно облегчен благодаря введению нового элемента управления Wizard, который мы рассмотрели во всех подробностях.
242 Часть I Разработка страниц ASP.NET Настоящей главой завершается первая часть книги, посвященная разработке стра- ниц ASP.NET. В следующей части мы углубимся в мир доступа к данным и исследуем способы размещения данных на Web-сайтах. Только факты Эталонная страница — это отдельный файл, содержащий общую для группы стра- ниц сайта разметку; ссылка на него может задаваться на уровне приложения, папки или страницы. В составе эталонной страницы определяются области, за заполнение которых от- вечают производные страницы. Производная страница, называемая также страницей контента, представляет собой совокупность блоков разметки, используемых исполняющей средой для заполне- ния соответствующих областей эталонной страницы. • Страница контента не может содержать информации, не предназначенной для заполнения настраиваемых областей эталонной страницы. Для настраиваемых областей эталонной страницы можно задавать контент, вы- водимый по умолчанию, то есть в случае, когда их содержимое не определено в странице контента. В одной странице контента можно задать несколько эталонных страниц, выбор между которыми будет осуществляться системой в зависимости от значения строки пользовательского агента. Эталонные страницы могут быть вложенными. Их объектная модель строго ти- пизирована. Тема — это набор установок, распределенных между несколькими файлами и ис- пользуемых исполняющей средой для придания сайту или странице согласован- ного пользовательского интерфейса. Темы задаются посредством атрибутов; их можно экспортировать из одного при- ложения в другое и применять к страницам на лету. Отличие тем от CSS-файлов заключается в том, что они позволяют задавать стиле- вые свойства не только HTML-элементов, но и серверных элементов управления. В состав темы могут входить файлы обложек, каскадных таблиц стилей и изо- бражений, а также любые другие файлы, которые вы захотите использовать при оформлении элементов управления и страниц. Файл обложки содержит набор объявлений элементов управления ASP.NET. Си- стема гарантирует, что после создания элемента управления ему будет присвоен набор атрибутов, определенных для его типа в файле связанной с ним обложки. Элемент управления, реализующий интерфейс мастера, управляет несколькими определенными вами экранами (реализованными в виде представлений) и по- зволяет пользователю переходить от одного представления к другому, используя типичные для мастеров навигационные кнопки.
Часть II Размещение данных на сайте
Глава 7 Провайдеры данных ADO.NET ADO.NET — это подсистема доступа к данным, входящая в состав Microsoft .NET Frame- work. Она многое взяла от своей предшественницы, ActiveX Data Objects (ADO), за- рекомендовавшей себя как очень удачная объектная модель для приложений, которым необходим доступ к базам данных. Главным критерием при разработке ADO.NET была простота и эффективность. Конечно, такую цель ставят перед собой создатели каж- дой системы, но в данном случае ее действительно удалось реализовать. В результате ADO.NET, с одной стороны, обладает мощью и производительностью низкоуровневых интерфейсов, а с другой, предоставляет в распоряжение разработчика исключительно простую, удобную и современную объектную модель. Обычно эти две характеристики несовместимы и каждую из них можно реализовать лишь в ущерб другой, но ADO.NET оказалась счастливым исключением. Следует также добавить, что она в значительно меньшей степени, чем ADO, ориентирована на работу с базами данных, и предназна- чается для доступа к любым данным в каком бы виде они ни хранились. Между ADO и ADO.NET имеется несколько важных различий, и все же их объект ные модели очень похожи. Разработчики из Microsoft приложили немало усилий для того, чтобы тем программистам, которые перейдут на ADO.NET от ADO (а таких, поверьте, огромное количество), как можно легче было ее освоить. Поэтому опытным разработчикам приложений баз данных, переходящим на платформу .NET, не придется изучать множество новых концепций — напротив, они могут буквально сразу же при- ступать к работе с новыми средствами. Правда, повторно использовать свой старый код в ADO.NET они, скорее всего, не смогут. А вот опыт и навыки, приобретенные за годы работы с ADO, еще им послужат. Что касается разработчиков-новичков, для которых ADO.NET станет первым в их жизни средством для работы с данными, то им наверняка легко будет понять и освоить основные принципы ее применения ADO.NET состоит из двух разных, но тесно связанных между собой наборов клас- сов: провайдеров данных и контейнеров данных. Провайдеры мы изучим в этой главе, а контейнеры — в следующей. Инфраструктура доступа к данным .NET ADO.NET — последняя из длинной череды технологий доступа к данным, начало которой было положено более 10 лет назад благодаря появлению API Open Database Connectivity (ODBC). Реализованный в виде библиотеки С-типа, ODBC создавался как универсальный API для направления SQL-вызовов различным серверам баз дан- ных. Модель ODBC предполагает наличие у каждой СУБД собственного драйвера, скрывающего различия между языком SQL, используемым на уровне приложения, и разновидностью этого языка, реализованной в этой СУБД. В дальнейшем, когда повсеместное распространение получила технология СОМ, обосновавшаяся в том числе и на территории баз данных, появилась новая техно- логия, называемая OLE DB. Она основана на тех же принципах, что и ODBC, но ее API является COM-совместимым. Он создает общий для всех приложений слой
Провайдеры данных ADO.NET Глава 7 245 кода, посредством которого осуществляется доступ к любым источникам, способ- ным предоставлять свои данные в виде табличных наборов строк. Архитектура OLE DB предполагает обязательное наличие двух элементов: потребителя и провайдера данных. Потребитель данных интегрируется в клиентское приложение и отвечает за установку основанного на СОМ взаимодействия с провайдером данных. Провайдер же OLE DB получает вызовы от потребителя и выполняет заданные операции над источником данных. Каким бы ни был формат данных и способ их хранения, про- вайдер OLE DB возвращает их в табличной форме, то есть в виде строк и столбцов. Для обеспечения взаимодействия между клиентским приложением и источником данных в OLE DB используется технология СОМ. OLE DB не отличается удобством применения и ориентирована в первую очередь на приложения C++, а потому она так никогда и не завоевала сердца программистов, несмотря на то, что обеспечивает довольно хороший баланс производительности и гибкости. Вслед за ней пришла ADO, которая, если говорить несколько упрощенно, является разновидностью OLE DB и использует принципы СОМ-автоматизации. Действующая как потребитель данных OLE DB, интегрированный в хост-приложе- ние, ADO сделала технологию OLE DB доступной для приложений Microsoft Visual Basic и классических приложений Active Server Pages (ASP). Она появилась в эпоху взаимосвязанных двухуровневых приложений, и ее объектная модель как нельзя луч- ше для них подходила. Объектная модель ADO обладает некоторой избыточностью, предоставляя несколько способов выполнения каждой из ключевых задач по работе с данными. Кроме того, в ее состав входит много вспомогательного кода. Из-за всего этого, а также вследствие тех компромиссов, на которые пришлось пойти создателям ADO для достижения исключительной простоты использования, приложения на ее основе не столь эффективны, как приложения на основе чистой OLE DB. [ । Примечание Объектную модель ADO и сейчас можно использовать в .NET, но по при- чинам, связанным с производительностью и совместимостью, ее применяют лишь в осо- бых случаях. Так, ADO — единственная технология, позволяющая работать с серверными курсорами. Кроме того, ADO предоставляет приложениям .NET Framework 1.x специальный API для управления схемой базы данных. В то же время наборы записей ADO невозможно непосредственно привязать к связанным с данными элементам управления, используемым в приложениях ASP.NET и Microsoft Windows Forms. (О связывании элементов управления с данными в приложениях ASP.NET мы поговорим в главах 9 и 10.) К числу важнейших усовершенствований, введенных в ADO.NET, является мощная автономная модель работы с данными, представленная объектом DataSet, тесная интеграция с XML и бесшовная инте- грация с остальной частью .NET Framework. Кроме того, ADO.NET отличается прекрасной производительностью, а степень ее поддержки со стороны Microsoft Visual Studio .NET просто беспрецедентна. Словом, если вы работаете над новым приложением .NET Frame- work, то задаваться вопросом, использовать ли в нем ADO.NET, вам не придется. Управляемые провайдеры данных .NET Важным архитектурным элементом инфраструктуры ADO.NET является управляемый провайдер — .NET-аналог провайдера данных OLE DB. Управляемый провайдер позво- ляет приложению подключаться к источнику данных, извлекать их и модифицировать По сравнению с провайдером OLE DB управляемый провайдер .NET имеет более простую архитектуру доступа к данным, реализует меньшее количество интерфейсов и использует типы данных .NET Framework. Строительные блоки провайдера данных .NET Классы управляемого провайдера взаимодействуют с определенным источником данных и возвращают их приложению, используя типы данных, которые определены
246 Часть II Размещение данных на сайте в .NET Framework. Логические компоненты такого провайдера графически представ- лены на рис. 7-1. Обратно к поиложению Рис. 7-1. Классы .NET Framework, образующие типичный управляемый провайдер, и их взаимосвязи Функции провайдера данных .NET подразделяются на две категории: поддержка автономной работы с данными, загруженными из базы данных в кон- тейнерные объекты; поддержка доступа к удаленным данным, то есть возможность установить под- ключение к данным и выполнить команду. Важнейшие компоненты провайдера данных .NET описаны в табл. 7.1. Табл. 7.1. Важнейшие компоненты провайдера данных .NET Компонент Описание Connection (подключение) Создает подключение к заданному источнику данных, напри- мер, к базе данных Microsoft SQL Server или Oracle. Может взаимодействовать с любым источником данных, для которого вами указан провайдер OLE DB или драйвер ODBC Transaction (транзакция) Представляет транзакцию, которая должна быть выполнена в ис- точнике данных Command (команда) Parameter (параметр) DataAdapter (адаптер данных) CommandBuilder (постро- итель команд) DataReader (объект чте- ния данных) Представляет команду, направляемую источнику данных Представляет параметр, который передается объекту-команде Представляет команду базы данных, выполняемую на заданном сервере базы данных, и возвращает автономный набор записей Вспомогательный объект, автоматически генерирующий коман- ды и параметры для адаптера данных Представляет доступный только для чтения однонаправленный курсор, созданный на сервере базы данных
Провайдеры данных ADO.NET Глава 7 247 Каждый управляемый провайдер, служащий оболочкой реального сервера базы данных, реализует все перечисленные в табл. 7-1 объекты способом, зависящим от конкретного источника данных. О Внимание! Вы не найдете в .NET Framework класса с именем Connection. Это условное название целого ряда классов-подключений, специфических для конкретных управляе- мых провайдеров .NET: SqlConnection, OracleConnection и т. д. То же можно сказать и об остальных объектах, перечисленных в табл 7-1. Интерфейсы провайдера данных .NET Компоненты, перечисленные в табл. 7-1, реализуют свойства и методы, определяемые интерфейсами, которые описаны в табл. 7-2. Табл. 7-2. Интерфейсы провайдеров данных .NET Интерфейс Описание IDbConnection Представляет один сеанс взаимодействия с источником данных IDbTransaction Представляет локальную (не распределенную) транзакцию IDbCommand Представляет команду, которая выполняется после подключения к ис- точнику данных IDataParameter Позволяет реализовать параметр команды IDataReader Читает однонаправленный доступный только для чтения поток дан- ных, созданный после выполнения команды IDataAdapter Заполняет объект DataSet и переносит изменения, сделанные в объекте DataSet, обратно в источник данных IDbDataAdapter Включает методы для выполнения типичных операций с реляционны- ми базами данных (вставка, обновление, выборка, удаление и т. п.) Заметьте, что ни один из перечисленных интерфейсов, за исключением IData- Adapter, не является обязательным. Однако любой полнофункциональный провайдер данных реализует их все. Примечание Функциональность конкретного управляемого провайдера данных вовсе не ограничена интерфейсами, перечисленными в табл. 7-1. С учетом характеристик источника данных и собственного уровня абстракции провайдер может содержать дополнительные элементы и предоставлять разработчику дополнительные функциональные возможности. Хорошим примером является провайдер для Microsoft SQL Server, входящий в состав .NET Framework 2.0. Он содержит классы для выполнения таких операций, как пакетное копирование, управление зависимостями данных и формирование строк подключения. Сравнение управляемых провайдеров .NET и провайдеров OLE DB Провайдеры OLE DB и управляемые провайдеры данных — это совершенно разные компоненты, служащие, однако, одной и той же цели: предоставить унифицированный программный интерфейс для доступа к данным разных источников. Ниже перечис- лены различия этих двух типов провайдеров Компонентная технология Провайдеры OLE DB являются внутренними (in-pr- ocess) серверами СОМ, предоставляющими модулям-потребителям набор СОМ- интерфейсов. Взаимодействие между потребителями и провайдерами осуществля- ется с использованием технологии СОМ и требует наличия большого количества интерфейсов. Провайдер данных .NET — это набор управляемых классов, общая структура которых определяется одним конкретным источником данных, а не неким абстрактным и универсальным источником, как в случае OLE DB.
248 Часть II Размещение данных на сайте Внутренняя реализация Провайдеры обоих типов осуществляют вызовы API источника данных. Для этого они реализуют довольно мощный слой кода, отде- ляющий источник данных от вызывающего приложения. Проанализировав опыт разработки и использования OLE DB, в Microsoft решили сделать провайдеры данных .NET более простыми и гибкими. Они реализуют меньшее количество интерфейсов, и взаимодействие между вызывающим и вызываемым объектами является в них более прямым и неформальным. Интеграция с приложением Выше было сказано, что провайдеры .NET, обеспе- чивают более неформальное взаимодействие между вызывающим и вызываемым объектами. Отчасти это удается им благодаря тому, что провайдеры .NET возвра- щают данные в том виде, в каком приложение будет их хранить и использовать. В OLE DB процесс извлечения данных является более гибким, но в то же время и более сложным, поскольку провайдеры помещают данные в плоские буферы памяти и предоставляют потребителю преобразовывать их в подходящие для ис- пользования структуры. Вызов провайдера OLE DB из приложения .NET является более дорогостоящей процедурой, поскольку при переходе из управляемой среды CLR в мир СОМ при- ходится осуществлять преобразование данных. Вызов СОМ-объекта из приложения .NET возможет только через interop-слой СОМ и также требует определенных затрат. В общем случае для доступа к источнику данных из приложения .NET всегда следует использовать управляемый провайдер, а не провайдер OLE DB или драйвер ODBC. Главной причиной этого являются лишние «транзитные» затраты, но примите во внимание и то обстоятельство, что управляемые провайдеры — средство более со- временное, со значительно оптимизированной архитектурой. Тем не менее не для всех источников данных имеются соответствующие провай- деры данных .NET, и в таких случаях все же приходится обращаться к устаревшим провайдерам OLE DB или драйверам ODBC — иного выхода просто не существует. Поэтому в состав .NET Framework входят управляемые классы-оболочки, которые реа- лизуют логику, необходимую для вызова провайдеров OLE DB и драйверов ODBC. Источники данных, доступные посредством ADO.NET Провайдер данных .NET Framework является управляемым компонентом, позволяю- щим производителю СУБД открыть доступ к ее данным наиболее эффективным на се- годняшний день способом. В идеале каждый производитель СУБД должен предостав- лять готовый .NET-совместимый API, предназначенный для вызова из управляемых приложений. Однако, к сожалению, производители СУБД не всегда столь любезны. Поэтому для большинства баз данных управляемые провайдеры разрабатываются либо Microsoft, либо сторонними производителями. В состав .NET Framework версии 2.0 входит группа провайдеров, перечисленных в табл. 7-3. Табл. 7-3. Управляемые провайдеры данных, входящие в состав .NET Framework Источник данных Пространство имен Описание SQL Server System.Data.SqlClient Работает с разными версиями SQL Server, включая SQL Server 7.0, SQL Server 2000 и новейшую SQL Ser- ver 2005 Провайдеры OLE DB System.Data. OleDb Работает с разными провайдерами OLE DB, включая SQLOLEDB, MSDAORA и ядро базы данных Jet
Провайдеры данных ADO.NET Глава 7 249 Табл. 7-3. (окончание) Источник данных Пространство имен Описание Драйверы ODBC System.Data.Odbc Работает с разными драйверами ODBC, включая драйверы для SQL Server, Oracle и ядра базы данных Jet Oracle System.Data. OracleChent Работает с Oracle 9i и поддерживает все ее типы данных Упомянутые в таблице управляемые провайдеры OLE DB и ODBC не связаны с конкретными серверами баз данных, они служат мостом между .NET и провайдерами OLE DB или драйверами ODBC. Например, при вызове провайдера OLE DB, при- ложение .NET выходит за пределы управляемой среды и генерирует вызовы СОМ, передаваемые через interop слой. Доступ к SQL Server Как уже говорилось, Microsoft предоставляет готовый управляемый провайдер, способ- ный работать с SQL Server 7.0 и более поздних версий. Использование классов этого провайдера — наиболее эффективный способ доступа к базам данных SQL Server. На рис. 7-2 показано, как осуществляется доступ к SQL Server клиентами .NET и СОМ. Управляемый код Рис. 7-2. Доступ к SQL Server с использованием управляемого провайдера OLE DB сопровождается дополнительными издержками, так как вызываемые объекты проходят через interop-слой СОМ В приложении .NET с базой данных SQL Server следует всегда работать посред- ством встроенного провайдера .NET. Для выбора иного решения, например для при- менения провайдера OLE DB (его имя — SQLOLEDB), необходимо иметь веские
250 Часть II Размещение данных на сайте основания. Например, одним из таких оснований может быть необходимость ис- пользовать в качестве библиотеки для доступа к данным ADO вместо ADO.NET. Встроенный провайдер SQL Server не только позволяет избежать лишних затрат, связанных с прохождением вызова через СОМ, но к тому же немного оптимизирует команду, направляемую SQL Server. Доступ к базам данных Oracle В состав .NET Framework версий 1.1 и 2.0 входит управляемый провайдер, пред- назначенный для взаимодействия с Oracle. Его классы относятся к пространству имен System.Data.OracleClient и содержатся в сборке System.Data.OracleClient. Вместо управляемого провайдера можно пользоваться действующим по технологии СОМ провайдером OLE DB (его имя — MSDAORA) или драйвером ODBC. Однако учтите, что Microsoft OLE DB provider for Oracle не поддерживает Oracle 9i и специфические для нее типы данных. В то же время они полностью поддерживаются управляемым провайдером .NET. Поэтому, используя для подключения к Oracle .NET-компонент, вы не только обеспечиваете более высокую производительность приложения, но и по- лучаете в свое распоряжение более мощные возможности программирования. Примечание Для работы провайдера данных .NET для Oracle необходимо, чтобы в си- стеме было установлено клиентское программное обеспечение Oracle версии 8.1.7 или выше. Microsoft — не единственная компания, разработавшая провайдер данных .NET для Oracle. Такие провайдеры создали Data Direct, Core Lab и сама Oracle. Каждый из них имеет уникальный набор функций. Например, провайдер, разработанный Oracle (его имя — ODP.NET), оптимизирован для извлечения данных встроенных типов Oracle (в частности, всевозможных разновидностей больших объектов, а так- же REF-курсоров) и манипулирования этими данными. Провайдер ODP.NET может использоваться в транзакционных системах, где Oracle исполняет роль менеджера ресурсов, а за координирование транзакций отвечает Microsoft Distributed Transac- tion Coordinator (DTC). Использование .NET-провайдера для OLE DB Своеобразным мостом, позволяющим приложениям .NET обращаться к источникам данных, для которых существуют только провайдеры OLE DB, является .NET data provider for OLE DB providers — провайдер данных .NET для провайдеров OLE DB, или просто .NET-провайдер для OLE DB. Конечно, с архитектурной точки зрения его применение менее эффективно, чем применение «родных» провайдеров .NET, однако при отсутствии таковых он вполне успешно справляется со своей задачей. Вам следует знать, что классы из пространства имен System.Data.OleDb, реали- зующие .NET-провайдер для OLE DB, не поддерживают всех типов провайдеров OLE DB и оптимизированы для работы лишь с некоторыми из них, перечисленными в табл. 7-4. Табл. 7-4. Провайдеры OLE DB, протестированные на совместимость с .NET Имя Описание Microsoft.Jet.OLEDB.4.0 Провайдер OLE DB для ядра базы данных Jet, входящего в со- став Microsoft Office Access MSDAORA Провайдер Microsoft OLE DB для Oracle 7, поддерживающий некоторые функции Oracle 8 SQLOLEDB Провайдер OLE DB для SQL Server версии 6.5 и выше
Провайдеры данных ADO.NET Глава 7 251 В табл. 7-4 включены не все провайдеры OLE DB, которые могут использоваться с провайдером .NET для OLE DB, однако надежное функционирование гарантировано только для перечисленных провайдеров. В частности, классами из пространства имен System.Data.OleDb не поддерживаются те провайдеры OLE DB, которые реализуют интерфейсы OLE DB 2.5, предназначенные для работы со слабоструктурированными и иерархическими наборами строк. К их числу относятся провайдеры OLE DB для Exchange (EXOLEDB) и для Internet Publishing (MSDAIPP). Корректно работать с провайдером .NET для OLE DB существующим провайдерам OLE DB мешает неполнота набора реализуемых ими интерфейсов. Некоторым из этих провайдеров, например тем, которые написаны с использованием Active Template Library (ATL) или Visual Basic и OLE DB Simple Provider Toolkit, обычно недостает одного или нескольких COM-интерфейсов, наличия которых требует .NET-оболочка. Использование драйверов ODBC Встроенный провайдер данных .NET для ODBC открывает управляемым приложе- ниям ADO.NET доступ к драйверам ODBC. Хотя провайдер .NET для ODBC пред- назначен для работы с любыми драйверами ODBC, правильная его работа гаранти- рована только с драйверами для SQL Server, Oracle и Jet. И хотя технология ODBC, безусловно, устарела, она все еще используется во многих приложениях и является единственным средством подключения к некоторым продуктам. Доступ к драйверу ODBC посредством провайдера OLE DB невозможен. Причина такого ограничения не является технической — это скорее вопрос целесообразности. Вызов OLE DB-провайдера MSDASQL из приложения .NET проводит ваше при- ложение через два моста доступа к данным: от .NET к провайдеру OLE DB и от него к драйверу ODBC. Модель фабрики провайдеров В отличие от ADO и OLE DB технология ADO.NET учитывает индивидуальные особенности каждой СУБД и для каждой из них определяет индивидуальную про- граммную модель. Все провайдеры данных .NET обладают рядом общих функций и характеристик, но в то же время каждый из них имеет свои особенности. Взаимодей- ствие между пользовательским кодом и СУБД осуществляется в ADO.NET более непосредственно. Такая модель работает лучше и быстрее, а кроме того, она, пожалуй, понятнее для большинства программистов. Однако до появления .NET Framework версии 2.0 у ADO.NET имелся один се- рьезный недостаток. Разработчик должен был заранее знать, с каким источником данных предстоит работать приложению. Поэтому было крайне сложно (если вообще возможно) написать универсальный код, способный работать с разными источника- ми данных. Можно было создать универсальный объект, представляющий команду, и универсальный объект чтения данных, но не универсальный адаптер данных и не универсальный объект-подключение. Теперь же, благодаря интерфейсу IDbConnection, с объектом подключения можно работать, не имея информации об источнике данных. Но учтите, что такой объект нельзя создавать слабо типизированным способом, то есть без помощи оператора new. Создание экземпляра провайдера программным способом В ADO.NET 2.0 архитектура провайдеров расширена и усовершенствована. Одним из таких усовершенствований является введение особого класса-фабрики, производного от базового класса DbProviderFactory и входящего теперь в состав каждого провайдера данных. Важнейшие методы этого класса-фабрики перечислены в табл. 7-5. Каж- дый из них возвращает тот или иной объект, связанный с конкретным провайдером.
252 Часть II Размещение данных на сайте Табл. 7-5. Важнейшие методы класса фабрики провайдеров Метод Возвращаемый объект CreateCommand Create CommandBuilder Create Connection CreateDataAdapter CreateParameter Команда Построитель команд Подключение Адаптер данных Параметр А как получить саму фабрику, связанную с определенным провайдером? Для этой цели предназначен новый класс DbProviderFactories, имеющий несколько статических методов. Например, следующий код показывает, как получить объект-фабрику для провайдера SQL Server: DbProviderFactory fact; fact = DbProviderFactories.GetFactory("System.Data.SqlClient"); Метод GetFactory принимает строку, содержащую инвариантное имя провайдера. Это имя жестко закодировано в том конфигурационном файле, где зарегистрирован сам провайдер. По общему соглашению оно должно совпадать с именем уникального пространства имен провайдера. Когда вы вызываете метод GetFactory, он просматривает перечень всех зарегистри- рованных провайдеров и извлекает информацию о сборке и имя класса, соответству- ющие заданному инвариантному имени. Затем метод создает экземпляр класса-фа- брики, но делает это не обычным способом, а используя технологию рефлексии: он запрашивает значение статического свойства Instance класса-фабрики, возвращающего готовый к использованию экземпляр этого класса. Получив экземпляр класса-фабрики, возвращенный методом GetFactory, можно вы- зывать его члены, перечисленные в табл. 7-5. Взглянув на следующий псевдокод, вы получите представление о внутренней реализации одного из них — метода Create Con- nection класса SqlClientFactory, являющегося фабрикой .NET-провайдера SQL Server: public DbConnection CreateConnection() { return new SqlConnection(); } Просмотр перечня установленных провайдеров данных В .NET Framework 2.0 можно использовать провайдеры данных, зарегистрированные в конфигурационном файле. В качестве примера рассмотрим выдержку из файла machine.config: <system.data> <DbProviderFactories> <add name=”SqlClient Data Provider" invariant="System.Data.SqlClient" description^'. NET Framework Data Provider for SqlServer" type="System Data.SqlClient.SqlClientFactory, System.Data "/> Odd name="OracleClient Data Provider” invariant="System.Data.OracleClient" description^' NET Framework Data Provider for Oracle" type-"System.Data.OracleClient.OracleFactory, System.Data.OracleClient" />
Провайдеры данных ADO.NET Глава 7 253 </DbProvideгFactoгies> </system.data> При регистрации провайдера для него, помимо обычного имени, задаются инва- риантное имя, описание и тип (идентификатор которого включает имя сборки и имя класса). Метод GetFactoryClasses класса DbProviderFactories возвращает эту информа- цию в виде удобного объекта DataTable. Ниже приведено содержимое исходного файла демонстрационной страницы (рис. 7-3), на которой выводится список всех установ- ленных в системе провайдеров. Код этой страницы, заключенный внутрь тэга <script>, показывает, как получить такой список самым простым и быстрым способом. < %@ раде 1апдиаде="С#" %> < %@ import namespace=”System.Data" %> < %@ import namespace="System.Data.Common" %> <script runat="server"> void Page_Load (object sender, EventArgs e) { DataTable providers = DbProviderFactories.GetFactoryClassesO; provList.DataSource = providers; provList. DataBindO; } </script> <html> <head runat="server"xtitle>List Factory Objects</title></head> <body> <form runat="server"> <asp datagrid runat="server" id="provList" /> </form> </body> </html> Рис. 7-3. Список установленных провайдеров данных .NET Страницы, не зависящие от СУБД Теперь рассмотрим пример страницы, осуществляющей доступ к данным, но при этом не зависящей от конкретного их источника. Эта страница содержит три текстовых поля, куда пользователь может ввести имя провайдера, строку подключения и команду на языке SQL, которую он хочет выполнить.
254 Часть II Размещение данных на сайте protected void RunButton_Click(object sender, EventArgs e) { string provider = ProviderNameBox.Text; string connString = ConnectionStringBox.Text; string commandText = CommandTextBox.Text; // Получаем объект-провайдер DbProviderFactory fact = DbProviderFactories.GetFactory(provider); // Создаем подключение DbConnection conn = fact.CreateConnection(); conn.Connectionstring = connString; // Создаем адаптер данных DbDataAdapter aoapter = fact.CreateDataAdapter(); adapter. SelectCommand = conn. Cr eat eCommandO; adapter.SelectCommand.CommandText = commandText; // Выполняем запрос DataTable table = new DataTableO; adapter.Fill(table); // Выводим результаты Results DataSource = table; Results. DataBindO; } Как видите, один и тот же код может использоваться для работы с разными сер- верами базы данных, поскольку имя провайдера, строка подключения и команды являются в нем переменными. ©Внимание! Собственно говоря, этот код не содержит ничего нового для вас: такой спо- соб подключения к источнику данных применяется уже давно. Однако внешняя простота такого подхода не должна вводить в заблуждение. В реальных приложениях код доступа к данным обычно содержится в отдельном программном слое, называемом слоем до- ступа к данным, причем для каждого поддерживаемого приложением источника данных создается свой такой слой. Так поступают потому, что каждый сервер данных имеет свои особенности, и чтобы получить от него максимум, на что он способен, все их необходимо учитывать. Ведь в реальном приложении, как правило, требуется оптимизированный код доступа к данным, эффективно использующий возможности СУБД, а не некий универ- сальный код, работающий везде и всюду. Подключение к источникам данных В основу модели программирования ADO.NET положена типичная, независимая от базы данных, последовательность операций. Вы создаете подключение, подготавлива- ете и выполняете команду, а затем обрабатываете полученные данные. Пока речь идет о базовых операциях и типах данных, эта модель применима к любому провайдеру. Исключения составляют операции с большими двоичными объектами (BLOB) Oracle, а также операции пакетного копирования и управления XML-данными SQL Server. Оставшаяся часть этой главы посвящена работе классов данных ADO.NET с SQL Server версии 7.0 и выше. Однако я буду обращать ваше внимание на те аспекты рабо- ты, которые при использовании других провайдеров данных .NET могут существенно отличаться. А начнем мы с изучения того, как в ADO.NET осуществляется установка подключения к источнику данных.
Провайдеры данных ADO.NET Глава 7 255 g ’ j Примечание Подробный материал об ADO.NET 2.0 вы найдете в книгах «Programming Microsoft ADO.NET 2.0 Applications: Advanced Topics», автор Гленн Джонсон (Glenn Johnson) и «Programming ADO.NET 2.0 Core Reference», автор Дэвид Сеппа (David Sceppa) — обе книги вышли в издательстве Microsoft Press в 2005 г. Класс SqlConnection Для того чтобы получить возможность взаимодействовать с источником данных по- средством ADO.NET, приложение должно первым делом установить подключение к этому источнику данных. Классом, представляющим физическое подключение к SQL Server, является SqlConnection из пространства имен System.Data.SqlClient. Это скрытый (не наследуемый) клонируемый класс, реализующий интерфейс IDbConnec- tion. В ADO.NET 2.0 данный интерфейс реализован промежуточным базовым классом DbConnection, у которого имеются и другие элементы, используемые всеми провай- дерами. (Добавление новых членов в сам этот интерфейс привело бы к нарушению работы существующего кода.) У класса SqlConnection есть два конструктора. Один из них используется по умол- чанию и не имеет параметров, а другой имеет один параметр — строку подключения: public SqlConnection(); public SqlConnection(string) Следующий код демонстрирует типичный способ конфигурирования и установки подключения к SQL Server: string connString = "SERVER=...;DATABASE^..;UID=...;PWD=... SqlConnection conn = new SqlConnection(connString); conn.Open(); conn.Close(); Свойства класса SqlConnection В табл. 7-6 перечислены и кратко описаны открытые свойства класса SqlConnection. Табл. 7-6. Свойства класса SqlConnection Свойство Входит ли в состав интерфейса IDbConnection Описание Connectionstring Да Возвращает и позволяет задать строку, используемую для открытия подключения к базе данных ConnectionTimeout Да Возвращает длительность тайм-аута в се- кундах, по истечении которого попытка подключения считается неудачной Database Да Возвращает имя используемой базы данных DataSource Нет Возвращает имя экземпляра SQL Server, к которому производится подключение. Соответствует атрибуту Server строки под- ключения PacketSize Нет Возвращает размер сетевых пакетов, пере даваемых при взаимодействии с SQL Server, в байтах. Значение по умолчанию — 8192, но может иметь любое значение в диапазоне от 512 до 32767 см. след, стр
256 Часть II Размещение данных на сайте1 Табл. 7-6. (окончание) Свойство Входит ли в состав интерфейса IDbConnection Описание ServerVersion Нет Возвращает строку, содержащую номер версии текущего экземпляра SQL Server в форме старшиймладший.выпуск State Да Возвращает информацию о текущем со- стоянии подключения: открыто оно или закрыто. По умолчанию подключение за- крыто StatisticsEnabled Нет Разрешает извлечение статической инфор- мации через текущее подключение. В ASP.NET 1 л данное свойство не поддер- живается WorkStationld Нет Возвращает сетевое имя клиента, обычно соответствующее атрибуту Workstation ID строки подключения Важной особенностью классов, представляющих подключение, является то, что все их свойства, за исключением Connectionstring, доступны только для чтения. Иными словами, конфигурировать подключение разрешается только посредством токенов строки подключения, но при этом вы можете считывать значения его параметров, пользуясь удобными свойствами. Таким образом, свойства класса подключения в ADO.NET отличаются от аналогичных свойств ADO, многие из которых, например Connection Timeout и Database, доступны не только для чтения, но и для записи. Методы класса SqlConnection Методы класса SqlConnection перечислены и описаны в табл. 7-7. Табл. 7-7. Методы класса SqlConnection Метод Входит ли в состав интерфейса IDbConnection Описание Begin Transaction Да Начинает транзакцию базы данных. По- зволяет задать имя и уровень изоляции транзакции ChangeDatabase Да Позволяет сменить текущую базу данных подключения. Его параметром является имя базы данных Close Да Закрывает подключение к базе данных- Этот метод используется для закрытия открытого ранее подключения CreateCommand Да Создает и возвращает объект SqlCommand, связанный с подключением Dispose Нет Вызывает метод Close EnlistDistributedTransaction Нет Если автоматическое связывание отклю- чено, связывает подключение с заданной DTC-транзакцией Enterprise Services. В .NET Framework 1.0 данный метод не под- держивается
Провайдеры данных ADO.NET Глава 7 257 Табл. 7-7. (окончание) Метод Входит ли в состав интерфейса IDbConnection Описание EnlistTransaction Нет, но определен в ADO.NET 2.0 в составе класса DbConnection Связывает подключение с заданной ло- кальной или распределенной транзакцией. В ADO.NET 1х данный метод не поддер- живается GetSchema Нет, но определен в ADO.NET 2.0 в составе класса DbConnection Извлекает информацию схемы для за- данного уровня (то есть таблиц или баз данных). В ADO.NET 1х данный метод не поддерживается ResetStatistics Нет Выполняет сброс службы статистики. В ADO.NET 1х данный метод не поддер- живается Retrievestatistics Нет Возвращает хэш-таблицу с информацией о подключении, такой как сведения о пере- сланных данных, пользователях и тран- закциях. В ADO.NET 1х данный метод не поддерживается Open Да Открывает подключение к базе данных Следует заметить, что когда объект подключения выходит из области видимости приложения, он автоматически закрывается. Позднее, но не обязательно вскоре, сбор- щик мусора удалит этот объект, однако подключение останется открытым, поскольку сборщик не знает особенностей каждого объекта и не может выполнять действия, которые определяются их семантикой. Так что не забывайте явно закрывать под- ключение, вызывая метод Close или Dispose до того, как объект выйдет за пределы видимости. Примечание Подобно многим другим уничтожаемым объектам, объекты подключений имеют интерфейс IDisposable. Стандартным средством уничтожения объектов является метод Dispose этого интерфейса. Что касается метода Close, то он не является стан- дартным, хотя и реализован в большинстве классов. Изменение паролей В ADO.NET 2.0 класс SqlConnection содержит статический метод ChangePassword, с помощью которого разработчик может изменить пароль пользователя SQL Server, заданного в строке подключения: public static void ChangePassword( string connectionstring, string newPassword) Если в этой строке указано, что подключение осуществляется с использовани- ем механизма интегрированной защиты (то есть строка подключения содержит то- кен Integrated Security=True или эквивалентный ему), выбрасывается исключение. Метод ChangePassword открывает новое подключение к серверу, запрашивает изме- нение пароля и по завершении этой операции закрывает подключение. Подключение, используемое для изменения пароля, берется не из пула подключений. Новый пароль должен отвечать установленной на сервере политике безопасности, которая может определять его минимальную длину и требования относительно входящих в его со- став символов. Учтите, что метод ChangePassword работает только с SQL Server 2005.
258 Часть II Размещение данных на сайте Доступ к информации схемы В ADO.NET 2.0 все управляемые провайдеры реализуют метод GetSchema, служащий для извлечения информации схемы. Стандартные провайдеры имеют три перегру- женные версии этого метода: public override DataTable GetSchemaO; public override DataTable GetSchema(string collection) public override DataTable GetSchema(string collection, string[] filterVal) Какую именно информацию схемы можно получить с помощью этих методов, за- висит от конкретного источника данных. Чтобы запросить полный набор поддержи- ваемых видов информации, вызовите метод GetSchema без параметров. Следующий код показывает, как извлечь список всех доступных коллекций (наборов данных) схемы и связать результат с раскрывающимся списком: // Получаем список коллекций схемы SqlConnection conn = new SqlConnection(connString); conn.Open(); DataTable table = conn.GetSchemaO; conn.Close(); // Связываем имена коллекций с элементами раскрывающегося списка CollectionNames.DataSource = table; CollectionNames.DataTextField = "collectionname CollectionNames.DataBind(); На рис. 7-4 представлен набор коллекций, возвращаемый SQL Server 2000 (в SQL Server 2005 в него добавлена коллекция UserDefinedTypes). Если вызвать метод GetSchema, передав ему имя конкретной коллекции, скажем, "Databases”, он вернет список всех баз данных того экземпляра SQL Server, к которому вы подключены. А если передать этому методу аргумент "Tables ”, вы увидите список таблиц текущей базы данных. .SERVtR= (local); DATABASE=northwind;UIOsa, I Connect Query Рис. 7-4. Список коллекций схемы, возвращаемый SQL Server 2000 Примечание В приведенном выше коде фигурирует класс DataTable и выполняется свя- зывание элемента управления с данными. Указанный класс, являющийся одним из важней- ших контейнерных классов ADO.NET, мы рассмотрим в следующей главе, а о связывании элементов управления с данными речь пойдет в главе 9. Список коллекций схемы возвращается в виде объекта DataTable с тремя столб- цами; один из них, CollectionName, содержит имена коллекций. Вот как производится
Провайдеры данных ADO.NET Глава 7 259 извлечение информации для выбранной пользователем коллекции (в данном слу- чае — коллекции Views): string coll = CollectionNames.SelectedValue; string connString = ConnStringBox.Text; SqlConnection conn = new SqlConnection(connString); conn.Open(); DataTable table = conn.GetSchema(coll); conn.CloseO; GridViewl.DataSource = table; GridViewl.DataBind(); Полученные данные наша демонстрационная страница связывает с табличным элементом управления GridView (рис. 7-5). Рис. 7-5. Список представлений из базы данных Northwind В ADO.NET 2.0 все объекты подключений поддерживают метод GetSchema, опреде- ленный в новом промежуточном классе DbConnection. В ADO.NET 1.x используется иной подход, зависящий от источника данных. Если вы работаете с OLE DB, то ин- формацию схемы получаете с помощью встроенного провайдера OLE DB, вызывая метод GetOleDbSchemaTable: OleDbConnection conn = new OleDbConnection(connString); conn.Open();
260 Часть II Размещение данных на сайте DataTable schema = cConn.Get01eDbSchemaTable( OleDbSchemaGuid.Tables, new object[] {null, null, null, "TABLE: }), conn.CloseO; Метод GetOleDbSchemaTable принимает член класса OleDbSchemaGuid, указываю- щий, какая информация схемы вам нужна, и массив значений, определяющих, какие именно столбцы вас интересуют. Данный метод возвращает объект DataTable, запол- ненный информацией схемы. В примере мы передали аргумент OleDbSchemaGuid. Tables и получили информацию о таблицах. Тем же способом, задавая другие аргу- менты, можно получить сведения о базах данных, представлениях, ограничениях и т. д. Но есть и альтернативный метод выполнения этой задачи, а именно с помощью функциональных средств, предоставляемых конкретным источником данных, таких как хранимые процедуры и представления. Вот пример: SqlConnection conn = new SqlConnection(connString); SqlDataAdapter adapter = new SqlDataAdapter( "SELECT * FROM INFORMATION-SCHEMA.TABLES " + WHERE TABLE_TYPE = ’BASE TABLE' ” + ORDER BY TABLE_TYPE'. conn); DataTable schema = new DataTableO; adapter.Fill(schema); Фигурирующий здесь класс SqlDataAdapter — это специализированный команд- ный класс, который мы рассмотрим в следующей главе. Одной из его особенностей является то, что он возвращает автономные данные, упакованные в объект DataTable или DataSet. В ADO.NET 2.0 метод GetSchema унифицирует данный подход к извле- чению информации схемы. Строки подключения Для того чтобы подключиться к источнику данных, необходимо задать строку подклю- чения. Обычно она состоит из последовательности токенов в формате имя^ значение, разделенных символами точки с запятой. Типичная строка подключения содержит имя базы данных, информацию о местонахождении сервера и идентификационные данные пользователя. Может задаваться и дополнительная информация, такая как тайм-аут подключения и параметры пулинга подключений. Во многих приложениях класса предприятия с использованием строк подключений связан целый ряд проблем: необходимо решить, как их создавать, хранить, защищать и как ими управлять. Для всех этих проблем, как вы сейчас сможете убедиться, .NET Framework 2.0 предлагает отличные решения. Очевидно, что точный формат строки подключения зависит от конкретной СУБД, но существенных различий между строками подключения к таким распространенным системам, как SQL Server и Oracle, не существует Повторюсь, что в этой главе мы сконцентрируемся на взаимодействии с SQL Server, но я буду отмечать те случаи, где эта СУБД значительно отличается от остальных. Конфигурирование свойств подключения Свойство Connectionstring класса подключения можно устанавливать только тогда, когда подключение закрыто. Многим элементам строки подключения соответствуют свойства данного класса, которые обновляются, когда свойству ConnectionString при- сваивается новое значение. Имена атрибутов в строке подключения не чувствительны к регистру, а если определенное имя встречается в ней несколько раз. используется значение, соответствующее последнему вхождению. Имена атрибутов, которые можно задавать в строке подключения, перечислены в табл. 7-8.
Провайдеры данных ADO.NET Глава 7 261 Табл. 7-8. Атрибуты строки подключения SQL Server Атрибут Описание Application Nalne Имя клиентского приложения в SQL Profiler. По умолчанию ис- пользуется имя Net SqlClient Data Provider Async 1 Когда этот атрибут имеет значение true, включена поддержка асинхронных операций. В ADO.NET 1х данный атрибут не под- держивается Attach DBFileName или Initial File Name Connection Timeout Current Language Database или Initial Catalog Encrypt Полное имя файла базы данных (.mdf), подлежащего присоединению Время ожидания подключения, в секундах (по умолчанию — 15 с) Имя языка SQL Server Имя базы данных, к которой производится подключение Указывает, должен ли для пересылки данных между клиентом и сервером использоваться протокол Secure Sockets Layer (SSL). Для этого необходимо, чтобы на сервере был установлен сертифи- кат. По умолчанию данный атрибут имеет значение false Failover Partner Имя сервера-партнера, который будет использоваться в случае ошибки доступа к основному серверу. Благодаря установке это- го атрибута приложение получает возможность подключиться к альтернативному или резервному серверу базы данных, если основной ее сервер недоступен. В AD0.NET 1х данный атрибут не поддерживается Integrated Security или Trusted_Connection Указывает, используется ли для аутентификации текущая учетная запись пользователя в Windows. Когда данный атрибут установлен в false, необходимо явно задавать идентификатор и пароль пользо- вателя. Поддерживается особое значение sspi, эквивалентное true. Значение по умолчанию — false MultipleActiveResultSets Когда этот атрибут установлен в true (значение по умолчанию), приложение может поддерживать несколько активных результи- рующих наборов записей. Для использования данной функции необходим SQL Server 2005. В ADO.NET 1х данный атрибут не поддерживается Network Library или Net Указывает, какая сетевая библиотека должна использоваться для установления подключения к SQL Server. По умолчанию данный атрибут имеет значение dbmssocn (данная библиотека основана на TCP/IP) Packet Size Password или pwd Размер пакетов, передаваемых между клиентом и сервером, в байтах Пароль пользователя, от имени которого осуществляется подклю- чение Persist Security Info Указывает, должен ли управляемый провайдер включать пароль в строку, возвращаемую в качестве строки подключения. По умол- чанию данный атрибут имеет значение false Server или Data Source Имя или сетевой адрес экземпляра SQL Server, к которому осу- ществляется подключение UserID или uid Workstation ID Имя пользователя, от имени которого осуществляется подключение Имя компьютера, который подключается к SQL Server Сетевая DLL, заданная в атрибуте Network Library, должна быть установлена в сис- теме, к которой производится подключение. Если используется локальный сервер, по умолчанию этой библиотекой является dbmslpcn, осуществляющая взаимодействие через общую память. Полный список допустимых значений атрибута Network Library вы найдете в документации MSDN.
262 Часть II Размещение данных на сайте Попытка подключения к экземпляру SQL Server всегда ограничена по времени: если в течение определенного времени сервер не отвечает, такая попытка считается неудачной. Этот тайм-аут и задается в атрибуте Connection Timeout. Значение 0 ука- зывает, что ожидание ответа сервера может длиться бесконечно; заметьте, что оно не означает «не ждать вообще». Размер пакета изменять не стоит, он подобран с учетом типичных операций и сред- ней рабочей нагрузки. Но если вы намерены выполнять пакетные операции, да еще с участием больших объектов, увеличение размера пакета может помочь ускорить работу приложения, поскольку при этом уменьшится число операций чтения и записи. Некоторые из атрибутов, перечисленных в табл. 7-8, введены в ADO.NET 2.0 — они связаны с функциями, появившимися относительно недавно. К числу таких функций относится поддержка асинхронных команд и нескольких активных наборов записей (Multiple Active Result Sets, MARS). Введение технологии MARS устранило давнее ограничение SQL Server, состоявшее в том, что в рамках одного сеанса нельзя было одновременно направить на выполнение более одного запроса. До появления ADO. NET 2.0 и SQL Server 2005 существовало несколько способов обойти это ограничение, среди которых наибольшее распространение получило применение серверных кур- соров, для чего использовалась ADO. К теме MARS мы вернемся позднее, в разделе, посвященном SQL Server 2005. Построители строк подключения Как лучше составлять используемые в приложении строки подключения? Во многих случаях строки подключения являются константными, и тогда их считывают из внеш- него защищенного источника. Иногда строку подключения приходится составлять динамически, с учетом введенной пользователем информации, например его имени и пароля. В ADO.NET 1.x строку подключения можно составить путем обычной кон- катенации пар имя—значение. Но у этого метода есть два существенных недостатка. Во-первых, ошибки при написании ключевых слов обнаруживаются лишь на этапе тестирования. А во-вторых, что гораздо серьезнее, такой метод формирования строки подключения оставляет лазейку для злоумышленников, совершающих атаки вне- дрением (то есть пытающихся вставить в строку подключения собственные токены, которые открывают доступ к другой базе данных или как-то иначе изменяют результат подключения). Любые меры, предпринимаемые для парирования таких атак и про- верки синтаксиса результирующей строки подключения, требуют написания вручную соответствующего кода, а именно специализированного класса-построителя. Однако в ADO.NET 2.0 такой класс — построитель строк подключения — уже существует! Точнее, каждый стандартный провайдер данных предоставляет собственный класс- построитель. Например, следующий код составляет и отображает с помощью такого класса строку подключения к SQL Server (рис. 7-6). SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(); builder.DataSource = serverName; builder.UserID = userid; builder.Password = pswd; NewConnString.Text = builder.Connectionstring; Применение построителей строк подключения позволяет значительно улучшить их защиту и кардинально уменьшить риск атаки внедрением. Представьте себе, что злонамеренный пользователь вводит пароль Foo;Trusted_Connection^true. Если вы, не глядя, присоедините его к строке подключения, эта строка получится такой: Password=Foo;Т rusted_Connection=true
Провайдеры данных ADO.NET Глава 7 263 Рис. 7-6. Составление строки подключения программным способом Как уже было сказано, выигрывает всегда последняя пара, а потому подключение будет открыто от имени текущего пользователя. Но если воспользоваться классом- построителем, то строка подключения станет такой: Password="Foo; Тrusted .Connections rue” А это уже совсем другое дело! Здесь пароль заключен в кавычки, и никакая его часть не может интерпретироваться как отдельный токен строки подключения. К тому же класс-построитель поддерживает более двух десятков ключевых слов, для кото- рых у него имеются свойства с легко запоминающимися именами, распознаваемыми функцией Microsoft IntelliSense. Сохранение и считывание строк подключения Знающие разработчики избегают жесткого кодирования строк подключения в ком- пилируемом коде. В конфигурационных файлах ASP.NET (таких как web.config) специально предусмотрен раздел <appSettings>, предназначенный для хранения поль- зовательских данных в виде пар имя-значение. Этими данными автоматически запол- няется коллекция AppSettings, откуда их удобно извлекать программным способом: string connString = Configurationsettings.AppSettingsf"NorthwindConn"]; Однако и такой подход весьма далек от совершенства. Во-первых, строки подклю- чения — это не просто данные, а данные особого рода, которые не нужно смешивать с установками общего назначения. Во-вторых, строки подключения являются кри- тическим параметром приложения, ведь они обычно содержат важную информацию, нуждающуюся в хорошей защите. Поэтому для них как минимум требуется прозрачное шифрование. В .NET Framework 2.0 в конфигурационных файлах появился новый раздел, предназначенный специально для хранения строк подключения. Он называется <connectionStrings> и имеет такую структуру: <connectionStrings> <add name="NWind" COnnectionString="SERVER=...;DATABASE^..; UID=...; PWD=...; " providerName=”System.Data.SqlClient" /> </connect ionSt rings> Содержимым этого раздела можно манипулировать с использованием особых узлов: <add>, <remove> и <clear>. Узел <add> служит для добавления новой строки
264 Часть II Размещение данных на сайте подключения к имеющемуся списку, <remove> — для удаления строки подключения, определенной ранее, a <clear> — для удаления всех ранее определенных строк под- ключения. Поместив файл web.config в тот или иной раздел приложения, можно соз- дать для содержащихся в этом разделе страниц свою коллекцию строк подключения (удалив некоторые из строк, определенных на более высоких уровнях, и добавив другие строки). Заметьте, что каждая строка подключения имеет собственное имя, по которому на нее можно ссылаться в приложении, а также в других разделах кон- фигурационных файлов, например в подразделе <providers> разделов <membership> и <profile>. Все строки подключения, определенные в файле web.config, загружаются в но- вую коллекцию ConfigurationManager.ConnectionStrings. Открытие подключения с использованием одной из таких строк, хранящихся в файле web.config, выполняется таким образом: string connStr; connStr = ConfigurationManager.ConnectionStringsfNWind"].Connectionstring; SqlConnection conn = new SqlConnection(connStr); Полная поддержка co стороны конфигурационного API открывает интересную возможность применения строк подключения — декларативное связывание с источ- никами данных. Как вы увидите в главе 9, ASP.NET 2.0 предоставляет в распоря- жение разработчика немалое количество самых разных объектов, представляющих источники данных. Так называются серверные элементы управления, управляющие различными аспектами взаимодействия с источником данных, в том числе установкой подключения и выполнением команд. Вы размещаете на странице элемент управле- ния, связанный с данными, связываете его с объектом-источником данных и дает е последнему указание извлечь данные из определенного источника. Теперь вы можете задать строку подключения декларативно <asp:SqlDataSource id="MySource” runat="server" Р rovide гName="System.Data.SqlClient” ConnectionSt ring='<%# Configurationsettings.ConnectionStrings["NWind"].Connectionstring %>’ SelectCommand="SELECT * FROM employees”» Однако для правильного применения указанной возможности потребуются допол- нительные знания, и вы получите их в следующих главах книги. А пока достаточно сказать, что в .NET Framework 2.0 строки подключения являются особым видом строк и что для работы с ними применяются особые методы. Защита строк подключения В ASP.NET 2.0 введена система защиты критических данных, хранящихся в конфи- гурационных файлах. Их защита заключается в том, что отдельные разделы таких файлов прозрачно для приложения шифруются по технологии XML encryption. Особен- ностью этой технологии (о ней вы можете прочитать по адресу http://www.w3.org/TR/ xmlenc-core), ставшей уже промышленным стандартом, является то, что зашифрованные данные представляются в виде текста в формате XML. В ASP.NET 1.x шифрование по методу XML encryption не поддерживалось, данные можно было шифровать лишь с использованием машинно-зависимого ключа и сохранять в реестре. Применение такого подхода требовало от разработчиков принятия мер по защите собственных секретных данных: строк подключения, идентификационной информации пользова- теля и ключей шифрования.
Провайдеры данных ADO.NET Глава 7 265 В .NET Framework 2.0 шифрование конфигурационных разделов не является обя- зательным, но вы можете включить шифрование любого раздела, указав его имя в разделе <protectedData> файла web.config: <protectedData> <protectedDataSections> <add name=’connectionstrings” proviaer= RSAProtectedConfigurationProvider” /> </protectedDataSections> </protectedData> При этом у вас имеется возможность указать желаемый тип шифрования, выбрав любой подходящий провайдер из числа доступных провайдеров шифрования. В состав .NET Framework 2.0 входят два предопределенных провайдера: DPAPIProtectedConfigurationProvider — для шифрования и дешифрации данных использует Windows Data Protection API (DPAPI); RSAProtectedConfigurationProvider — для шифрования и дешифрации данных ис- пользует алгоритм шифрования RSA; задействуется по умолчанию. Функция защиты данных, хранящихся в файле web.config, предназначена не только для защиты строк подключения. Она применима к любым разделам за несколькими исключениями. Давайте посмотрим, как с ее помощью осуществляется шифрование строк подключения, хранящихся в файле web.config. Для этой цели можно воспользоваться последней версией популярной сис- темной утилиты aspnet_regiis.exe или написать собственное средство, используя конфигурационный API ASP.NET 2.0. Следующий вызов утилиты aspnet_regiis.exe позволяет зашифровать строки подключения приложения ProAspNet20: aspnet_regiis.exe -ре connectionstrings -арр /ProAspNet20 Учтите, что имена разделов чувствительны к регистру. Зашифрованные строки подключения будут сохранены в защищенных областях конфигурационного файла, но для приложения этот факт останется абсолютно прозрачным, и оно сможет работать с такими строками, как прежде. Зато если вы откроете файл web.config, то увидите примерно следующее: <configuration> <protectedData> <protectedDataSections> <add name="connectionStrings ' provider="RSAProtectedConfigurationProvider" /> </protectedDataSections> </protectedData> <connectionStrings> <EncryptedData . > <CipherData> <CipnerValue>cQyofWFQ... =</CipherValue> </CipherData> </EncryptedData> </connectionStrings> </configuration> Для восстановления файла web.config к его исходному состоянию нужно снова вызвать ту же утилиту, но на этот раз с ключом -pd, а не -ре.
266 Часть II Размещение данных на сайте О Внимание! Любая страница, в которой используются данные из защищенных разделов конфигурационного файла, прекрасно работает на локальном Web-сервере, интегрирован- ном в Visual Studio .NET 2005. Однако при попытке доступа к ней из виртуальной папки IIS вы можете получить сообщение об ошибке конфигурации провайдера RSA. Чем же это можно объяснить? Провайдеру, использующему метод шифрования RSA (таковым является задействуемый по умолчанию провайдер), для работы необходим контейнер ключей. Используемый по умолчанию контейнер ключей создается при инсталляции ASP.NET и носит имя NetFrame- WorkConfigurationKey. Вызывая утилиты aspnet_regiis.exe с разными ключами командной строки, можно создавать новые контейнеры ключей, удалять их и редактировать. Однако важно, чтобы контейнер был создан до использования провайдера конфигурации, защи- щенной по методу RSA. Причем контейнер должен не только существовать, но и быть ассоциирован с учетной записью пользователя, от имени которого к нему производится обращение. Системной учетной записи (от имени которой функционирует локальный Web- сервер) назначен такой контейнер, но учетная запись ASP.NET на вашем Web-сервере может его не иметь. Если предположить, что ASP.NET функционирует у вас с учетной записью NETWORK SERVICE (используемой по умолчанию на компьютерах с Windows Server 2003), то, для того чтобы разрешить этой учетной записи доступ к контейнеру, необходимо выполнить следующую команду: aspnet_regiis.exe -pa "NetFrameworkConfigurationKey" "NT AUTHORITY\NETWORK SERVICE" Важно задать полное имя учетной записи, как в приведенном примере. Запомните, что предоставлять доступ к контейнеру ключей необходимо только при использовании RSA- провайдера. Провайдеры RSA и DPAPI прекрасно подходят для шифрования важных данных, нуждающихся в защите. Провайдер DPAPI существенно упрощает процесс управле- ния ключами — они генерируются на основе данных о компьютере и доступны всем процессам, которые на нем выполняются. Но по этой же причине провайдер DPAPI не идеален для защиты разделов на Web-ферме, где один и тот же файл web.config копируется на несколько серверов. В такой ситуации придется либо вручную зашиф- ровать данный файл на каждом сервере, либо скопировать на все серверы один и тот же контейнер ключей. Для этого нужно создать для приложения контейнер ключей, экспортировать его в файл XML, а потом импортировать на каждый сервер, где по- требуется расшифровывать файл web.config. Контейнер генерируется такой командой: aspnet_regiis.exe -рс имя_контейнера -ехр Затем вы экспортируете контейнер ключей в XML-файл: aspnet_regiis.exe -рх имя_контейнера имя_Хт1_файла.хт1 копируете XML-файл на каждый сервер и импортируете его следующим образом: aspnet_regiis.exe -pi имя_контейнера имя_Хт1_файла.xml В заключение не забудьте предоставить учетной записи ASP.NET разрешение на доступ к контейнеру. Ц Примечание Конфигурационный API .NET Framework в этой книге не освещается. Под- робную информацию о данном API и о структуре конфигурационных файлов вы найдете в моей книге «Programming Microsoft ASP.NET 2.0 Applications: Advanced Topics» (Microsoft Press, 2005). Пулинг подключений Функция пудинга подключений играет очень важную роль в обеспечении высокой производительности приложений. Для однопоточных локальных приложений и на-
Провайдеры данных ADO.NET Глава 7 267 стольных приложений, функционирующих в интранет-сети, наличие или отсутствие пулинга подключений не играет большой роли — в том и другом случае их производи- тельность примерно одинакова. Более того, его отсутствие дает вам больший контроль над временем жизни подключений. А вот для многопоточных приложений пулинг исключительно важен, он позволяет повысить их производительность и избежать неприятных ошибок, связанных с аппаратным обеспечением. Ну а когда речь идет о приложениях ASP.NET, каждая миллисекунда простоя подключения отбирает ценные ресурсы у других запросов. Поэтому вам следует не только использовать функцию пулинга подключений, но и открывать подключения как можно позже и закрывать их как можно раньше. Использование функции пулинга подключений снижает для приложения стои- мость открытия и закрытия подключения к базе данных, даже если указанные опе- рации осуществляются часто. (Позже мы поговорим об этом подробнее.) Во всех стандартных .NET-провайдерах данных пулинг подключений по умолчанию включен. Провайдеры для SQL Server и Oracle управляют пудингом подключений самостоя- тельно, посредством специализированных классов. Пулинг подключений провайдера данных OLE DB реализован в инфраструктуре сервиса OLE DB, предназначенной для пулинга сеансов. Для включения и отключения разных сервисов OLE DB, к числу которых относится и пулинг, можно использовать атрибуты строки подключения (например, OLE DB Service). Аналогична ситуация и с ODBC — для этого провайдера пудингом управляет менеджер драйверов ODBC. Конфигурирование пулинга подключений Некоторые атрибуты строки подключения непосредственно связаны с механизмом пулинга. Атрибуты, применяемые для его конфигурирования при взаимодействии с SQL Server, перечислены в табл. 7-9. Табл. 7-9. Атрибуты, управляющие пулингом подключений к SQL Server Атрибут Описание Connection Lifetime Устанавливает максимальную длительность использования объекта подключения из пула (иными словами, определяет время жизни объек- та). Значение этого атрибута проверяется только при возврате объекта в пул. Если время, прошедшее с момента установки подключения, больше заданного, объект подключения уничтожается. (Ниже этот вопрос рассмотрен подробнее) Connection Reset Определяет, должно ли при извлечении объекта из пула переустанав- ливаться подключение к базе данных. По умолчанию этот атрибут имеет значение true Enlist Указывает, что подключение должно автоматически включаться пу- лером в контекст текущей транзакции создавшего это подключение потока Max Pool Size Максимальное количество подключений в пуле (по умолчанию — 100) Min Pool Size Минимальное количество подключений в пуле (по умолчанию — 0) Pooling Если этот атрибут имеет значение true (значение по умолчанию), объект подключения должен быть взят из соответствующего пула или, если необходимо, создан и добавлен в подходящий пул Все приведенные в таблице атрибуты, кроме атрибута Connection Reset, можно использовать и в строках подключения Oracle. Когда используется провайдер Oracle или SQL Server, автоматически активизи- руется функция пулинга подключений; чтобы ее отключить, в строке подключения
268 Часть II Размещение данных на сайте нужно задать атрибут Poolings false. Управления пудингом подключений к источникам данных ODBC осуществляется посредством утилиты ODBC Data Source Administra- tor, запускаемой из Панели управления Windows. На ее вкладке Connection Pooling можно задать параметры пудинга подключений для каждого из установленных драй- веров ODBC. Только не забывайте, что любые изменения параметров определенного драйвера отражаются на всех использующих его приложениях. Провайдер данных .NET для OLE DB автоматически осуществляет пудинг подключений с использова- нием сеансового пудинга OLE DB. Данную функцию можно отключить, присвоив атрибуту OLE DB Services значение -4. В ADO.NET 2.0 автоматическое связывание с текущей транзакцией (атрибут Enlist) осуществляется для всех стандартных провайдеров данных, включая провайдеры для OLE DB и ODBC. В ADO.NET 1.x эту функцию поддерживали только управляемые провайдеры для SQL Server и Oracle, так как они состоят из «родного* управляемого кода .NET, а не являются оболочками для унаследованного кода. Новый метод Enlist- Transaction классов подключений позволяет программным способом связать объект подключения с транзакцией, независимо от того, взят он из пула или создан независимо. Получение и освобождение объектов Каждый пул подключений связан с определенной строкой подключения и опреде- ленным контекстом транзакции. Когда приложение запрашивает открытие нового подключения, то в том случае, если не существует пула, соответствующего заданной строке подключения, создается новый пул, который не уничтожается до завершения процесса. Применение такой технологии пудинга не приводит к снижению произ- водительности системы, поскольку для поддержания неактивных и пустых пулов требуется минимум ресурсов. При создании нового пула в него тут же добавляется определенное минимальное количество подключений. В дальнейшем подключения добавляются в пул по требо- ванию, вплоть до достижения максимального его размера. Добавление в пул нового объекта подключения — операция дорогостоящая, требующая обращения к базе дан- ных. Когда приложение запрашивает объект подключения, он извлекается из пула, если там имеются свободные объекты. Свободным объект подключения считается в том случае, когда он не используется, имеет действующее соединение с сервером и содержит ссылку на действительный контекст транзакции (либо этот контекст равен nu.lt). Если свободный объект подключения не найден, пулер предпринимает попытку создать новый такой объект либо, если размер пула исчерпан, ставит запрос в очередь и обслуживает его, когда освобождается какой-нибудь другой объект подключения. Освобождение объекта подключения осуществляется с помощью метода Close или Dispose. Подключения, не закрытые явно, могут не возвращаться в пул до тех пор, пока его размер не увеличится до максимума (тогда они будут возвращены в пул автоматически при условии, что останутся действительными). Объект подключения удаляется из пула по истечении срока его жизни или при возникновении серьезной ошибки. Пулер периодически очищает находящиеся под его управлением пулы, удаляя из них недействительные объекты подключений. О Внимание! В отличие от пулов подключений ADO, пулы подключений ADO.NET соз- даются на основе строк подключения с применением алгоритма точного соответствия. Это означает, что во избежание создания лишних пулов вам нужно позаботиться о том, чтобы любые две строки подключения, содержащие одинаковый набор параметров, были идентичны (то есть совпадали с точностью до байта). Обратите внимание: речь идет не о семантической идентичности — строки с разным порядком атрибутов или разным количеством пробелов считаются различными и для них создаются два разных пула со всеми вытекающими отсюда издержками.
Провайдеры данных ADO.NET Глава 7 269 Чтобы пудинг подключений осуществлялся эффективно, исключительно важно как можно раньше возвращать ненужные более объекты подключений в пул. И, конечно, тем более важно возвращать эти объекты явно. Заметьте, что вышедшее за пределы области видимости подключение не закрывается, и немедленное возвращение его объекта в пул не производится. Поэтому с объектами подключений рекомендуется работать по следующей схеме: SqlConnection conn = new SqlConnection(connString); try { conn.OpenO; // Сделайте здесь что-нибудь } catch { // Обработайте здесь ошибки } finally { conn.Close() } В качестве альтернативы в коде, написанном на С#, можно прибегнуть к опера- тору using: using (SqlConnection conn = new SqlConnection(connString)) { // Сделайте здесь что-нибудь // Обработайте здесь ошибки } Оператор using эквивалентен конструкции try...catch...finally, в блоке finally кото- рой вызывается метод Close или Dispose. Пользоваться можно любым из этих двух методов или даже обоими — они делают одно и то же. (Точнее, метод Dispose удаля- ет информацию строки подключения и затем вызывает метод Close.) Добавлю, что многократный вызов любого из этих методов не приведет к каким-либо проблемам времени выполнения, поскольку при повторном вызове метода Close или Dispose для одной и той же объектной переменной просто ничего не происходит. ] ~~ Примечание До выхода .NET Framework 2.0 в Visual Basic.NET не было ничего подобного оператору using. Однако начиная с Visual Studio .NET 2005 в коде на этом языке можно использовать сокращенный вариант блока try., catch...finally — Using...End Using: Using conn As New Sq]Qonnection() End Using Выявление утечки подключений В ADO.NET 2.0 появились новые счетчики производительности, благодаря которым стало проще выяснять, не происходит ли в приложении утечка подключений. В част- ности, теперь можно отслеживать показания счетчика NumberOfReclaimedConnections: если он увеличивается, значит, приложение некорректно работает с объектами под- ключений. Характерный симптом утечки подключений — появление исключений тайм-аута, когда истекает время ожидания при попытке получить объект подключения из пула. Корректировка значений определенных параметров строки подключения по- зволяет устранить такие исключения или снизить частоту их возникновения. Однако утечка подключений этим, конечно же, не предотвращается — вы лишь изменяете условия времени выполнения, от которых зависит частота возникновения исключе- ний Ниже приведен краткий перечень действий, которые не нужно совершать, если вы хотите обеспечить правильную работу приложения с подключениями.
270 Часть II Размещение данных на сайте Не отключайте пулинг подключений Если пулинг отключен, каждый раз, когда приложение запрашивает объект подключения, создается новый такой объект. При этом никогда не возникают исключения тайм-аута, зато сильно снижается про- изводительность и, что более важно, все равно происходит утечка подключений. Не уменьшайте время жизни подключения Уменьшение времени жизни под- ключения приводит к тому, что пулер чаще создает новые объекты подключений. Когда это время слишком мало (скажем, несколько секунд), истечение тайм-аута запроса объектов маловероятно, но снижается производительность системы, так что проблему нельзя считать решенной. Эта мера немногим лучше отключения пулинга приложений. Не увеличивайте тайм-аут получения объекта подключения Увеличивая тайм- аут, вы даете пулеру указание дольше ждать освобождения какого-нибудь подклю- чения, прежде чем выбрасывать исключение тайм-аута. Однако какое бы значение данного атрибута вы ни задали, ASP.NET останавливает процесс спустя три ми- нуты. Как правило, увеличение тайм-аута лишь приводит к снижению произво- дительности, но не решает проблемы. Не увеличивайте размер пула подключений Если установить достаточно боль- шой максимальный размер пула (насколько большой, зависит от контекста), вы перестанете получать сообщения об исключениях тайм-аута и пулинг по-прежнему будет осуществляться. Однако при этом снизится масштабируемость приложения, поскольку оно будет потреблять гораздо больше подключений, чем ему в действи- тельности требуется. Единственный действенный способ избежать утечки подключений заключается в том, чтобы закрывать подключения, когда они больше не требуются, причем делать это как можно раньше. В предыдущем разделе я подчеркивал важность явного закрытия подключений. Однако иногда случается так, что приложение вроде бы и вызывает метод Close, а под- ключение не закрывается. Рассмотрим пример: SqlConnection conn = new SqlConnection(connString); conn.0pen(); SqlCommand cmd = new SqlCommand(cmdText, conn); cmd.ExecuteNonQueryO; conn.Close(); Представьте, что во время выполнения команды будет выброшено исключение. Тогда поток выполнения не достигнет вызова метода Close, и поэтому подключение не будет возвращено в пул. В подобных случаях вам поможет оператор using — он гарантирует вызов метода Dispose для заданного в нем объекта подключения. Вот правильная версия приведенного выше кода: using (SqlConnection conn = new SqlConnection(connString)) { conn.0pen(); SqlCommand cmd = new SqlCommand(cmdText, conn); cmd.ExecuteNonQuery(); conn.Close(); // В случае исключения не вызывается } // Метод Dispose вызывается всегда Управление временем жизни подключений В атрибуте Connection Lifetime задается время, в секундах, в течение которого объект подключения должен считаться действительным. Когда этот срок истечет, объект
Провайдеры данных ADO.NET Глава 7 271 будет удален. Но зачем же, спросите вы, избавляться от хорошего, действующего объ- екта? Атрибут Connection Lifetime используется лишь в особых, совершенно опреде- ленных случаях, и в других ситуациях задавать его не следует. Представьте себе, что у вас имеется кластер серверов, между которыми распределяется рабочая нагрузка. Распределение должно быть равномерным; но в какие-то моменты, например сразу после добавления нового сервера, это может оказаться не так, поскольку компоненты промежуточного слоя кэшируют подключения и никогда не открывают новые. В таком случае можно удалить часть подключений и тем самым заставить эти компоненты создать новые подключения, которые будут назначены новому серверу. Таким образом, атрибут Connection Lifetime следует использовать только в кластерных системах. За- метьте, что классы-построители подключений из ADO.NET 2.0 используют для этого атрибута иное (и, надо сказать, более подходящее) имя — Load Balance Timeout. । Примечание Load Balance Timeout — это новый атрибут строк подключения. Если вы используете для их программного построения класс SqlConnectionStringBuilder, то сможете устанавливать данный атрибут посредством свойства LoadBalanceTimeout. Очистка пула подключений До выхода ADO.NET 2.0 не существовало программного способа очистки пула от- крытых подключений. Очевидно, что эта операция не из тех, которые выполняются часто, но в случае, когда сервер базы данных по той или иной причине останавливается и запускается снова, выполнить ее необходимо. Возьмем такой пример. Страницы приложения ASP.NET открывают и успешно закрывают подключения из одного пула. Затем сервер внезапно останавливается и запускается снова. В результате все объекты подключений в пуле становятся недействительными, поскольку содержат ссылки на несуществующие теперь серверные подключения. А что же произойдет, когда очеред- ная страница снова запросит подключение? Ответ прост: пулер вернет объект подключения, кажущийся действительным, и страница попытается выполнить посредством него команду. Однако сервером базы данных этот объект распознан не будет, в результате чего произойдет исключение. После этого объект подключения будет удален из пула и заменен новым. Так будет происходить с каждым имеющимся в пуле подключением, пока пулер не удалит и не заменит их все. Как видите, перезапуск сервера базы данных без перезапуска при- ложения приводит к тому, что пул оказывается разрушенным. Данная ситуация типична для приложений, работающих в среде, где возмож- на перезагрузка серверов, например в отказоустойчивых кластерах. Единственным решением проблемы в подобном случае является сброс пула подключения. Однако реализовать его не так просто, как может показаться. Наиболее очевидный способ — перехватывать исключение и слегка изменять строку подключения, чтобы для нее был создан новый пул. В ADO.NET 2.0 у описанной проблемы есть другое решение. Эта система доста- точно интеллектуальна, чтобы распознать исключения, говорящие о разрушении пула. Когда исключение выбрасывается при выполнении команды доступа к базе данных, ADO.NET 2.0 выясняет, не свидетельствует ли оно о разрушении пула. Если это так, все содержащиеся в пуле объекты помечаются как недействительные. В каком случае исключение указывает на разрушение пула? Это должно быть фатальное исключение, сгенерированное на уровне сети для открытого ранее подключения. Все остальные исключения игнорируются подсистемой пудинга и всплывают как обычно. Для программной очистки пула, когда приложению становится известно о пере- запуске сервера, могут использоваться новые статические методы ClearPool и Clear- AllPools, которые определяются в составе классов SqlConnection и OracleConnection.
272 Часть II Размещение данных на сайте Эти методы вызывает и сама ADO.NET 2.0, когда в описанной выше ситуации ей нужно очистить пул. Выполнение команд После установки между клиентом и базой данных физического канала связи можно начинать подготовку и выполнение команд. Объектная модель ADO.NET определяет два типа командных объектов — традиционный одноразовый командный объект и адаптер данных. Первый выполняет команду SQL или хранимую процедуру и воз- вращает курсор, с помощью которого можно осуществить проход по записям и про- читать данные. Пока курсор используется, подключение активно и открыто. Адаптер данных — более мощный объект; сам он использует одноразовый командный объект и курсор, а программисту предоставляет интерфейс, позволяющий работать с данными после отключения от их источника. Для этого адаптер извлекает данные и загружает их в контейнерный класс DataSet или DataTable. Мы поговорим о контейнерных классах и адаптерах данных в следующей главе. А пока давайте сконцентрируемся на одноразовых командных объектах, уделяя осо- бое внимание объектам SQL Server. Класс SqlCommand Класс SqlCommand представляет оператор SQL Server или хранимую процедуру. Это клонируемый скрытый класс, реализующий интерфейс IDbCommand. В ADO.NET 2.0 класс SqlCommand является производным от класса DbCommand, который также реали- зует данный интерфейс. Команда выполняется в контексте подключения и, возможно, транзакции. Данную ситуацию отражает набор конструкторов класса SqlCommand'. public SqlCommandO; public SqlCommand(string); public SqlCommand(string, SqlConnection); public SqlCommand(string, SqlConnection, SqlTransaction); Аргумент string содержит текст подлежащей выполнению команды или имя хра- нимой процедуры, аргумент SqlConnection — объект предназначенного для этой цели подключения, а аргумент SqlTransaction, если он задан, представляет транзакционный контекст, в котором будет выполняться команда. Командные объекты ADO.NET никогда сами не открывают подключение. Программист же должен явно связать его объект с объектом команды, а также явно открыть и закрыть подключение. Сказанное относится и к транзакции. Свойства класса SqlCommand В табл. 7-10 перечислены элементы, из которых состоит командный объект провайдера данных .NET для SQL Server. Команды можно связывать с параметрами, которые должны быть представлены объектами, специфическими для конкретного провайдера. Классом параметра для управляемого провайдера SQL Server является SqlParameter. Тип команды задается в свойстве CommandType, допустимыми значениями которого являются: Text — значение по умолчанию, указывающее, что свойство содержит код на языке Transact-SQL, подлежащий непосредственному выполнению; StoredProcedure — указывает, что значением свойства является имя хранимой про- цедуры, содержащейся в текущей базе данных; TableDirect — указывает, что свойство содержит разделенный запятыми список имен таблиц, все строки и столбцы которых должны быть возвращены; поддерживается только провайдером данных для OLE DB.
Чровайдеры данных ADO.NET Глава 7 273 Габл. 7-10. Свойства класса SqlCommand . Щг- Свойство Входит ли в состав интерфейса IDbCommand Описание CommandText Да Возвращает и позволяет задать Подлежащий вы- полнению оператор или имя хранимой процедуры CommandTimeout Да Возвращает и позволяет задать максимальную длительность выполнения команды в секундах (значение по умолчанию — 30) CommandType Да Указывает и позволяет задать способ интерпрета- ции содержимого свойства CommandText. По умолчанию имеет значение Text, указывающее, что свойство CommandText содержит текст команды Connection Да 1 г Возвращает и позволяет задать объект подключе- ния, который будет использоваться для выполне- ния команды. По умолчанию имеет значение null Notification Нет Возвращает и позволяет задать объект SqlNoti- ficationRequest, связанный с данной командой. Для использования этого свойства необходим SQL 1 .“У /Ои Server2005 NotificationAutoEnlist Нет Указывает, будет ли команда автоматически свя- ; • i .4 * зана с сервисом уведомлений SQI. Sen ei 2005. Для использования этого свойства необходим SQL Server2005 Parameters Да Возвращает коллекцию параметров команды Transaction Да Возвращает и позволяет задать транзакцию, в рам- ках которой будет выполняться команда Транзак- ция должна быть связана с тем же. подключением, что и команда UpdatedRowSource *• Ar. v; . : , ; 1 , Да Указывает и позволяет определить способ при- менения результатов запроса к обновляемой строке. Значение этого свойства используется только в том случае, когда команда выполняется с помощью метода Update адаптера данных. До- пустимыми значениями свойства являются члены перечисления UpdateRowSourcc Приведем пример кода, с помощью которого выполняется вызов хранимой про- цедуры: using (SqlConnection conn = new SqlConnection(ConnString)) { SqlCommand cmd = new SqlCommand(sprocName, conn); cmd.ComjnandType = ComroandT^pe.StoredProcedure;. cmd.Connection.Open(); cmd.ExecuteNonQue ry();' } В ADO.NET 2.0 у командных объектов появились две новые функции: асинхрон- ное выполнение и поддержка сервисов уведомления. Мы рассмотрим их чуть позже. Методы класса SqlCommand У класса SqlCommand имеется немало методов, и все они так или иначе связаны с вы- полнением команд. Их перечень приведен в табл. 7-11.
274 Часть II Размещение данных на сайте Табл. 7-11. Методы класса SqlCommand Метод Входит ли в состав интерфейса IDbCommand Описание BeginExecuteNonQuery Нет Выполняет команду, не являющуюся запросом на выборку, без блокирования записей. В ADO.NET 1х данный метод не поддерживается BeginExecuteReader Нет Выполняет запрос на выборку без блокирования записей. В ADO.NET 1х данный метод не под- держивается BeginExeciiteXmlReader Нет Выполняет XML-запрос без блокирования за- писей. В ADO.NET 1.x данный метод не поддер- живается Cancel Да Осуществляет попытку отменить выполнение команды. В случае неудачи исключение не гене- рируется CreateParameter Да Создает новый экземпляр объекта SqlParameter EndExecuteNonQuery Нет Завершает асинхронное выполнение команды, не являющейся запросом на выборку. В ADO. NET 1.x данный метод не поддерживается EndExecuteReader Нет Завершает выполнение асинхронного запроса на выборку. В ADO.NET 1х данный метод не под- держивается EndExecuteXmlReader Нет Завершает выполнение асинхронного запроса на выборку XML-данных. В ADO.NET 1 х данный метод не поддерживается ExecuteNonQuery Да Выполняет команду, не являющуюся запросом на выборку, и возвращает количество обработан- ных строк ExecuteReader Да Выполняет запрос на выборку и возвращает курсор, доступный только для чтения — объект чтения данных ExecuteScalar Да Выполняет запрос на выборку и возвращает зна- чение первого столбца первой строки результи- рующего набора данных. Все остальные данные игнорируются ExecuteXmlReader Нет Выполняет запрос на выборку, возвращающий XML-данные и создает объект XmlReader Prepare Да Создает подготовленную версию команды в эк- земпляре SQL Server ResetCommandTiTneout Нет Сбрасывает значение тайм-аута команды, уста- навливая тайм-аут по умолчанию Аргументы параметризированных команд задаются в виде экземпляров класса SqlParameter. Каждый параметр характеризуется именем, значением, типом, направ- лением передачи и размером. В некоторых случаях параметры можно связывать со столбцами источника данных. Связывание параметра с командным объектом осу- ществляется через его коллекцию Parameters’. SqlParameter parm = new SqlParameter(); parm.ParameterName = "@employeeid"; parm.DbType = DbType.Int32; parm.Direction = ParameterDirection.Input; cmd. Pa ramete rs. Add (pa rm);
Провайдеры данных ADO.NET Глава 7 275 Приведем пример параметризированного SQL-запроса: SELECT * FROM employees WHERE employeeid=@employeeid Провайдер данных .NET для SQL Server идентифицирует параметры по именам (в составе SQL-команды они всегда имеют префикс @). Поэтому порядок связывания параметров с командным объектом не имеет значения. fg] Примечание Именованные параметры поддерживаются и управляемым провайдером для Oracle, а вот провайдеры для источников данных OLE DB и ODBC их не поддерживают. В таких источниках данных применяются позиционные параметры, местоположение ко- торых в SQL-команде отмечают вопросительные знаки (?). Для позиционных параметров порядок их задания имеет существенное значение. Способы выполнения команд Как видно из табл. 7-11, заданная в объекте SqlCommand команда может быть вы- полнена как синхронным способом, так и асинхронным. Мы же сначала посмо- трим, как осуществляется асинхронное выполнение команд, поддерживаемое всеми платформами .NET. Для этой цели могут использоваться четыре метода: ExecuteNonQuery, ExecuteReader, ExecuteScalar и ExecuteXmlReader. Они действуют практически одинаково, но возвращают разные значения. Обычно для выполнения операций обновления, таких как UPDATE, INSERT и DELETE, используется метод ExecuteNonQuery, возвращающий количество задействованных в операции строк. Для операторов других типов, таких как SET или CREATE, он возвращает значение -1. Метод ExecuteReader предназначен для выполнения запросов на выборку; он воз- вращает объект чтения данных — экземпляр класса SqlDataReader. Такой объект пред- ставляет собой род доступного только для чтения однонаправленного курсора, исполь- зуемого клиентским кодом для последовательного считывания результатов выполнения запроса. Если попытаться выполнить посредством метода ExecuteReader запрос на обновление, то обновление данных будет произведено успешно, но обработанных строк вы не получите. К теме объектов чтения данных мы вернемся чуть позже. Когда требуется получить единственное значение, удобнее всего воспользоваться методом ExecuteScalar. Он прекрасно работает с операторами SELECT COUNT или командами, возвращающими агрегированные данные. Этот метод можно вызывать и для обычных запросов — в таком случае вы получите значение первого столбца первой строки результирующего набора, а все остальные данные будут проигнорированы. При использовании метода ExecuteScalar код получается более компактным, при вызове метода ExecuteReader, сопровождающемся получением полного результирующего набора и последующим извлечением из него требуемого значения. Три перечисленных метода имеются у всех командных объектов. Класс SqlCom- mand поддерживает также метод ExecuteXmlReader, предназначенный для выполне- ния команды с возвратом XML-данных. Эти данные возвращаются в виде объекта XmlReader, с помощью которого очень удобно осуществлять навигацию по дереву XML. Метод ExecuteXmlReader идеально подходит для выполнения запросов, завер- шающихся предложением FOR XML, и запросов на выборку данных из текстовых полей XML-формата. Заметьте: пока объект XmlReader используется, подключение остается активным. Объекты чтения данных ADO.NET Классы семейства XXXDataReader предназначены для приложений, работающих с ба- зами данных. Их объекты, называемые объектами чтения данных (data reader), пред- ставляют собой род курсоров, позволяющих выполнять проход по данным и считывать
276 Ч1СТЬII Размещение данных на сайте один или более сгенерированных командой результирующих наборов Объект Чтений данных работает только при наличии подключения и является однонаправленным, то есть позволяет выполнять проход по данным только в прямом, но не в обратном направлении. Такой объект создается при выполнении метода ExecuteReader. Резулъ- таты выполнения этого метода хранятся в буфере на клиенте и доступны объекту XXXDataReader. При использовании объекта чтения данных записи считываются по одной, по мере того как они становятся доступны. Такой подход довольно эффективен с точки зрения производительности и затрат: поскольку в каждый момент времени на кли- енте кэшируется единственная запись, не приходится дожидаться загрузки всего результирующего набора. В табл. 7-12 приведен перечень свойств класса SqlDataReader — класса объектов чтения данных, предназначенного для SQL Server. Табл. 7-12. Свойства класса SqlDataReader Свойство Описание Depth Указывает глубину вложенности текущей строки. Для класса SqlDa- taReader всегда возвращает значение 0 FieldCount Возвращает количество столбцов в текущей строке HasRows Возвращает значение, указывающее, сколько строк содержит объект чтения данных: одну или более. В ADO NET 1.0 данное свойство не под- держивается • IsClosed Возвращает значение, указывающее, закрыт ли объект чтения данных Item Индексное свойство, возвращающее значение столбца в его исходном формате RecordsAffected Возвращает количество строк, модифицированных при выполнении пакет ной команды Свойство Depth служит для определения уровня вложенности текущей строки. Уро- вень вложенности самой внешней таблицы равен 0, таблицы, содержащейся в ее столб- це, — 1, таблицы, находящейся в столбце последней, — 2 и т. д. Большинство объектов чтения данных, включая объекты классов SqlDataReader и OracleDataReader, не поддер- живают вложенности таблиц, так что их свойство Depth всегда возвращает значение 0. Свойство RecordsAffected не устанавливается до тех пор, пока все строки не про- читаны и объект чтения данных не закрыт. По умолчанию оно имеет значение -1. Заметьте, что IsClosed и RecordsAffected — единственные свойства, доступные, когда объект чтения данных закрыт. Важнейшие методы объекта чтения данных SQL Server приведены в табл. 7-13 Табл. 7-13. Методы класса SqlDataReader Метод Описание Close Закрывает объект чтения данных. Заметьте, что при этом автоматиче- ского закрытия используемого подключения не происходит GetBoolean Возвращает содержимое заданного столбца в виде значения булева типа GetByte Возвращает содержимое заданного столбца в виде однобайтового зна- чения GetBytes Считывает в буфер поток байтов из заданного источника Позволяет задавать смещение, причем как для чтения, так и для записи GetChar Возвращает содержимое заданного столбца в виде одного символа GetChars Считывает поток символов из заданного столбца в буфер. Вы.можете задать смещение как для чтения, так и для записи
Провайдеры данных ADO.NET Глава 7 277 Табл. 7-13. (окончание) Ме-од Описание GetDataTypeName Возвращает имя типа заданного столбца в его источнике GetDateTime Возвращает содержимое заданного столбца в виде объекта DateTime GetDecimal Возвращает содержимое заданного столбца в виде значения десятично- го типа GetDouble Возвращает содержимое заданного столбца в виде числового значения двойной точности GetFieldType Возвращает для заданного столбца объект Туре GetFloat Возвращает содержимое заданного столбца в виде числового значения одинарной точности GetGuid Возвращает содержимое заданного столбца в виде глобально уникаль- ного идентификатора (GUID) Getlnt16 Возвращает содержимое заданного столбца в виде 16-битового целочис- ленного значения Getlnt32 Возвращает содержимое заданного столбца в виде 32-битового целочис- ленного значения Getlnt64 Возвращает содержимое заданного столбца в виде 64-битового целочис- ленного значения GetName Возвращает имя заданного столбца GetOrdinal Возвращает порядковый номер столбца, заданного его именем GetSchemaTable Возвращает объект DataTable с метаданными столбцов, которыми управляет объект чтения данных GetString Возвращает содержимое заданного столбца в виде строки GetValue Возвращает содержимое заданного столбца в его исходном формате GetValues Копирует значения всех столбцов в заданный массив объектов IsDbNull Указывает, содержит ли столбец значение null. Типом такого столбца является System.DBNull NextResult Перемещает указатель объекта чтения данных к началу следующего результирующего набора, если таковой имеется Read Перемещает указатель объекта чтения данных к началу следующей за- (' i писи, если таковая имеется У объекта чтения данных SQL Server имеются также специфические для этой СУБД методы семейства GetXXX. К их числу относятся GetSqlDouble, GetSqlMoney, GetSqlDed- mal и т. д. Методы семейств GetXXX и GetSqlXXX различаются типами возвращаемых значений: у первых это базовые типы данных .NET, а у вторых — оболочки .NET Fra- mework для типов данных SQL Server (например, SqlDouble, SqlMoney или SqlDedmaT). .NET-типы данных SQL Server принадлежат к перечислению SqlDbType. Все методы GetXXX, возвращающие значение столбца, идентифицируют столбец по О-базированному индексу. Заметьте, они даже не пытаются выполнить преобразование значений, а возвращают данные как есть, приведя их к заданному типу. Если значение столбца и тип, к которому оно приводится, несовместимы, выбрасывается исключение. РД Примечание Метод GetBytes полезен для побайтового чтения данных из больших полей. Кроме того, метод может использоваться для определения длины содержимого столбца в байтах. Для этого нужно передать ему буфер в виде ссылки null, и тогда он просто вернет длину содержимого столбца. Чтение данных с помощью объекта SqlDataReader Используя объекты XXXDataReader никогда не забывайте, что они работают в режи- ме подключения. Объект XXXDataReader реализует самый быстрый способ чтения
278 Часть II Размещение данных на сайте данных из источника, но важно считывать доставленные им данные как можно быстрее и тут же освобождать подключение. Строки результирующего набора передаются на клиент по одной, и перемещение по ним осуществляется с помощью метода Read. Ниже демонстрируется типичный цикл чтения полного набора строк, возвращенных запросом: using (SqlConnection conn = new SqlConnection(connString)) { string cmdText = "SELECT * FROM customers"; SqlCommand cmd = new SqlCommand(cmdText, conn) cmd.Connection.Open(); SqlDataReader reader = cmd.ExecuteReaderO; while (reader.Read()) Custome rList.Items.Add(reade r["companyname"].ToSt ring()); reader.Close(); } Вам не нужно явно перемещать указатель на строку вперед и не нужно выпол- нять проверку на конец набора записей. Когда записей больше не останется, метод Read просто вернет значение false. С помощью объекта чтения данных очень удобно выполнять их последовательное чтение и обработку, однако в случае, когда данные нужно кэшировать для последующего использования, он непригоден — тогда при- меняется объект-контейнер. (О работе с контейнерными объектами рассказывается в главе 8.) Примечание Хотя доступ к строковым полям по имени является наиболее простым и естественным методом доступа, он, к сожалению, не самый быстрый. В случае его применения объекту чтения данных приходится самостоятельно разрешать каждое имя как О-базированный индекс. Поэтому, задав индекс явно, вы получите чуть быстрее вы- полняющийся код: const int Customers_CustomerID = 0; Response.Write(reader[Customers_CustomerID].ToString()); Как видите, применение констант позволяет достичь удачного компромисса между чита- бельностью и производительностью. Поведение командных объектов Вызывая метод ExecuteReader для командного объекта — любого, независимо от типа СУБД, — необходимо задать режим его работы, называемый поведением. У метода ExecuteReader имеется перегруженная версия, принимающая аргумент типа Command- Behavior. cmd.ExecuteReader(CommandBehavior.CloseConnection); Данный тип является перечислимым. Его значения описаны в табл. 7-14. Режим последовательного доступа распространяется не только на строки, но также и на столбцы результирующего набора. Это означает, что доступ к столбцам можно осуществлять лишь в том порядке, в каком они размещены в результирую- щем наборе, то есть вы не можете прочитать, скажем, столбец 2 до того, как будет прочитан столбец 1. Если говорить более точно, то, выполнив чтение за опреде- ленной позицией, вы уже не сможете к ней вернуться. Применяемый в сочетании с методом GetBytes последовательный доступ к данным является особенно полезным в тех в случаях, когда приходится загружать BLOB-данные в буфер ограниченного размера.
Провайдеры данных ADO.NET Глава 7 279 Табл. 7-14. Значения перечислимого типа CommandBehavior, определяющие поведение командного объекта Значение Описание CloseConnection Командный объект автоматически закрывает подключение к источнику данных, когда закрывается объект чтения данных Default В поведении командного объекта нет никаких особенностей. Установка этой опции функционально эквивалентна вызову ExecuteReader без пара- метров Keyinfo Запрос возвращает только метаданные столбцов и информацию о пер- вичном ключе. Запрос выполняется без блокировки выбираемых строк SchemaOnly Запрос возвращает только столбцы метаданных и выполняется без бло- кировки выбираемых строк SequentialAccess Объект чтения данных считывает их в виде последовательного потока. Данная установка используется совместно с методами GetBytes и GetChars, с помощью которых считываются байты или символы; размер буфера возвращаемых данных при этом ограничен SingleResult SingleRow Запрос возвращает только первый результирующий набор Запрос возвращает одну строку Примечание Установку SingleRow можно задавать и для тех запросов, которые возвра- щают несколько результирующих наборов. Вы получите все эти наборы, но каждый из них будет содержать только одну строку. Установки SingleRow и SingleResult позволяют сообщить провайдеру о структуре ожидаемых результирующих данных, чтобы он при необходимости мог произвести некоторую внутреннюю оптимизацию. Закрытие объекта чтения данных Создание объекта чтения данных никогда не осуществляется явно. Конструктор у его класса, конечно же, имеется, но его нельзя вызывать из пользовательских приложений. Этот конструктор помечен как предназначенный для внутреннего использования и мо- жет вызываться только из классов, определенных в общей сборке System.Data. Объект чтения данных создается неявно при вызове метода ExecuteReader. Операции открытия и закрытия этого объекта выполняются явно, отдельно от операции его создания. Метод Read перемещает внутренний указатель объекта к следующей доступной для чтения записи текущего результирующего набора и возвращает булево значение, указывающее, остались ли еще непрочитанные записи. Пока производится чтение, объект подключе- ния занят и никакие другие операции с его помощью выполняться не могут. Обращаю ваше внимание на тот факт, что объект чтения данных и объект под- ключения — это совершенно разные объекты, управление которыми и их закрытие производится независимо. Оба они имеют метод Close, который, таким образом, при- ходится вызывать дважды: сначала для объекта чтения данных, а затем — для под- ключения. Если задана поведенческая установка CloseConnection, закрытие объекта чтения данных вызывает и закрытие связанного с ним подключения. Кроме того, метод Close объекта чтения данных заполняет свойства, соответствующие выходным параметрам команды, и устанавливает свойство RecordsAffected. /К. Совет Поскольку метод Close объекта чтения данных производит некоторую дополни- «Г тельную работу, его выполнение — довольно дорогостоящая операция, особенно когда речь идет о сложных и долго выполняющихся запросах. Поэтому в тех случаях, когда важно достичь максимально возможной производительности, а возвращаемые значения и сведения о количестве обработанных записей приложению не нужны, можно вместо явного закрытия объекта чтения данных вызвать метод Cancel объекта SqlCommand. Он прервет операцию и закроет объект чтения более быстрым способом. Однако за закрытие подключения при этом отвечаете вы сами.
280 Часть II Размещение данных на сайте Доступ к нескольким результирующим наборам В зависимости от синтаксиса запроса в результате его выполнения может быть воз- вращен либо один, либо несколько наборов данных. Когда их несколько, по умолчанию объект чтения данных связывается с первым из них и вы можете выполнить проход по строкам этого набора с помощью метода Read. Когда будет достигнута последняя запись, метод вернет значение false и не станет перемещать указатель дальше. Для перехода к следующему результирующему набору нужно воспользоваться методом NextResult Он возвращает значение false, когда все результирующие наборы прочита- ны. Следующий код показывает, как перебрать строки всех результирующих наборов данных. using (SqlConnection conn = new SqlConnection(connString)) { string cmdText = Query.Text; SqlCommand cmd = new SqlCommand(cmdText, conn); cmd.Connection.Open(); SqlDataReader reader = cmd.ExecuteReaderQ; do { // Проходим по записям очередного набора while (reader Read()) sb.AppendFormat("{0}, {1}<br/>", reader[0], reader[1]); // Разделяем группы записей sb.Append("<hr />"); } while (reader.NextResultO); reader.Close(); } /I Выводим результаты на странице Results.Text = sb.ToStringO; На рис. 7-7 представлен вывод демонстрационной страницы, которая содержит приведенный код. Примечание В .NET Framework версии 1.1 программный интерфейс объектов чтения данных был расширен — в его состав добавлен метод HasRows, который возвращает булево значение, указывающее, остались ли еще непрочитанные строки. Однако не су- ществует способа узнать заранее, сколько всего строк содержит результирующий набор и сколько всего результирующих наборов было возвращено при выполнении запроса. Асинхронное выполнение команд Операции с базами данных обычно производятся в синхронном режиме: на время выполнения операции вызывающее приложение блокируется и получает управление лишь после ее завершения. Если операции длительны, применение такого подхода может приводить к серьезному снижению производительности и масштабируемости приложения, что и случается нередко с приложениями, взаимодействующими с ба- зами данных. Система .NET Framework 1.x поддерживает асинхронные операции, но модель их выполнения основана на пользовательском коде: вы пишете асинхронную процедуру и уже в ней подключаетесь к базе данных и вызываете нужные команды, однако в рамках этой процедуры управление подключением и выполнение команд все же осуществляются синхронно. А вот ADO.NET 2.0 и провайдер данных .NET для SQL Server позволяют выполнять команды по-настоящему асинхронно. Это большое
Провайдеры данных ADO.NET Глава 7 281 преимущество с точки зрения производительности, поскольку в то время, пока вы- полняется запрос к базе данных, приложение может заниматься другой работой. Про- граммный код, обеспечивающий возможность осуществления асинхронных операций, входит в состав класса SqlCommand. Он позволяет асинхронно выполнять команды, не являющиеся запросами на выборку, а также асинхронно получать объекты чтения данных — как обычные, так и XML. В ADO.NET 2.0 поддерживаются три метода асинхронного выполнения команд: без блокирования приложения, с опросом и с об- ратным вызовом. Рис. 7-7. Обработка нескольких результирующих наборов данных Подготовка к асинхронному выполнению команд Для того чтобы через определенное подключение можно было выполнять команды в асинхронном режиме, необходимо в строке этого подключения задать новый атри- бут Async со значением true. Если этого не сделать, при попытке вызова того или иного асинхронного метода будет сгенерировано исключение. Учтите, что установка данного атрибута отрицательно сказывается на производительности приложения, поэтому желательно устанавливать его лишь для тех подключений, через которые будут осуществляться только асинхронные операции. Если вашему приложению нужно работать с базой данных как синхронно, так и асинхронно, лучше установить два разных подключения. При необходимости син- хронную команду можно выполнить и через такое подключение, для которого атрибут Async установлен в true, но в этом случае на ее выполнение будет затрачено слишком много ресурсов.
282 Часть II Размещение данных на сайте Примечание В ADO.NET 2.0 асинхронные команды реализуются не так, как прежде, когда создавался новый поток и в нем инициировалось выполнение команды, до за- вершения которого поток блокировался. Следует знать, что ADO.NET не ориентирована на многопоточное выполнение кода и при ее использовании создание блокирующих по- токов может привести к серьезному снижению производительности. Когда разрешено выполнение асинхронных команд, ADO.NET просто открывает TCP-сокет к базе данных в режиме с перекрытием и связывает его с портом завершения ввода-вывода. Таким об- разом, асинхронные операции просто эмулируются системой, на самом деле работающей в синхронном режиме, и это объясняет, почему в ней и синхронные операции при таком способе работы оказываются более дорогостоящими. Разновидности асинхронных команд Простейшей разновидностью асинхронных команд являются неблокирующие коман- ды. Принцип их действия прост: приложение инициирует операцию с базой данных и продолжает свою работу, выполняя другие, не связанные с ней задачи, а потом воз- вращается к этой операции, чтобы получить результаты. При выполнении любой асинхронной команды первым делом вызывается метод BeginExecuteXXX. Например, если нужно выполнить запрос на выборку, то следует вызвать метод BeginExecuteReader. // Начинаем неблокирующее выполнение lAsyncResult iar = cmd.BeginExecuteReader(); // Пока команда выполняется, делаем что-то другое If Блокируем дальнейшую работу до тех пор, пока выполнение команды не завершится SqlDataReader reader = cmd.EndExecuteReader(iar); // Обрабатываем полученные данные .. ProcessData(reader); Функция BeginExecuteReader возвращает объект lAsyncResult, с помощью которого вы позднее сможете завершить вызов. Далее выполняется любой код, не имеющий отношения к инициированной операции, а потом, когда приходит время получить результаты, вызывается метод EndExecuteReader. С этого момента работа приложения блокируется — до тех пор, пока выполнение операции не завершится. Таким образом, вызов EndExecuteReader автоматически синхронизирует команду с остальным кодом приложения, блокируя его до тех пор, пока не будут готовы результаты выполнения этой команды. Альтернативой такому подходу может стать метод опроса, то есть периодическая проверка состояния асинхронно выполняющейся операции на предмет ее завершения. Следующий код показывает, как это делается для запроса на выборку: // Инициируем выполнение запроса lAsyncResult iar = cmd.BeginExecuteReader(); do { // Делаем здесь что-нибудь } while (!iar.IsCompleted); // Синхронизируемся SqlDataReader reader = cmd.EndExecuteReader(iar); ProcessData(reader);
Провайдеры данных ADO.NET Глава 7 283 Заметьте, что пока вызов iarJsCompleted возвращает true, метод EndExecuteReader не вызывается и выполнение кода приложения не блокируется. Третьим способом асинхронного выполнения команд является применение ме- тода обратного вызова. В этом случае приложение инициирует выполнение опера- ции с базой данных и продолжает свою работу, более к этой операции не возвраща- ясь. Позднее, когда ее выполнение завершается, приложение получает вызов извне. Разумеется, чтобы обеспечить получение такого вызова, необходимо задать делегат вызываемой функции. Он передается методу BeginExecuteXXX наряду с информаци- ей состояния — любой, какую вы захотите передать функции обратного вызова. Так, в следующем примере передается командный объект: // Инициируем выполнение команды lAsyncResult iar = cmd.BeginExecuteReader( new AsyncCallback(ProcessData), cmd): Инициировав асинхронную операцию, приложение может о ней забыть и продол- жать свою работу как обычно. Когда выполнение операции завершится, будет акти- визирована функция обратного вызова, которая должна иметь такую структуру: public void ProcessData(lAsyncResult iar) { // Извлекаем контекст вызова SqlCommand cmd = (SqlCommand) iar.AsyncState; // Завершаем асинхронную операцию SqlDataReader reader = cmd.EndExecuteReader(iar); } Контекст вызова, заданный вами во втором аргументе метода BeingExecuteReader, упаковывается в свойство AsyncState объекта LAsyncResult. L3 Примечание Функция обратного вызова, скорее всего, будет активизирована в потоке из пула, отличном от того, который инициировал операцию. Поэтому необходима правильная синхронизация потоков. Кроме того, при использовании такого подхода встает проблема пользовательского интерфейса приложения, особенно если речь идет о приложении Win- dows Forms: ведь необходимо обеспечить обновление пользовательского интерфейса в нужном потоке. Элементы управления и формы Windows Forms предоставляют механизм, позволяющий узнать, в каком потоке выполняется код, и, если это не тот поток, кото- рый вам нужен, получить доступ к нужному потоку. За более детальной информацией о многопоточном программировании для приложений Windows Forms можно обратиться к хорошей книге по этой теме и документации MSDN. Заметьте, что при неправильном применении многопоточной модели выполнения ваше приложение наверняка будет бло- кироваться и даже завершаться аварийно. Параллельное выполнение команд из страниц ASP.NET Наличие механизма асинхронного выполнения команд еще не означает, что им можно пользоваться без тщательного взвешивания достоинств и недостатков такого решения для каждого конкретного случая. Давайте рассмотрим две ситуации, в которых асин- хронное выполнение команд действительно идет на пользу приложению. Первая из них связана с параллельным выполнением нескольких SQL-операторов, направленных одному или нескольким серверам базы данных. Предположим, что на некоторой странице выводится информация о клиенте — дан- ные о нем самом и определенные учетные данные. Первый блок информации поступа- ет из базы данных клиентов, а второй — из базы данных учета. Запросы к этим базам
284 Часть II Размещение данных на сайте данных можно направить одновременно — в таком случае они будут выполняться параллельно на разных компьютерах и вы сможете воспользоваться преимуществами настоящего параллелизма. protected void QueryButton_Click(object sender, EventArgs e) String custID = CustomerList.SelectedValue; using (SqlConnection connl = new SqlConnection(ConnStringl)) using (SqlConnection conn2 = new SqlConnection(ConnString2)) { // Инициируем выполнение первой команды: получения информации о клиенте SqlCommand cmd1 = new SqlCommand(CustomerInfoCmd, connl); cmd1 Parameters.Add( ©customerid", SqlDbType.Char, 5).Value = custID; connl.Open(); lAsyncResult arCustomerlnfo = cmd1.BeginExecuteReader(); // Инициируем выполнение второй команды: получения информации о заказах SqlCommand cmd2 = new SqlCommand(CustomerOrderHistory, conn2); cmd2.CommandType = CommandType.StoredProcedure; cmd2.Parameters.Add("@customerid"। SqlDbType.Char, 5).Value = custID; conn2.0pen(); lAsyncResult arOrdersInfo = cmd2.BeginExecuteReader(); // Подготавливаем wait-объекты для синхронизации WaitHandle[] handles = new WaitHandle[2]; handles[0] = arCustomerlnfo AsyncWaitHandle; handiest 1] = arOrdersInfo.AsyncWaitHandle; SqlDataReader reader; // Ждем завершения выполнения всех команд (не более 5 секунд) for (int i=0; i<2; i++) { StringBuilder builder = new StringBuilder(), int index = WaitHandle.WaitAny(handles, 5000, false); if (index == WaitHandle WaitTimeout) throw new Exception("Timeout expired"); if (index == 0) { // Информация о клиенте reader = cmd1.EndExecuteReader(arCustomerlnfo); if (!reader.Read()) continue; builder.AppendFormat("{0}<br>”, readerf"companyname"]); builder.AppendFormat("{0}<br>”, reader["address"]); builder.AppendFormat("{0}<br>", reader["country"]); Info.Text = builder.ToStringO; reader.Close(); } if (index == 1) { // Информация о заказах reader = cmd2.EndExecuteReader(arOrdersInfo); 1 gridOrders DataSource = reader; g ridO rde rs.DataBind(); reader.Close(); } } }
Провайдеры данных ADO.NET Глава 7 265 Страница инициирует выполнение двух команд и затем дожидается завершения первой из них. Для этого она сохраняет дескрипторы ожидания (AsyncWaitHandle) объектов lAsyncResult в массиве и передает этот массив методу WaitAny класса Wait- Handle. Когда одна из двух команд завершается, метод WaitAny сообщает об этом, но цикл for, внутри которого размещен его вызов, повторяет итерацию, поскольку нам нужно дождаться завершения выполнения всех команд. Можно, конечно, воспользо- ваться методом WaitAll, а не WaitAny, но последнее решение имеет то преимущество, что результаты операции, завершившейся первой, обрабатываются сразу после их поступления. Это дает выигрыш в производительности, особенно когда операции выполняются долго. Примечание Такое поведение приложения можно реализовать и в ADO.NET 1.x, где нет специальных механизмов асинхронного выполнения, — для этого достаточно инициировать выполнение каждой команды в отдельном потоке, либо созданном пользователем, либо взятом из пула. Однако при этом команда будет блокировать свой поток, что нормально для клиентских приложений, но может негативно сказаться на производительности сер- верных, таких как приложения ASP.NET. Неблокирующие страницы ASP.NET, управляемые данными Представьте себе управляемую данными страницу ASP.NET, которая в синхронном режиме выполняет длительные команды доступа к данным. Чем чаще запрашивается эта страница, тем выше становится вероятность отказа в доступе к Web-серверу или сильного снижения его производительности из-за того, что большое количество си- стемных потоков заблокировано в ожидании результатов от базы данных. При этом на самом деле Web сервер не занят, напротив, он практически простаивает (с минималь- ной загрузкой ЦПУ и сети), но не может принимать запросы от клиентов вследствие недостатка свободных потоков. Для решения этой проблемы еще в ASP.NET 1.1 были введены асинхронные об- работчики HTTP - особые классы страниц, реализующие интерфейс IHttpAsyncHand- ler, а не IHttpHandler, как обычные синхронные страницы. Асинхронный обработчик HTTP принимает от клиента запрос, инициирует ту или иную связанную с ним опера- цию и возвращает управление ASP.NET. В .NET Framework 2.0 при создании страниц, управляемых данными, асинхронные обработчики могут использоваться в сочетании с асинхронными командами ADO.NET 2.0. В состав интерфейса IHttpAsyncHandler входят методы BeginProcessRequest и End ProcessRequest. В первом из них вы подключаетесь к базе данных и направляете ей запрос. Функцию обратного вызова этот метод получает непосредственно от ASP.NET, и она же используется в качестве детектора завершения асинхронной команды. Когда метод BeginProcessRequest возвращает управление вызывающему коду, стра- ница передает управление ASP.NET, как если бы она была полностью выполнена. После этого ASP.NET может повторно использовать ее поток для обработки других HTTP-запросов, а сервер базы данных тем временем занимается выполнением SQL- команды. Когда эта команда будет выполнена, сработает механизм сигнализации и будет вызван метод EndProcessRequest, хотя не обязательно в том же потоке, в кото- ром начиналось выполнение страницы. В этом методе вы обрабатываете полученные данные и выводите страницу Об асинхронных обработчиках HTTP подробно говорится в моей книге «Program- ming Microsoft ASP.NET 2.0 Applications: Advanced Topics» (Microsoft Press, 2005). [T?] Примечание Многие методы работают синхронно даже тогда, когда разрешено асин- хронное выполнение команд. Это методы BeginXXX и большая часть методов объектов чтения данных, в частности методы Read, Close и Dispose объектов GetXXX.
286 Часть II Размещение данных на сайте Выполнение транзакций В ADO.NET можно выбирать между двумя типами транзакций: локальными и распре- деленными. Локальные транзакции выполняются над одним ресурсом, обычно базой данных, к которой подключилось приложение. Вы начинаете транзакцию и включаете в ее контекст одну или несколько команд. Спустя какое-то время вы решаете, успешно ли завершилась операция и в зависимости от принятого решения либо фиксируете транзакцию, либо выполняете ее откат. Такой подход функционально сходен с запу- ском хранимой процедуры, содержащей несколько команд, выполняемых в рамках одной транзакции. Реализация этой задачи с помощью кода ADO.NET обеспечивает вам большую гибкость, но результат получается таким же. Распределенная транзакция охватывает несколько гетерогенных ресурсов и гаран- тирует, что осуществляемые при этом изменения либо будут внесены во все ресурсы, либо вообще не будут вноситься. Для выполнения подобных транзакций необходимо средство их мониторинга — Transaction Processing Monitor. Роль такого средства в операционных системах Microsoft Windows версии 2000 и выше играет Distributed Transaction Coordinator (DTC). В ADO.NET 1.x управление локальной транзакцией осуществляется с помощью транзакционного объекта, предназначенного для конкретной СУБД; например для SQL Server таким объектом является SqlTransaction. Вы начинаете транзакцию, свя- зываете с ней команды, а потом принимаете решение о способе ее завершения. Для выполнения распределенных транзакций вам потребуются служба Enterprise Services и сервисные компоненты. Регистрация подключений к базе данных в Enterprise Ser- vices DTC и их связывание с определенными транзакциями осуществляются с по- мощью уже упоминавшегося метода EnlistDistributedTransaction класса подключения. В ADO.NET 2.0 управлять локальными и распределенными транзакциями стало проще — для этого теперь используются новые классы, определенные в пространстве имен System.Transactions. Ключевую роль среди них играет класс Trans action Scope. Управление локальными транзакциями способом, применявшимся в ADO.NET 1.x Если вы хотите осуществлять управление локальными транзакциями традиционным способом, то первым делом начните новую транзакцию с помощью метода BeginTrans- action класса подключения. Ей можно присвоить имя и назначить уровень изоляции. Данный метод соответствует оператору BEGIN TRANSACTION, применяемому в SQL Server. Ниже демонстрируется типичная схема выполнения транзакции в приложении ADO.NET. SqlTransaction tran; tran = conn.BeginTransaction(); SqlCommand cmd1 = new SqlCommand(cmdTextl); cmd1.Connection = conn; cmd1.Transaction = tran; SqlCommand cmd2 = new SqlCommand(cmdText2); cmd2.Connection = conn; cmd2.Transaction = tran; try { cmd1.ExecuteNonQuery(); cmd2.ExecuteNonOuery(); tran.CommitO; }
Провайдеры данных ADO.NET Глава 7 287 catch { tran.Rollback(); } finally { conn.Close(); } Созданный вами объект SqlTransaction действует через то подключение, с исполь- зованием которого вы его создали. Команды включаются в состав транзакции по- средством свойства Transaction представляющих их объектов. Учтите, что если вы зададите в этом свойстве объект транзакции, связанный с другим подключением, при попытке выполнить команду будет выброшено исключение. Когда выполнение всех команд транзакции завершается, вы вызываете метод Commit ее объекта для фиксации изменений или метод Rollback для их отката. Уровень изоляции транзакции определяет, какие блокировки будут накладываться при ее выполнении и какие данные будут ей доступны. Типичными значениями этого параметра являются ReadCommitted (по умолчанию), ReadUncommitted, RepeatableRead и Serializable. Представьте себе, что некоторая транзакция изменяет значение, которое должна прочитать другая транзакция. В такой ситуации можно установить для этой второй транзакции уровень изоляции ReadCommitted и тем самым обеспечить блокиро- вание строки, чтобы вторая транзакция не могла ее прочитать, пока первая не сохранит либо не отменит изменения. Если задана установка ReadUncommitted, блокировка не накладывается, чем повышается общая производительность системы. Однако при этом вторая транзакция получает возможность прочитать модифицированную строку до того, как внесенные первой транзакцией изменения будут зафиксированы. Это на- зывается «чтением черновых данных» (dirty read), поскольку в случае, если первая транзакция будет отменена, прочитанное значение окажется неверным и с этим уже ничего нельзя будет сделать. (Конечно, установку ReadUncommitted задают лишь в том случае, когда считываемые данные не являются критичными для приложения.) Следует иметь в виду, что при запрете чтения черновых данных снижается общий уровень параллелизма выполнения транзакций в системе. Теперь представьте себе другую ситуацию. Первая транзакция считывает некую строку, после чего вторая транзакция изменяет или удаляет эту строку и фиксирует изменения. Затем первая транзакция снова считывает ту же самую строку и, естествен- но, получает другие данные. Если необходимо предотвратить такого рода несогласо- ванность получаемой транзакцией информации, нужно установить для нее уровень изоляции RepeatableRead, запретив тем самым обновление прочитанных транзакцией данных и чтение ее черновых данных до ее завершения. Однако при этом останутся возможными другие операции, в результате которых могут генерироваться фантомные строки. Пусть, например, некая транзакция выпол- няет запрос на выборку. Одновременно другая транзакция вносит изменения в тот набор исходных данных, которые считывает первая транзакция, и таким образом ре- зультаты выполнения запроса оказываются несогласованными — часть их транзакция успела прочесть до внесения изменений, а другую часть прочла уже измененными. Предотвратить такую ситуацию позволяет установка Serializable, которая запрещает другим транзакциям выполнять обновление, удаление и вставку строк, пока данная транзакция не завершится. Сводные сведения обо всех поддерживаемых уровнях изоляции транзакций при- ведены в табл. 7-15. Из этой таблицы видно, что наивысшим уровнем изоляции транзакций является Serializable, обеспечивающий полную надежность полученных данных, но требующий
288 Часть li Размещение данных на сайте f завершения до начала транзакции любых других транзакций, работающих с теми же данными. • ! - гы/ . . . ....шзая Табл. 7-15. Уровни изоляции транзакций Уровень изоляции Чтение черновых данных Расхождения при повторном чтении Фантомные строки ReadUnCommitted Да Да Да ReadCommitted Йет Да Да RepeatableRead Нет Нет Да Serializable Нет Нет Нет Уровень изоляции транзакции можно в любой момент изменить, и ваша новая установка будет действовать до тех пор, пока опять не будет изменена явно. Если вы меняете уровень изоляции прямо по ходу транзакции, сервер применяет новую установку ко всем оставшимся операторам Для явного завершения транзакции, как уже было сказано, применяется метод Commit или Rollback. Класс SqlTransaction поддерживает именованные точки сохра- нения транзакции, используемые для ее частичного отката. Они предназначены для выполнения оператора T-SQL SAVE TRANSACTION. Описанный подход к выполнению локальных транзакций является единственно возможным в системе ADO.NET 1.x, и, конечно, он полностью поддерживается в ADO.NET 2.0. А теперь давайте рассмотрим его альтернативу. Знакомство с объектом TransactionScope Приведенный выше код основан на применении метода Begin Transaction, который связывает вас с определенной базой данных и инициирует начало новой транзакции, в состав которой вы по очереди включаете команды. Но что если вам нужно порабо- тать с несколькими базами данных и затем отправить сообщение в очередь сообще- ний? В ADO.NET 1.x в подобных случаях приходилось создавать распределенную транзакцию в Enterprise Services. Но в ADO.NET 2.0 теперь появился новый объект TransactionScope, позволяющий выполнять как локальные, так и распределенные тран- закции. Вот как это делается: using (Transactionscope ts = new TransactionScopeO) { using (SqlConnection conn = new SqlConnection(ConnString)) { SqlCommand cmd = new SqlCommand(cmdText, conn); cmd.Connection.Open(); try { cmd.ExecuteNonQuery(); } catch (SqlException ex) { // Здесь размещается код обработки ошибок IblMessage.Text = ex.Message; } ) // Вызывается для завершения транзакции; если не вызвать Ц этот метод, транзакция будет отменена ts.CompleteO; }
Провайдеры данных ADO.NET Главе? 289 Объект подключения определен в области действия транзакции, поэтому он авто- матически включается в эту транзакцию, а вам остается лишь сохранить ее результаты, вызвав метод Complete. Если вы этого не сделаете, система посчитает, что транзакция завершилась неудачей, и автоматически произведет ее откат. Очевидно, что в случае исключения транзакцию нужно отменить. Внимание! ' Необходимо гарантировать, что объект TransactionScope будет своевременно - уничтожен. При выполнении этой операции транзакция автоматически фиксируется или отменяется. Забывать своевременно удалять объект Transactionscope, полагаясь в этом деле на сборщик мусора, — слишком дорогое удовольствие, поскольку по умолчанию для распределенных транзакций установлен тайм-аут длительностью в целую минуту. Держать несколько баз данных заблокированными столько времени совершенно недо- пустимо — такое поведение убийственно для производительности и масштабируемости системы. И не забывайте, что недостаточно просто вызвать метод TransactionScope.Dis- pose в конце линейной последовательности операторов программы, поскольку в случае выброса исключения этот вызов произведен не будет. Следует воспользоваться либо оператором using, либо блоком try...catch...finally. Объект Transactionscope и распределенные транзакции Рассмотрим транзакцию, в ходе которой выполняются операции над разными базами данных: Northwind.mdf, функционирующей под управлением SQL Server 2000, и Му- Data.mdf, которой управляет SQL Server 2005 Express. (Файл MyData.mdf вы найдете в папке app_Data демонстрационного проекта.) Таблица, которая используется в этом примере, создается такой командой: CREATE TABLE Numbers (ID int, Text varchar(50)) ADO.NET позволяет создать единственный «всеохватный» объект TransactionScope и выполнить под его контролем несколько команд даже через разные подключения. Проанализировав результаты их выполнения и убедившись, что все в порядке, вы вызываете метод Complete. Вот пример: bool canCommit = true; using (TransactionScope ts = new TransactionScopeO) { // ***.^***************************************************** // Обновляем Northwind в SQL Server 2000 using (SqlConnection conn = new SqlConnection(ConnString)) { SqlCommand cmd = new SqlCommand(UpdateCmd, conn); cmd.Connection.Open(); try { cmd.ExecuteNonQue ry(); } catch (Sq[Exception ex) { canCommit &= false; } } // ********************************************************* I/ Обновляем Numbers в SQL Server 2005 using (SqlConnection conn = new SqlConnection(ConnString05)) { SqlCommand cmd = new SqlCommand(InsertCmd, conn); cmd.Connection.Open(); try { cmd ExecuteNonQueryO;
290 Часть II Размещение данных на сайте catch (SqlException ex) { canCommit &= false; } } // Этот метод необходимо вызвать для завершения // транзакции; иначе она будет отменена if (canCommit) ts. CompleteO; } Если происходит исключение, скажем, в базе данных SQL Server 2005, любые изменения, успешно внесенные в базу данных SQL Server 2000, автоматически от- меняются. Класс TransactionScope разработан для удобства программистов; для него под- держивается автоматическое удаление объекта оператором using, а внутри он сам организует транзакцию и отслеживает область ее действия. Заключив транзакцион- ный код внутрь блока using с объектом TransactionScope, вы избавитесь от рутинной работы, связанной с организацией транзакции, и сможете полностью сосредоточиться на логике выполняемых операций. Объект TransactionScope сам определяет, локальной или удаленной является транзакция, и в первом случае обеспечивает ее локальное выполнение, а во втором, когда код достигает точки, в которой локальное выполнение невозможно, обращается к DTC, инициирует распределенную транзакцию и регистри- рует задействованные в ней ресурсы. Какие объекты допустимо регистрировать как задействованные в распределенной транзакции? Это может быть любой объект, который реализует интерфейс ITransaction и определен в пространстве имен System.Transactions. Классы и интерфейсы указанного пространства имен поддерживаются всеми стандартными провайдерами данных ADO. NET 2.0. В режиме совместимости с транзакционной системой работает и MSMQ. Когда код, принадлежащий к области действия объекта TransactionScope, вызывает метод Complete, это означает, что все операции транзакции завершились успешно и теперь она может быть зафиксирована. Заметьте: этот метод не осуществляет физиче- ского завершения распределенной транзакции, так как собственно фиксация транзак- ции производится при освобождении объекта TransactionScope. Однако после вызова метода Complete использовать распределенную транзакцию вы больше не можете. Примечание В отношении выполнения распределенных транзакций между система- ми System.Transactions и Enterprise Services существует множество различий. Прежде всего, System. Transactions — это транзакционный механизм, разработанный специально для управляемой среды и естественным образом интегрирующийся в приложения .NET. Разумеется, классы System. Transactions могут делегировать определенную работу DTC и СОМ+, но это не более чем детали реализации. Еще одним важным отличием указанных систем является наличие у System. Transactions легковесного менеджера транзакций, реализованного в управляемой части кода и позволяющего производить разнообразную оптимизацию. Например System. Transactions позволяет регистрировать для участия в транзакции DTC несколько объектов одновременно, а кроме того, поддерживает рас- ширяемые (promotable) транзакции, которые начинаются как локальные, а затем, по- сле открытия второго подключения, автоматически превращаются в распределенные. Присоединение к распределенной транзакции в ADO.NET 1.x При использовании объекта TransactionScope не обязательно явно присоединять объект подключения к транзакции. Однако если вы захотите это сделать, то можете восполь- зоваться методом EnlistTransaction — он предназначен именно для такой цели.
Провайдеры данных ADO.NET Глава 7 291 Присоединение объектов подключений к распределенной транзакции можно было выполнять и в ADO.NET 1.1, с помощью метода EnlistDistributedTransaction класса подключения Этот метод автоматически присоединяет объект подключения к тран- закции, осуществляемой под управлением Enterprise Services DTC. s Примечание Метод EnlistDistributedTransaction полезен, когда к моменту создания транз- акции в приложении имеются бизнес-объекты с уже открытыми подключениями. Автома- тическое присоединение подключения к транзакции происходит только при его открытии. Если же объект участвует в нескольких транзакциях, связанное с ним подключение не открывается каждый раз заново, и поэтому автоматического присоединения подключения к новым транзакциям не происходит. В такой ситуации можно отключить автоматическое присоединение подключений к транзакциям и производить его явно, с помощью метода EnlistDistributedTransaction. Нововведения, ориентированные на SQL Server 2005 У провайдера данных .NET для SQL Server появилось несколько новых функций, связанных с нововведениями SQL Server 2005. Эта версия СУБД была значительно усовершенствована во многих отношениях, теперь она поддерживает типы данных CLR и пользовательские типы данных, зависимости и запросы с уведомлением, а так- же технологию MARS. Поддержка типов данных CLR SQL Server 2005 поддерживает все типы данных CLR, позволяя сохранять в таблицах базы данных и извлекать из них объекты любых типов, которые поддерживает .NET, причем к их числу относятся как системные типы, например Point, так и определяе- мые пользователем классы. Все эти новые возможности отражены в провайдере ADO. NET 2.0 для SQL Server. Для объекта чтения данных типы данных CLR представляются классами, экзем- плярами которых могут быть параметры команд. Следующий код показывает, как извлечь из таблицы MyCustomers значение, соответствующее экземпляру пользова- тельского класса Customer. string cmdText = “SELECT CustomerData FROM MyCustomers"; SqlConnection conn = new SqlConnection(connStr); SqlCommand cmd = new SqlCommand(cmdText, conn); cmd.Connection.Open(); SqlDataReader reader = cmd. ExecuteReaderQ; while( reader. ReadO) { Customer cust = (Customer) reader[O]; // Выполните здесь необходимую обработку данных } cmd.Connect]on Close(); В SQL Server 2005 значение пользовательского типа хранится как двоичный поток байтов. Аксессор объекта чтения данных считывает этот поток байтов и десериализует его в действительный экземпляр исходного класса. Обратный процесс (сериализа- ция) происходит в том случае, когда объект пользовательского класса помещается в столбец SQL Server. Поддержка XML как встроенного типа данных В SQL Server 2005 реализована встроенная поддержка типа данных XML, а это озна- чает, что XML-данные можно хранить в столбцах таблиц. На первый взгляд в этом нет ничего особенного, поскольку XML-данные, в сущности, являются обычным текстом
292 Часть II Размещение данных на сайте и их можно записать в любой текстовый столбец. Однако встроенная поддержка XML означает нечто иное — вы можете объявить столбец как имеющий тип XML, а нё про- сто создать текстовый столбец и записывать туда XML-данные. В ADO.NET 1.x имеется метод ExecuteXmlReader, позволяющий обрабатывать результаты запроса как XML-поток, — он создает объект XmlTextReader, инкапсулиру- ющий данные, полученные от SQL Server. Для того чтобы этот метод мог выполнить свою работу, результирующий набор должен представлять собой текст в формате XML. Метод ExecuteXmlReader полезен в случае выполнения запроса с предложением FOR XML или запроса, который возвращает скалярное значение, являющееся текстом в формате XML. В ADO.NET 2.0 при взаимодействии с SQL Server 2005 для каждой ячейки столбца, имеющего тип XML, можно получить отдельный объект чтения XML-данных. Для этого вы сначала получаете объект SqlDataReader, представляющий весь результи- рующий набор, и в цикле по строкам запрашиваете у него XML-данные с помощью метода GetSqlXml'. string cmdText = " SELECT * FROM MyCustomers"; SqlCommand cmd = new SqlCommand(cmdText, conn), SqlDataReader reader = cmd.ExecuteReader(); while(reader.Read()) IJ Предполагается, что поле 3 содержит XML-данные // Получаем эти данные и выполняем их обработку SqlXml xml - reader. GetSqlXml(3); ProcessData(xml.Value); } Класс SqlXml представляет извлеченное из базы данных значение XML-типа, а свойство Value этого класса возвращает XML-текст в виде строки. Уведомления об изменении данных и зависимости запросов Приложениям, которые выводят или кэшируют постоянно изменяющиеся данные, SQL Server 2005 теперь предоставляет новый сервис — уведомления об изменении интересующих их данных. Представьте себе, что некое приложение выполняет за- прос, а потом работает с его результатами. Если, направляя этот запрос серверу, оно зарегистрируется Kart получатель уведомлений об изменении результирующего набора данных, то в случае, когда выбираемые запросом строки будут обновлены или удалены, либо когда добавится новая строка, удовлетворяющая критерию запроса, SQL Server уведомит об этом приложение. Заметьте, что уведомление достигнет приложения только при условии, что приложение все еще будет активно, а Для страниц ASP.NET это составляет некоторую проблему. Но давайте обо веем по порядку. В ADO.NET 2.0 провайдер SQL Server позволяет применять функцию уведом- ления двумя разными способами, для чего предоставляет два класса: SqlDependency и SqlNotificationRequest. Один из них, низкоуровневый класс SqlNotificationRequest, открывает доступ к серверной функциональности, позволяя Выполнять команды с запросами об уведомлении. После выполнения заданного оператора Т-SQL механизм уведомлений SQL Server 2005 начинает отслеживать изменения, которые могут отра- зиться на результирующем наборе данных, и, обнаружив такое изменение, направляет в очередь соответствующее сообщение. Очередь — это новый объект базы данных SQL Server 2005, для его создания и управления им введено новое подмножество опера- торов языка Т-SQL. Способ опроса очереди и интерпретации сообщений зависит от конкретного приложения.
Провайдеры данных ADO.NET Глава 7 293 j, Что касается класса SqlDependency, то он создает высокоуровневую абстракцию механизма уведомлений и позволяет на уровне приложения определить зависимость от данных, выбираемых с помощью определенного запроса, так чтобы в случае из- менения этих данных на сервере клиентское приложение немедленно уведомлялось об этом посредством события. Следующий код связывает команду с объектом, пред- ставляющим такую зависимость: string connString = "SERVER=(local); database=northwind;INTEGRATED SECURITY=sspi"; conn = new SqlConnection(connString); SqlCommand cmd = new SqlCommand("SELECT * FROM Employees", conn); SqlDependency dep = new SqlDependency(cmd); dep.OnChange += new OnChangeEventHandler(OnDependencyChanged); SqlDependency.start(connString), SqlDataReader reader = cmd.ExecuteReader(); С помощью вызова SqlDependency.start(connString) запускается слушатель посту- пающих от SQL Server уведомлений об изменении данных. При получении такого уведомления генерируется событие OnChange и вызывается его обработчик. void OnDependencyChanged(object sender, SqlNotificationsEventArgs e) { } Для приложений ASP.NET такой способ уведомления не представляет особого интереса, поскольку после выполнения запроса страница возвращает управление системе. Однако API кэширования, входящий в состав ASP.NET 2.0, реализует по- хожую функцию, позволяющую автоматически отслеживать изменения результи- рующих данных запроса с использованием кэша ASP.NET. В этой системе имеется специализированный тип объектов зависимостей кэша, позволяющий отслеживать изменения в результатах запросов, направляемых не только SQL Server 2005, но и SQL Server 2000, хотя и совершенно разными способами. Вы определяете зависимость от команды или таблицы в виде особого объекта и включаете его в состав ASP.NET- объекта Cache. Как только в отслеживаемой команде или таблице обнаруживаются изменения, соответствующий элемент кэша помечается как недействительный. При использовании SQL Server 2000 изменения можно обнаружить только в одной из таблиц, которым адресован запрос; в случае же с SQL Server 2005 отслеживаются из- менения всех результатов запроса. О кэшировании в системе ASP.NET мы подробно поговорим в главе 14, Множественные активные результирующие наборы Управляемый провайдер версии 1.x и ODBC-драйвер для SQL Server поддерживают не более одного активного результирующего набора данных на подключение. Не- управляемый провайдер OLE DB и самая внешняя библиотека ADO.NET, на первый взгляд, позволяют программисту получить несколько таких наборов данных, однако это лишь иллюзия. Для ее создания провайдер OLE DB открывает дополнительное подключение, не принадлежащее пулу. Как уже было сказано, в SQL Server 2005 реализована функция поддержки не- скольких активных результирующих наборов данных — MARS, которая позволяет приложениям иметь несколько открытых объектов SqlDataReader, созданных в ре- зультате выполнения разных команд, но при этом связанных с одним подключением. Потенциально такой способ работы позволяет повысить производительность при- ложения, поскольку содержать несколько объектов чтения данных гораздо дешевле, чем несколько подключений. В то же время применение технологии MARS добавляет
294 Часть II Размещение данных на сайте некоторые скрытые затраты на каждую операцию, являющиеся следствием реализации этой технологии. Так что выбор остается за вами. Классическое применение технологии MARS — получение объекта чтения данных для прохода по результирующему набору данных с одновременной выдачей через то же подключение другой команды. В качестве примера ниже приведен код демон- страционной страницы, которая выполняет проход по данным с помощью объекта их чтения и одновременно с помощью еще одной команды обновляет текущую запись. Если вы попробуете применить этот подход в ADO.NET 1.x или ADO.NET 2.0 при отключенной функции MARS, то получите сообщение об исключении, к котором будет сказано, что объект чтения данных, связанный с текущим подключением, все еще открыт и сначала его необходимо закрыть. using (SqlConnection conn = new SqlConnection(connString)) { SqlCommand cmd1 = new SqlCommand("SELECT * FROM employees", conn); cmd1.Connection.Open(); SqlDataReader reader = cmd1.ExecuteReader(); // Проходим по записям результирующего И набора с помощью объекта чтения данных while (reader.Read()) { // Записываем имя наоборот string firstNameReversed = reader["firstname”].ToString(); char[] buf = firstNameReversed.ToCharArray(); Array.Reverse(buf); firstNameReversed = new string(buf);. // Обновляем имя сотрудника через то же подключение int id = (int)reader["employeeid"]; SqlCommand cmd2 = new SqlCommand( "UPDATE employees SET firstname=@newFirstName WHERE employeeid=@empID", conn); cmd2.Parameters.AddWithValue(”@newFirstName", fi rstNameReve rsed); cmd2.Parameters.AddWithValueC'empID”, id); cmd2 ExecuteNonQuery(); } reader Close(); // Получаем объект чтения данных для обновления пользовательского интерфейса grid DataSource = cmd1.ExecuteReader(); grid. DataBindO; cmd1.Connection.Close(); } Заметьте, что при использовании технологии MARS для каждой команды нужно создавать отдельный объект SqlCommand. Если потом вы захотите с помощью третьего командного объекта повторно выполнить запрос для получения обновленных данных, явно закрывать объект чтения данных не обязательно. Еще одним важным достоинством MARS является то, что операция, включаемая в состав транзакции, может выполняться на том же уровне изоляции, на котором функционирует исходное подключение. Если за сценой устанавливается другое под- ключение, такой возможности вы не имеете.
Провайдеры данных ADO.NET Глава 7 295 В ADO.NET 2.0, когда сервером базы данных является SQL Server 2005, функ- ция MARS включена по умолчанию. Для того чтобы ее отключить, нужно в строке подключения установить атрибут MultipleActiveResultSets в false. Следует знать, что с применением технологии MARS связаны некоторые дополнительные затраты. Во- первых, для каждой команды необходим отдельный объект SqlCommand, поэтому система поддерживает пул командных объектов. Во-вторых, неизбежны затраты на сетевом уровне, являющиеся результатом мультиплексирования потока ввода-вывода данных. Большая часть этих затрат имеет структурный характер, и от отключения функции MARS не следует ожидать значительного повышения производительности. Для чего же тогда был введен атрибут MultipleActweResultSets? Главным образом ради обеспечения обратной совместимости. Если разработанное еще до появления MARS приложение ожидает, что при попытке открытия нескольких наборов данных через одно подключение будет сгенерировано исключение, то после установки атрибута MultipleActiveResultSets в false такое приложение сможет работать и на новой плат- форме. Примечание Поведение управляемых провайдеров .NET Framework 2.0 для OLE DB и Oracle напоминает поведение провайдера для SQL Server с включенной функцией MARS. И хотя провайдер для Oracle не поддерживает атрибут MultipleActiveResultSets строки подключения, он автоматически включает данную функциональность. Что каса- ется провайдера для OLE DB, то он тоже не поддерживает упомянутый атрибут строки подключения, но при этом эмулирует получение нескольких результирующих наборов данных через одно подключение при работе с ранними версиями SQL Server или когда недоступна библиотека MDAC 9.0. Если же открыть через OLE DB подключение к SQL Server 2005 с MDAC 9.0, будет использоваться встроенная реализация MARS. Заключение Подсистема доступа к данным .NET включает два поддерева классов: те, что входят в состав управляемых провайдеров, и контейнерные классы, не зависящие от СУБД. Управляемые провайдеры ADO.NET — это новый тип коннекторов, через которые осуществляется взаимодействие с источниками данных; они пришли на смену СОМ- базированным провайдерам OLE DB, использовавшимся в ADO и ASP. На момент написания этой книги в состав .NET Framework входят два встроенных провайдера: для SQL Server и для Oracle. Кроме того, по-прежнему поддерживаются провайдеры OLE DB и драйверы ODBC. Сторонними производителями созданы также провайдеры для MySQL, DB2 и Sybase. Технология управляемых провайдеров работает быстрее и лучше, чем любая дру- гая из существующих на сегодняшний день, и оптимально подходит для доступа к данным в .NET. Особенно она эффективна при осуществлении доступа к SQL Server: управляемый провайдер подключается к этой СУБД на уровне «проводов» и устраняет какие бы то ни было уровни абстрагирования. Благодаря этому управляемый про- вайдер позволяет ADO.NET возвращать вызываемому коду данные в виде значений тех же типов, какие используются при формировании пользовательского интерфейса. Управляемый провайдер предоставляет разработчику объекты для подключения к ис- точнику данных, выполнения команд, организации транзакций, а также для выборки и обновления данных. Итак, в этой главе мы сконцентрировались на установке подключения к источ- нику данных и выполнении команд и транзакций. В следующей главе мы завершим изучение ADO.NET исследованием классов-контейнеров данных, таких как DataSet и DataTable.
296 Часть II Размещение данных на сайте Только факты .. t , i~4 'J J ADO.NET — это подсистема Microsoft .NET Framework, предназначенная для до- ступа к данным; она состоит из двух разных, но тесно взаимосвязанных наборов классов: провайдеров данных и контейнеров данных. Каждый провайдер данных .NET способен выполнять два рода функций: во-первых, устанавливать подключение и направлять источнику данных команды, а во-вторых, заполнять объект контейнерного класса результирующими записями. В состав .NET Framework входят провайдеры данных для SQL Server, Oracle, а также для источников данных OLE DB и ODBC. Технология доступа к данным, основанная на применении провайдеров, является наиболее эффективной из всех ныне существующих и оптимально подходит для приложений .NET. При взаимодействии с SQL Server управляемый провайдер данных .NET подключается к этой СУБД на самом низком уровне, благодаря чему устраняются любые промежуточные слои абстрагирования. Провайдер данных предоставляет класс для выполнения команд и транзакций через открытое подключение. Этот класс имеет целый ряд методов, предназна- ченных для команд разных типов, а также для синхронного и асинхронного их выполнения. Управление локальными и распределенными транзакциями осуществляется по- средством класса TransactionScope, введенного в ADO.NET 2.0.
9;м. Глава 8 Контейнеры данных ADO.NET Для усовершенствования технологии сбора данных и их доставки на разные уровни распределенной системы предприятия в ADO.NET был введен новый объект, который действует аналогично базе данных, размещенной в оперативной памяти, — DataSet. В сущности, это просто контейнер данных (нечто наподобие суперсловаря), специ- ально разработанный для управления данными, структурированными в виде таблиц, столбцов и строк. Объект DataSet никаким образом не связан с физической базой данных, будь то база данных Microsoft SQL Server, Microsoft Office Access или даже Oracle. Ничего не знает он и о провайдере, предоставившем ему данные. Это про- сто контейнер — сериализуемый, обладающий богатой функциональностью и тесно интегрированный с провайдерами ADO.NET. Объект DataSet и все связанные с ним объекты, в частности DataTable, DataView и DataRelation, образуют вторую ветвь дере- ва классов ADO.NET, состоящую из интеллектуальных контейнеров данных, которые заполняются управляемыми провайдерами. У контейнеров данных ADO.NET имеется API, позволяющий разработчикам при- ложений заполнять их любыми данными. Однако чаще всего вам будет нужен объект DataSet, заполненный результатами запроса к базе данных. Для получения такого объекта следует произвести ряд действий: выполнить запрос, обработать результа- ты, заполнить контейнер и закрыть подключение. Вам потребуется объект команды и объект чтения данных, а также некоторый код, который выполнит проход по за- писям, предоставляемым объектом чтения данных, и скопирует их в необходимой форме в объект DataSet. Все эти операции уже реализованы в специальном объекте ADO.NET — адаптере данных. С адаптеров данных мы и начнем эту вторую главу, посвященную ADO.NET. Затем мы углубимся в изучение классов DataSet, DataTable и DataView, воспроизводящих поведение одноименных объектов базы данных, но ничего о базах данных не знающих, поскольку это сугубо программные объекты. Адаптеры данных В ADO.NET объект адаптера данных служит двунаправленным мостом между ис- точником данных и объектом DataSet. Поскольку последний является всего лишь автономным контейнером данных, именно адаптер берет на себя заботу о его за- полнении, а также о передаче данных обратно в источник данных. Если взглянуть на это с абстрактной точки зрения, то адаптер данных подобен командному объекту и реализует еще один способ выполнения команды доступа к базе данных. Главное различие между командными объектами и адаптерами данных состоит в том, как именно каждый из них возвращает полученные от сервера данные. Команд- ный объект возвращает их в виде однонаправленного доступного только для чтения курсора — объекта чтения данных. Адаптер данных сам обрабатывает полученные данные и упаковывает их в размещенный в памяти контейнер — объект DataSet или DataTable. Собственно адаптер данных является ни чем иным, как еще одним слоем абстрагирования, построенным поверх пары командный объект -объект чтения данных.
298 Часть II Размещение данных на сайте Он использует командный объект для выполнения запроса к базе данных, а объект чтения данных — для прохода по результирующему набору записей и заполнения заданного пользователем объекта DataSet. [ Примечание Концепции командного объекта, объекта чтения данных и адаптера данных порождены разделением функций классического объекта Recordset из ADO. Созданный как обычная COM-оболочка набора данных, полученных в результате выполнения SQL- команды, ADO-объект Recordset со временем сильно разросся, и в окончательной версии в его состав входило три типа курсоров: только для чтения, автономный и серверный. По сравнению с объектной моделью ADO объектная модель ADO.NET намного проще и, что особенно важно, состоит из более простых объектов. Вместо огромного монолитного объекта Recordset она содержит три меньших сильно специализированных объекта — ко- мандный, чтения данных и DataSet. Два последних генерируются при выполнении запроса: объект чтения данных — командным объектом, а объект DataSet — адаптером данных. Для полноты сравнения добавлю, что в ADO.NET отсутствует встроенная поддержка серверных курсоров. Подобно командным объектам и объектам чтения данных, адаптеры данных свя- заны с конкретными провайдерами данных. Это значит, что вы найдете в ADO.NET классы адаптеров данных для SQL Server, Oracle и т. д. Чтобы лучше понять, что такое адаптеры данных и как с ними работать, давайте рассмотрим конкретный при- мер — адаптер для SQL Server. Класс SqlDataAdapter Адаптер данных определяется как класс, реализующий интерфейс IDataAdapter. Од- нако если вы посмотрите на реальную реализацию адаптеров для поддерживаемых провайдеров, то увидите, что они состоят из нескольких слоев кода. В частности, все адаптерные классы наследуют базовый класс DbDataAdapter и реализуют интерфейс IDbDataAdapter. Взаимосвязь между классами и интерфейсами адаптеров представ- лена в виде схемы на рис. 8-1. Рис. 8-1. Иерархия адаптеров данных и реализуемых ими интерфейсов Программирование адаптера данных для SQL Server С логической точки зрения адаптер данных — это однонаправленный канал, исполь- зуемый для чтения данных из их источника в таблицу, находящуюся в оперативной памяти, а также для записи данных из этой таблицы в источник данных. Как правило, исходный и принимающий источники данных совпадают. Операции чтения данных из источника и их записи в источник называются соответственно заполнением (fill) и обновлением (update). Именно они упоминаются в табл. 8-1, где перечислены свой- ства адаптера данных для SQL Server — класса SqlDataAdapter.
Контейнеры данных ADO.NET Глава 8 299 Табл. 8-1. Свойства класса SqlDataAdapter Свойство Описание AcceptChangesDuringFi.il Указывает, должны ли фиксироваться операции вставки строк, осуществляемые при выполнении операции заполне- ния. По умолчанию данное свойство имеет значение true AcceptChangesDuringUpdate Указывает, должны ли фиксироваться изменения, внесенные в ходе выполнения операции пакетного обновления. По умол- чанию имеет значение true. В ADO.NET 1л данное свойство не поддерживается ContinueUpdateOnError Указывает, следует ли в случае конфликта строк продолжать пакетное обновление, или нужно генерировать исключение DeleteCommand Возвращает и позволяет задать оператор или хранимую про- цедуру для удаления записей из базы данных в ходе пакетно- го обновления. Является членом интерфейса IDbDataAdapter FillLoadOption Указывает, как адаптер данных поступит в случае, если при выполнении операции заполнения в таблице базы данных уже присутствует строка, извлеченная из базы данных. Допустимые значения этого свойства перечислены ниже в табл. 8-3. В ADO.NET 1л данное свойство не поддерживается InsertCommand Возвращает и позволяет задать оператор или хранимую про- цедуру для вставки записей в базу данных в ходе пакетного обновления. Является членом интерфейса IDbDataAdapter MissingMappingAction Определяет действие которое должно быть совершено в случае, если для таблицы или столбца исходных данных не указан соответствующий элемент схемы объекта DataSet. Допустимые значения этого свойства перечислены в табл. 8-4. Свойство является членом интерфейса IDbDataAdapter MissingSchemaAction Определяет действие, которое должно быть совершено в случае, если схема исходных данных не соответствует схеме объекта DataSet. Допустимые значения этого свойства пере- числены в табл. 8 5. Свойство является членом интерфейса IDbDataAdapter RetumProuiderSpecificTypes Указывает, должны ли при создании таблиц, в которые при осуществлении операции заполнения будут помещены ре- зультирующие наборы данных, использоваться типы данных, специфические для провайдера. В ADO.NET 1л данное свой- ство не поддерживается SelectCommand Возвращает и позволяет задать оператор или хранимую про- цедуру для выборки записей из базы данных. При пакетном обновлении этот оператор или процедура используются для загрузки метаданных, а при выполнении оператора выбор- ки — для извлечения записей. Является членом интерфейса IDbDataAdapter TableMappings Возвращает коллекцию соответствий между таблицами ис- точника данных и таблицами в памяти. Является членом ин- терфейса IDbDataAdapter UpdateBatchSize Определяет размер блока записей, передаваемого за один раз при пакетном обновлении. По умолчанию имеет значение 1. В ADO.NET 1л данное свойство не поддерживается UpdateCommand Возвращает и позволяет задать оператор или хранимую про- цедуру, которые будут использоваться для обновления дан- ных при пакетном обновлении. Является членом интерфейса IDbDataAdapter
ЗбО Часть II Размещение данных на сайте Четыре члена XXXCommand интерфейса IDbDataAdapter позволяют выбрать спо- соб записи информации в базу данных, осуществляемой при выполнении операции обновления. Некоторое исключение составляет свойство SelectCommand, которое, хотя и воздействует определенным образом на процесс обновления данных, прежде всего определяет способ осуществления операции заполнения. Свойства MissingXXX, коллекция TableMappings, а также свойства FillLoadOption и RetumProviderSpecificTypes, добавленные в ADO.NET 2.0, указывают, как именно прочитанные из источника дан- ные помещаются в клиентскую структуру данных в памяти. После загрузки в память автономные теперь данные становятся доступными клиент- скому приложению Windows Forms или ADO.NET, которое может их считывать и обновлять, в том числе добавлять новые строки и удалять существующие. Пакетное обновление выполняется провайдером данных, который по команде клиентского при- ложения переносит произведенные в памяти изменения в источник данных. При пакетном обновлении используются команды, специфические для конкретной СУБД, но все они представляют три базовых операции: вставку, обновление и удаление. Эти команды задаются в свойствах InsertCommand, UpdateCommand и DeleteCommand объекта SqlCommand. ©Внимание! Выполняемая ADO NET операция пакетного обновления заключается в том, что посредством адаптера серверу базы данных направляется определенная последова- тельность команд. Как разработчик вы инициируете этот процесс с помощью единственной команды. Однако помните, что концептуально пакетное обновление ADO.NET не экви- валентно направлению серии запросов СУБД одной командой — не нужно думать, что пакет команд и данных просто направляется СУБД и там выполняется. Хотя обновление и называется пакетным, на самом деле в ADO.NET 1 х строки обновляются по одной. Таково же поведение ADO.NET 2.0 по умолчанию, хотя эта система позволяет обновлять несколько строк за раз, используя новое свойство UpdateBatchSize. Пакетное обновление — очень мощная технология, но она, к сожалению, не подходит для приложений ASP.NET. Дело в том, что Web-приложения работают через НТТР-соединение, а данный протокол, как известно, не сохраняет состояние. Поэтому, чтобы указанная технология заработала, необходимо кэшировать находящуюся в памяти таблицу между сеансами HTTP-подключения, что допустимо не для каждого приложения. В то же время применение технологии пакетного обновления позволяет сэкономить время и усилия; кроме того, процедуру обновления легко сконфигурировать для выполнения относительно сложных задач. А вот существенного выигрыша в производительности эта технология не дает, поскольку в ADO.NET 1.x обновление каждой строки требует отдельной команды. В ADO.NET 2.0 можно обновлять несколько строк за раз, но все равно для обновления большого массива данных приходится направлять базе данных целую серию команд. Теперь рассмотрим методы класса адаптера данных, перечисленные в табл. 8-2. Табл. 8-2. Методы класса SqlDataAdapter Метод Описание Fill Заполняет таблицу в памяти строками, прочитанными из источника данных FillSchema Конфигурирует таблицу в памяти таким образом, чтобы ее схема соот- ветствовала схеме источника данных GetFillParameters Update Возвращает параметры, заданные пользователем в строке запроса Обновляет источник данных текущим содержимым заданной таблицы в памяти. С этой целью для каждой вставляемой, обновляемой или удаляемой строки вызывает оператор INSERT, UPDATE или DELETE
Контейнеры данных ADO.NET Глава 8 301 Для извлечения схемы данных из источника адаптер данных использует команду, заданную вами в свойстве SelectConimand. О гкрывать связанное с объектом SelectCom- mand подключение не обязательно. Если оно закрыто, адаптер откроет его, извлечет данные и снова закроет; в противном случае адаптер воспользуется имеющимся под- ключением и оставит его открытым. Заполнение объекта DataSet с использованием адаптера данных Для заполнения находящегося в памяти объекта DataSet или DataTable результатами выполнения запроса используется метод Fill адаптера данных. Мы уже говорили, что объект DataSet является аналогом базы данных в памяти: он может содержать несколько таблиц (представленных объектами DataTable), Определения отношений между этими таблицами и определения ограничений. Каждая таблица, как и положено, состоит из строк и столбцов. Под заполнением объекта DataSet обычно понимается заполнение одной из его таблиц. Для каждого набора данных, полученного в результате выполнения запроса, адаптер данных создает отдельную таблицу. Соответствие между этой таблицей и результирующим набором данных определяется программистом. Если заданная вами таблица уже имеется в составе объекта DataSet, она обновляется, если нет — создается. Процесс сопоставления результатов запроса структурному элементу объекта DataSet осуществляется в два этапа: сопоставление таблиц и сопоставление столбцов. На первом этапе адаптер данных определяет имя объекта DataTable, который будет со- держать строки текущею результирующего набора. Каждому создаваемому объекту DataTable назначается некоторое имя по умолчанию, но его можно изменить. о Примечание Объект DataTable можно заполнить любой извлеченной из источника данных информацией. Следует понимать, что имя этого объекта предназначено лишь для его иден- тификации и поэтому вовсе не обязательно чтобы оно совпадало о именем таблицы базы данных. Поиск заполняемого объекта DataTable осуществляется не по совпадению имен, а по соответствию, заданному в свойстве TableMappings объекта адаптера данных. н По умолчанию имя-объекта DataTable зависит от сигнатуры метода Fill, использо- вавшегося для выполнения запроса. Для примера рассмотрим следующие два вызова: DataSet ds = new DataSetO; adapter.Fill(ds); adapter. Fill(ds, ."MyTaDle"),; При осуществлении первого вызова именем набора данных, создаваемого в результа- те выполнения запроса, по умолчанию является Table Если запрос возвращает несколько таких наборов, остальные таблицы получают имена Tablei, ТаЫе2 и т. д. После второго вызова первый результирующий набор получает имя МуТаЫе, а остальные — MyTablel, МуТаЫе2 и т. д. Такие же имена получают и таблицы объекта DataSet. После заполнения объекта DataSet имена его таблиц можно изменить Если же вы хотите сделать это заранее, определите в свойстве TableMappings адаптера данных соответствие между именами результирующих наборов данных и таблиц объекта Da- taSet. Заданные вайи имена будут использоваться во время создания таблиц объекта DataSet и во время их поиске. га Примечание При желании с помощью метода Fill можно заполнить отдельный объект DataTable. В этом случае будет использоваться только первый из результирующих на- боров данных и выполнится только один этап сопоставления — сопоставление столбцов. Вот как осуществляется заполнение отдельной таблицы в памяти: DataTable dt: = new DataTableO; adapter. Fil.l(dt j;
302 Часть II Размещение данных на сайте Параметры загрузки В ADO.NET 2.0 были улучшены возможности управления загрузкой данных в объек- ты DataTable, осуществляемой при выполнении операции заполнения. Задав значе- ние свойства FillLoadOption, вы теперь можете явно указать, как уже имеющиеся в этом объекте строки будут соединены со строками, загружаемыми из базы данных. Значениями свойства FillLoadOption могут быть члены перечисления LoadOption, описанные в табл. 8-3. Табл. 8-3. Элементы перечисления LoadOption Значение Описание Overwrite Changes Текущая и исходная версии строки объекта DataTable обновляются значениями из загруженной строки данных Preserve Changes Значение по умолчанию. Исходная версия строки объекта DataTable обновляется значениями из загруженной строки данных Upsert Текущая версия строки объекта DataTable обновляется значениями из загруженной строки данных Соответствие между строкой объекта DataTable и строкой исходной таблицы уста- навливается по первичному ключу. Значение OverwriteChanges задается в том случае, когда нужно инициализировать таблицы свежими данными. Установка PreserveChanges применяется при синхрони- зации текущих данных объекта DataTable с данными из базы данных. В этом случае важно сохранить изменения, внесенные на клиенте, то есть текущие значения, с ко- торыми вы работаете и которые планируете в дальнейшем записать в базу данных. Но в то же время нужно обновить те строки объекта DataSet, которые представляют исходные значения, прочитанные из базы данных. Что касается установки Upsert, то она производит обратный эффект: текущие данные заменяются, а исходные остаются нетронутыми. Вы, наверное, уже поняли, что объекты DataSet и DataTable поддерживают два на- бора значений: текущие и исходные. Текущее значение ячейки — это то, которое вы получаете, когда считываете ее содержимое, и если вы его уже изменяли, то текущее значение ячейки может не совпадать с исходным. Ну а исходное значение — это то, которое было прочитано из базы данных или же было сохранено в ней последним, если в процессе работы с таблицей внесенные вами изменения записывались обратно в базу данных. Когда клиентское приложение добавляет в таблицу объекта DataSet новую строку, исходным значением всех ее ячеек является null. Кроме того, ADO.NET хранит вместе с каждой строкой информацию о ее со- стоянии в виде установки свойства RowState объекта DataRow. Когда приложение изменяет значение той или иной строки объекта DataTable, свойство RowState этой строки автоматически получает значение, указывающее, какое именно изменение было в нее внесено. Это изменение можно зафиксировать с помощью метода AcceptChanges, и тогда свойство RowState строки получит значение Unchanged, а текущие значения ее полей станут их исходными значениями Сохранение измененных строк в базе данных осуществляется с помощью метода Update адаптера данных, выполняющего операцию пакетного обновления. Этот метод сохраняет только те строки, у которых свойство RowState содержит значение Modified, Added или Deleted, то есть те стро- ки, в которые внесены изменения, еще не зафиксированные методом AcceptChanges. Если для объекта DataSet только что выполнена операция заполнения, текущие и исходные значения полей помещенных в него строк совпадают, а их свойство Row- State имеет значение Unchanged. Таково поведение системы по умолчанию, но его
Контейнеры данных ADO.NET Глава 8 303 можно изменить, установив свойство AcceptChangesDuringFill в false, и тогда добавлен- ные в объект строки будут помечаться как Added. После загрузки данных клиентское приложение может работать с ними и вносить изменения: DataTable table = _data.Tables[O]; DataRow row = table.Rows[0]; rowffirstname"] = "Lucy"; При выполнении присваивания изменяется текущее значение ячейки, но ее ис- ходное значение остается прежним. Для того чтобы строка "Lucy" стала исходным значением ячейки, нужно явно зафиксировать изменения: // Фиксация всех несохраненных изменений в строке row.AcceptChanges(); Следует помнить, что при выполнении операции пакетного обновления в базу данных переносятся только незафиксированные изменения. Чтение текущего значения ячейки строки производится следующим образом: Response.Write(row["firstname"].ToSt ring()); Если нужно прочитать исходное значение, сделайте так: Response.Write(row["firstname", DataRowVersion.Original].ToString()); Вывод страницы, демонстрирующей применение свойства FillLoadOption и метода Fill адаптера, показан на рис. 8-2. Как видно на этом рисунке, когда задана установка Upsert, при загрузке информации из базы данных заменяется только текущее значение ячейки, а исходное остается нетронутым. Рис. 8-2. Страница для исследования эффекта разных установок свойства FillLoadOption gjj Примечание В ADO.NET 1 ,х при осуществлении операции заполнения по умолчанию при- меняется установка OverwriteChanges, а если свойство AcceptChangesDuringFill содержит значение false, — тогда установка Upsert. Клиентские изменения в этой системе никогда не сохраняются. В ADO.NET 2.0 значение свойства AcceptChangesDuringFill принимается во внимание только для добавляемых строк, но не для существующих, обновляемых в ходе операции заполнения.
304 Часть II Размещение данных на сайте Сразу после своего создания объект DataSet является пустым контейнером даш гых, и адаптер данных заполняет его результатами запроса. Количество таблиц, которое объект DataSet содержит после заполнения, определяется количеством результирую- щих наборов данных запроса, а структура этих таблиц — механизмом сопоставления. Механизм сопоставления таблиц Провайдер данных .NET присваивает отдельное имя каждому набору данных, по- лученному в результате выполнения запроса. По умолчанию используется имя Table или то, которое программист задал в вызове метода Fill. Затем адаптер просматривает коллекцию TableMappings и ищет в ней элемент, соответствующий имени результи- рующего набора. Если соответствие найдено, адаптер данных считывает заданное в нем имя объекта DataTable, а потом ищет этот объект среди таблиц объекта DataSet (рис. 8-3). Если, к примеру, таблице Employees поставлен в соответствие результирующий набор Table, в объекте DataSet ищется таблица DataTable. В случае отсутствия таковой она создается и заполняется. Если же таблица обнаружена, ее содержимое соединяется с содержимым результирующего набора. TableMappings Результирующий набор В памяти Table Employees Tablel Products Table2 Orders Рис. 8-3. Сопоставление результирующего набора с объектом DataSet Свойство TableMappings адаптера данных содержит коллекцию типа DataTableMap- pingCollection. Каждый объект этой коллекции определяет пару имен: исходного ре- зультирующего набора и таблицы в памяти. Вот пример определения нескольких табличных соответствий: DataSet ds = new DataSetO: DataTableMapping dtm1, dtm2, dtm3; dtm1 = adapter.TableMappings.Add("Table", "Employees"): dtm2 = adapter.TableMappings.Add("Table1", "Products"); dtm3 = adapter.TableMappings.Add("Table2", "Orders"): adapter.Fill(ds): Имена, задаваемые в первом параметре метода TableMappingsAdd, должны соот- ветствовать тем именам, которые адаптер данных назначает результирующим наборам при выполнении метода Fill. Если, к примеру, в приведенном коде последнюю строку заменить такой: adapter.Fill(ds, "MyTable"): то работа всего кода нарушится, поскольку результирующие наборы получат имена не Table, Tablei и ТаЫе2, а МуТаЫе, MyTablel и МуТаЫе2, а в коллекции TableMappings для них не найдется соответствий. Добавлю, что вы можете определять сколько угодии
Контейнеры данных ADO.NET Глава 8 305 табличных соответствий, их количество вовсе не должно соответствовать ожидаемому количеству результирующих наборов. Механизм сопоставления столбцов Задача механизма сопоставления, используемого адаптером данных, заключается не только в том, чтобы найти таблицы объекта DataSet, соответствующие результирую- щим наборам данных, но и в том, чтобы определить соответствия между их столбца- ми. Если бы речь шла только об именах таблиц, мы вполне могли бы ограничиться следующим простым кодом: DataSet ds = new DataSetO; adapter.Fill(ds); ds.Tabies["Table"].TableName = "Employees": ds.Tables[ Table1"].TableName = "Products"; Но. как я уже сказал, важно ещё задать соответствие между столбцами результи- рующего набора и столбцами таблицы объекта DataSet. Информация о соответствии столбцов представлена коллекцией ColumnMappings объекта DataTabU Mapping. Как формируется эта коллекция, показывает следующий пример: DataSet ds = new DataSetO; DataTableMapping dtml; dtm1 = adapter.TableMappings.Add("Table", "Employees"); dtml.ColumnMappings.Add("employeeid", "ID"); dtml ColumnMappings.AddC'firstname", "Name"); dtml.ColumnMappings.Add("lastname", "FamilyName"); adapter.Fill(ds); На рис. 8-4 представлена расширенная версия предыдущей диаграммы (см. рис. 8-3), отражающая детали работы механизма сопоставления столбцов. Результирующий набор В памяти Table Employees Tablet Products ColumnMappings Результирующий набор В памяти employeeid ID lastname FamilyName firstname Name Заполнение Результирующий набор Результирующий набор В памяти productname Name Кэширование DataSet Рис. 8-4. Сопоставление таблиц и столбцов для заполнения объекта DataSet В приведенном выше коде исходный столбец employeeid помещается в таблицу Employees объекта DataSet под именем ID. Поиск соответствий осуществляется ав- томатически в теле метода Fill, и когда этот метод завершает свою работу, каждый
306 Часть II Размещение данных на сайте столбец результирующего набора превращается в объект DataColumn, входящий в со став объекта DataTable. Если соответствие не найдено Метод Fill выполняет две основные операции: сопоставляет результирующие наборы данных таблицам в памяти и заполняет эти таблицы указанными данными. В ходе выполнения этих операций метод может генерировать исключения, если не найдет таблицу или столбец в списке соответствий либо если не найдет заданный в соот- ветствии объект DataTable или DataColumn в объекте DataSet. Однако исключения, генерируемые методом Fill, не являются полновесными про- граммными исключениями — их можно разрешать декларативно, выбрав одно из до- пустимых действий по их обработке. Адаптер данных генерирует два вида легковесных исключений: отсутствие соответствия (missing mapping) и отсутствие элемента схемы (missing schema). А действия по их обработке задаются соответственно в свой- ствах MissingMappingAction и MissingSchemaAction адаптера. Действие, заданное в свойстве MissingMappingAction адаптера, выполняется в двух случаях: когда имя набора данных не найдено в коллекции TableMappings и когда имя столбца не найдено в коллекции ColumnMappings. Допустимые значения свойства MissingMappingAction определены в одноименном перечислении и описаны в табл. 8-4. Табл. 8-4. Элементы перечисления MissingMappingAction Значение Описание Error Если столбец или таблица пропущены в списке соответствий, генерируется исключение Ignore Столбец или таблица, пропущенные в списке соответствий, игнорируются Passthrough Значение по умолчанию. Столбец или таблица, пропущенные в списке соот- ветствий, добавляются в принимающую структуру в памяти Если не задать значение свойства MissingMappingAction до заполнения объекта DataSet, оно получит значение Passthrough, в результате чего неуказанные в списке соответствий таблицы и столбцы будут добавляться в объект DataSet со своими исход- ными именами. Если же присвоить свойству MissingMappingAction значение Ignore, такие таблицы и столбцы будут просто игнорироваться. При этом исключение вы- брошено не будет, а в целевом объекте DataSet может попросту не оказаться тех или иных возвращенных запросом данных. Ну а когда свойство MissingMappingAction имеет значение Error, адаптер выбрасывает исключение каждый раз, когда не находит имени таблицы или столбца исходных данных в своем списке соответствий. Покончив с сопоставлением таблиц и столбцов, адаптер данных приступает к за- полнению объекта DataSet данными из результирующих наборов. При этом также могут возникнуть исключительные ситуации, если адаптер не обнаружит в структуре объекта DataSet элементов, соответствующих таблицам и столбцам результирующих наборов. Эти исключительные ситуации обрабатываются аналогичным способом. Если не найден элемент схемы Под отсутствием элемента схемы понимается отсутствие в составе объекта DataSet таблицы с именем, заданным в коллекции TableMappings, или же столбца с именем, заданным в коллекции ColumnMappings. Действие, выполняемое в такой ситуации, за дается в свойстве MissingSchemaAction объекта адаптера. Допустимые значения этого свойства определены в одноименном перечислении и описаны в табл. 8-5.
Контейнеры данных ADO.NET Глава 8 307 Табл. 8-5. Элементы перечисления MissingSchemaAction Значение Описание Error Если в структуре объекта DataSet отсутствует необходимый столбец или таблица, генерируется исключение Ignore Столбец или таблица, отсутс гвующие в структуре объекта DataSet игнори- руются Add Значение по умолчанию. Столбец или таблица, отсутствующие в структуре объекта DataSet, добавляются в этот объект AddWithKey Действует подобно Add, но добавляются также первичный ключ и ограни- чения По умолчанию свойство MissingSchemaAction имеет значение Add, то есть в объект DataSet добавляется недостающий элемент — DataTable или DataColumn. Однако имейте в виду, что добавляемый столбец получает только имя и тип ис- ходного столбца — остальная информация схемы (включая такие параметры, как первичный ключ, автоинкрементирование, доступ только для чтения, допустимость значений null) не добавляется. Если она нужна, задайте значение AddWithKey, а не Add. Но учтите, что и в этом случае в объект DataColumn будет загружена не вся информация о столбце. Например, он будет помечен как автоинкрементируемый, но исходное значение и приращение для него заданы не будут, как не будет скопировано из источника данных и значение по умолчанию, если таковое имеется. Кроме того, хотя адаптер данных импортирует первичный ключ таблицы, имеющиеся в базе данных дополнительные индексы он игнорирует. Что касается двух других опций, Ignore и Error, то они работают так же, как для свойства MissingMappingAction. Предварительное заполнение схемы При использовании свойств MissingMappingAction и MissingSchemaAction код получает- ся не столь тяжеловесным, как при обычной программной обработке исключений, но все же установка этих свойств отражается на быстродействии кода. Иными словами, код выполняется быстрее, когда заполняемый объект DataSet уже содержит всю не- обходимую информацию схемы. Преимущество заблаговременного формирования схемы этого объекта становится более очевидным, если приложение многократно заполняет пустые объекты DataSet данными с одной и той же схемой. В этом случае предварительное создание глобального объекта DataSet и формирование его схемы позволяет предотвратить многократное выполнение действий по обработке легко- весных исключений адаптера. Формирование схемы объекта DataSet выполняется с помощью метода FillSchema\ DataTable[] FillSchema(DataSet ds, SchemaType mappingMode); Этот метод получает в качестве аргумента объект DataSet и добавляет в него столь- ко таблиц, сколько нужно для выполнения запроса, связанного с адаптером данных. Метод возвращает массив созданных им объектов DataTable (только схемы, без дан- ных). Значением параметра mappingMode может быть одно из значений, определенных в перечислении SchemaType (табл. 8-6). Таким образом, параметр mappingMode определяет, следует ли при формировании схемы объектов DataTable учитывать соответствия, заданные в коллекциях TableMap- pings и ColumnMappings адаптера данных. Когда указанный параметр имеет значение Source, эти соответствия не учитываются и таблицы и столбцы объекта DataSet по- лучают имена исходных таблиц и столбцов запроса.
308 Часть II Размещение данных на сайте Табл. 8-6. Элементы перечисления SchemaType Значение Описание Mapped При создании схемы использовать существующие соответствия таблиц. В ре- зультате схема исходных данных при переносе в объект DataSet может быть трансформирована. Рекомендуемая опция Source Игнорировать существующие соответствия таблиц. Схема исходных данных переносится в объект DataSet без изменений Пакетное обновление данных Операция пакетного обновления, выполняемая с помощью метода Update адаптера данных, состоит в передаче в базу данных всех изменений, внесенных клиентским приложением в данные объекта DataSet. Обычно при этом в базе данных как бы по- вторяются действия пользователя, выполненные им в клиентском приложении над данными объекта DataSet. В многопользовательской среде это становится причиной дополнительных сложностей, поскольку автономно работающие пользователи могут изменить одни и те же строки исходных данных и тогда при их обновлении встанет вопрос о том, какие из изменений следует применить и что делать с остальными. Конфликты изменений и оптимистическая блокировка Возможность конфликтов изменений, внесенных разными пользователями, ставит определенные вопросы перед архитектором приложения, но решения этих вопросов уже давно выработаны и остается лишь выбирать между разными их вариантами. В многопользовательской среде пакетное обновление становится причиной кон- фликтов лишь в том случае, если внесенные клиентским приложением изменения так или иначе обусловлены исходными значениями, прочитанными из базы данных. Если за время, прошедшее от выборки данных и до их пакетного обновления, кто-то другой изменил эти данные, возможно, придется пересмотреть свои изменения или даже отказаться от них. Выявление и обработка конфликтов обновления требует дополнительных затрат. Однако в средах, где вероятность конфликтов изменений невелика, технология пакетного обновления эффективна, поскольку позволяет раз- рабатывать приложения, действующие в автономном режиме и отличающиеся мас- штабируемостью и простотой кодирования. Для передачи клиентских изменений на сервер используется метод Update адап- тера данных. Имейте в виду, что данные можно передавать только потаблично. Когда метод Update вызван без указания имени таблицы, по умолчанию используется имя Table, и если таблицы с таким именем нет, генерируется исключение: adapter.Update(ds, "MyTable"); Для каждой вставленной, обновленной и удаленной строки метод Update подготав- ливает и выполняет команду INSERT, UPDATE или DELETE. Строки обрабатываются в их естественном порядке, и по состоянию строки адаптер определяет, какую имен- но команду необходимо для нее сгенерировать. У метода Update имеется несколько перегруженных версий. Он возвращает целочисленное значение, представляющее количество успешно обновленных строк. Когда обновление строки завершается ошибкой, генерируется исключение и про- цесс пакетного обновления останавливается. Во избежание этого можно установить свойство ContinueUpdateOnError адаптера в true, и тогда пакетное обновление завер- шится только после того, как будут обработаны все строки. Те строки, обновление которых завершено успешно, фиксируются и помечаются в объекте DataSet как неиз-
Контейнеры данных ADO.NET Глава 8 309 мененные. В отношении остальных приложение должно принять то или иное решение и при необходимости повторно инициировать обновление. Построители команд У адаптера данных имеется группа командных свойств — InsertCommand, DeleteCom- mand и UpdateCommand, позволяющих программисту управлять способом передачи изменений, внесенных в таблицы в памяти, серверу базы данных. Введение этих свойств является значительным шагом вперед по сравнению с технологией ADO, в которой команды обновления генерировались без участия программиста. Это изме- нение имеет очень важное значение, поскольку ADO.NET позволяет использовать для пакетного обновления хранимые процедуры и даже работать с источниками данных, не поддерживающими язык SQL. Команды обновления данных в источнике могут генерироваться автоматически и передаваться непосредственно провайдеру данных. Эту задачу выполняют объекты, называемые построителями команд. Однако построитель команд, такой как объект класса SqlCommandBwlder, не является универсальным средством. Автоматическое генерирование команд возможно только в определенных случаях. Так, построитель команд ничего не сгенерирует, если объект DataTable содержит столбцы нескольких таблиц базы данных или в нем есть столбцы с агрегированными данными. В то же время построители команд очень помогают программистам, избавляя их от лишней работы, когда дело касается обновления единственной таблицы. Как построитель команд генерирует операторы обновления строк таблицы? Он ис- пользует для получения необходимых метаданных команду, заданную вами в свойстве SelectCommand. Поэтому для того чтобы воспользоваться построителем, нужно задать в свойстве SelectCommand строку запроса, содержащую первичный ключ и несколько имен столбцов. Только указанные вами столбцы будут участвовать в обновлении и вставке, а по ключевым столбцам будут идентифицироваться строки, подлежащие обновлению или удалению. Провайдер откорректирует вашу команду таким образом, чтобы она вернула только метаданные, а не строки таблицы. Соответствие между адаптером данных и построителем команд устанавливается с помощью конструктора построителя: SqlCommand cmd = new SqlCommandO; cmd.CommandText = "SELECT employeeid, lastname FROM Employees": cmd.Connection = conn; adapter.SelectCommand = cmd; SqlCommandBuilder builder = new SqlCommandBuilder(adapter); Построитель запрашивает метаданные и генерирует команды, когда они потребу- ются впервые, а затем их кэширует. Каждая команда доступна посредством отдельного метода: GetlnsertCommand, GetUpdateCommand или GetDeleteCommand. Заметьте, что создание построителя команд не означает автоматической установки соответствующих командных свойств адаптера данных. Примечание Поведение адаптера данных и построителей команд других управляемых провайдеров мало чем отличается от описанного здесь поведения этих объектов, опреде- ленных в составе провайдера данных .NET для SQL Server. Объекты-контейнеры данных В пространстве имен System.Data определено несколько объектов, напоминающих кол- лекции и вместе образующих представление реляционной базы данных в оперативной памяти. Класс DataSet можно считать аналогом каталога, а класс DataTable — аналогом
310 Часть II Размещение данных на сайте отдельной таблицы. Класс DataRelation представляет отношение между таблицами, а класс DataView — фильтрующее представление табличных данных. Кроме того, в пространстве имен System.Data определены ряд констант и относительно простая модель индексации. Перечисленные классы реализуют программную модель, главной особенностью которой является автономная работа, то есть отсутствие соединения с базой данных и непрерывного обмена с ней информацией. Например, используя объект DataSet, можно фильтровать и сортировать данные на клиенте до того, как они достигнут самого внешнего слоя приложения. Как только данные поступают в объект DataSet, вам больше не требуется обращаться к базе данных для получения различных их представлений. Этот объект совершенно самодостаточен, благодаря чему такая про- граммная модель и является автономной. Примечание Интересным применением объекта DataSet, подходящим как для настоль- ных, так и для Web-приложений, является передача данных между компонентами и слоями программной системы. Этот объект замечательно инкапсулирует таблицы данных и их отношения, и его можно передавать между слоями как монолитную структуру. Более того, он сериализуем, благодаря чему данные и схема могут передаваться даже между слабосвязанными слоями. Кроме класса DataSet — одного из важнейших компонентов объектной модели ADO.NET, есть в ней и другие контейнерные классы, роль которых не менее важна. Все эти классы перечислены в табл. 8-7. Табл. 8-7. Контейнерные классы ADO.NET Класс Описание DataSet Программная структура, предназначенная для кэширования в памяти клиент- ского приложения таких элементов исходной базы данных, как таблицы, их отношения и ограничения. Объект DataSet поддерживает сериализацию и автономную работу с данными. Его можно заполнять информацией из самых разных источников данных, причем способ дальнейшей работы с ним совершенно не зависит от этих источников DataTable DataColumn DataRow DataView Представляет реляционную таблицу данных, состоящую из строк и столбцов Представляет столбец объекта DataTable Представляет строку объекта DataTable Определенный поверх объекта DataTable, объект Data View создает фильтро- ванное представление данных. В зависимости от того как вы его сконфигу- рируете. может поддерживать редактирование и сортировку данных. Следует понимать, что представление данных не является их копией, а лишь своего рода маской DataRelation Представляет отношение между двумя таблицами одного объекта DataSet. Это отношение связывает их на основе значений общего столбца Важной особенностью всех контейнерных классов ADO.NET является то, что способ работы с ними не зависит от источника данных, использовавшегося для их заполнения. Вы можете заполнить таблицы объекта DataSet результатами выполнения запроса к базе данных SQL Server, информацией из файловой системы или данными, которые прочитаны с устройства, работающего в реальном времени. И еще очень важ- но, что ни один из контейнерных классов ADO.NET вообще не содержит информации об источнике данных. Подобно массиву или коллекции, такой объект просто вмещает некоторые данные. Но в отличие от массива или коллекции, он имеет средства, по- зволяющие ему связывать данные и управлять ими на манер СУБД.
Контейнеры данных ADO NET Глава 8 311 Объект DataSet Класс DataSet реализует три важных интерфейса: IListSource обеспечивает возмож- ность возврата списка элементов данных, ISerializable — возможность сериализации данных для средства форматирования .NET, a IXmlSerializable — возможность сери- ализации класса в XML-формат. Свойства класса DataSet перечислены в табл. 8-8. Табл. 8-8. Свойства класса DataSet Свойство Описание CaseSensitive Возвращает и позволяет задать значение, указывающее, будет ли сравнение строк, содержащихся в объектах DataTable, осущест- вляться с учетом регистра DataSetName Возвращает и позволяет задать имя объекта DataSet DefaultViewManager Возвращает и позволяет задать используемый по умолчанию объект менеджера представлений (экземпляр класса DefaultView- Manager), содержащий установки для каждой таблицы объекта DataSet EnforceConstraints Возвращает и позволяет задать значение, указывающее, будут ли при выполнении операций обновления применяться ограничения ExtendedProperties Возвращает коллекцию пользовательской информации, связан- ной с объектом DataSet HasErrors Возвращает значение, указывающее, есть ли ошибки в дочерних объектах DataTable Locale Возвращает и позволяет задать локализационную информацию, используемую для сравнения строк таблиц Namespace Возвращает и позволяет задать пространство имен объекта DataSet Prefix Возвращает и позволяет задать префикс, который будет служить псевдонимом пространства имен объекта DataSet Relations Возвращает коллекцию отношений между парами дочерних таблиц RemotingFormat Определяет желаемый формат сериализации: двоичный или XML. В ADO.NET 1х данное свойство не поддерживается SchemaSerializationMode Указывает, следует ли включить в состав сериализуемых данных информацию схемы. В ADO.NET 1х данное свойство не поддержи- вается Tables Возвращает коллекцию таблиц объекта DataSet Значения свойств Namespace, Prefix и DataSetName используются при сериализа- ции объекта DataSet в XML-формат. В частности, имя объекта DataSet назначается корневому узлу XML-представления. Если свойство DataSetName пусто, этот узел получает имя NeuDataSet. Методы класса DataSet перечислены в табл. 8-9. Табл. 8-9. Методы класса DataSet Метод Описание AcceptChanges Фиксирует изменения, внесенные во все таблицы объекта DataSet со времени его загрузки или со времени последнего вызова данного метода Clear Clone Удаляет все строки всех таблиц Копирует структуру объекта DataSet, включая схемы всех таблиц, отно- шения и ограничения. Данные при этом не копируются Copy Делает полную копию объекта, включая схему и данные см. след. стр.
312 Часть II Размещение данных на сайте Табл. 8-9. {окончание) Метод Описание CreateDataReader Возвращает объект чтения данных, предназначенный специально для работы с объектом DataTable. Каждая таблица представляется объекту DataReader одним результирующим набором, а последовательность таких наборов совпадает с последовательностью таблиц в коллекции Tables. В ADO.NET 1.x данный метод не поддерживается GetChanges Возвращает копию объекта DataSet, содержащую только те изменения, которые были внесены со времени последней загрузки данных или по- следнего вызова метода AcceptChanges GetXml Возвращает XML-представление данных, хранящихся в объекте DataSet GetXmlSchema Возвращает XSD-схему XML-представления данных, хранящихся в объекте DataSet HasChanges Указывает, есть ли в таблицах объекта DataSet новые, удаленные или модифицированные строки InferXmlSchema Реплицирует в объект DataSet табличную структуру, определенную в заданном ХМ L-документе Merge Выполняет слияние содержимого объекта ADO.NET {DataSet, DataTable или массива объектов DataRow) с содержимым текущего объекта DataSet ReadXml Заполняет объект DataSet путем чтения схемы и данных из заданного XML-документа ReadXmlSchema Реплицирует в объект DataSet табличную структуру, прочитанную из заданной XML-схемы Rejectchanges Выполняет откат всех изменений, внесенных во все таблицы со времени их создания или последнего вызова метода AcceptChanges Reset Очищает таблицы, отношения и ограничения и возвращает объект DataSet в его исходное состояние WriteXml Сериализует содержимое объекта DataSet в XML-формат WriteXmlSchema Выводит структуру DataSet в виде XML-схемы Запомните, что полную копию объекта DataSet можно создать только с помощью ме- тода Сору. Если вы просто присвоите объект DataSet другой переменной того же типа: DataSet tmp = ds; то сдублируете ссылку, но не сам объект. Дублирование же объекта выполняется таким образом: DataSet tmp = ds.CopyO; Метод Сору создает и возвращает новый экземпляр объекта DataSet и гарантирует дублирование всех таблиц, отношений и ограничений. Когда нужно не переносить в новый объект данные исходного, а лишь воспроизвести его структуру, пользуй- тесь методом Clone. Чтение данных, хранящихся в объекте DataSet Объекты DataSet и объекты чтения данных часто считают взаимоисключающими и альтернативными средствами считывания данных в приложениях ADO.NET. Однако на самом деле в ADO.NET существует только один физический способ считывания данных — использование объектов чтения данных. Объекты DataSet — это автономные автоматически заполняемые с использованием объекта чтения данных контейнеры данных, идеально подходящие для их кэширования. Что касается объектов чтения данных, то они используются для выполнения прохода по данным объекта DataSet.
Контейнеры данных ADO.NET Глава 8 313 Представьте себе, что приложение имеет доступ к определенным ранее кэширо- ванным данным — скажем, объекту DataSet, сохраненному в состоянии сеанса. Как можно найти в нем и прочитать определенное значение? Обычно вы указываете его координаты (строку и столбец) и осуществляете произвольный доступ. Если требу- ется прочесть более одного значения, то вы циклически повторяете эту операцию. Но в ADO.NET 2.0 существует лучший способ выполнения рассматриваемой задачи: применение объектов чтения данных, не связанных с источником данных и создава- емых с помощью метода CreateDataReader. Полученный таким способом объект чте- ния данных отличается от курсороподобного объекта чтения данных, возвращаемого методом ExecuteReader командного объекта. Вы получаете объект DataTableReader, специально предназначенный для прохода по содержимому таблицы объекта DataSet с использованием того же программного интерфейса, какой предоставляют объекты чтения данных, связанные с базой данных. Вот пример: DataSet data = new DataSetO; SqlDataAdapter adapter = new SqlDataAdapter( "SELECT * FROM employees;SELECT * FROM customers", ConfigurationManager.ConnectionStrings["LocalNWind"].Connectionstring); adapter.Fill(data); // Последовательный, запись за записью, доступ к набору данных DataTableReader reader = data.CreateDataReader(); do { while (reader.Read()) { // reader(1] указывает на второй столбец Response.Write(String.Format("{0} <br>”, reader[1])); } Response.Write("<hr>"); } while (reader.NextResultO); reader. CloseO; Оператор do выполняет проход по записям результирующего набора и выводит список значений второго поля каждой из записей. Этот код мало отличается от при- веденного в главе 7 (подраздел «Доступ к нескольким результирующим наборам» раздела «Выполнение команд»), но работает без подключения к базе данных. Итак, каково же назначение объектов DataTableReader? Благодаря их применению быстрее выполняется последовательный доступ к серии записей. Слияние объектов DataSet Операция слияния обычно выполняется клиентским приложением, обновляющим существующий объект DataSet последними изменениями из источника данных. Для слияния двух объектов DataSet с идентичными или почти идентичными схемами используется метод Merge. Первым делом этот метод сравнивает схемы двух объектов DataSet, чтобы вы- яснить, соответствуют ли они друг другу. Если объект DataSet, из которого будут импортироваться данные, содержит новые столбцы или новую таблицу, результат слияния зависит от того, какое действие вы зададите для обработчика исключения отсутствующего элемента схемы. По умолчанию недостающий элемент схемы до- бавляется в целевой объект DataSet, но вы можете изменить поведение метода Merge, воспользовавшись его перегруженной версией с дополнительным параметром Missing- SchemaAction, и задать требуемое действие.
314 Часть II Размещение данных на сайте Далее метод Merge выполняет слияние данных, для чего ищет в импортируемом объекте DataSet измененные строки. Соответствие измененных и удаленных строк исходного объекта строкам целевого объекта устанавливается по первичному ключу. Новые строки просто добавляются в целевой объект DataSet с сохранением значений их первичных ключей. Операция слияния является атомарной, то есть для нее должна быть гарантирована целостность и согласованность результатов. Поэтому на время выполнения этой опе- рации отключаются заданные для таблиц ограничения, для чего метод устанавливает свойство Enforce Constraints целевого объекта в false. Если по завершении операции слияния действие ограничений не может быть восстановлено, из-за того что, к при- меру, нарушилась уникальность ключей, выбрасывается исключение и все строки, вызвавшие нарушение ограничений, помечаются как ошибочные. Никакие загружен- ные в целевой объект данные при этом не утрачиваются, и после выполнения метода Merge ограничения целевого объекта остаются отключенными. Для восстановления действия ограничений необходимо вначале устранить все ошибки. Модель фиксации изменений в объекте DataSet При первоначальной загрузке данных в объект DataSet все строки всех таблиц помеча- ются как неизмененные. (Если свойство AcceptChangesDuringFill адаптера, заполнявше- го объект DataSet, имеет значение false, все строки помечаются как Added.) Состояние строки таблицы хранится в свойстве с именем RowState. Допустимые значения этого свойства определены в перечислении DataRowState и описаны в табл. 8-10. Табл. 8-10. Состояния строки таблицы Значение Описание Added Deleted Detached Строка добавлена в таблицу Строка помечена как подлежащая удалению из таблицы Строка либо создана, но еще не добавлена в таблицу, либо удалена из коллек- ции строк Modified Unchanged Некоторые столбцы строки изменены Никакие изменения со времени последнего вызова метода AcceptChanges в строку не вносились. Таково состояние всех строк таблицы сразу после ее создания При программном выполнении любой операции над строкой объекта DataSet со- стояние этой строки изменяется. Изменения данных строки остаются незафиксирован- ными, пока не будет вызван фиксирующий их метод. Метод AcceptChanges фиксирует все изменения и делает текущие значения полей строк их исходными значениями. После его вызова информация о том, что в строку были внесены изменения, удаляется, и строка представляется предложению неизмененной — ее свойство RowState снова принимает значение Unchanged. Метод RejectChanges позволяет выполнить откат изменений и восстановить исходные значения полей строки. Как уже упоминалось, объект DataSet сохраняет исходные значения полей строки до тех пор, пока изменения не будут зафиксированы или отменены. Фиксация изменений может выполняться на разных уровнях. В частности, вызвав метод AcceptChanges или RejectChanges объекта DataSet, вы фиксируете или отменя- ете изменения всех строк всех таблиц данного объекта. При вызове этих же методов объекта DataTable фиксируются или отменяются изменения в данной таблице. Ну а если вызвать эти методы для строки таблицы, фиксация или отмена изменений будет выполнена только для этой строки.
Конте жеры данных ADO.NET Глава 8 315 Сериализация данных объекта DataSet в XML-формат Сериализацию данных объекта DataSet в XML-формат можно выполнить двумя раз- ными способами: с сохранением и без сохранения состояния. Хотя понятие сохра- нения состояния не используется в документации по ADO.NET, на мой взгляд, оно отлично характеризует суть двух упомянутых способов сериализации. В решении без сохранения состояния делается снимок текущего состояния базы данных и результат выводится в соответствии с определенной схемой XML: <MyDataSet> <Employees> <ID>...</ID> <Name>...</Name> </Employees> <Employees> <ID>...</ID> <Name>...</Name> </Employees> <Orders> <OrderID>..,</OrderID> <OrderDate>...</OrderDate> <Amount>...</Amount> </Orders> <Orders> <OrderID>...</OrderID> <OrderDate>...</OrderDate> <Amount>...</Amount> </Orders> </MyDataSet> Корневому узлу присваивается имя объекта DataSetName. На следующем уровне вложенности располагаются узлы, названные по именам таблиц и представляющие их строки. Уровнем ниже располагаются дочерние узлы, представляющие отдельные столбцы Приведенный фрагмент HTML-кода соответствует объекту DataSet с двумя таблицами — Employees и Orders, содержащими два и три столбца соответственно. Метод GetXml объекта DataSet возвращает XML-представление объекта в виде строки, а метод WriteXml записывает его в файл или выходной поток. У метода Write- Xml имеются две перегруженные версии: dataSet.WriteXml(fileName); dataSet.WriteXml(fileName, mode); При вызове первой из них используется режим записи по умолчанию — без со- хранения состояния и без вывода информации схемы. Когда сериализация данных объекта DataSet выполняется с сохранением состояния, результирующий XML-текст содержит не только эти данные, но и информацию о незафиксированных изменениях и об ошибках. В табл. 8-11 приведен перечень режимов записи XML-текста с помощью метода WriteXml, задаваемых в параметре mode-, эти режимы представлены элементами перечисления WriteXmlMode. Опция IgnoreSchema используется по умолчанию. Следующий код демонстрирует ти- пичный способ сериализации объекта DataSet с записью его содержимого в XML-файл: Streamwriter sw = new StreamWriter(fileName); // По умолчанию применяется метод записи XmlWriteMode.IgnoreSchema
316 Часть II Размещение данных на сайте dataset.WriteXml(sw); sw.Close(); DiffGram — это особый XML-формат, позволяющий записать в файл текущие и ис- ходные значения каждой строки таблицы. Они хранятся в двух разных разделах файла, причем для каждой строки записывается уникальный идентификатор, позволяющий связать две ее версии. Вот какова структура файла DiffGram-формата: <diffgr:diffgram xmlns:msdata="urn:schemas-mic rosoft-com:xml-msdata" xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1"> <DataSet> </DataSet> <diffgr:before> </diffgr:before> <diffgr:errors> </diffgr:errors> </diffgr:diffgram> Табл. 8-11. Элементы перечисления WnteXmlMode Режим записи Описание IgnoreSchema Содержимое объекта DataSet выводится в формате XML без схемы WriteSchema Содержимое объекта DataSet выводится в формате XML и дополняется XSD-схемой. Схема не может быть вставлена в результирующий документ в виде XDR или добавлена в него как ссылка DiffGram Содержимое объекта DataSet выводится в специальном XML-формате DiffGram, позволяющем включить в документ не только текущие, но и ис- ходные значения данных Корневой узел <diffgr:diffgram> может иметь до трех дочерних узлов. Первый из них содержит текущие данные объекта DataSet, включая добавленные и моди- фицированные строки (но без удаленных). Имя этого узла определяется значением свойства DataSetName объекта DataSet. Если объект не имеет имени, корневой узел DiffGram-файла получает имя NewDataSet. Поддерево <diffgr:before> содержит ин- формацию, необходимую для восстановления исходного состояния всех модифици- рованных строк. Например, оно содержит все удаленные строки и исходные данные всех модифицированных строк, но только те столбцы, которые были модифицированы. Что касается последнего поддерева, <diffgr:errors>, то оно содержит информацию об ошибках данных в каждой из строк объекта DataSet. Сериализация и свойство RemotingFormat В дополнение к сериализации в XML-формат класс DataSet поддерживает двоичную сериализацию .NET Иными словами, он содержит код, генерирующий поток байтов, который может быть сохранен как сериализованная версия объекта. Этот класс поме- чен атрибутом [Serializable] и реализует интерфейс ISerializable, причем предоставляет программисту полный контроль над процессом сериализации. В ADO.NET 1.x объект DataSet сериализуется в XML-формат даже в том случае, если через средство форматирования .NET запрошена двоичная сериализация. Хуже того, для сериализации используется тяжеловесный формат DiffGram, еще и дополнен- ный информацией схемы. Распределенные системы .NET, интенсивно использующие
Контейнеры данных ADO.NET Глава 8 317 автономные данные (как настойчиво рекомендует Microsoft, предлагая такое решение в своих примерах и архитектурных шаблонах), весьма чувствительны к объемам сериализованных данных. Чем больше размер передаваемого объекта DataSet, тем сильнее страдают такие системы от нехватки процессорного времени, памяти и по- лосы пропускания. И вот теперь в ADO.NET 2.0 упомянутый недостаток механизма сериализации объектов DataSet был исправлен. В этой системе появилось новое свойство RemotingFormat. Оно принимает значения из перечисления SerializationFormat. Xml (по умолчанию) или Binary. Когда экземпляр объекта DataSet сериализуется с помощью средства форматирования .NET (скажем, в случае применения технологии .NET Remoting), оно заглядывает в свойство Remoting- Format и выясняет, в каком формате следует вернуть данные. Установив это свойство в Binary, вы получите значительно более компактный результат: DataSet ds = GetDataO; ds.RemotingFormat = SerializationFormat.Binary: Streamwriter writer = new StreamWriter(BinFile); BinaryFormatter bin = new BinaryFormatter(); bin.Serialize(writer BaseStream, ds); writer.Close(); В данном примере сериализованная версия объекта DataSet записывается на диск в настоящем двоичном формате. Если же убрать из кода строку, в которой свойству RemotingFormat присваивается значение Binary, он будет вести себя так же, как в ADO.NET 1.x, то есть сохранять данные в XML-формате DiffGram. В случае, когда данные подлежат передаче по каналу .NET Remoting, нужно обязательно задавать для свойства RemotingFormat значение Binary. Объект DataTable Объект DataTable представляет одну таблицу в памяти клиентского компьютера. Хотя этот объект чаще всего используется как контейнер данных в составе объекта DataSet, он может существовать и независимо. DataTable и DataSet — единственные объекты ADO NET, которые можно сериализовать и передавать на удаленные компьютеры. Подобно объекту DataSet, объект DataTable можно создавать программно. В этом случае вы сначала определяете его схему, а затем добавляете строки. Следующий код показывает, как создать в составе объекта DataSet новую таблицу: DataSet ds = new DataSetO; DataTaole tableEmp = new DatafableC’Employees"); tableEmp.Columns AddC'ID", typeof(int)); tableEmp.Columns.Add("Name", typeof(st ring)); ds.Tables.Add(tableEmp); В данном примере таблица получает имя Employees и состоит из двух столбцов: ID и Name. Она пуста, поскольку строки в нее пока не добавлялись. Для того чтобы добавить в объект DataTable новую строку, нужно вначале с помощью метода NewRow создать объект Data Row, заполнить его, а потом добавить в объект DataTable'. DataRow row = tableEmp.NewRowQ; row["ID'] = 1; row["Name"] = "Joe Users"; tableEmp Rows.Add(row); Объект DataTable может содержать коллекцию объектов-ограничений, исполь- зуемых для обеспечения целостности данных. Давайте подробнее рассмотрим его программный интерфейс, начиная со свойств, список которых приведен в табл. 8-12.
318 Часть II Размещение данных на сайте Табл. 8-12. Свойства класса DataTable Свойство Описание CaseSensitive Возвращает и позволяет задать значение, указывающее, должен ли при сравнении строк учитываться регистр символов ChildRelations Возвращает коллекцию дочерних отношений таблицы (в которых она выступает в качестве родительской) Columns Constraints DataSet DefaultView Возвращает коллекцию принадлежащих таблице столбцов Возвращает коллекцию ограничений, налагаемых на данные таблицы Возвращает объект DataSet, в состав которого входит таблица Возвращает используемый по умолчанию объект DataView, связанный с данной таблицей DisplayExpression Возвращает и позволяет задать строку отображения таблицы. Исполь- зуется в методе ToString совместно со свойством TableName ExtendedProperties HasErro>s Возвращает коллекцию пользовательских данных Возвращает значение, указывающее, есть ли ошибка хотя бы в одной из строк таблицы Locale Возвращает и позволяет задать локализационную информацию, ис- пользуемую при сравнении строк MinimumCapacity Namespace Возвращает и позволяет задать начальный размер таблицы Возвращает и позволяет задать пространство имен для XML-представ- ления таблицы ParentRelations Возвращает коллекцию родительских отношений таблицы (в которых она выступает в качестве дочерней) Prefix Возвращает и позволяет задать префикс, используемый в качестве псевдонима пространства имен таблицы PrimaryKey Возвращает и позволяет задать массив столбцов, составляющих пер- вичный ключ таблицы RemotingFormat Возвращает и позволяет задать формат сериализации: двоичный или XML. В ADO.NET 1х данное свойство не поддерживается Rows TableName Возвращает коллекцию строк таблицы Возвращает и позволяет задать имя объекта DataTable Коллекция ExtendedProperties имеется у нескольких объектов ADO.NET. Она со- стоит из пар имя-значение, причем значения имеют тип object. Вы можете использо- вать ее для хранения любой информации, имеющей смысл для вашего приложения. Методы класса DataTable перечислены в табл. 8-13. Табл. 8-13. Методы класса DataTable Метод Описание AcceptChanges Beginlnit Фиксирует все внесенные в таблицу изменения Запускает процедуру инициализации таблицы. Применяется в случаях, когда таблица используется формой или другим элементом управления BeginLoadData На время загрузки данных отключает уведомления, поддержку индек- сов и ограничений Clear Clone Удаляет все содержащиеся в таблице данные Клонирует структуру таблицы. Копирует ограничения и схему, но не данные Compute Вычисляет заданное выражение для строк, удовлетворяющих задан- ному критерию фильтрации. Результат вычислений возвращает в виде объекта
Контейнеры данных ADO.NET Глава 8 319 Табл. 8-13. (окончание) Метод Описание Сору CreateDataReader Копирует структуру и данные таблицы Возвращает для таблицы объект DataTableReader. В AD0.NET 1л дан- ный метод не поддерживается Endlnit Завершает инициализацию таблицы, которая была начата с помощью метода Beginlnit EndLoadData После загрузки данных включает уведомления, поддержку индексов и ограничений GetChanges Возвращает копию таблицы, содержащую все изменения, которые были внесены в нее со времени ее заполнения или с момента вызова метода AcceptChanges GetErrors ImportRow Возвращает массив объектов DataRow, содержащих ошибки Создает полную копию объекта DataRow и загружает ее в таблицу При этом сохраняются все установки, начальные и текущие значения LoadDataRow Находит и обновляет заданную строку. Если строка не найдена, созда- ется новая строка, куда записываются заданные значения. Для поиска строки используется первичный ключ NewRow ReadXml Создает новый объект DataRow с такой же схемой, как у таблицы Заполняет объект DataTable, считывая схему и данные из указанного XML-документа. В AD0.NET 1л данный метод не поддерживается ReadXmlSchema Копирует в объект DataTable структуру таблиц из указанного XML-до- кумента. В AD0.NET 1 л данный метод не поддерживается Rejectchanges Выполняет откат изменений, внесенных в таблицу со времени ее по- следнего заполнения или последнего вызова метода AcceptChanges Reset Select Возвращает объект DataTable к его состоянию по умолчанию Возвращает массив объектов DataRow, которые отвечают заданному критерию WriteXml Сериализует данные объекта DataTable в XML-формат. В AD0.NET 1л данный метод не поддерживается WriteXmlSchema Выводит структуру объекта DataTable в виде схемы XML. В ADO.NET 1л данный метод не поддерживается В ADO.NET 2.0 класс DataTable реализует интерфейс IXmlSerializable и предостав- ляет открытые методы для загрузки и сохранения содержимого объекта как XML-по- тока. Благодаря поддержке этого интерфейса объект DataTable может использоваться в качестве параметра и возвращаемого значения методов Web-сервисов .NET Строка объекта DataTable представлена объектом DataRow, а столбец — объек- том DataColumn. Простой, но в то же время эффективный метод выборки данных из объекта DataTable реализует метод Select, возвращающий результирующий набор данных в виде массива объектов DataRow. Критерий отбора задается на внутреннем языке ADO.NET и имеет ту же структуру, что и предложение WHERE оператора SELECT в языке SQL. Например, ниже приведено выражение, используемое для вы- борки всех строк, в которых значение столбца ID больше 5, а значение столбца Name начинается с буквы «А»: tableEmp.Select("ID >5 AND Name LIKE A%'"); За полным описанием синтаксиса критериев отбора метода Select можно обратить- ся к документации по .NET Framework. Заметьте, что тот же синтаксис используется и при определении вычисляемых столбцов объекта DataTable.
320 Часть II Размещение данных на сайте Выполнение вычислений Метод Compute класса DataTable вычисляет значение заданного выражения на осно- ве строк, соответствующих определенному критерию. Выражение может содержать любые логические и арифметические операторы и агрегатные функции — Min, Мах, Count, Sum и т. д. Могут использоваться и некоторые специальные статистические функции, такие как среднеарифметическое, среднеквадратическое отклонение и дис- персия. Следующий код подсчитывает строки, в которых значение столбца Name начинается с буквы «А»: int numRecs = (int) tableEmp.Compute("Count(ID)", " Name LIKE 'A%"'); У метода Compute имеются две перегруженные версии — одна из них принима- ет только выражение, а вторая еще и строку фильтрации, как в приведенном выше коде. Следует заметить, что все агрегатные функции оперируют значениями одного столбца. Иными словами, вы не можете подсчитать сумму произведений значений двух столбцов: Sum(quantity * price) При попытке вычислить такое выражение будет сгенерировано исключение. Для того чтобы получить действующий эквивалент этого выражения, нужно создать вы- числяемый столбец: tableEmp Columns.Add("order_item_price", typeof(double), "quantity*price"); В данном случае в таблицу tableEmp добавляется столбец с именем order_item_price, содержащий произведения значений столбцов quantity и price. Подсуммирбвав данные созданного столбца, вы получите желаемый результат: Sum(о rde r_item_p гice) Столбцы таблицы Объект DataColumn представляет схему столбца объекта DataTable и обладает свой- ствами, определяющими характеристики столбца. К числу таких свойств относятся AllowDBNull, Unique, Readonly, DefaultValue и Expression. Как рассказывалось ранее, в некоторые из них автоматически записывается информация, прочитанная из ис- точника данных, по крайней мере если им является база данных. Объект DataColumn имеет имя и тип, а иногда (если столбец Является вычис- ляемым) для него также задано выражение, используемое для формирования его значений. Содержимое вычисляемого столбца является функцией одного или более столбцов той же таблицы. При создании вычисляемого столбца ADO.NET сразу вы- числяет все его значения и кэширует их подобно обычным данным. В дальнейшем она отслеживает изменения аргументов функции столбца в каждой строке и по мере необходимости пересчитывает его значения. Для этой цели ADO.NET регистрирует внутренний обработчик события RowChanged объекта DataTable. Как вы увидите в следующей главе, вычисляемые столбцы интенсивно исполь- зуются при связывании элементов управления с данными. Кроме того, совместно с отношениями таблиц они используются для реализации весьма мощных функцио- нальных возможностей, о которых я расскажу в разделе «Связи данных*. Строки таблицы Данные таблицы представлены коллекцией строк — объектов DataRow. Для каждой строки в объекте DataRow хранится состояние, массив значений (в двух версиях — те- кущей и исходной) и информация об ошибках. Чтобы запросить определенную версию того или иного значения, нужно обратиться к свойству-аксессору Item. Следующий
Контейнеры данных ADO.NET Глава 8 321 код показывает, как прочитать исходное значение столбца определенного объекта DataRow (по умолчанию возвращается текущее значение): Response.Write(row["Name", DataRowVersion Original].ToString()); Доступ к значениям строки может осуществляться как по отдельности, так и ко всем сразу. Во втором случае используется свойство ItemArray, возвращающее массив объектов (по одному на каждый столбец). С его помощью можно быстро прочитать значения из строки или обновить ее данные одной командой. Класс DataRow не имеет открытого конструктора. Поэтому строка данных может быть создана только неявно с помощью метода NewRow таблицы. Этот метод воспро- изводит в новой строке структуру таблицы, для которой он вызван, однако возвра- щенная им строка не принадлежит этой таблице, а является независимым объектом. Добавление ее в объект DataTable производится явно: taoleEmp.Rows.Add(row); Заметьте, что объект DataRow не может быть связан более чем с одной таблицей за раз. Для загрузки строки в другую таблицу можно использовать метод Import Row, дублирующий объект DataRow и добавляющий результат в заданную таблицу. Строка может быть отсоединена от таблицы с помощью метода Remove. Если же вызвать для этой строки метод Delete, она будет помечена как удаленная, но останется в составе таблицы (пока изменения в последней не будут зафиксированы). Примечание Объекты, которые удаляются из родительской коллекции, не уничтожают- ся автоматически до тех пор, пока не выйдут за пределы области видимости приложе- ния, после чего сборщик мусора их уничтожит. Это верно и для некоторых объектов ADO.NET, включая DataRow. Например, объект DataTable может быть отсоединен от объекта DataSet путем удаления из коллекции Tables. Однако это не значит, что сам объект будет уничтожен. Ограничения таблиц Ограничением называется заданное для таблицы логическое правило, соблюдение которого необходимо для обеспечения целостности данных. Например, ограничение может определять, что должно происходить при удалении из таблицы записи, свя- занной с другой таблицей. В .NET Framework поддерживаются два типа ограниче- ний: ForeignKey Constraint и UniqueConstraint. Объект класса ForeignKeyConstraint определяет правило, управляющее каскадным удалением и обновлением записей таблиц. Для примера предположим, что у вас имеются две взаимосвязанные таблицы — сотрудников и заказов. Что должно проис- ходить при удалении записи о сотруднике, принявшем определенное число заказов? Следует ли удалять при этом записи о принятых им заказах? Для того чтобы задать соответствующую установку, необходимо вначале определить отношение между двумя таблицами в виде объекта ForeignKeyConstraint. Делается это так: DataColumn d = tableEmp.Columns("empID"); DataColumn с2 = tableOrd.ColumnsC’empID"); ForeignKeyConstraint fk = new ForeignKeyConstraint('EmpOrders", c1, c2); // Конфигурирование объекта-ограничения tableOrd Constraints Add(fk); Конструктор класса ForeignKeyConstraint принимает имя создаваемого объекта и два объекта типа DataColumn. Первый из них представляет столбец (или столбцы)
322 Часть II Размещение данных на сайте родительской таблицы, а второй — дочерней. Ограничение добавляется в дочернюю таблицу и конфигурируется с помощью свойств UpdateRule (действие при обновлении строки родительской таблицы), DeleteRule (действие при удалении строки родитель- ской таблицы) и AcceptRejectRule (действие при фиксации изменений в родительской таблице). Свойства UpdateRule и DeleteRule принимают значения из перечисления Ride, а свойство AcceptRejectRule — из одноименного ему перечисления. Изменения строк родительской таблицы могут каскадно распространяться на дочерние таблицы. Так, когда строка родительской таблицы удаляется, каскадное распространение изменений происходит по одной из двух схем: соответствующие строки дочерней таблицы каскадно удаляются либо поля их внешнего ключа по- лучают значение null или значения по умолчанию. В качестве альтернативы для до- черней таблицы могут просто игнорироваться изменения, внесенные в родительскую таблицу. Соответствующее поведение дочерней таблицы определяется свойствами UpdateRule и DeleteRule. Обращение к свойству AcceptRejectRule производится при выполнении метода AcceptChanges для фиксации изменений в родительской таблице. Это свойство опреде- ляет, должны изменения игнорироваться или каскадно распространяться на дочерние таблицы. Связанный с таблицей объект класса UniqueConstraint призван гарантировать, что определенный столбец будет содержать уникальные, неповторяющиеся, значения. Можно также использовать его для обеспечения уникальности сочетания значений нескольких столбцов. Существует несколько способов задания ограничения уникальности. Один из них заключается в том, чтобы с помощью конструктора класса UniqueConstraint явно создать его объект, а затем добавить его в коллекцию Constraints объекта DataTable-. UniqueConstraint ис; uc = new UniqueConstraint(tableEmp.Columns('empID )); tableEmp.Const raints.Add(uc); Второй способ — создать объект ограничения неявно, установив свойство Unique столбца в true. (Для отключения ограничения достаточно присвоить этому свойству значение false.) Существует и третий способ: включить столбец в состав первичного ключа таблицы в памяти — тогда будет гарантирована уникальность сочетания зна- чений столбцов, составляющих первичный ключ (или одного столбца, если ключ со- держит только его). Первичным ключом объекта DataTable называется массив объектов DataColumn, используемый для индексации и сортировки строк. При наличии такого ключа метод Select может быстрее извлечь из таблицы запрошенные значения, если среди них присутствуют значения столбцов первичного ключа; ускоряется и работа некоторых методов класса DataView. Примечание Когда вы определяете объект DataColumn как первичный ключ объекта DataTable, свойство AllowDBNull данного столбца автоматически устанавливается в false, а свойство Unique— в true. Если первичный ключ является составным, тогда свойство Unique не устанавливается. Связи данных Связь данных представляет отношение родительский-дочерний между двумя объек- тами DataTable одного объекта DataSet. В NET Framework такая связь представлена объектом DataRelation. Отношение между двумя таблицами основывается на соот- ветствии одного или нескольких их столбцов. Столбцы, по которым устанавливается соответствие, могут иметь в двух таблицах разные имена, но обязательно должны при-
Контейнеры данных ADO.NET Глава 8 323 надлежать к одному и тому же типу. Все отношения таблиц объекта DataSet хранятся в его коллекции Relations. В табл. 8-14 перечислены свойства класса DataRelation. Табл. 8-14. Свойства класса DataRelation Свойство Описание ChildColumns Возвращает дочерние объекты DataColumn отношения ChildKey Constraint Возвращает объект ForeignKeyConstraint отношения ChildTable Возвращает дочерний объект DataTable отношения DataSet Возвращает объект DataSet, которому принадлежит отношение ExtendedProperties Возвращает коллекцию пользовательских данных Nested Возвращает и позволяет задать значение, указывающее, должны ли дочерние таблицы отношения выводиться в виде поддеревьев при сериализации объекта DataSet в формат XML (см. ниже раздел «Сериализация связи данных») ParentColumns Возвращает родительские объекты DataColumn отношения ParentKeyConstraint Возвращает объект UniqueConstraint, обеспечивающий уникальность значений родительских столбцов отношения ParentTable Возвращает родительский объект DataTable отношения RelationName Возвращает и позволяет задать имя объекта DataRelation. Оно ис- пользуется для идентификации отношения в коллекции Relations родительского объекта DataSet При создании объекта DataRow автоматически создаются два ограничения. С до- черней таблицей связывается ограничение внешнего ключа, предотвращающее созда- ние строк, не имеющих соответствия в родительской таблице, а с родительской — огра- ничение уникальности, предотвращающее дублирование значений столбцов, по ко- торым установлена связь. Указанные ограничения создаются по умолчанию, но если воспользоваться другим конструктором, можно пропустить этот шаг. Важных методов класс DataRelation не имеет. Создание связи данных Объект DataRelation можно рассматривать как программный аналог межтаблич- ного отношения, действующего в базе данных. Однако при заполнении объекта DataSet определенные в базе данных отношения не обрабатываются и не загружаются в объект вместе с данными. Поэтому связь данных является исключительно программ- ным объектом, который необходимо явно создавать программным способом: DataColumn d = tableEmp.Columns( empID"); DataColumn c2 = tableOrd.Columns("empID”); DataRelation rel = new DataRelation("Emp20rders", c1, c2) DataSet.Relations.Add(rel); Приведенный код устанавливает в объекте DataSet отношение между таблицами Employees и Orders, основываясь на значениях их общего столбца empID. Каковы прак- тические преимущества создания такого отношения? При его наличии родительский объект DataTable знает, что с каждой его строкой может быть связана группа строк дочернего объекта. В этом случае для каждого сотрудника, запись о котором имеется в таблице Employees, может существовать группа дочерних записей о заказах в таблице Orders. Дочерними являются те записи, в которых значения столбца Orders.empID со- ответствуют значению столбца empID текущей строки таблицы Employees. В ADO.NET реализован механизм автоматического извлечения связанных строк, активизируемый с помощью метода GetChildRows класса DataRow. Данный метод
324 Часть II Размещение данных на сайте принимает в качестве аргумента отношение и возвращает массив объектов Data Rote, дочерних по отношению к строке, для которой он вызван: foreach(DataRow chiloRow in parentRow GetChildRows("Emp2Crders")) *•’ ! { г // Обработка дочерних строк } , Еще одной важной функцией ADO .NET, связанной с отношениями, является вы- полнение вычислений над данными таблиц и поддержка вычисляемых столбцов. Выполнение вычислений для отношений Среди типичных задач, с которыми сталкиваются создатели приложений баз данных, распространена следующая обработка двух связанных таблиц: выполняется проход по родительской таблице и для каждой ее строки обрабатывается подмножество дочер- них строк Как правило, обработка дочерних строк заключается в получении тех или иных агрегированных данных. При наличии связи данных выполнить в ADO.NET такую обработку исключительно просто. Предположим, что для упомянутого выше отношения сотрудники-заказы необходимо вычислить сумму заказов, принятых каждым сотрудником. В таком случае вы просто добавляете в родительскую табли- цу вычисляемый столбец и связываете его с данными дочерней таблицы: tableEmp.Columns.Add("Total", typeof(int), ”Sum(child(Emp20rders).Amount)"); Новый столбец Total таблицы Employees будет содержать сумму значений столбца Amount всех дочерних строк. Ключевое слово child в приведенном операторе является специальным синтаксическим элементом выражений ADO.NET. Функция child при- нимает имя отношения и возвращает массив его дочерних объектов DataRow. Сериализация связи данных Свойство Nested объекта DataRelation определяет способ рендеринга родительского объекта DataSet в формат XML. По умолчанию наличие отношения не отражается на схеме результирующего XML-документа: все таблицы выводятся последовательно в виде узлов, дочерних по отношению к корневому узлу документа. Но если установле- но свойство Nested объекта DataRelation, связанные между собой таблицы выводятся иерархически, так что дочерние строки оказываются внутри узла родительской строки Например, по умолчанию объект DataSet, содержащий таблицы Employees и Orders, выводится в таком виде: <MyDataSet> Employees empid="1" name="Joe Users" /> <Orders empid="1" amount="6897" .. /> <Orders empid='1" amount= 19713" ... /> </MyDataSet> Если же между таблицами существует отношение, для которого задано свойство Nested, XML-схема меняется и дочерние строки оказываются на своем естественном месте: <MyDataSet> Employees empid="1" name="Joe Users"> <Orders empid="1" amount="6897" ... /> cOrders empid="1" amount="19713" ... /> </Employees> </MyDataSet>
Контейнеры данных ADO.NET Глава 8 325 Объект DataView Настраиваемые представления данных объекта DataTable определяются в виде объек- тов класса DataView. Связь между объектами DataTable и DataView соответствует классической модели документ-представление: роль документа играет объект Data- Table, а роль представления — DataView. В любой момент можно получить несколько разных представлений одних и тех же данных, не затрагивая сами эти данные. При этрм у каждого объекта-представления имеется собственный набор свойств, методов и событий. Представление реализуется в виде отдельного массива индексов исходных строк, соответствующих заданному критерию. По умолчанию оно не является фильтро- ванным, то есть содержит все строки таблицы. Однако с помощью свойств RowFilter и RowStateFilter можно ограничить набор строк представления, с тем чтобы они отвечали требуемому критерию. Можно также отсортировать строки представле- ния, задав выражение сортировки в свойстве Sort. Полный список свойств класса DataView приведен в табл. 8-15. Табл. 8-15. Свойства класса DataView Свойство Описание AllowDelete Возвращает и позволяет задать значение, указывающее, может ли через данное представление осуществляться удаление строк AllowEdit Возвращает и позволяет задать значение, указывающее, может ли через данное представление осуществляться редактирование строк AllowNew Возвращает и позволяет задать значение, указывающее, может ли через данное представление осуществляться добавление строк Apply DefaultSort Возвращает и позволяет задать значение, указывающее, следует ли по умолчанию выполнять стандартную сортировку Count Возвращает количество строк представления после наложения фильтра Data ViewManager Возвращает связанный с представлением объект DataViewManager Item Возвращает строку данных базовой таблицы; свойство является индексным RowFilter Возвращает и позволяет задать выражение фильтрации строк представ- ления RowStateFilter Возвращает и позволяет задать используемый представлением фильтр состояния строк Sort Возвращает и позволяет задать выражение сортировки строк представ- ления, определяющее перечень столбцов и направление сортировки Table Возвращает и позволяет задать базовый объект DatdTable представления В качестве критерия фильтрации может использоваться выражение, определен- ное состояние строк или и то и другое вместе. Допустимыми значениями свойства RowStateFilter являются элементы перечисления DataViewRowState — это свойство по- зволяет включить в состав представления исходные или текущие значения строк, мо- дифицированные, добавленные или удаленные строки. Синтаксис свойства RowFilter такой же, как у метода Select объекта DataTable. О&ьекх DataView не содержит копий строк таблицы, а лишь массив индексов, об- новляемый при изменении свойств фильтрации. Такой объект всегда связан с базовым объектом DataTable и представляет строки последнего, отфильтрованные и отсортиро- ванные определенным образом. Свойства AllowXXX позволяют определить, является
326 Часть II Размещение данных на сайте ли представление редактируемым. По умолчанию оно полностью редактируемое, то есть через него можно модифицировать, добавлять и удалять строки базовой таблицы. В табл. 8-16 перечислены методы класса DataView. Табл. 8-16. Методы класса DataView Метод Описание AddNew Добавляет новую строку в представление и базовую таблицу Beginlnit Запускает процесс инициализации представления Copy To Копирует элементы представления в массив Delete Удаляет строку представления с заданным индексом. Эта строка удаляется из таблицы Endlnit Завершает процесс инициализации представления Find Находит в представлении строку по одному или нескольким значениям ключа сортировки представления и возвращает ее индекс FindRows Находит в представлении строки по одному или нескольким значениям ключа сортировки представления и возвращает массив соответствующих объектов DataRowView GetEnumerator Возвращает для объекта Data View объект-перечислитель Имейте в виду, что методы AddNew и Delete воздействуют и на базовый объект DataTable. Когда требуется внести серию изменений, их можно сгруппировать, раз- местив соответствующие операторы между вызовами методов Beginlnit и Endlnit. Навигация по представлению Для доступа к содержимому объекта DataView могут использоваться разные программ- ные интерфейсы — коллекции, списки и перечислители. Так, метод GetEnumerator позволяет осуществлять проход по строкам в цикле for...each: DataView myView = new DataView(table); foreach(DataRowView rowview in myView) { // Разыменование объекта DataRow DataRow row = rowview.Row; } Когда клиентское приложение обращается к определенной строке представления, класс DataView находит ее во внутреннем кэше строк. Если этот кэш не пуст, строка сразу возвращается вызывающему коду в виде промежуточного объекта DataRowView, являющегося оболочкой объекта DataRow. Для доступа к самому объекту DataRow нужно вызвать свойство Row. Если же упомянутый кэш пуст, класс DataView сна- чала заполняет его массивом объектов DataRowView, каждый из которых содержит ссылку на исходный объект DataRow. При обновлении выражения сортировки или фильтрации кэш строк обновляется. Внутренняя архитектура объекта DataView пред- ставлена на рис. 8-5. Поиск строк Связь между объектами DataTable и DataView обычно устанавливается при создании последнего через его конструктор: public DataView(DataTable table);
Контейнеры данных ADO.NET Глава 8 327 Сортировка, фильтрация Рис. 8-5. Внутренняя архитектура объекта DataView Однако можно вначале создать новое представление, а затем связать его с таблицей с помощью средства Table объекта DataView. DataView dv = new DataView(); dv.Table = dataSet.Tables["Employees"]; Кроме того, объект Data View можно получить от объекта DataTable, для чего нужно обратиться к его свойству DefaultView. DataView dv = dt.Defaultview; Это свойство возвращает представление, инициализированное для работы с теку- щей таблицей. Первоначально представление не является фильтрованным и содержит столько же строк, сколько исходная таблица. Для быстрого поиска строк представления можно воспользоваться методом Find или FindRows. Оба они принимают одно или несколько значений ключа сортировки, но метод Find возвращает индекс первой из найденных строк, а метод FindRows — массив объектов DataRowView, представляющих все найденные строки. Вот пример вызова метода Find для поиска сотрудника по имени Margaret в представлении, связанном с таблицей Employees'. dv = dataSet.Tables["Employees"].Defaultview; dv.Sort = "firstname"; object val = "Margaret"; int i = dv.Find(val); Щ Примечание Содержимое объекта DataView может быть отсортировано по значениям нескольких столбцов, и тогда эти столбцы следует задавать в свойстве Sort в виде раз- деленного запятыми списка. При желании в конец этого списка можно добавить ключевое слово DESC или ASC, указывающее порядок сортировки. Заключение В основу ADO.NET положен принцип автономной работы клиентского приложения с данными. Эта система предоставляет разработчику API, позволяющий загружать информацию из базы данных в специальные объекты-контейнеры, хранящиеся в памяти клиентского компьютера и обладающие двумя важными характеристиками. Во-первых, они ведут себя как настоящие базы данных и их интерфейс подобен ин- терфейсу серверных СУБД. А во-вторых, они являются сериализуемыми и поддер- живают модель фиксации изменений, обеспечивающую исключительную гибкость их обработки.
328 Часть II Размещение данных на сайте Классы DataSet и DataTable идеально подходят для упаковки данных, передаваемых между слоями распределенной системы. Они обладают рядом важных функций, таких как поддержка целостности данных, их оптимистической блокировки, ограничений, индексации и фильтрации. Эти объекты могут участвовать в пакетном обновлении данных, при котором клиентский код подключается к серверной базе данных и пере- носит в нее изменения, сделанные на клиенте. Сам по себе объект DataSet является лишь контейнером и ничего не знает о базе данных и провайдерах данных. Его можно программно заполнить любой информацией, представленной в табличной форме. За связь с базой данных отвечает адаптер данных — специальный объект, который заполняет объекты DataSet и DataTable результатами выполнения запроса, а позднее передает внесенные на клиенте изменения обратно в базу данных. Объекты DataSet и DataTable часто используются в качестве источников данных элементов управления, связанных с данными. О том, как это происходит, вы узнаете в следующей главе. Только факты Контейнерные классы ADO.NET DataSet и DataTable ничего не знают о провай- дере, предоставившем им данные. Это сериализуемые классы с богатой функцио- нальностью, напоминающей функциональность баз данных: они поддерживают реляционную целостность данных, оптимистическую блокировку, ограничения, индексы и фильтрацию. Адаптером данных называется аналог командного объекта, осуществляющий до- ступ к источнику базы данных и помещающий извлеченную оттуда информацию в контейнеры DataSet и DataTable. Подобно командным объектам и объектам чтения данных, адаптеры специфичны для провайдеров данных. Метод Fill адаптера данных находит табличные объекты, которые соответствуют наборам данных, полученным в результате выполнения запроса, и заполняет объек- ты этими данными. В ADO.NET 2.0 можно гибко управлять осуществлением операции заполнения, указывая адаптеру, как ему следует поступать с исходными и текущими значени- ями строк заполняемых таблиц. Объект DataSet инкапсулирует набор таблиц и межтабличных связей и позволяет передавать все это между слабосвязанными слоями прикладной системы. Операция пакетного обновления ADO NET заключается в выполнении серии команд, которые адаптер данных последовательно направляет базе данных. Хотя этот процесс запускается одним оператором, не обязательно, что все команды обновления строк будут переданы единым пакетом. Технология пакетного обновления особенно эффективна в средах с низкой вероят- ностью конфликтов данных, она позволяет разрабатывать приложения с автономно работающими слоями, характеризующиеся высокой степенью масштабируемости и состоящие из более простого кода, чем приложения с непрерывно взаимодей- ствующими слоями. В ADO.NET 2.0 объект DataSet может быть сериализован в настоящий двоичный формат, значительно более компактный, чем формат XML. Класс DataTable из ADO.NET 2.0 реализует интерфейс IXmlSerializable и предо- ставляет открытые методы для загрузки его содержимого из XML-потока и за- писи его в такой поток. Благодаря наличию этого интерфейса объект DataTable может использоваться в качестве параметра или возвращаемого значения метода Web-сервиса .NET.
Глава 9 Модель связывания с данными Для того чтобы создавать в ASP.NET 1.x эффективные приложения, управляемые данными, необходимо хорошо знать объектную модель ADO.NET. Вы должны быть знакомы с объектами, представляющими подключения, команды, транзакции, пара- метры, то есть со всеми объектами, которые мы рассматривали в двух предыдущих главах. В ASP.NET 2.0 объекты ADO.NET как бы ушли на второй план с появлением нового семейства связанных с данными и более дружественных к программисту ком- понентов — элементов управления, которые представляют источники данных. Поэтому объекты ADO.NET в ASP.NET 2.0 используются программистами не столь уж часто и областью их применения являются лишь более-менее стандартные управляемые данными страницы относительно простых Web-сайтов. Но значит ли это, что объекты ADO.NET стали ненужными? Неужели ASP.NET 2.0 позволяет каким-то чудесным образом вовсе обходиться без них при создании приложений? Конечно же, нет. В сложных многокомпонентных системах масштаба предприятия код ADO.NET обычно изолирован в слое доступа к данным и не используется сам по себе — обо- лочкой для него служат вспомогательные библиотеки, такие как Microsoft Data Access Application Block. Web-страницы в подобных системах никогда не вызывают объекты ADO.NET, как это делали демонстрационные страницы, описанные в двух предыдущих главах. Тем не менее объекты ADO.NET по-прежнему остаются важным компонентом .NET Framework, просто теперь они стали частью внутренней инфраструктуры, обе- спечивающей выполнение типичных операций связывания с данными. Многие важные шаги доступа к данным выполняются в ASP.NET 2.0 за сценой, и рутинная работа по открытию и закрытию соединений, подготовке командных объектов и получению от них результатов теперь выполняется автоматически — упомянутыми элементами управления, представляющими источники данных. Действующая в ASP.NET модель связывания элементов управления с данными базируется на трех столпах: выражениях связывания с данными, классическом свя- зывании с источниками данных и элементах управления, представляющих источни- ки данных, которые доступны только в ASP.NET 2.0. Давайте начнем рассмотрение этой модели со связывания элементов управления с обычными источниками данных Связывание элементов управления с данными В большинстве своем Web-приложения управляются данными, а потому их разработчи- кам необходима возможность связывать HTML-элементы, такие как раскрывающиеся списки и таблицы, со структурированными данными. Связыванием элемента управле- ния с данными называется процесс извлечения данных из определенного источника и динамического присваивания их свойствам элемента управления. Такую возможность поддерживают специально предназначенные для этой цели элементы управления, ко- торые называют элементами управления, связанными с данными. Это не какое-то особое семейство элементов, а просто серверные элементы управления, имеющие несколько стандартных свойств, заполняемых с помощью стандартных объектов.
330 Часть II Размещение данных на сайте Поддерживаемые источники данных Источниками данных элементов управления могут служить многие классы .NET, и не только те, которые связаны с содержимым баз данных. В ASP.NET любой объект, реализующий интерфейс lEnumerable, обладает API. необходимым для доступа к на- бору данных этого объекта: public interface lEnumerable { lEnumerator GetEnumerator(); } Однако многие связываемые объекты реализуют более продвинутую версию ин- терфейса lEnumerable — ICollection или IList. Любой элемент управления Web можно связывать с объектами перечисленных ниже классов: контейнерными классами ADO.NET, такими как DataSet, DataTable и DataView, классами чтения данных; пользовательскими коллекциями, словарями и массивами. Следует отметить, что классы DataSet и DataTable не реализуют интерфейс lEnume- rable или другие наследующие интерфейсы, однако в их состав входят коллекции для хранения данных. Доступ к этим коллекциям осуществляется с помощью методов промежуточного интерфейса IListSource. Классы ADO.NET Как вы уже знаете из главы 8, в ADO.NET определен набор контейнерных классов, объекты которых можно заполнять любыми данными, например, результатами вы- полнения запросов к базам данных. Эти классы являются прекрасными источниками для заполнения элементов управления, связанных с данными, в частности списочных и табличных элементов. Интересно, что к числу объектов, которые могут служить источниками данных, относятся не только контейнеры, но и объекты чтения дан- ных. Вы передаете элементу управления открытый объект чтения данных, и элемент управления с его помощью выполняет проход по записям, заполняя свой пользова- тельский интерфейс. 1’^1 Примечание В приложениях для Web и Windows связывание элементов управления сданными осуществляется по-разному. Эти различия касаются внутренней реализации механизма связывания, а также заключаются в том, что объекты чтения данных могут ис- пользоваться для такой цели только в приложениях ASP.NET, а объекты DataViewManager, упоминавшиеся в главе 8, — только в приложениях Windows Forms. Объект DataSet может содержать несколько таблиц, однако стандартные элементы управления ASP.NET допускают связывание только с одной из них. Связывая такой элемент управления с объектом DataSet, вы задаете эту таблицу в соответствующем свойстве. Однако указанное ограничение является просто особенностью реализации стандартных элементов управления ASP.NET, связанных с данными, и оно вовсе не обусловлено какими-либо характеристиками самой этой платформы. Поэтому ничто не мешает вам написать собственный специализированный элемент управления, не имеющий такого ограничения. Для взаимодействия с объектами как с источниками данных классы DataSet и Data- Table предоставляют интерфейс IListSource, а класс DataView и класс чтения дан- ных — интерфейс lEnumerable.
Модель связывания с данными Глава 9 331 Классы, в которых используются коллекции Если говорить абстрактно, то коллекция — это контейнер для экземпляров другого клас- са. Все классы-коллекции реализуют интерфейс ICollection, который, в свою очередь, реализует интерфейс lEnumerable. Таким образом, каждый класс-коллекция обладает определенной стандартной функциональностью. У всех таких классов имеется свойство Count, возвращающее число кэшированных элементов, метод СоруТо, предназначенный для копирования элементов (всех или выборочно) во внешний массив, и метод GetEnu- merator, который создает и возвращает объект-перечислитель для перебора элементов коллекции в цикле. Именно метод GetEnumerator вызывается за сценой при выполнении цикла foreach в коде C# или цикла For...Each в коде Microsoft Visual Basic .NET. Интерфейсы IList и IDictionary являются расширенными и в то же время более специализированными версиями интерфейса ICollection. Сам по себе этот интер- фейс определяет лишь минимальную функциональность коллекции. Например, он не содержит методов для добавления и удаления ее элементов. Зато такие методы имеются в составе интерфейса IList. методы Add и Insert вставляют новый элемент со- ответственно в начало коллекции и по заданному индексу, методы Remove и RemoveAt удаляют элементы, а метод Clear очищает коллекцию. Кроме того, у интерфейса IList есть метод Contains, проверяющий, принадлежит ли коллекции заданный элемент, и метод IndexOf, возвращающий индекс заданного элемента. Наиболее популярными контейнерными классами, реализующими интерфейсы ICollection и IList, являют- ся Array, ArrayList и StringCollection. Интерфейс IDictionary определяет API словаря — коллекции, предназначенной для хранения пар ключ-значение. Он содержит такой же набор методов, как интерфейс IList, но с другими сигнатурами. Кроме того, в его состав входят два дополнительных свойства: Keys и Values, возвращающие соответственно коллекцию ключей и коллек- цию значений. Типичными словарными классами являются ListDictionary, Hashtable и SortedList. Осуществляя связывание элементов управления с данными, вы чаще будете ис- пользовать не стандартные, а специализированные классы-коллекции. Самый про- стой способ создания такой коллекции в .NET 1.x заключается в том, чтобы написать класс, производный от CollectionBase, и переопределить в нем метод Add и свойство Item (а при желании и другие члены): public class OrderCollection : CollectionBase { public OrderCollectionO } // Метод Add public void Add(OrderInfo o) { InnerList.Add(o); } // Индексное свойство ("Item”) public Orderinfo this[int index] get { return (Orderinfo) InnerList[index]; } set { InnerList[index] = value; } } }
332 Часть II Размещение данных на сайте public class Orderinfo { private int _id; public int ID { get { return _id; } set { id = value; } } private DateTime _date; public DateTime Date { get { return _date; } set { _date = value; } } } Обратите внимание, что в классе, представляющем элемент коллекции, члены- данные реализуются в виде свойств, а не полей: public class Orderinfo public int ID; public DateTime Date; } Конечно, поля определить проще, но они недоступны во время выполнения, если только класс не содержит дескриптор пользовательского типа (то есть не реализует интерфейс ICustomTypeDescriptof), открывающий доступ к этим полям как к свойствам. В ASP.NET 2.0 благодаря введению родовых типов разработка пользовательских коллекций существенно упростилась — настолько, что в некоторых случаях для реа- лизации такой коллекции достаточно следующего кода: public class OrderCollection : List<OrderInfo> { Свойства для связывания с данными Элементы управления ASP.NET, связанные с данными, делятся на две категории: списочные и итеративные. Первые выводят список элементов данных, повторяя для каждого из них один и тот же фиксированный шаблон. Вторые, обладая большей гибкостью, дают возможность явно определить шаблон отображения элемента данных, а также другие шаблоны, используемые при формировании итоговой разметки. Все элементы управления, связанные с данными, имеют ряд общих свойств (рис. 9-1), к числу которых относятся свойства DataSource и DataSourcelD. Эта диаграмма соответствует объектной модели ASP.NET 2.0. В ASP.NET 1.x у эле- ментов управления отсутствует свойство DataSourcelD и нет промежуточных классов BaseDataBoundControl и DataBoundControl, так что все итеративные элементы управ- ления основываются прямо на классах ListControl и BaseDataList га Примечание И в ASP.NET 1.x, и в ASP.NET 2.0 есть низкоуровневый итеративный эле- мент управления Repeater. Он является производным непосредственно от класса Control и не наследует ни один из классов, представленных в диаграмме.
Модель связывания с данными Глава 9 333 DataTextField DataTextFormatString DataValueField AppendDataBoundltems Рис. 9-1. Диаграмма классов элементов управления, связанных с данными Свойство DataSource В свойстве DataSource задается объект-источник данных, с которым будет связан элемент управления. Связь между этими двумя объектами является логической, и ее создание не приводит к выполнению каких бы то ни было операций до тех пор, пока вы явно не дадите указание связать элемент управления с данными. Это указание дается путем вызова метода DataBind. Только в таком случае элемент управления загружает данные из источника, заполняет соответствующие свойства и генерирует разметку. public virtual object DataSource {get; set;} Свойство DataSource имеет тип object и может принимать любые объекты, реали- зующие интерфейс lEnumerable (включая объекты чтения данных) или IListSource. Сразу отмечу, что интерфейс IListSource реализуют только классы DataSet и DataTable. Обычно значение свойства DataSource задается программным способом, однако это можно сделать и декларативно: <asp:DropDownList runat="server" id="theList" DataSource="<%# GetDataQ %>" /> В данном примере функция GetData является открытым или защищенным членом класса хост-страницы, возвращающим связываемый объект. [.^"1 Примечание Как элемент управления, связанный с данными, может определить, с каким конкретно объектом он связан: коллекцией, объектом чтения данных или, может быть, объектом DataTable? Все стандартные элементы управления, связанные с данными, взаи- модействуют с объектом через интерфейс lEnumerable, и поэтому любой объект, заданный в свойстве DataSource, они нормализуют, преобразуя к типу, реализующему интерфейс lEnumerable. В одних случаях нормализация заключается просто в приведении объекта к типу lEnumerable, тогда как в других, в частности когда речь идет об объекте DataTable или DataSet, выполняется дополнительный шаг — поиск именованной коллекции данных, соответствующей значению свойства DataMember. Эта тема подробно рассмотрена в моей книге «Programming Microsoft ASP.NET 2.0 Applications: Advanced Topics» (Microsoft Press, 2005), в главе, посвященной разработке специализированных элементов управления. Свойство DataSourcelD Свойство DataSourcelD, введенное в ASP.NET 2.0, возвращает и позволяет задать иден- тификатор объекта, от которого элемент управления получает свои данные. Именно через это свойство осуществляется связь между элементами управления, связанными
334 Часть II Размещение данных на сайте с данными, и новым семейством элементов управления, представляющих источники данных, например элементами SqlDataSource и ObjectDataSource: public virtual string DataSourcelD {get; set;} Устанавливая свойство DataSourcelD, вы сообщаете элементу управления, что, когда ему потребуется доступ к данным (чтобы извлечь их, перейти к следующей странице, отсортировать, подсчитать количество строк или обновить), он должен обращаться к заданному вами объекту. Подобно свойству DataSource свойство Data- SourcelD имеется у всех элементов управления, связанных с данными, причем эти свойства являются взаимоисключающими — если установить их оба, то во время вы- полнения будет сгенерировано исключение. То же самое произойдет и в случае, если в свойстве DataSourcelD задать строку, не соответствующую ни одному из доступных элементов управления, представляющих источники данных. Свойство DataMember Свойство DataMember возвращает и позволяет задать имя коллекции данных, ко- торую нужно извлечь из объекта-источника при связывании элемента управления с данными. public virtual string DataMember {get; set;} Например, когда в свойстве DataSource задается объект DataSet, в свойстве Data- Table нужно указать имя объекта DataTable: DataSet data = new DataSetO; SqlDataAdapter adapter = new SqlDataAdapter(cmdText, connString); adapter.Fill(data); // Table - это имя, присваиваемое по умолчанию первой таблице // объекта DataSet при его заполнении адаптером grid.DataMember = "Table"; grid.DataSource = data; grid.DataBind(); Свойства DataMember м. DataSource можно устанавливать в любом порядке — лишь бы оба они были установлены к моменту вызова метода DataBind. Когда связывание осуществляется со стандартными элементами управления, представляющими ис- точники данных, то есть с использованием свойства DataSourcelD, значение свойства DataMember игнорируется. Примечание Это не ограничение самой технологии связывания, а особенность стандарт- ных элементов управления, представляющих источники данных, — они не поддерживают множественных наборов данных. Мы еще вернемся к этой теме, когда будем обсуждать компоненты, представляющие источники данных. Свойство DataTextField Свойство DataTextField обычно используется списочными элементами управления — в нем задается имя столбца источника данных, в котором содержатся значения, под- лежащие выводу в качестве элементов списка. public virtual string DataTextField {get; set;} Например, если речь идет о раскрывающемся списке (рис. 9-2), то свойство DataTextField используется для доступа к строкам, которыми заполняется список: CountryList.DataSource = data; CountryList.DataTextField = "country"; Count ryList.DataBind();
Модель связывания с данными Глава 9 335 То же касается элементов управления ListBox, CheckBoxList и других списочных элементов. В отличие от свойства DataMember свойство DataTextField используется и в том случае, когда осуществляется связывание со стандартными элементами управ- ления, представляющими источники данных. Рис. 9-2. Раскрывающийся список значений столбца country таблицы базы данных f^.-j Примечание Списочные элементы управления могут автоматически форматировать содержимое связанного поля, указанного в свойстве DataTextField. Выражение форма- тирования задается в свойстве DataTextFormatString. Свойство DataValueField Подобно свойству DataTextField свойство DataValueField определяет, какое поле ис- точника данных должно использоваться для заполнения набора данных списочного элемента управления, соответствующих выводимым строкам и возвращаемых свой- ством SelectedValue после выбора элемента списка пользователем. public virtual string DataValueField {get; set;} Чтобы вы лучше поняли, в чем заключается роль этого свойства, рассмотрим раз- метку, генерируемую после выполнения приведенного выше кода: <select name="CountryList” id=”CountryList"> <option selected="selected" value="[All]">[All]</option> <option value="Argentina">Argentina</option> <option value="Austria">Austria</option> </select> Текст тэгов <option> извлечен из столбца, имя которого задано в свойстве DataText- Field списочного элемента управления, а значения атрибута value этих тэгов — из столбца, заданного в свойстве DataValueField. Если таковой не задан, тогда атрибуту value просто присваивается текст элемента списка. Теперь рассмотрим код, заполня- ющий элемент управления ListBox именами клиентов: CustomeгList.DataMember = "Table"; CustomerList.DataTextField = "companyname";
336 Часть II Размещение данных на сайте CustomerList.DataValueField = "customerid"; CustomerList.DataSource = data; CustomerList. DataBindO; Вот соответствующая ему разметка: <select size="4" name="CustomerList" id="CustomerList"> <option value="BOTTM">Bottom-Dollar Markets</option> <option value="LAUGB">Laughing Bacchus Wine Celiars</option> </select> Как видите, атрибуту value каждого тэга <option> здесь присвоен идентификатор клиента — уникальное невидимое пользователю значение, взятое из поля customerid источника данных. Повторю, что значение этого атрибута для выбранного пользова- телем элемента возвращает свойство SelectedValue списочного элемента управления. Текст этого элемента можно получить с помощью выражения Selectedltem.Text. Свойство AppendDataBoundltems Булево свойство AppendDataBoundltems, введенное в ASP.NET 2.0, указывает, нужно ли добавлять загружаемые в элемент управления новые данные в конец существу- ющего набора его данных (значение true), или существующие данные должны быть просто заменены. По умолчанию свойство AppendDataBoundltems имеет значение false и элемент управления ведет себя так же, как в ASP.NET 1.x, где это свойство не поддерживается. public virtual bool AppendDataBoundltems {get; set;} Обычно данное свойство используется в тех случаях, когда необходимо соеди- нить в элементе управления данные, взятые из некоторого источника, с несколькими фиксированными значениями. Предположим, вам необходимо заполнить раскры- вающийся список названиями стран, а пользователь будет выбирать в этом списке страну, в которой находятся интересующие его клиенты. Чтобы пользователь также мог просмотреть общий перечень клиентов из всех стран, добавьте в раскрывающийся список фиксированный элемент [Allf. <asp:DropDownList runat="server" ID="CQuntryList" AppendDataBoundItems="true*'> <asp:Listitem Text="[All]" /> </asp:DropDownList> Если бы атрибут AppendDataBoundltems не был задан (то есть имел задаваемую по умолчанию установку false, как в ASP.NET 1.x), перед заполнением элемента управ- ления данными его элемент [АН] был бы удален. В ASP.NET 1.x такой элемент при- ходится добавлять программно по завершении операции связывания. Свойство DataKeyFleld В свойстве DataKeyField задается ключевое поле источника данных. Оно используется табличными элементами управления ASP.NET 1.x (DataList и DataGrid) и позволяет им идентифицировать запись. Заметьте, что идентификация однозначна только в том случае, когда источник данных имеет поле с ограничением уникальности. public virtual string DataKeyField {get; set;} Свойство DataKeyField используется в паре co свойством DataKeys. Когда первое установлено, второе содержит коллекцию значений заданного ключевого поля всех эле- ментов данных, отображаемых элементом управления в данный момент. Зачем это нужно, вы узнаете в следующей главе, когда мы будем обсуждать элемент управления DataGrid.
Медель связывания с данными Глава 9 337 В новом табличном элементе управления ASP.NET GridView свойство DataKeys было превращено в массив строк и переименовано в DataKeyNames. Само свойство DataKeys сохранилось, но определено оно теперь по-другому — как именно, вы также узнаете в следующей главе. Списочные элементы управления Списочный элемент управления одновременно выводит на странице (или, по крайней мере, держит в памяти) набор элементов данных, содержащихся в заданном источнике. К числу таких элементов управления относятся DropDownList, CheckBoxList, RadioBut- tonList, ListBox, а в ASP.NET 2.0 также BulletedList. И в ASP.NET 1.x, и в ASP.NET 2.0 все списочные элементы управления наследуют базовый класс ListControl. Элемент управления DropDownList Элемент управления DropDownList позволяет пользователю выбрать в раскрывающем- ся списке один элемент. Вы можете задать размеры элемента управления на странице, указав его высоту и ширину в пикселах, но не количество элементов, отображаемых при раскрытии списка. Наиболее важные свойства элемента управления DropDownList перечислены в табл. 9-1. Табл. 9-1. Свойства элемента управления DropDownList Свойство Описание AppendDataBoundltems Указывает, должны ли быть сохранены при заполнении элемента управления его статически определенные элементы. В ASP.NET 1х данное свойство не поддерживается AutoPostBack Указывает, должен ли элемент управления автоматически иниции- ровать возврат формы серверу, когда пользователь выбирает в нем другой элемент списка DataMember Имя таблицы элемента управления DataSet, с которой осуществля- ется связывание I DataSource Источник данных для заполнения списка DataSourcelD Идентификатор элемента управления, представляющего источник данных. В ASP.NET 1х данное свойство не поддерживается DataTextField Имя столбца источника данных, из которого будет извлекаться текст элёментов списка DataTextFormatString Строка форматирования, используемая при отображении элемен- тов списка Data ValueField Имя столбца источника данных, из которого будут извлекаться значения элементов списка Items Возвращает коллекцию элементов списочного элемента управления Selectedlndex Возвращает и позволяет задать индекс выделенного элемента списка Selectedltem Возвращает выделенный элемент списка SelectedValue Возвращает значение выделенного элемента списка В состав программного интерфейса элемента управления DropDownList входят так- же три свойства для конфигурирования его рамки, а именно: BorderColor, BorderStyle и BorderWidth. Хотя применение стилей правильно отражается на значениях этих свойств, следует помнить, что их поддерживают не все браузеры. Свойства DataTextField и DataValueField не принимают выражений, только имена столбцов. Поэтому, для того чтобы составить список из строк, полученных в результате соединения значений нескольких столбцов, вам придется определить вычисляемый столбец. Это можно сделать как в запросе к базе данных, так и в объекте ADO.NET
338 Часть II Размещение данных на сайте DataTable. Следующий SQL-запрос возвращает столбец, полученный путем конкате- нации значений столбцов lastname и firstname'. SELECT lastname + + firstname AS ’EmployeeName’ FROM Employees Тот же результат можно получить и после заполнения объекта DataTable, добавив в него вычисляемый столбец EmployeeName-. dataTable.Columns.Add("EmployeeName", typeof(string), "lastname + ’, ' + firstname"); Его не нужно заполнять явно — значения всех ячеек этого столбца объект DataTable вычислит автоматически, а впоследствии будет отслеживать изменения значений ис- пользуемых в выражении столбцов и обновлять данные вычисляемого столбца. Элемент управления CheckBoxList CheckBoxList — это монолитный элемент управления, состоящий из набора флажков, выводимых с помощью отдельных элементов управления Checkbox. Свойства дочер- них флажков считываются из источника данных. Для того чтобы разместить элемент управления CheckBoxList на странице, достаточно написать такой код: <asp:CheckBoxList runat="server" id="employeesList"> Наиболее важные свойства элемента управления CheckBoxList перечислены в табл. 9-2. Табл. 9-2. Свойства элемента управления CheckBoxList Свойство Описание AppendDataBoundltems Указывает, должны ли быть сохранены при заполнении элемента управления его статически определенные элементы. В ASP.NET 1 х данное свойство не поддерживается AutoPostBack Указывает, должен ли элемент управления автоматически иници- ировать возврат формы серверу, когда пользователь устанавливает либо снимает в нем тот или иной флажок CellPadding Расстояние в пикселах между рамкой и содержимым ячейки CellSpacing Расстояние в пикселах между ячейками DataMember Имя таблицы элемента управления DataSet, с которой осуществля- ется связывание DataSource Источник данных для заполнения элемента управления DataSourcelD Идентификатор элемента управления, представляющего источник данных. В ASP.NET 1 х данное свойство не поддерживается DataTextField Имя столбца источника данных, из которого будет извлекаться текст, выводимый рядом с флажками DataTextFormatString Строка форматирования, используемая при отображении текста, выводимого рядом с флажками Data ValueField Имя столбца источника данных, из которого будут извлекаться значения элементов Items Возвращает коллекцию элементов RepeatColumns Возвращает и позволяет задать количество столбцов элемента управления RepeatDirection Возвращает и позволяет задать значение, указывающее, будут ли элементы выводиться по горизонтали или по вертикали RepeatLayout Возвращает и позволяет задать значение, определяющее способ вывода элемента управления, — Table (в виде таблицы) или Flow (просто группа элементов)
Модель связывания с данными Глава 9 339 Табл. 9-2. (окончание) Свойство Описание Selectedlndex Возвращает и позволяет задать индекс первого установленного флажка Selectedltem SelectedValue TextAlign Возвращает первый элемент, флажок которого установлен Возвращает значение первого элемента, флажок которого установлен Возвращает и позволяет задать значение, определяющее способ выравнивания элементов Элемент управления CheckBoxList не имеет свойств, с помощью которых можно было бы определить, какие из флажков установил пользователь. Однако приложению это знать необходимо, и существует способ это выяснить. У любого списочного элемента управления имеется свойство Items, содержащее коллекцию дочерних элементов. Это свойство реализовано в виде объекта класса List- ItemCollection, а его элементы доступны через свойство Listitem. Ниже показано, как пройти в цикле по элементу управления CheckBoxList и проверить значение свойства Selected каждого его элемента: foreach(Listitem item in chkList.Items) { if (item.Selected) { // Этот элемент выбран } } На рис. 9-3 представлена демонстрационная страница, которая позволяет поль- зователю отметить названия стран и потом составляет запрос на получение из базы данных списка клиентов из этих стран. Рис. 9-3. Элемент управления с горизонтальным размещением элементов
340 Часть II Размещение данных на сайте Хочу обратить ваше внимание на то обстоятельство, что свойства SelectedXXX у элемента управления CheckBoxList работают немного иначе, чем у других списочных элементов управления. Так, свойство Selectedlndex возвращает наименьший йз индек- сов выбранных пользователем элементов. Присвоив ему определенное значение, вы снимете выделение со всех элементов с меньшими индексами. Свойство Selectedltem возвращает текст первого выбранного элемента, а свойство SelectedValue — значение этого элемента. Элемент управления RadloButtonList Элемент управления RadloButtonList действует как контейнер группы переключате- лей. Разметка его дочерних элементов выводится с помощью элементов управления RadioButton. По умолчанию элемент управления RadioButtonList позволяет установить не более одного переключателя. Свойство Selectedltem возвращает выбранный элемент в виде объекта Listitem. Однако учтите, что это свойство может вернуть и null, если ни один элемент-переключатель не выбран: if (radioButtons.SelectedValue 1= null) { // Обработка выделенного элемента } Элемент управления RadioButtonList имеет такой же набор свойств, как элемент управления CheckBoxList, и поддерживает те же параметры раскладки. В частности, вы можете управлять процессом рендеринга его элементов с помощью свойств Repeat- Layout и RepeatDirection. Раскладка элемента управления RadioButtonList определяется значением свойства RepeatLayout. По умолчанию элементы выводятся в виде таблицы, чем обеспечивается их выравнивание по вертикали. В качестве альтернативы они мо- гут выводиться просто как последовательность HTML-элементов, разделенных для минимальной структуризации пробелами и разрывами строк. Свойство RepeatDirec- tion определяет, как будут выводиться элементы: Horizontal — по горизонтали, Verti- cal (значение по умолчанию) — по вертикали. В свойстве RepeatColumns вы можете указать, сколько столбцов переключателей будет содержать элемент управления. По умолчанию это свойство имеет значение 0, то есть все элементы выводятся в один ряд, по горизонтали или по вертикали, в зависимости от значения свойства RepeatDirection. Элемент управления ListBox Элемент управления ListBox представляет самый обыкновенный список — прокру- чиваемую последовательность элементов в небольшом окошке. Он допускает мно- жественный выбор, а его содержимое доступно через коллекцию Items. Объявляется этот элемент управления так: <asp:listbox runat="server" id="theListBox" rows="5" selectionmode="Multiple" /> Высота списка определяется свойством Rows и измеряется в строках. В отношении связывания с данными ListBox ведет себя так же, как элементы управления, описанные ранее в этой главе. Но есть у списка два свойства, которые немного отличают его от других элементов управления, а именно: Rows, содержащее число видимых строк элемента, и Selec- tionMode, определяющее, допустим ли множественный выбор. Кроме того, в составе программного интерфейса элемента управления имеется набор свойств, SelectedXXX, рассмотренных нами ранее Они действуют так, как у элемента управления CheckBox- List' возвращают выделенный элемент с наименьшим индексом.
Модель связывания с данными Глава 9 341 7J Примечание Все рассмотренные нами списочные элементы управления поддерживают событие SelectedlndexChanged, генерируемое, когда пользователь выбирает другой эле- мент списка и осуществляется возврат формы. Благодаря этому событию вы можете на- писать серверный код, выполняющийся при получении и потере фокуса самим элементом управления. Элемент управления BulletedList Вывод элемента управления BulletedList состоит из HTML-тэгов <ul> и <ol>, то есть представляет собой маркированный или нумерованный список Однако этот элемент управления обладает некоторыми дополнительными функциями, в частности позво- ляет задать стиль маркера, поддерживает связывание с данными и пользовательские изображения. В ASP.NET 1.x элемент управления BulletedList не поддерживается. Вот пример определения маркированного списка, состоящего из трех элементов с квадратными маркерами: osp.bulletedlist runat="server" bulletstyle="Square"> <asp:listitem>One</asp:listitem> <asp:listitem>Two</asp:listitem> <asp:listitem>Th ree</asp:listitem> </asp:bulletedlist> Атрибут bulletstyle элемента управления BulletedList служит для указания стиля маркера или номера, выводимого перед элементами списка. Допускается использование квадратных и круглых маркеров, а также чисел и букв верхнего и нижнего регистров. Дочерние текстовые элементы могут выводиться как чистый текст, гиперссылки и кнопки. Основные свойства элемента управления BulletedList перечислены в табл. 9-3. Табл. 9-3. Свойства элемента управления BulletedList Свойство Описание AppendDataBoundltems Указывает, должны ли быть сохранены при заполнении элемента управления его статически определенные элементы. В ASP.NET 1х данное свойство не поддерживается BulletlmageUrl Возвращает и позволяет задать путь к изображению, которое будет использова - ъся в качестве маркера BulletStyle Определяет стиль маркера или номера DataMember Имя таблицы элемента управления DataSet, с которой осущест- вляется связывание DataSource Источник данных для заполнения элемента управления DataSourcelD Идентификатор элемента управления, представляющего источник данных. В ASP.NET 1х данное свойство не поддерживается DataTextField Имя столбца источника данных, из которого будет извлекаться текст элементов , DataTextFormatString Строка форматирования, используемая при отображении текста элементов Data Value Field Имя столбца источника данных из которого будут извлекаться значения элементов DisplayMode Определяет, как будут отображаться элементы: в виде чистого текста, ссылочных кнопок или гиперссылок FirstBulletNumber Возвращает и позволяет задать значение, с которого будет начи- наться нумерация Items Возвращает коллекцию элементов списка Target Указывает на целевой фрейм, когда элемент управления работает в режиме гиперссылок
342 Часть II Размещение данных на сайте В качестве маркеров элемента управления BulletedList могут использоваться стан- дартные изображения (квадратики и кружки), пользовательские изображения, а также числа, в том числе и написанные римскими цифрами. Начальное число можно задать программно с помощью свойства FirstBulletNumber. Свойство DisplayMode определяет, как выводится текст элементов списка: в виде обычного текста (по умолчанию), ссы- лочных кнопок или гиперссылок. В случае использования ссылочных кнопок на сервере генерируется событие Click, позволяющее обработать щелчок кнопки после возврата формы. Когда используются гиперссылки, браузер просто отображает целевую стра- ницу во фрейме, заданном в свойстве Target. Целевые URL для перехода по ссылкам должны содержаться в столбце источника данных, указанном в свойстве DataValueField. На рис. 9-4 представлена демонстрационная страница, содержащая элементы управления RadioButtonList и BulletedList. Первый из них связан с набором значений системного перечислимого типа BulletStyle. Связывание осуществляется так: BulletOptions.DataSource = Enum.GetValues(typeof(BulletStyle)); BulletOptions.Selectedlndex = 0; BulletOptions.DataBind(); Для извлечения и установки выбранного значения используется следующий код: BulletStyle style = (BulletStyle) Enum.Parse(typeof(BulletStyle), BulletOptions.SelectedValue); BulletedListl BulletStyle = style; 3 Bullets If Rudins Microsoft Internet Explorer _;;D X Be B* Bbw Favorites loots ЦЫр О Back ’ O ' 0 a Search -^ Favorites © ©NotSet ’©Numbered ©LowerAlpha ©UpperAlpha ©Lower-Roman ©UpperRoman ©Disc ©Circle ©Square © Customimage - * Argentina - * Austria Belgium * Brazil * > Canada - > Denmark Finland - > France + Germany - > Ireland - > Italy • + Mexico • > Norway - > Poland • + Portugal + Spain > Sweden - * Switzerland - ♦ UK - * USA - > Venezuela Рис. 9-4. Страница для демонстрации стилей маркеров элемента управления BulletedList Итеративные элементы управления Итеративные элементы управления — это особый тип элементов управления, свя- занных с данными, в состав которых входит механизм создания пользовательского интерфейса произвольной формы-на базе шаблонов. Такие элементы управления подключаются к источнику данных, проходят в цикле по его строкам и применяют
Модель связывания с данными Глава 9 343 к каждой из них определенные пользователем шаблоны. Это базовое поведение ха- рактерно для всех трех итераторов ASP.NET — Repeater, DataList и DataGrid. Но их возможности структуризации и оформления вывода, а также функциональные воз- можности существенно различаются. От списочных элементов управления итера горы отличаются прежде всего гибкос- тью в отношении рендеринга. Они позволяют применить к каждой строке источника данных пользовательский шаблон, тогда как у списочных элементов этот шаблон фиксирован и кроме выводимого в них текста вы ничего изменять не можете. В то же время пользоваться списочными элементами управления намного проще, поскольку для определения шаблона итератора приходится писать довольно большое количество декларативного кода или же определять класс, реализующий интерфейс ITemplate, тогда как для списочного элемента управления достаточно определить лишь несколько свойств, управляющих связыванием с данными. Об элементе управления DataGrid я подробно расскажу в главе 10, а элементы управления Repeater и DataList опишу лишь в самых общих чертах, поскольку вы можете прочитать о них в моей книге ^Programming Microsoft ASPNET 2.0 Applica- tions: Advanced Topics» (Microsoft Press. 2005). Элемент управления Repeater Элемент управления Repeater выводит данные, пользуясь сформированным вами шаблоном, который он повторяет для каждой записи. Это относительно простой элемент управления, не имеющий ни встроенной разметки, ни стилевых свойств. Вся выводимая разметка должна быть явно определена вами с помощью тэгов HTML или классов ASP.NET. Класс Repeater реализует маркерный интерфейс INamingContamer и действует как контейнер именования (см. главу 3). В табл. 9 4 перечислены основные свойства этого класса, не являющиеся унаследованными. Табл. 9-4. Свойства элемента управления Repeater Свойство Описание Altematingltem Template Шаблон, используемый при выводе каждого второго элемента DataMember Имя таблицы элемента управления DataSet, с которой осущест- вляется связывание DataSource Источник данных для заполнения элемента управления DataSourcelD Идентификатор элемента управления, представляющего источ- ник данных. В ASP.NET 1.x данное свойство не поддерживается FooterTemplate Шаблон, используемый при выводе нижнего колонтитула HeaderTemplate Шаблон, используемый при выводе верхнего колонтитула (за- головка) Items Возвращает объект RepeaterltemCollection — коллекцию объектов Repeateritem. Каждый объект из этой коллекции представляет одну строку данных элемента управления Repeater ItemTemplate Шаблон, используемый при выводе элементов SeparatorTemplate Шаблон, используемый при выводе разделителей элементов Большинство из свойств элемента управления Repeater служат для определения шаблонов, используемых при формировании его пользовательского интерфейса. Этот элемент управления заполняет коллекцию Items, осуществляя проход по строкам (элементам данных) источника данных, и для каждой из них создает объект Repeater- Item, помещаемый в коллекцию RepeaterltemCollection. Класс RepeaterltemCollection — самая обыкновенная коллекция, не имеющая никаких дополнительных функций.
344 Часть II Размещение данных на сайте Что касается класса Repeateritem, то он представляет один отображаемый элемент и содержит свойство, указывающее на соответствующий элемент данных (напри- мер, строку таблицы), а также свойства, определяющие индекс и тип этого элемента (обычный, четный, заголовок, нижний колонтитул и т. д.). Вот пример определения элемента управления Repeater. <asp:Repeater ID="Repeater1" runat="server"> <HeaderTemplate> < h2>We have customers in the following cities</h2> < hr /> </HeaderTemplate> <SeparatorTemplate> < hr noshade style="border:dashed 1px blue" /> </SeparatorTemplate> <ItemTemplate> < %# Eval("City")%> &nbsp;&nbsp;<b><%# Eval("Country”)%></b> </ItemTemplate> <FooterTemplate> < hr /> < %# CalcTotalO %> cities </FooterTemplate> </asp: Repeated Если связать элемент управления Repeater с результатами выполнения приведен- ного ниже запроса, он сгенерирует вывод, представленный на рис. 9-5. Заметьте, что ни один списочный элемент управления на подобное не способен: SELECT DISTINCT country, city FROM customers WHERE country=@TheCountry В параметре ©TheCountry запроса задается название страны, выбранное пользо- вателем в раскрывающемся списке: data = new DataTableO; SqlDataAdapter adapter = new SqlDataAdapter(cmdText, connString); adapter.SelectCommand.Parameters AddWithValue("@TneCountry", Countries SelectedValue); adapter.Fill(data); Repeaterl.DataSource = data; Repeaterl DataBindO; Рис. 9-5. Элемент управления Repeater
Модель связывания с данными Глава 9 345 Из всех шаблонов только ItemTemplate и AlternatingltemTemplate связаны с данными и повторяются для каждого их элемента. Очевидно, что при формировании шаблона должна существовать возможность связывать данные с его элементами. Для этой цели используют метод Eval, принимающий имя свойства (скажем, имя столбца таблицы) и возвращающий его содержимое. Немного погодя я подробнее расскажу об этом методе и о блоках <% # ... % >, которые вы видели в приведенном выше примере. Элемент управления DataList DataList — это связанный с данными элемент управления, область применения ко- торого начинается там, где недостаточно возможностей элемента управления Re- peater, и распространяется до области применения табличных элементов управления. В отдельных, очень простых, случаях можно даже взять некий код, в котором исполь- зуется элемент управления Repeater, и заменить последний элементом управления DataList — разница будет незаметной. Элемент управления DataList превосходит элемент управления Repeater в нескольких отношениях, но главным образом в том, что касается его пользовательского интерфейса. Например, данный элемент поддержи- вает направленный рендеринг, то есть позволяет размещать элементы по горизонтали либо по вертикали (когда нужно сформировать определенное число столбцов). Кроме того, элемент управления DataList позволяет извлечь ключевое значение, связанное с текущей строкой данных, и имеет встроенную функцию поддержки выделения и редактирования данных. Еще элемент управления DataList поддерживает больше шаблонов и может генери- ровать больше событий, чем элемент управления Repeater. А вот механизм связывания с данными и общая схема поведения обоих элементов практически идентичны. Работа элемента управления DataList основана на определенных предположениях относительно ожидаемых результатов. Для вас как для программиста в этом есть по- ложительные и отрицательные моменты. С одной стороны, в некоторых случаях вам придется писать меньше кода, но с другой стороны, чтобы управлять этим элементом управления, необходимо знать все нюансы его функционирования. Например, DataList рассчитывает на то, что ни один HTML-тэг не будет распределен между несколькими шаблонами. Если окажется иначе, элемент управления не сгенерирует ошибку, но в результате может получиться плохо организованный или вообще неожиданный HTML-вывод. По умолчанию DataList выводит данные в виде HTML-таблицы, а это значит, что вам не придется возиться с отдельными элементами <table> или <td>. Элемент управления DataList является контейнером именования, а кроме того, реализует интерфейс IRepeatlnfoUser, определяющий свойства и методы, которые должны быть реализованы любым списочным элементом управления. Этот интерфейс поддерживается также элементами управления CheckBoxList и RadioButtonList и яв- ляется основой упоминавшихся ранее свойств RepeatXXX. Вот как можно переписать приведенный выше пример, когда требуется более детальное управление выводом результирующей разметки: <asp:DataList ID="DataList1" runat="server” RepeatColumns="5" GridLines="Both"> <FooterStyle Font-Bold="true" ForeColor="blue" /> <HeaderTemplate> <h2>We have customers in the following cities</h2> </HeaderTemplate> <ItemTemplate> <%# Eval("City") %> &nbsp;&nbsp;<b><%# Eval ("Count ry")%x/b> </ItemTemplate>
346 Часть II Размещение данных на сайте <FooterTemplate> <%# CalcTotalO %> cities </FooterTemplate> </asp:DataList> Обратите внимание на тэг FooterStyle — элемент управления позволяет явно на- значать шаблонам стили. В данном случае текст на панели нижнего колонтитула будет выведен синим полужирным шрифтом. Результат обработки этого элемента управления представлен на рис. 9-6 Заметьте, что для разделения его вывода на столбцы достаточно задать соответствующее свойство. Рис. 9-6. Пример элемента управления DataList Элемент управления DataGrid DataGrid — исключительно гибкий элемент управления, благодаря чему он завоевал популярность у программистов и использовался во многих приложениях ASP.NET 1.x. Он полностью поддерживается и в ASP.NET 2.0, но теперь несколько отодвинулся на задний план с появлением более мощного табличного элемента управления GridView. Оба эти элемента мы подробно рассмотрим в следующей главе. Элемент управления DataGnd выводит многоколоночную таблицу с гибко настра- иваемым интерфейсом, подобным интерфейсу электронных таблиц Microsoft Office Excel. Несмотря на продвинутый программный интерфейс и богатый набор атрибутов, элемент управления DataGrid просто генерирует HTML-таблицу с гиперссылками, посредством которых выполняются такие операции, как сортировка, навигация по страницам, выделение и редактирование данных. Столбцы элемента управления DataGrid бывают разных типов: текстовые, основан- ные на шаблонах и командные. Его связывание с источником данных осуществляется путем установки свойства DataSource и последующего вызова метода DataBind. grid.DataSource = data; grid.DataBind(); Простейший способ вывода таблицы данных с помощью элемента управления DataGrid таков: <asp:DataGrid runat="server" id="grid" /> При обработке приведенного кода будет сгенерирована HTML-таблица, содержа- щая по одному столбцу на каждый столбец источника данных. Но это лишь самый простой случай. При желании можно явно указать, какие именно столбцы данных вы хотите отобразить в таблице. Для примера на рис. 9-7 показана таблица с тремя столбцами.
Модель связывания с данными Глава 9 347 Рис. 9-7. Пример элемента управления DataGrid Выражения связывания с данными Вы познакомились с наиболее распространенной формой связывания элементов управления с данными, характерной для списочных и итеративных элементов. Од- нако существует простейшая форма связывания с данными, которая поддерживается всеми элементами управления ASP.NET, в том числе полями и надписями, — это свя- зывание с помощью метода DataBind. При его осуществлении устанавливается связь между одним элементом данных и текущим значением заданного свойства серверного элемента управления, для чего используется особое выражение, вычисляемое при вызове метода DataBind из кода страницы. Простейшие случаи связывания с данными Выражения связывания с данными — это исполняемый код, заключенный внутрь спе- циальной конструкции <% ... % > и начинающийся с символа-префикса #. Обычно такие выражения используются для установки значения атрибута в открывающемся тэге серверного элемента управления. Во время выполнения они представлены эк- земплярами класса DataBoundLiteralControl. Примечание Выражение связывания с данными может содержать любой код, который может быть выполнен во время работы приложения. Его задача — сгенерировать данные, которые элемент управления сможет вывести на странице. Обычно они извлекаются из источника данных, но это не обязательно. Главное, чтобы код вернул подходящие для связывания данные, а где он их возьмет, не имеет значения. Например, в следующем фрагменте кода исходной разметки формируется надпись, содержащая текущее время: <asp:label runat="server" Text='<%# DateTime.Now %>' /> Внутри конструкции <%# ... %> можно вызывать пользовательские методы страницы, ее статические методы, а также методы и свойства компонентов страницы.
348 Часть II Размещение данных на сайте Следующий пример показывает, как создать надпись, связанную с именем выделен- ного элемента раскрывающегося списка' <asp:label runat="server" Text='<%# dropdown.Selectedltem.Text %>' /> Учтите, что если вы собираетесь использовать в выражении кавычки, то все вы- ражение следует заключить в двойные кавычки Выражение связывания с данными может содержать некоторые операторы; чаще всего в таких выражениях используют операторы конкатенации для объединения подвыражений. Если же требуется более сложная обработка и нужно использовать внешние аргументы, напишите пользова- тельский метод, объявив его как открытый или защищенный. ©Внимание! Любое выражение связывания с данными вычисляется лишь после вызова метода DataBind. Его можно вызвать либо для объекта страницы, либо для ее элемента управления. В первом случае этот метод будет рекурсивно вызван для всех элементов управления страницы. Если метод DataBind не вызывается, выражение не вычисляется. Процесс связывания с данными Выражения связывания с данными особенно часто используются для динамического, но определенного декларативно обновления значений свойств элементов управления, зависящих от других элементов управления той же страницы. Предположим, напри- мер, что у вас имеется раскрывающийся список названий цветов и надпись, на которой должно отражаться название выбранного цвета: <asp:DropDownList ID="SelColors" runat="server" AutoPostBack="True"> <asp:ListItem>Orange</asp:Listltem> <asp:ListItem>G reen</asp:Listltem> <asp:ListItem>Red</asp:Listltem> <asp:ListItem>Blue</asp:Listltem> </asp:DropDownList> <asp:Label runat="server" ID="lblColor" Text=’<%# "<b>You selected: </b>" + SelColors.SelectedValue %>’ /> Заметьте, что в выражении <% #...%> можно использовать любую комбинацию методов, констант и свойств, лишь бы тип результирующего значения отвечал типу свойства, которому вы его присваиваете. Также обратите внимание, что для вычис- ления выражения необходимы возврат формы и вызов метода DataBind. В данном примере атрибут AutoPostBack специально установлен в true, чтобы при изменении выделения в раскрывающемся списке выполнялся возврат формы. Вызов метода DataBind обеспечивается так: protected void Page_Load(object sender, EventArgs e) { DataBind(); } Выражения можно связывать с любыми свойствами элементов управления, не- зависимо от их типа. Посмотрим, например, как можно связать свойство ForeColor элемента управления с цветом, выбранным в раскрывающемся списке ForeColor=’<%# Color.FromName(SelColors.SelectedValue) %>’ Обратите внимание, что вы не можете просто присвоить свойству ForeColor вы- ражение, возвращающее строку названия цвета: ForeColor='<%# SelColors.SelectedValue %>’ поскольку автоматическое преобразование строкового значения к цветовому типу (каковой имеет свойство ForeColor) не производится. Так, из двух операторов: ForeColor=<%# "orange" %>' ForeColor=”orange"
Модель связывания с данными Глава 9 349 сработает только второй. Тип значения, возвращаемого выражением связывания с дан- ными, должен в точности соответствовать типу свойства. А вот обычную строку этому свойству присвоить можно — анализатор страниц распознает выражение и выполнит необходимое преобразование, если таковое возможно. На рис. 9-8 вы видите демон- страционную страницу со списком для выбора цвета в действии. Рис. 9-8. Раскрывающийся список и надпись, при создании которых используются выражения связывания с данными Реализация выражений связывания с данными Что происходит, когда в исходном файле Web-страницы обнаруживается выражение связывания с данными? Как оно обрабатывается исполняющей средой ASP.NET? Рассмотрим такой пример: <asp:label runat="server" id="today" text=<%# DateTime.Now %>' /> Обрабатывая исходный код, содержащийся в файле .aspx, анализатор страниц генерирует класс, в котором каждому серверному элементу управления соответству- ет метод-фабрика. Этот метод сопоставляет имени тэга класс серверного элемента управления и присваивает значения атрибутов тэга свойствам объекта данного класса. Кроме того, обнаружив выражение связывания с данными, анализатор связывает с объектом элемента управления (в данном случае элемента Label) обработчик события DataBinding. Вот псевдокод этого процесса: private Control __BuildControlTodayO { Label __Ctrl = new Label(); this.today = __ctrl; __Ctrl.ID = "today"; „Ctrl.DataBinding += new EventHandler(this.__DataBindToday); return __Ctrl; } Указанный обработчик присваивает свойству значение выражения связывания с данными: public void __DataBindToday(object sender, EventArgs e) { Label target; target = (Label) sender; target Text = Convert.ToString(DateTime.Now); }
350 Часть II Размещение данных на сайте Если тип значения не соответствует типу свойства, происходит ошибка компи- ляции. Однако если свойство имеет тип string, анализатор пытается выполнить стан- дартное преобразование с помощью метода Convert.ToString. Все типы .NET Framework могут быть преобразованы к строковому типу, поскольку они наследуют от корневого типа object метод ToString. Класс DataBinder Ранее я уже говорил о выражениях <% # ... % > — в одном контексте с шаблонами и методом Eval. Этот метод представляет собой нечто вроде специализированного оператора, используемого в выражениях связывания с данными для доступа к от- крытым свойствам элемента данных, связанного с элементом управления. Метод Eval был введен только в ASP.NET 2.0, и при попытке использовать его в приложениях ASP.NET 1.x вы получите сообщение об ошибке Но во всех версиях ASP.NET под- держивается функционально эквивалентный метод с тем же именем, принадлежащий другому классу, — DataBinder. ©Внимание! Метод Eva! (класса DataBinder или Раде) используется для доступа к открытым свойствам элемента данных, связанного с элементом управления. Позвольте объяснить, почему речь идет именно об открытых свойствах и почему я называю их свойствами, а не, скажем, столбцами или полями. Любой класс, реализующий интерфейс /Enumerable, может быть связан с элементом управления. К их числу, безусловно, относится класс DataTable (и тогда под элементом данных понимается строка таблицы), но не только он — пользова- тельские коллекции также реализуют этот интерфейс, и в них элементом данных является экземпляр определенного класса. Метод Eval запрашивает у объекта, представляющего элемент данных, набор его свойств. Если этот объект представляет строку таблицы, то он возвращает дескрипторы столбцов; другой объект вернет дескрипторы иных свойств. Класс DataBinder позволяет генерировать выражения связывания с данными и осу- ществлять их разбор. В этом отношении особо важен его перегруженный статический метод Eval. Данный метод, используя технологию рефлексии, выполняет синтак- сический анализ выражения и вычисляет его значение. Методом Eval пользуются RAD-средства, такие как дизайнеры Microsoft Visual Studio .NET, а также элементы управления Web, декларативно вызывающие его для назначения свойствам динами- чески меняющихся значений. Метод Eval Синтаксис вызова метода DataBindei.Eval обычно бывает таким: <%# DataBinder.Eval(контейнер.Dataltem, выражение) %> Здесь опущен третий, необязательный, параметр — строка, содержащая установки форматирования возвращаемого значения. Выражение контейнер.Dataltem возвращает объект, для которого вычисляется выражение. Последнее обычно представляет собой строку, содержащую имя того поля объекта элемента данных, к которому производится обращение. Это выражение может содержать индекс свойства или его имя. Свойство Dataltem представляет объект элемента данных, связанный с текущим контейнером. Обычно контейнером является текущий объект элемента управления, представляю щий один отображаемый элемент данных, например объект DataGridltem. Приведенный выше код обычно используется именно в таком виде — меняется лишь имя контейнера, выражение и строка форматирования. Компактная форма вызова метода Eval Классический синтаксис вызова метода DataBinder.Eval в ASP.NET 2.0 может быть несколько упрощен. Вы уже встречались с этой его новой версией в примере с элемен- том управления Repeater. Иными словами, эквивалентом выражения из ASP.NET 1.x
Модель связывания с данными Глава 9 351 <%# DataBinder.Eval(контейнер Dataltem, выражение) %> в ASP.NET 2.0 является выражение <%# EvaY(выражение) %> Разумеется, исходный синтаксис по-прежнему поддерживается. Любой код, который размещен внутри конструкции <% # ... % >, обрабатывается в ASP.NET особым образом. Давайте вкратце рассмотрим это процесс. При компиля- ции страницы в код ее класса вставляется вызов метода Eval\ object о = EvalC'lastname"); string result = Convert.ToString(о); Его результат преобразуется к строковому типу и присваивается литеральному эле- менту управления, связанному с данными, — экземпляру класса DataBoundLiteralControl. Затем литеральный элемент вставляется в дерево элементов управления страницы. В ASP.NET 2.0 в состав класса TemplateControl, являющегося родительским для Page, добавлен новый защищенный (но не виртуальный) метод Eval. Следующий псевдокод показывает, как он работает: protected object Eval(string expression) { if (Page == null) throw new InvalidOperationException(...); return DataBinder. Eval(Page, GetDataltemO, expression); } Как видите, этот метод Eval — просто оболочка для вызова одноименного статиче- ского метода класса DataBinder. Методу DataBinder.Eval передается элемент данных текущего контейнера. Очевидно, что вне операции связывания с данными, то есть в стеке вызовов, следующих за вызовом метода DataBinder, данные у контейнера отсутствуют. Этот факт составляет главное отличие методов Eval и DataBinder.Eval. • Внимание! Метод Eval класса TemplateControl служит для связывания с данными и мо- жет использоваться только в контексте элемента управления, связанного с данными, во время выполнения операции связывания. В то же время метод DataBinder.Eval может использоваться в любом месте программного кода. Обычно именно он применяется при реализации специализированных элементов управления, связанных с данными. Эта тех- нология демонстрируется в моей книге «Programming Microsoft ASP.NET 2.0 Applications: Advanced Topics» (Microsoft Press, 2005). Получение элемента данных, используемого по умолчанию В псевдокоде, демонстрирующем поведение метода Eval, вызывается метод GetDa - taltem класса Page. Что это за вызов? Как уже упоминалось, упрощенный синтаксис вызова метода Eval предполагает обращение к объекту контейнер.Dataltem, являюще- муся используемым по умолчанию. Ну а метод GetDataltem просто возвращает этот объект. Точнее, метод GetDataltem является конечной точкой механизма, который, используя собственный стек, отслеживает текущий контекст связывания страницы. В этом стеке содержатся элементы управления страницы, проталкиваемые туда при вызове их метода DataBind и выталкиваемые оттуда, когда метод возвращает управ- ление. Если стек пуст, а вы пытаетесь программным способом вызвать метод Eval, GetDataltem выбрасывает исключение с сообщением о попытке выполнить недо- пустимую операцию. Таким образом, сокращенный синтаксис вызова метода Eval можно использовать только в шаблонах; если же необходим программный доступ к свойствам элемента данных, следует пользоваться методом DataBinder.Eval, явно указывая объект элемента данных.
352 Часть II Размещение данных на сайте Z* Совет Как уже упоминалось, явно вызывать метод DataBinder.Eval необходимо только в ко- * де специализированных элементов управления, связанных с данными. (О таких элементах управления рассказывается в моей книге «Programming Microsoft ASP.NET 2.0 Applications: Advanced Topics» (Microsoft Press, 2005)). Но в подобных случаях можно сэкономить внутрен- ние вызовы и циклы ЦПУ, вызвав вместо этого метод DataBinder.GetPropeityValue — именно к нему обращается в конечном счете метод DataBinder.Eval. Другие средства связывания с данными В ASP.NET 2.0 выражения связывания с данными используются не только для полу- чения доступных лишь для чтения элементов перечислимых или табличных данных. Помимо класса DataBinder в ASP.NET 2.0 существует класс, позволяющий связать элемент управления с результатом выполнения выражения XPath объектом, реали- зующим интерфейс IXPathNavigable. Этот класс называется XPathBinder, он играет в ASP.NET такую же роль, как DataBinder, с тем отличием, что работает с XML- данными. Класс XPathBinder предоставляет для связывания с данными новый метод с именем XPath. Кроме того, ASP.NET 2.0 поддерживает двунаправленное декларативное связыва- ние с данными, то есть возможность не только считывать, но и записывать значения свойств элементов данных посредством нового метода Bind. И еще ASP.NET 2.0 поддерживает выражения, определяемые пользователем и не ограниченные рамками операций связывания с данными. Может показаться странным, что я говорю о таких выражениях в разделе, посвященном выражениям связывания с данными, но я упоминаю о них для того, чтобы избежать путаницы, поскольку синтаксис пользовательских выражений почти идентичен синтаксису выражений связывания с данными. Класс XPathBinder В ASP.NET 2.0 можно связывать элементы управления с данными, хранящимися в формате XML. В ASP.NET 1.x для этого необходимо было вначале церенести такие данные в реляционную структуру, скажем, в объект DataSet. Но теперь элементы управления, в которых используются шаблоны, например DataList или Repeater, мож- но связывать прямо с источником XML-данных (в частности, с новым элементом управления XmlDataSource, о котором я расскажу в следующем разделе), при этом отдельные XML-фрагменты могут быть встроены в шаблон с использованием объ- екта XPathBinder. Метод XPathBindenEval принимает объект XmlNode и выражение XPath. Он вы- числяет это выражение и возвращает результат. Выводимая строка может быть от- форматирована, если методу передан третий аргумент — строка форматирования. Метод XPathBindenEval приводит контейнерный объект к типу IXPathNavigable, что необходимо для применения к нему выражения XPath. Если объект не реализует данный интерфейс, выбрасывается исключение. Интерфейс IXPathNavigable нужен потому, что весь API XPath ориентирован на работу с навигационными объектами. Задача данного интерфейса — создать навигационный объект XPath, необходимый для выполнения запроса. Подобно классу DataBinder класс XPathBinder поддерживает упрощенный синтак- сис вызова метода Eval, предназначенный для работы с используемым по умолчанию контейнером. Следующий пример демонстрирует применение этого упрощенного синтаксиса: <%# XPath("Orders/Order/Customer/LastName") %>
Модель связывания с данными Глава 9 353 Результатом вычисления данного выражения является объект, возвращенный методом XPathBinder.Eval, преобразованный к строковому типу. Указанный метод получает от источника данных объект навигатор и вычисляет выражение. При этом используется управляемый API XPath. [ Примечание В настоящей книге XML классы .NET Framework не рассматриваются. Хороший материал по этой теме вы найдете в моей книге «Applied XML with the .NET Framework» (Microsoft Press, 2003). Хотя ее предметом является .NET Framework версии 1 .x, в том, что касается XPath, все сказанное в ней по-прежнему верно, и этого материала достаточно для получения практических сведений по данному предмету. Метод XPathSelect Класс XPathBinder имеет также метод XPathSelect, выполняющий запрос XPath и из- влекающий перечислимую коллекцию XML-узлов. Эту коллекцию методом позднего связывания можно присвоить элементу управления, связанному с данными (скажем, элементу управления Repeater). Для такого случая также предусмотрен упрощенный синтаксис: <asp:Repeater runat="server" DataSource='<%# XPathSelect(”orders/order/summary") %>’> </asp: Repeated Ключевое слово XPathSelect используется в выражениях связывания с данными для указания на результат выполнения XPath-запроса для контейнерного объекта. Если контейнерный объект не реализует интерфейс IXPathNacigable, выбрасывается исключение. Подобно методам Eval и XPath метод XPathSelect применяется к объекту элемента данных, используемому в текущем контексте по умолчанию. Метод Bind Как будет показано в главе И, ASP.NET 2.0 поддерживает двунаправленное связыва- ние с данными, то есть возможность не только загрузить данные в элемент управле- ния, но и записать изменения обратно в их источник. Метод Eval предназначен для однонаправленного связывания, он автоматизирует процесс чтения данных, но не их записи. Когда требуется двунаправленное связывание, вместо метода Eval следует применять метод Bind, имеющий аналогичный синтаксис: <asp:TextBox Runat="server” ID="TheNotes" Text='<%# BindC'notes") %>’ /> Главное отличие метода Bind от метода Eval заключается в его двунаправленно- сти — он обеспечивает не только чтение, но и запись данных. Например, при установ- ке свойства Text метод Bind ведет себя в точности как Eval, но, к тому же, прочитав значение свойства Text, сохраняет его в специальной коллекции. Новые элементы управления ASP.NET 2.0, связанные с данными и поддерживающие двунаправленное связывание (например, элемент Form View и другие элементы управления, использую- щие шаблоны), автоматически извлекают значения из этой коллекции и используют список параметров командного объекта для записи измененных данных обратно в их источник. Аргумент метода Bind должен соответствовать имени параметра команды. Так, текстовое поле из приведенного выше примера поставляет значение для пара- метра ©notes. Динамические выражения, определяемые пользователем Выражения связывания с данными не являются динамическими, поскольку они вы- числяются только при выполнении метода, осуществляющего связывание с данными.
354 Часть II Размещение данных на сайте Однако в составе ASP.NET 2.0 имеется специальная инфраструктура для выполнения по-настоящему динамических выражений, в основе которой лежит новое семейство компонентов — построители выражений. (О построителях выражений подробно рас- сказывается в моей книге «Programming Microsoft ASP.NET 2.0 Applications: Advanced Topics* (Microsoft Press, 2005)). Синтаксис динамических выражений подобен синтаксису выражений связывания с данными, но вместо символа # в качестве префикса в них используется символ $. Такие выражения анализируются при компиляции страницы. Выражение извлекается из ее исходного кода, преобразуется в код на языке программирования и вставляется в класс страницы. Существует несколько предопределенных построителей выражений, описанных в табл. 9-5. Табл. 9-5. Пользовательские выражения Синтаксис__________________Описание________________________________________________ AppSettings:XXX Возвращает значение заданной установки из раздела <appSettings> конфигурационного файла ConnectionStrings:XXX[.YYY] Возвращает заданную строку (XXX) из раздела <connectionStrings> конфигурационного файла. Необязатель- ный параметр YYY позволяет указать, какой атрибут вас ин- тересует — connectionstring (по умолчанию) или providerName Resources:XXX, YYY Возвращает значение глобального ресурса YYY, прочитанное из .resx-файла ресурсов XXX Точный синтаксис выражения связывания с данными определяется конкретным построителем, но в общем случае декларативное связывание свойств элементов управ- ления с данными осуществляется следующим образом: атрибуг=<%$ выражение %> Заметьте, что использование выражений в теле страницы не допускается. Иными словами, вы не можете написать так: <h1><%$ AppSettings:AppVersionNumber %></h1> Выражение нужно заключить внутрь определения серверного элемента управле- ния, простейшим из которых является элемент Literal. Следующий код генерирует вывод, представленный на рис. 9-9. <h1><asp:Literal runat="server" Text="<%$ Resources:Resource, AppWelcome %>" /></h1> <hr /> <b>Code version <asp.Literal runat="server" Text="<%$ AppSettings:AppVersionNumber %>" /></b> Рис. 9-9. Текст заголовка и номер версии получены с помощью выражений
Модель связывания с данными Глава 9 355 Нет нужды говорить, что в папке App_GlobalResources должен существовать ресурс с именем Арр Welcome, а установка AppVersionNumber должна быть определена в раз- деле AppSettings конфигурационного файла. Третий вид выражений, Connectionstrings, полезен тем, что позволяет избежать жесткого кодирования строк подключения в .aspx-файле. ЦД Примечание Microsoft предлагает всего три встроенных построителя выражений (см. табл. 9-5), но разработчики могут определять собственные построители, создавая классы, производные от ExpressionBuilder. Чтобы такой пользовательский построитель правильно распознавался системой, его нужно зарегистрировать в файле web.config. Подробнее эта тема рассматривается в моей книге «Programming Microsoft ASP.NET 2.0 Applications: Advanced Topics» (Microsoft Press, 2005). Компоненты, представляющие источники данных В составе ASP.NET 1.x реализована исключительно гибкая архитектура связывания с данными, обеспечивающая разработчику полный контроль над жизненным циклом страницы. Элементы управления, поддерживающие такую возможность (например, DataGrid), разработчик может связывать с перечислимыми коллекциями данных. Хотя это большой шаг вперед по сравнению с классической ASP, разработчику по-преж- нему приходится изучать множество архитектурных деталей, даже если он создает относительно простые доступные только для чтения страницы. Особенно серьезной эта проблема становится для неопытных разработчиков, поскольку перед ними встает целый ряд задач, решить которые без специальных знаний им не по силам: как ре- ализовать разбиение на страницы (и стоит ли это делать), сортировку, обновление или представление данных в виде двух форм, главной и подчиненной. Но и опытные разработчики испытывают трудности при работе с данной технологией, поскольку им приходится многократно реализовывать одну и ту же схему подключения к источни- ку данных, получения этих данных и согласования их с программным интерфейсом элементов управления. Таким образом, технологии связывания с данными ASP.NET 1.x явно недостает высокоуровневой и, желательно, декларативной модели извлечения данных и мани- пулирования ими. Поэтому написание слоя доступа к данным является сложной и утомительной задачей; даже в самом простом случае программисту приходится писать сотни строк кода. Ну а теперь давайте посмотрим, как описанные проблемы решены в ASP.NET 2.0, и познакомимся с ее новыми компонентами, представляющими ис- точники данных. Обзор компонентов, представляющих источники данных Компонент, представляющий источник данных, — это серверный элемент управления, подключаемый к элементам управления, связанным с данными, и скрывающий от программиста сложности процесса связывания с данными. Такой компонент не только предоставляет элементу управления данные, но и позволяет ему выполнять некото- рые стандартные операции, в частности вставку данных, их удаление, сортировку и обновление. Компонент, представляющий источник данных, служит оболочкой для провайдера данных и их настоящего источника, будь то реляционная база данных, XML-документ или пользовательский класс. Наличие поддержки пользовательских классов означает, что теперь элементы управления можно связывать с написанными ранее классами прикладной системы, например классами из бизнес-слоя или слоя доступа к данным. (Позднее мы еще вернемся к этой теме.) Для того чтобы существующие элементы управления могли поддерживать свя- зывание с компонентами, представляющими источники данных (по крайней мере,
356 Часть II Размещение данных на сайте для извлечения данных), в ASP.NET 2.0 в их состав было включено свойство Data- SourcelD, которое служит точкой соприкосновения между элементами управления старого образца и новыми компонентами, представляющими источники Данных. Так что теперь такой элемент управления, как DataGrid, можно связать с компонентом, представляющим источник данных, не написав ни строчки кода и даже не вызывая метод DataBind. Однако не это является главной целью создания компонентов, пред- ставляющих источники данных. Они создавались как естественное средство простого полуавтоматического взаимодействия между элементами управления и разнообраз- ными источниками данных. Существующие элементы управления, такие как DataGrid и Repeater, не позволяют разработчику воспользоваться всеми преимуществами компонентов, представляющих источники данных. И только новые элементы управления ASP.NET 2.0 — GridView, FormView и DetailsView — в полной мере выявляют возможности этих компонентов. Они имеют другую внутреннюю структуру и специально предназначены для работы с компонентами, представляющими источники данных. Щелчок в жизни элемента управления DataGrid Для того чтобы понять, в чем заключается главная задача компонентов, представляющих источники данных, давайте посмотрим, что происходит, когда пользователь выполняет то или иное действие при работе с данными, выводимыми с помощью элемента управ- ления DataGrid. Предположим, что размещенная на странице таблица доступна для редактирования. Пользователь щелкает ячейку, желая отредактировать ее содержимое. Элемент управления осуществляет возврат формы и генерирует событие. Выполняется написанный автором страницы код обработки этого события, переводящий элемент управления в режим редактирования. Вместо кнопки редактирования выводится пара кнопок для подтверждения или отмены изменений. Пользователь редактирует содер- жимое строки и щелкает одну из этих двух кнопок. Что происходит дальше? Элемент управления DataGrid перехватывает событие щелчка кнопки, проверяет введенные данные и генерирует событие UpdateCommand. Автор страницы отвечает за обработку этого события, получение новых данных, формирование команды, на- правляемой источнику данных, и ее выполнение. Все эти действия требуют написания программного кода. То же самое происходит в случае, когда пользователь дает команду сортировки данных, перехода к другой странице или вывода детальных сведений, связанных с текущей строкой таблицы. Щелчок в жизни элемента управления GridView Теперь давайте посмотрим, что происходит при использовании элемента управления GridView, пришедшего в ASP.NET 2.0 на смену элементу управления DataGrid. Сцена- рий тот же: пользователь щелкает ячейку, после чего элемент управления переходит в режим редактирования. Первое отличие элемента управления GridView заключается в том, что для перевода элемента управления в режим редактирования программисту не приходится писать ни строчки кода. Когда пользователь щелкает ячейку редак- тируемого столбца, элемент управления понимает, что он собирается делать, и сам осуществляет следующий шаг — переход в режим редактирования. Когда пользователь щелкает кнопку сохранения изменений, элемент управле- ния GridView также сам обрабатывает это действие пользователя и дает компоненту, представляющему источник данных, указание выполнить запрошенную операцию над источником данных. Все это происходит безо всякого вмешательства со стороны автора страницы, которому достаточно задать несколько декларативных установок, например задать текст команды и строку подключения.
Модель,связывания с данными Глава 9 357 Сочетание компонентов, представляющих источники данных, и новых интеллек- туальны^ элементов управления, связанных с данными, особенно эффективно для наиболее типичных сценариев, то есть примерно в 70-80 случаях использования этих элементов управления из 100. Ну а когда необходимо реализовать какое -то особое поведение элемента управления, вы можете действовать прежним способом, благо у вас есть полный контроль над жизненным циклом страницы. Новые интеллектуальные элементы управления не только способны самостоя- тельно участвовать в жизненном цикле станицы, но и обладают рядом других воз- можностей: они поддерживают декларативное определение параметров, прозрачное кэширование данных, серверное разбиение на страницы, иерархические данные, а кро- ме того, способны работать в асинхронном режиме. Для самостоятельной реализации всех этих функций вам пришлось бы написать немало кода. Внутренняя структура компонентов, представляющих источники данных Компонент, представляющий источник данных, может содержать не один, а несколько разных наборов данных. Каждый из них хранится в виде коллекции. С исходными дан- ными этот компонент работает посредством SQL команд, таких как SELECT, INSERT, DELETE, COUNT и т. д. Все компоненты, представляющие источники данных, делятся на две категории: табличные (табл. 9-6) и иерархические (табл. 9-7). Табл. 9-6. Табличные компоненты, представляющие источники данных Класс Описание AccessDataSource ObjectDataSource SqlDataSource Представляет подключение к базе данных Microsoft Office Access. Наследует класс SqlDataSource, но связан с .mdb-файлом и использу- ет для подключения к базе данных OLE DB-провайдер Jet 4.0 Служит для подключения к бизнес-объектам .NET, возвращающим данные. Классы этих объектов должны иметь определенную струк- туру, в частности иметь используемый по умолчанию конструктор и методы, действующие определенным образом Представляет подключение к провайдеру данных ADO.NET, возвра- щающему SQL данные, в том числе из источников, взаимодействие с которыми осуществляется посредством технологий OLE DB и ODBC. Имя провайдера и строка подключения задаются в свой- ствах объекта данного класса Обратите внимание, что класс SqlDataSource предназначен не только для работы с SQL Server. Он может подключаться к любому источнику реляционных данных, используя подходящий провайдер ADO.NET. Табл. 9-7. Иерархические компоненты, представляющие источники данных Класс Описание SiteMapDataSource XmlDataSource Может использовать для связи с источником данных любой провай- дер, предоставляющий информацию карты сайта. Задействуемый по умолчанию провайдер карты сайта считывает ее из XML-файла, хра- нящегося в корневой папке приложения Позволяет использовать в качестве источников данных XML-файлы и строки XML-формата, содержащие или не содержащие информа- цию схемы *> ' Заметьте, что элементы управления, представляющие источники данных, не име- ют пользовательского интерфейса. Сами они не выводят разметку, а в состав ис- ходных файлов aspx включаются лишь для того, чтобы их экземпляры автоматически
358 Часть II Размещение данных на сайте создавались при обработке запроса и получали доступ к состоянию представления страницы. Класс DataSourceView Экземпляр класса DataSourceView — это настраиваемое именованное представление данных, для которого можно задавать установки сортировки, фильтрации и других операций над данными. Указанный класс является базовым для всех представлений, связанных с компонентом-источником данных. Количество представлений, которые поддерживаются таким компонентом, зависит от строки подключения, а также от характеристик и содержимого исходного источника данных. Встроенные элементы управления ASP.NET 2.0 поддерживают только одно, используемое по умолчанию, представление. В табл. 9-8 приведен перечень свойств класса DataSourceView. Табл. 9-8. Свойства класса DataSourceView Свойство Описание CanDelete Указывает, допускается ли удаление данных из их исходного источника, осуществляемое с помощью метода Delete Caninsert Указывает, допускается ли вставка данных в их исходный ис- точник, осуществляемая посредством метода Insert CanPage Указывает, допускается ли разбиение данных представления на страницы CanRetrieveTotalRowCount Указывает, доступна ли информация об общем количестве строк CanSort Указывает, допускается ли сортировка данных представления CanUpdate Указывает, допускается ли обновление данных в их исходном источнике, осуществляемое с помощью метода Update Name Возвращает имя текущего представления Свойства СапХХХ указывают не только на то, способен ли компонент-источник данных выполнять определенные операции, но также, возможно ли их выполнение с учетом текущего состояния данных. В табл. 9-9 перечислены все методы, поддержи- ваемые классом DataSourceView. Табл. 9-9. Методы класса DataSourceView Метод Описание Delete Удаляет из источника данные, связанные с представлением Insert Выполняет вставку данных, связанных с представлением Select Возвращает перечислимый объект, заполненный данными из исходного источника Update Обновляет в источнике данные, связанные с представлением Все объекты-представления поддерживают извлечение данных из источника с по- мощью метода Select. Этот метод возвращает объект, реализующий интерфейс lEnume- rable. Реальный тип объекта зависит от компонента, представляющего источник дан- ных, и значений атрибутов, заданных вами для этого компонента. Взаимодействие с элементами управления, связанными с данными На рис. 9-10 изображена действующая в ASP.NET 2.0 схема взаимодействия между компонентом, представляющим источник данных, и элементом управления, связанным с данными. Как видно из схемы, элемент управления, связанный с данными, получает объект-представление и выясняет, каковы его возможности.
Модель звязывания с данными Глава 9 359 Свойства Элемент управления ASP.NET 2.0 Шаг 2 Установка свойств и вызов методов Шаг1 Подключение и получение ссылки на представление IDataSource GetView GetViewNames ♦ Представление источника данных Данны» Компонент, представляющий источник данных Рис. 9-10. Взаимодействие между компонентом, представляющим источник данных, и элементом управления, связанным с данными Новые элементы управления ASP.NET 2.0, связанные с данными, способны ис- пользовать весь потенциал компонентов, представляющих источники данных. Они подключаются к хранилищу данных посредством методов, входящих в состав интер- фейса IDataSource. Реализация этого интерфейса является единственным требованием к компоненту, представляющему источник данных. Получив объект DataSourceView (представление источника данных), элемент управления может вызывать его свойства и методы, описанные в табл. 9-8 и 9-9. Иерархические представления источников данных В отличие от табличных компонентов-источников, обычно имеющих лишь одно именованное представление, иерархические компоненты поддерживают по одному представлению для каждого набора данных. Иерархические и табличные компонен- ты-источники данных предоставляют элементам управления, связанным с данными, концептуально одинаковый программный интерфейс, а единственное их различие за- ключается в природе данных, с которыми они работают: у первых она иерархическая, а у вторых — табличная. Класс иерархического представления имеет другое имя —HierarchicalDataSource- View. У него лишь один метод, Select, возвращающий перечислимый иерархический объект. Поэтому данные иерархических источников доступны только для чтения. Компонент SqlDataSource SqlDataSource — это компонент, представляющий источник данных и способный под- ключаться к реляционным СУБД, таким как SQL Server или Oracle, а также к другим источникам, доступным через OLE DB и ODBC. Подключение к источнику данных конфигурируется с помощью двух свойств: Connectionstring и ProviderName. Первое из них представляет строку подключения и содержит информацию, необходимую для соединения с определенной базой данных. Второе содержит имя пространства имен управляемого провайдера ADO.NET, исполь- зуемого для взаимодействия с базой данных. По умолчанию свойство ProviderName содержит значение System.Dota.SqlClient, то есть источником данных является SQL
360 Часть II Размещение данных на сайте Server. Для того чтобы воспользоваться, например, провайдером OLE DB, задайте в этом свойстве строку System.Data.OleDb. Компонент SqlDataSource может извлекать данные посредством ада птера данных или командного объекта. В зависимости от вашего выбора извлеченные данные будут упакованы в объект DataSet или объект чтения данных. Ниже приведен минималь- ный код, необходимый для активизации компонента, представляющего базу данных SQL Server: <asp:SqlDataSource runat=”server" ID="MySqlSource" ProviderName='<%$ Connectionstrings:LocalNWind.ProviderName %>' ConnectionString=’<%$ Connectionstrings:LocalNWind %>’ SelectCommand="SELECT * FROM employees" /> <asp:DataGrid runat="server" ID="grid" DataSourceID="MySqlSource" /> Программный интерфейс компонента SqlDataSource Операции над данными, поддерживаемые классом-представлением компонента Sql- DataSource, определяются описанными в табл. 9-10 группами свойств. Табл. 9-10. Свойства для конфигурирования операций над данными Группа свойств Описание DeleteCommand, DeleteParameters, DeleteCommandType Возвращают и позволяют задать SQL-команду, используемую для удаления строк из исходного источника данных, ее параметры и тип (запрос или хранимая процедура) FilterExpression, FilterParameters Возвращают и позволяют задать выражение фильтрации данных и его параметры. Эти свойства действуют только при условии, что элемент управления управляет данными посредством объекта DataSet InsertCommand, Insertparameters, InsertCommandType Возвращают и позволяют задать SQL-команду, используемую для вставки строк в исходный источник данных, ее параметры и тип (за- прос или хранимая процедура) SelectCommand, SelectParameters, SelectCommandType Возвращают и позволяют задать SQL-команду используемую для извлечения строк из исходного источника данных, ее параметры и тип (запрос или хранимая процедура) SortParameterName Возвращает и позволяет задать имя входного параметра, который хра- нимая процедура команды будет использовать для сортировки данных. (В качестве команды может выступать только хранимая процедура.) При отсутствии указанного параметра генерируется исключение UpdateCommand, UpdateParameters, UpdateCommandType Возвращают, и позволяют задать SQL-команду, используемую для обновления строк в исходном источнике данных, ее параметры и тип (запрос или хранимая процедура) Каждое командное свойство — это строка, содержащая SQL-текст. Если у коман- ды есть параметры, они должны находиться в соответствующей коллекции. Точный синтаксис команды зависит от конкретного управляемого провайдера и реляционной СУБД. Например, если речь идет об SQL Server, то имена команд имеют префикс а если о провайдере OLE DB, тогда параметры не имеют имен, а места их размещения отмечены символом «?». Ниже приведен более сложный пример элемента управления, представляющего источник данных, для которого разрешено параметризированное удаление и обновление данных: <asp:SqlDataSource runat="server” ID="MySqlSource” ConnectionString=’<%$ ConnectionStrings:LocalNWind %> SelectCommand="SELECT * FROM employees” UpdateCommand="UPDATE employees SET lastname=@lname”
Модель связывания с данными Глава 9 361 DeleteCommand=”DELETE FROM employees WHERE emp.loyeeid=@TheEmp" FilterExpression="employeeid > 3 "> <!— Здесь размещаются параметры --> </asp:SqlDataSou rce> Свойство FilterExpression имеет тот же синтаксис, что и свойство RowFilter класса DataView, подобный синтаксису SQL-предложения WHERE. Если фильтру необходимы параметры, их можно задать в коллекции FilterParameters. Фильтрация осуществляется только в том случае, если свойство DataSourceMode имеет значение DataSet. I" J 'l Примечание Обратите внимание, что выражения фильтрации и параметры команды выборки данных действуют по-разному. Если параметры команды выборки определяют, какой набор данных вернет провайдер, то выражение фильтрации воздействует лишь на отображаемый набор. В табл. 9-11 перечислены другие свойства класса SqlDataSource. Здесь не указаны свойства, связанные с кэшированием, которые мы рассмотрим немного погодя. Табл. 9-11. Другие свойства класса SqlDataSource Свойство Описание CancelSelectOnNullParameter Указывает, следует ли отменить операцию выборки дан- ных, если значением параметра оказывается null. По умол- чанию данное свойство имеет значение true ConflictDetection Определяет, как элемент управления должен обрабаты- вать конфликты данных, обнаруженные при выполнении операции удаления или обновления. По умолчанию при- оритет имеют изменения, выполненные последними Connectionstring Строка подключения к базе данных DataSourceMode Указывает, в каком виде должны быть возвращены дан- ные — в виде объекта DataSet или объекта чтения данных OldValuesParameterFormatString Возвращает и позволяет задать строку форматирования, применяемого к именам параметров метода Delete или Update для получения парных параметров, представляю- щих исходные значения полей удаляемой или обновляе- мой строки ProviderName Указывает пространство имен управляемого провайдера ADO.NET Интересно, что многие из этих свойств идентичны свойствам класса-представле- нйя (см. рис. 9-10). В составе класса SqlDataSource определено также несколько методов и событий, по большей части общих для всех элементов управления, представляющих источни- ки данных. Этими методами являются Delete, Insert, Select и Update, а реализованы они просто как оболочки соответствующих методов класса-представления источника данных. События класса SqlDataSource являются парными: Deleting-Deleted, Insert- ing-Inserted, Selecting-Selected, Updating-Updated’, они генерируются до и после вызова упомянутых методов. О начале операции фильтрации сигнализирует событие Filtering. Я уже говорил, что только новые элементы управления ASP.NET 2.0 способны в полной мере использовать возможности компонентов, представляющих источники данных. Поэтому в следующих двух главах, посвященных элементам управления Grid- View, DetailsView и FormView, будет много демонстрационного кода, показывающего, как использовать элемент управления SqlDataSource для выборки, обновления дан- ных, их разбиения на страницы и сортировки. В настоящей же главе мы рассмотрим
362 Часть II Размещение данных на сайте общие функции компонентов, представляющих источники данных, которые могут быть особенно полезны в реальных приложениях. Декларативно определяемые параметры С каждым командным свойством связан свой набор параметров — экземпляр класса- коллекции с именем Parametercollection. ASP.NET 2.0 поддерживает довольно большое количество типов параметров; все они перечислены в табл. 9-12. Табл. 9-12. Типы параметров в ASP.NET 2.0 Параметр Описание ControlParameter Значение параметра берется из открытого свойства серверного эле- мента управления CookieParameter Значение параметра считывается из заданного cookie-файла FormParameter Значение параметра считывается из входного поля формы НТТР-за- проса Parameter Значение присваивается параметру программным способом ProfileParameter Значение параметра берется из заданного свойства объекта профиля, предоставленного персонализационной системой приложения QueryStringParameter Значение параметра считывается из заданной переменной строки запроса SessionParameter Значение параметра извлекается из заданного слота состояния сеанса Каждый из перечисленных классов имеет свойство Name и набор свойств, опреде- ляющихся его назначением и реализацией. Чтобы понять, как осуществляется де- кларативное определение параметров для компонента, представляющего источник данных, взгляните на следующий код: <asp:SqlDataSource runat="server" ID="MySource" ConnectionString='<%$ Connectionstrings:LocalNWind %>' SelectCommand="SELECT * FROM employees WHERE employeeid > @MinID"> <Select Pa ramete rs> <asp:ControlParameter Name="MinID" ControlId="EmpID” PropertyName="Text" /> </Select Pa ramete rs> </asp:SqlDataSou rce> Запрос, заданный в атрибуте SelectCommand, содержит параметр @MinID, на место которого компонент SqlDataSource автоматически подставит информацию, возвра- щенную объектом ControlParameter. Значение этого параметра зависит от значения свойства одного из элементов управления. Имя свойства определяется атрибутом PropertyName, а идентификатор элемента управления — атрибутом Controlld. Для того чтобы приведенный код сработал, автор страницы должен гарантировать, что таковая содержит элемент управления с заданным идентификатором и свойством. В против- ном случае будет выброшено исключение. В данном примере в качестве значения параметра используется значение свойства Text элемента управления EmpID. Способ связывания формальных параметров запроса с фактическими зависит от конкретного провайдера. Если провайдер поддерживает именованные параметры, как в случае с провайдерами SQL Server и Oracle, связывание осуществляется по именам. В противном случае действует позиционный принцип связывания, то есть первый фактический параметр подставляется на место первого маркера в запросе, второй — на место второго и т. д. Именно так происходит, когда для доступа к данным применяется технология OLE DB.
Модель связывания с данными Глава 9 363 Выявление конфликтов Когда элемент управления связан с компонентом, представляющим источник данных, то в некоторый момент времени данные считываются из источника, затем модифи- цируются на клиенте, после чего обновляются в их источнике. Если доступ к базе данных для чтения и записи имеют несколько пользователей, то может оказаться так, что между моментами чтения и записи данных одним из них эти данные будут обновлены другим. Действия элемента управления SqlDataSource в случае обнаружения подобного конфликта определяются значением свойства ConflictDetection. Это свойство имеет перечислимый тип Conflictoptions, а его значением по умолчанию является Осег- writeChang.es, указывающее, что операция обновления или удаления должна произ- водиться независимо от того, изменилась ли строка с тех пор, как была прочитана приложением. В качестве альтернативы можно задать установку CompareAllValues, чтобы элемент управления SqlDataSource передал методу Delete или Update исходные данные, прочитанные из базы данных, для сверки их с текущими значениями полей строки в базе данных. Учтите: изменение значения свойства ConflictDetection оказывает нужный эффект лишь при условии, что команды обновления и удаления написаны таким образом, чтобы команда не выполнялась, если обновляемая или удаляемая строка отличается от первоначально прочитанной из базы данных. Для этого команду обновления, на- пример, нужно определить так: UPDATE employees SET firstname=@forstname WHERE employeeid=@employeeid AND firstname=@original_firstname Иными словами, вы должны явно добавить в команду предложение WHERE, чтобы проверить, совпадают ли текущие значения модифицируемых полей с их первона- чально прочитанными из базы данных значениями. Тогда в случае, если параллельно работающими пользователями изменения внесены в промежутке между чтением и обновлением данных текущим пользователем, обновление строки выполнено не бу- дет. Текст команды вы должны откорректировать самостоятельно, просто присвоить свойству ConflictDetection значение CompareAllValues недостаточно. А как формируются имена параметров, представляющих старые значения? Элемент управления SqlDataSource использует для этой цели значение свойства OldValuesPa- rameterFormatString. Оно представляет собой строку форматирования, применяемого к имени параметра для получения имени парного ему параметра, в котором задается исходное значение поля. По умолчанию свойство OldValuesParameterFormatString имеет значение original_{0}, то есть к имени параметра добавляется префикс original , как в приведенной выше SQL-команде. При использовании установки CompareAllValues можно обрабатывать события Deleted и Updated компонента, представляющего источник данных, выясняя, сколько строк задействовано в операции. Если ни одна строка не была задействована, это может служить признаком конфликта, связанного с параллельным выполнением операций: void OnUpdated(object sender, SqlDataSourceStatusEventArgs e) { if (e.AffectedRows == 0) {
364 Часть II Размещение данных на сайте Кэширование Связывание элемента управления с компонентом, представляющим источник данных производится автоматически каждый раз, когда этот элемент инициирует возврат формы. Вообразите себе страницу, в состав которой входит табличный элемент управ- ления, компонент, представляющий источник данных, и кнопка. Если переключить таблицу в режим редактирования, будет выполнена команда выборки данных; если щелкнуть кнопку, пользовательский интерфейс таблицы сформируется заново с ис- пользованием данных состояния представления и команда выборки данных не будет выполнена. Для того чтобы не выполнять запрос при каждом возврате формы, можно дать ком- поненту, представляющему источник данных, указание кэшировать результирующий набор данных в течение определенного времени. В таком случае метод Select будет извлекать данные из кэша, а не из базы данных, и лишь при устаревании данных в кэше обращаться к базе данных и записывать в кэш новый результирующий набор. Кэшированием данных компонента SqlDataSource управляют свойства, перечислен- ные в табл. 9-13. Табл. 9-13. Свойства, связанные с кэшированием данных элементом управления SqlDataSource Свойство Описание CacheDuration Срок хранения данных в кэше в секундах CacheExpirationPolicy Указывает, абсолютным или скользящим является срок хранения данных в кэше В первом случае данные становятся недействитель- ными спустя заданное время после их помещения в кэш, а во вто- ром — спустя заданное время после их последнего использования CacheKeyDependency Имя определяемого пользователем ключа кэша, связываемого со всеми элементами кэша, которые созданы определенным компонен- том, представляющим источник данных. После устаревания ключа кэшируемые компонентом данные могут быть удалены EnableCaching Позволяет включать и отключать функцию кэширования SqlCacheDependency Возвращает и позволяет задать строку, разделенную символами точки с запятой, в которой указано, на какие базы данных и табли- цы должно распространяться действие функции отслеживания за- висимостей кэша при использовании SQL Server Отдельный элемент кэша создается для каждой комбинации значений свойств SelectCommand, Connectionstring и SelectParameters. При этом несколько элементов управления SqlDataSource могут совместно использовать одни и те же элементы кэша, если загружают одни и те же данные из одной и той же базы данных. С помощью свойства CacheKeyDependency вы получаете некоторый контроль над элементами кэша, создаваемыми компонентом, представляющим источник данных. Если присво- ить этому свойству непустую стррку, компонент SqlDataSource создаст зависимость между ключом и всеми создаваемыми им элементами кэша. После этого для очистки кэша вам достаточно будет присвоить свойству CacheKeyDependency новое значение: Cachet "ClearAll"] = любое_начальное_значение-, SqlDataSoureel.CacheKeyDependency = "ClearAll"; Cachet"ClearAll"] = любое_другое_значение\ Компонент SqlDataSource может кэшировать данные только при работе в режиме DataSet. Если кэширование включено, а свойство DatcSourceMode имеет значение DataReader, генерируется исключение.
Модель связывания с данными Глава 9 365 Свойство SqlCach.eDepend.ency служит не только указанной выше цели; оно связы- вает кэшируемые данные компонента SqlDataSource с содержимым заданной таблицы базы данных (обычно той, откуда были загружены кэшируемые данные): <asp:SqlDataSource ID="SqlDataSource1" runat="server” CacheDu rat ion="1200” Connections!ring="<%$ Connectionstrings:LocalNWind %>" EnableCaching="true" SelectCommand="SELECT * FROM employees" SqlCacheDependency=”Northwind:Employees’^ </asp:SqlDataSou rce> Когда содержимое исходной таблицы изменяется, соответствующие данные авто- матически удаляются из кэша. О зависимостях кэша я подробно расскажу в главе 14. Компонент AccessDataSource Компонент AccessDataSource представляет базу данных Access. Он является про- изводным от рассмотренного выше компонента SqlDataSource, но обладает более простым программным интерфейсом. Класс AccessDataSource наследует все члены родительского класса и некоторые из них переопределяет. В частности, пара свойств ConnectionString и ProviderName заменена в нем единственным свойством DataFile, в котором задается .mdb-файл базы данных. Для подключения к ней компонент ис- пользует провайдер Microsoft Jet 4 .0 OLE DB. [7Z1 Примечание На самом деле, поскольку компонент AccessDataSource наследует класс SqlDataSource, члены последнего не могут просто исчезнуть. Поэтому, если говорить точ- но, то свойства Connectionstring и ProviderName в классе AccessDataSource не заменены новым свойством, а лишь переопределены таким образом, чтобы при попытке обращения к ним выбрасывалось исключение. Так же переопределено и свойство SqlCacheDepen- dency, поскольку зависимости кэша для баз данных Access отслеживаться, конечно же, не могут. Ну а новое свойство DataFile просто добавлено в класс AccessDataSource. Работа с базой данных Access Давайте посмотрим, как с помощью компонента AccessDataSource можно открыть файл .mdb и связать с содержимым одной из его таблиц раскрывающийся список, раз- мещенный на странице ASP.NET. Заметьте, что по умолчанию компонент открывает базу данных Access только для чтения. <asp:AccessDataSource runat="server" ID="MyAccessSource" DataFile=”nwind.mdb" SelectCommand="SELECT * FROM Customers" /> Select a Customer: <asp:DropDownList runat="server" DataSourceId="MyAccessSource" /> Некоторые функции компонента AccessDataSource унаследованы им от базового класса SqlDataSource. В сущности, это и есть SqlDataSource, оптимизированный для работы с базами данных Access. Подобно родительскому компоненту, AccessDataSource поддерживает два способа доступа к загруженным из файла данным: с использованием объекта DataSet или объекта DataReader. Фильтрация данных возможна только при использовании объекта DataSet. Кэширование данных осуществляется так же, как и в случае с компонентом SqlDataSource, с той лишь разницей, что функция управления зависимостями кэша в этом случае не действует. Обновление базы данных Access Компонент AccessDataSource способен вставлять, обновлять и удалять строки базы данных Access. Для этого он использует командные объекты ADO.NET и коллекции
366 Часть II Размещение данных на сайте параметров. Нужно сказать, что с обновлением баз данных Access из приложений ASP.NET связаны некоторые проблемы, поскольку такая база данных — просто файл, и учетная запись процесса ASP.NET (ASPNET или NETWORK SERVICE, в зависимости от операционной системы) может не иметь разрешения на запись в этот файл. Для того чтобы обновление данных выполнялось успешно, нужно позаботиться о предостав- лении учетной записи ASP.NET такого разрешения. В качестве альтернативы можно использовать другую учетную запись, обладающую необходимыми разрешениями. Pg] Примечание Большинство провайдеров интернет-услуг обычно предоставляют учетным записям ASPNET и NETWORK SERVICE разрешение на запись только в один каталог. Если поместить базу данных Access в этот каталог, то чтение и запись данных будут осуществляться без проблем. Однако помните, что на файлы баз данных Access рас- пространяется действие правил защиты ASP.NET. Компонент ObjectDataSource Класс ObjectDataSource обеспечивает разработчику приложения возможность свя- зывать элементы управления с данными, возвращаемыми методами произвольных пользовательских классов. Подобно другим компонентам, представляющим источники данных, ObjectDataSource поддерживает декларативное определение параметров и позволяет разработчикам передавать методам своих объектов значения переменных, объявленных на уровне страницы. Класс ObjectDataSource действует на основе неко- торых предположений об объектах, оболочкой которых он служит, так что не каждый класс может быть источником его данных. В частности, связываемые классы должны иметь используемый по умолчанию конструктор, а также методы, соответствующие традиционной семантике операций выборки, обновления, вставки и удаления данных. Кроме того, объект должен обновлять только один элемент за раз. Объекты, состо- яние которых обновляется в пакетном режиме, не поддерживаются классом Object- DataSource. Иными словами, управляемые классы, подходящие для использования с классом ObjectDataSource, должны разрабатываться с учетом такого применения. Программный интерфейс компонента ObjectDataSource У компонента ObjectDataSource такой же программный интерфейс, как у компонента SqlDataSource'. у него те же события, методы и свойства и ведет он себя аналогичным образом. Кроме того, в его класс добавлено несколько новых свойств и событий. Новые события — ObjectCreating, ObjectCreated и ObjectDisposing — связаны с жизнен- ным циклом используемого бизнес-объекта. В табл. 9-14 описаны ключевые свойства компонента ObjectDataSource. Табл. 9-14. Важнейшие свойства компонента ObjectDataSource Свойство Описание ConvertNullToDBNuU Указывает, должно ли значение null параметров, переда- ваемых командам вставки, удаления и обновления, пре- образовываться в значение System.DBNidl', по умолчанию данное свойство имеет значение false DataObjectTypeName Возвращает и позволяет задать имя класса, который будет использоваться в качестве параметра команды выборки, вставки, обновления или удаления данных DeleteMethod, DeleteParameters Возвращают и позволяют задать имя и параметры метода, используемого для выполнения операции удаления EnablePaging Указывает, поддерживает ли элемент управления разбие- ние на страницы
Модель связывания с данными Глава 9 367 Табл. 9-14. (окончание) Свойство Описание FilterExpression, FilterParameters Определяют выражение фильтрации, которое будет при- менено к результатам операции выборки данных, и его параметры InsertMethod, InsertParameters Возвращают и позволяют задать имя и параметры метода, используемого для выполнения операции вставки MaximumRowsParameterName Если свойство EnablePaging установлено в true, определяет имя параметра метода Select, принимающего число извле- каемых записей OldValuesParameterFormatString Возвращает и позволяет задать строку форматирования, применяемого к именам параметров метода Delete или Up- date для получения парных параметров, представляющих исходные значения полей удаляемой или обновляемой строки SelectCountMethod Возвращает и позволяет задать имя метода, используемого для выполнения операции выборки с подсчетом строк SelectMethod, SelectParameters Возвращают и позволяют задать имя и параметры метода, используемого для выполнения операции выборки SortParameterName Возвращает и позволяет задать имя входного параметра, используемого для сортировки извлекаемых данных. Если указанный параметр отсутствует, выбрасывается исклю- чение StartRowIndexParameterName Если свойство EnablePaging имеет значение true, указывает имя параметра метода Select, принимающего номер первой из извлекаемых строк UpdateMethod, UpdateParameters Возвращают и позволяют задать имя и параметры метода, используемого для выполнения операции обновления Для поиска и вызова методов, необходимых для выполнения той или иной опе- рации, компонент ObjectDataSource использует технологию рефлексии. Свойство TypeName возвращает полностью уточненное имя сборки, содержащей вызываемый класс. Если этот класс определен в папке Арр Code, имя сборки указывать не нужно. В противном случае задайте строку в форме [имя_класса, сборка], разделенную за- пятой. Чтобы все это стало вам понятнее, давайте рассмотрим пример применения компонента ObjectDataSource. Внимание! Нельзя держать в папке App_Code слишком большое количество классов, поскольку иначе любые изменения в файлах проекта Visual Studio .NET будут приводить к перекомпиляции всех его файлов. Реализация выборки данных Ниже демонстрируется структура класса, который может использоваться компонен- том ObjectDataSource в качестве источника данных. Построенный по шаблону TDG (Table Data Gateway — шлюз таблицы данных), этот класс представляет служащих компании и имеет как минимум два вспомогательных класса: Employee и EmployeeCol- lection. Объект класса Employee содержит информацию об одном служащем, а объект класса EmployeeCollection представляет собой коллекцию объектов класса Employee. Поведение сущности «служащий» представлено набором методов, экспортируемых шлюзовым классом Employees. public class Employees
368 Часть II Размещение данных на сайте public static string Connectionstring { } public static void Load(int employeelD) { } public static Employeecollection LoadAllO { } public static Employeecollection LoadByCountry(string country) { } public static void Save(Employee emp) { public static void Insert(Employee emp) { } public static void Delete(int employeelD) { } } Шаблон TDG требует, чтобы шлюз был общим для всех экземпляров класса сущ- ности В следующем примере я продемонстрирую реализацию этого класса со статиче- скими методами. Если статические методы вас не устраивают, рабочий класс, который используется вместе с ObjectDataSource, должен иметь применяемый по умолчанию конструктор и не должен сохранять никакой информации состояния. Внимание! С архитектурной точки зрения применение статических методов в контексте TDG — хорошее решение, но с ним могут быть связаны некоторые проблемы практи- ческого характера, возникающие при модульном тестировании. Предположим, что вы тестируете бизнес-класс, из кода которого вызывается слой доступа к данным, и в этом слое происходит ошибка. Как определить, что именно произошло и кто является виновни- ком ошибки: бизнес-класс или слой доступа к данным? Для эффективного тестирования бизнес-слоя, осуществляющего вызовы слоя доступа к данным, необходимо сконцентри- роваться на тестируемом объекте. В подобном случае можно прибегнуть к помощи объ- ектов-имитаторов — программируемых полиморфных объектов, которые имитируют другие объекты, способны обнаруживать аномалии в работе слоя доступа к данным и четко о них сигнализировать, чтобы тест проходил успешно, если более ничего не случится. Но дело в том, что инструментарий для создания объектов-имитаторов обычно «не дружит» со статическими методами. Вот почему в реализациях шаблона TDG, предназначенных для применения в реальных приложениях, лучше использовать не статические методы, а методы экземпляров. Рабочий класс должен быть доступен из страницы .aspx; с компонентом ObjectData- Source его можно связать следующим образом: <asp:ObjectDataSource runat="server" ID="MyObjectSource" TypeName="ProAspNet20.DAL.Employees" SelectMethod="LoadAll" /> Когда исполняющая среда HTTP встречает в составе Web-страницы подобный блок, она генерирует код, вызывающий метод LoadAll заданного класса. Возвращен- ные этим методом данные — экземпляр класса EmployeeCollection — связываются с элементом управления, который подключен к объекту MyObjectSource через свойство DataSourcelD. Теперь давайте познакомимся с реализацией метода LoadAll'.
Модель связывания с данными Глава 9 369 public static Employeecollection LoadAllO Employeecollection coll = new EmployeeCollectionO; using (SqlConnection conn = new SqlConnection(ConnectionString)) { •« SqlCommand cmd = new SqlCommand("SELECT * FROM employees", conn); <conn.OpenO; SqlDataReaoer reader = cmd. ExecuteReaderO; HelperMethods.FillEmployeeList(coll, reader); reader.Close(); conn.Close(); } return coll; } Это немного упрощенный код, но он позволяет составить представление о том, что должно происходить: вы выполняете команду, заполняете пользовательскую коллек- цию и возвращаете ее элементу управления, связанному с данными. Таким образом, единственное, что вам нужно написать, это рабочий класс — в файл отделенного кода страницы не придется добавлять ни строчки кода. Вот пример объявления элемента управления, связанного с пользовательским источником данных: <asp:DataGrid ID=”grid" runat="server" DataSourceID=”MyObjectSource" /> Элемент управления DataGrid получает коллекцию объектов Employee, опреде- ленных следующим образом: public class Employeecollection : List<Employee> { } Связывание осуществляется бесшовно, и в нем не участвуют контейнерные объек- ты ADO.NET. Полный код этого примера вы найдете в демонстрационном проекте ASP.NET, который прилагается к настоящей книге. Метод, связанный со свойством SelectMethod, должен вернуть объект одного из следующих типов: ТЕлмтега^/е-совместимого типа, например коллекцию, DataSet, DataTable или Object. Желательно, чтобы метод Select не был перегруженным, хотя компонент Object DataSource «не имеет ничего против» использования в бизнес-клас- сах перегруженных методов. Использование параметров В большинстве случаев методам необходимы параметры. Входные параметры ме- тода, осуществляющего выборку данных, задаются в коллекции SelectParameters. Представьте, что у вас имеется метод, загружающий данные о служащих из заданной страны. Его параметры задаются так: <asp:ObjectDataSource ID="0bjectDataSource1" runat="server" TypeName="ProAspNet20.DAL.Employees” SelectMethod="LoadByCount ry"> <Select Pa ramete rs> <азр:Control Parameter Name="country” ControlID="Countries" PropertyName="SelectedValue” /> J </Select Pa ramete rs> </asp: Ob j ectDataSou rce>
370 Часть II Размещение данных на сайте Данный фрагмент кода — это декларативная версия приведенного ниже псев- докода, в котором идентификатор Countries представляет раскрывающийся список названий стран: string country = Countries.SelectedValue; EmployeeCollection coll = Employees.LoadByCountry(country); Класс ControlParameter автоматизирует извлечение реального значения параметра и связывание с этим параметром списка методов. Но что если добавить в раскрываю- щийся список элемент [All Countries ft При его выборе нужно будет вызывать метод LoadAll без параметров, а когда выбрана определенная страна — метод LoadByCountry с параметрами. Декларативное программирование годится для простых случаев, но в более сложных приходится писать исполняемый код: void Page_Load(object sender, EventArgs e) { // Параметры должны каждый раз очищаться (или следует // отключить функцию поддержки состояния представления) ObjectDataSourcel.SelectParameters.Clear(); if (Countries.Selectedlndex == 0) ObjectDataSourcel. SelectMethod = "LoadAll"; else { ObjectDataSourcel.SelectMethod = "LoadByCountry"; ControlParameter cp = new ControlParameter("country", "Countries", "SelectedValue"); ObjectDataSourcel.SelectParameter s.Add(cp); } } Заметьте, что компоненты, представляющие источники данных, подобны обычным серверным элементам управления, так что их можно конфигурировать и вызывать программным способом. Приведенный код сначала выясняет, какой элемент выбран пользователем, и если это [All Countries], конфигурирует компонент, представляющий источник данных, таким образом, чтобы он вызвал метод LoadAll без параметров. Коллекцию SelectParameters при загрузке страницы необходимо очищать. Ком- понент, представляющий источник данных (а точнее входящий в его состав объ- ект-представление) кэширует большинство значений своих свойств в состоянии представления. В результате при обновлении страницы после выбора элемента в раскрывающемся списке коллекция SelectParameters оказывается уже заполненной. В приведенном коде она просто очищается, но, возможно, вы предпочтете вовсе от- ключить для компонента-источника данных функцию управления состоянием пред- ставления. Только помните, что в таком случае при загрузке данного компонента пустыми будут все его коллекции. ©Внимание! Компонент ObjectDataSource поддерживает извлечение данных из источника и их обновление, при этом позволяя отделить логику доступа к данным и бизнес-логику от пользовательского интерфейса. Но одного только его применения недостаточно для создания хорошо спроектированной и эффективно действующей многоуровневой систе- мы. Компоненты, представляющие источники данных, используются в паре с элемен- тами управления, связанными с данными, так что получается более интеллектуальный объектный комплекс. Однако чтобы в полной мере использовать преимущества, предо- ставляемые классом ObjectDataSource, необходимо иметь уже готовый слой доступа к данным. Необязательно связывать объект ObjectDataSource с корневым объектом слоя доступа к данным. Ведь последний может функционировать на удаленном компьютере
Модель связывания с данными Глава 9 371 и, возможно, даже за брандмауэром, и тогда лучше написать локальный промежуточный объект и соединить его с одной стороны с объектом ObjectDataSource, а с другой — со слоем доступа к данным. Этот промежуточный объект будет действовать как специа- лизированный прокси-слой приложения и по правилам, определяемым приложением. Введение объектов ObjectDataSource не нарушает многослойной структуры приложения, но и не делает существующую систему по-настоящему многослойной. При этом просто используются возможности существующих слоев бизнес-объектов и данных. Кэширование данных и бизнес-объекты Компонент ObjectDataSource поддерживает кэширование только в том случае, когда метод выборки данных возвращает объект DataSet или DataTable. Если же возвраща- ется пользовательская коллекция (как в рассматриваемом нами примере), выбрасы- вается исключение. Класс ObjectDataSource предназначен для работы с классами бизнес-слоя при- ложения. Для каждой выполняемой им операции создается отдельный экземпляр бизнес-класса, который уничтожается вскоре после ее завершения. Такая модель яв- ляется естественным развитием свойственной ASP.NET модели программирования без сохранения состояния. Поскольку инициализация бизнес-объектов нередко является дорогостоящей операцией, можно использовать статические классы или статические методы экземпляров классов. (В таком случае вспомните о том, что я говорил в от- ношении модульного тестирования классов со статическими методами.) Кэширование экземпляров бизнес-объектов и объединение их в пулы не произ- водится автоматически, но эти функции можно реализовать самостоятельно путем соответствующей обработки событий ObjectCreating и ObjectDisposing компонента ObjectDataSource. Событие ObjectCreating генерируется, когда компоненту требуется экземпляр бизнес-класса. В обработчике события можно получить существующий экземпляр этого класса и вернуть его компоненту: Ц Обработчик события ObjectCreating компонента, Ц представляющего источник данных public void BusinessObjectBeingCreated(object sender, ObjectDataSourceEventArgs e) { BusinessObject bo = RetrieveBusinessObjectFromPoolO; if (bo == null) bo = new BusinessObjectO; e.Objectlnstance = bo; } Аналогичным образом в обработчике события ObjectDisposing вы снова сохраняете экземпляр бизнес-объекта и отменяете операцию его уничтожения: И Обработчик события ObjectDisposing компонента, // представляющего источник данных public void BusinessObjectBeingDisposed(object sender, ObjectDataSourceDisposingEventArgs e) { Retu rnBusinessObj ectToPool(e.Obj ectInstance); e.Cancel = true; } Добавлю, что не кэшируются не только экземпляры бизнес-объектов. В некото- рых случаях извлеченные данные также не сохраняются в памяти даже на какое-то время. Если говорить точнее, то компонент ObjectDataSource, как и компонент Sql- DataSource, поддерживает кэширование, но только в случае, когда метод выборки
372 Часть II Размещение данных на сайте данных возвращает объект DataTable или DataSet. Что касается объекта DataView и любой другой простой перечислимой коллекции данных, то они не кэшируются, и если в такой ситуации попытаться активизировать функцию кэширования, будет выброшено исключение. [ . | Примечание Как и кэширование, фильтрация возвращаемых данных не поддерживается компонентом ObjectDataSource, если только эти данные не упакованы в контейнерный класс ADO.NET. Постраничный вывод данных В отличие от компонента SqlDataSource компонент ObjectDataSource поддерживает разбиение набора данных на страницы. Для этой цели в его состав включены три дополнительных свойства, упоминавшиеся в табл. 9-14, а именно EnablePaging, Start- RowIndexParameterName и MaximumRowsParameterName. Первое из них, EnablePaging, служит для включения и отключения функции раз- биения на страницы. По умолчанию оно имеет значение false, то есть автоматиче- ское разбиение данных на страницы не осуществляется. Компонент ObjectDataSource предоставляет инфраструктуру, необходимую для постраничной выдачи данных, но сама она должна осуществляться тем объектом, для которого этот компонент служит оболочкой. Так, в следующем фрагменте кода предполагается, что у класса Customers есть метод LoadBy Country, принимающий два дополнительных параметра, в которых задается размер страницы и индекс ее первой записи. Имена этих двух параметров должны быть присвоены атрибутам MaximumRQwsParameterName и StartRowindex ParameterName соответственно: <asp:ObjectDataSource ID="0bjectDataSource1" runat="setver" TypeName="ProAspNet20.DAL.Customers" Sta rtRowIndexParamete rName="firstRow" MaximumRowsParameterName="toTalRows" SelectMethod="LoadByCountry"> <SelectParameters> <asp:ControlParameter Name="country" ControlID="Countries" PropertyNgme="SelectedValue" /> <asp:ControlParameter Name=”totalRows" ControlID="PageSize" PropertyName=”Text" /> <asp:ControlParameter Name=”firstRow" ControlID=”FirstRow" PropertyName=”Text" /> </SelectPa ramete rs> </asp:ObjectDataSou rce> Собственно разбиение на страницы осуществляет метод LoadByCountry, и вы долж- ны написать его самостоятельно. У этого метода есть две перегруженные версии, одна из которых поддерживает постраничную выдачу данных. Разбиение на страницы выполняет вызываемый им метод FillCustomerList'. public static Customercollection LoadByCountry(string country) { return LoadByCountry(country, -1, 0); } public static Customercollection LoadByCountry(string country, int totalRows, int firstRow) { Customercollection coll = new CustomerCollection(); using (SqlConnection corn = new SqlConnection(ConnecuonString))
Модель связывания с данными Глава 9 373 ’ { SqlCommand cmd; , , cmd = new SqlCommand(CustomerCommands.cmdLoadByCountry. conn); cmd.Parameters.AddWithValue("©country", country); conn.0pen(); SqlDataReader reader = cmd.ExecuteReader(); HelperMethods.FillCustomerList(coll, reader, totalRows, firstRow); reader. CloseO; conn.CloseO; } return coll; } Метод FillCustomerList (с кодом которого вы можете ознакомиться самостоятельно, найдя его в сопутствующем проекте книги) просто выполняет проход по результйрую- щему набору данных с помощью объекта их чтения и удаляет все строки, не входящие в запрошенный диапазон. При желании этот алгоритм можно усовершенствовать, сделав его более интеллектуальным, но главное здесь то, что он реализуется бизнес - объектом и доступен компонентам, представляющим источники данных, посредством известного интерфейса. Обновление и удаление данных Для того чтобы иметь возможность модифицировать данные в их источйике с помо- щью объекта ObjectDataSource, необходимо определить методы обновления, вставки и удаления данных. Все используемые вами методы должны иметь соответствую- щую семантику, что нетрудно реализовать, когда слой доступа к данным построен по шаблону TDG. Вот хорошие прототипы указанных операций: public static void Save(Employee emp) public static void Insert(Employee emp) public static void Delete(int id) Всем им требуются параметры. Так, для обновления данных нужно передать но- вые значения и одно или несколько старых, чтобы, во-первых, найти по ним в ис- точнике нужную запись, а во-вторых, иметь возможность правильно действовать в случае конфликта обновлений. Для удаления данных необходимо идентифицировать строку по первичному ключу. Ну а для их вставки нужно просто задать новые дан- ные. Входные параметры можно задавать в командных коллекциях, таких как Update- Parameters, InsertParameters и DeleteParameters. Мы же для начала рассмотрим сценарий обновления/вставки. Итак, чтобы обновить существующую запись или вставить новую, требуется пере- дать несколько значений. Это можно сделать двумя способами: задать каждое в от- дельном параметре или упаковать их в одну комплексную структуру. В приведенных выше прототипах методов Save и Insert использовался второй подход, но эти методы могут быть и такими: public static void Save(int id, string firstName, string lastName, ...) public static void Insert(string firstName, string lastName, ...) ♦ Коллекции параметров команды можно использовать только для параметров про- стых типов: числовых, строковых, дат. Если слой доступа к данным построен по шабло- ну TDG (или другому подобному шаблону, например Data Mapper), методы вставки и обновления, вероятнее всего, будут принимать в качестве параметра комплексный пользовательский объект данных — в нашем примере объект класса Employee. Для
374 Часть II Размещение данных на сайте того чтобы компонент ObjectDataSource мог работать с таким классом, необходимо установить свойство DataObjectTypeName'. <asp ObjectDataSource ID="RowDataSource" runat="server" TypeName="ProAspNet20.DAL.Employees" SelectMet hod="Load" UpdateMethod="Save" Data0bjectTypeName=”ProAspNet20.DAL.Employee’^ <SelectPa ramete rs> <asp:ControlParameter Name="id ControlID="GridView1" PropertyName="SelectedValue" /> </SelectParameters> </asp:Obj ectDataSou rce> Компонент ObjectDataSource сохраняет строки с помощью метода Save, принима- ющего в качестве аргумента объект Employee. Заметьте, что когда вы устанавливаете свойство DataObjectTypeName, коллекция UpdateParameters игнорируется. Перед на- чалом операции компонент ObjectDataSource создает используемый по умолчанию экземпляр объекта и затем пытается заполнить его открытые свойства значениями входных полей связанного с данными элемента управления. Поскольку при выполне- нии этой задачи используется технология рефлексии, имена входных полей связанного элемента управления должны соответствовать именам открытых свойств класса, за- данного в свойстве DataObjectTypeName. Учтите, что при определении такого класса, как Employee, представляющего некую сущность, нельзя использовать сложные типы данных. Рассмотрим пример: public class Employee { public string LastName {. } public string FirstName {...} public Address HomeAddress {...} } Члены LastName и FirstName представляют отдельные значения (в данном случае строки), поэтому они прекрасно подойдут для записи данных из входных полей, чего нельзя сказать о члене HomeAddress, тип которого составной. При загрузке из полей ввода те данные, которые соответствуют членам структуры Address, будут просто про- игнорированы и соответствующие параметры команды вставки нечем будет заполнить. Поэтому все члены структуры данных Address следует сделать непосредственными членами класса Employee. Примечание Напомню, что компоненты, представляющие источники данных, исполь- зуются на полную мощность только некоторыми элементами управления ASP.NET 2.0, к числу которых относятся GridView (глава 10) и DetailsView (глава 11). Рассматривая их, мы еще вернемся к механизму связывания параметров. А пока достаточно сказать, что как автор страницы вы отвечаете за то, чтобы входные поля формы и члены класса, представляющего сущность, соответствовали друг другу. У столбцов элемента управления GridView и строк элемента управления DetailsView имеется атрибут DataField, в котором должно быть задано используемое поле источника данных (например, имя столбца базы данных, содержимое которого извлекается с помощью операции выборки данных). Зна- чение этого атрибута должно соответствовать (без учета регистра) открытому свойству класса, используемого в качестве параметра операции обновления или вставки, в нашем примере класса Employee. В отличие от операции вставки в операции обновления используется значение первичного ключа, по которому идентифицируется обновляемая запись. Если задается
Модель связывания с данными Глава 9 375 список параметров, то достаточно добавить в этот список дополнительный параметр, представляющий поле первичного ключа: <asp:ObjectDataSource runat="server" ID="MyObjectSource" TypeName=”ProAspNet20 SimpleBusinessObject SelectMethod="GetEmployees" UpdateMethod="SetEmployee"> <UpdateParameters> <asp:Parameter Name="employeeid" Type="Int32" /> <asp:Parameter Name="firstname" Type="string" /> <asp:Parameter Name="lastname" Type="string" /> <asp Parameter Name="country" Type="string" DefaultValue="null" /> </UpdatePa ramete rs> </asp:Obj ectDataSou rce> Заметьте, что в том случае, когда атрибут DefaultValue содержит значение null, параметр является необязательным. Разумеется, пустое значение параметра должно правильно обрабатываться методом, выполняющим обновление данных. Существует альтернативный способ определения первичного ключа — посредством свойства DataKeyNames элемента управления GridView или DetailsView. <asp:GridView runat="server" ID="grid1” DataKeyNames="employeeid DataSou rceId="MyObj ectSou rce" AutoGenerateEditButton="true"> </asp:GridView> Когда для элемента управления, связанного с данными, задан атрибут DataKeyNames, компонент, представляющий источник данных, автоматически добавляет в список параметров команд обновления и удаления еще один параметр, предназначенный для передачи этим командам первичного ключа. По умолчанию именем указанного пара- метра является original_ХХХ, где XXX — значение атрибута DataKeyNames. Для того чтобы операция была выполнена успешно, метод (или SQL-команда, если использу- ется компонент SqlDataSource') должен иметь параметр с таким именем. Вот пример: UPDATE employees SET lastname=@lastname WHERE employeeid=@original_employeeid Имя параметра, в котором задается первичный ключ, можно модифицировать с по- мощью свойства ParameterFormatString. Например, когда это свойство имеет значение {О}, можно использовать такую команду: UPDATE employees SET lastname=@lastname WHERE employeeid=@employeeid Установка свойства DataKeyNames связанного элемента управления (имейте в виду, что это не свойство компонента, представляющего источник данных) — простейший способ конфигурирования операции удаления. Ведь для ее выполнения незачем за- давать значения других полей строки — нужен лишь первичный ключ. Примечание Свойство DataKeyNames новых элементов управления ASP.NET 2.0, связан- ных с данными (таких как GridView и DetailsView), введено вместо свойства DataKeyField, которое имелось в ASP.NET 1.x у элементов управления DataGrid и DataList Разница между этими двумя свойствами заключается в том, что DataKeyNames поддерживает составные ключи, образуемые значениями нескольких полей. Если, к примеру, значением этого свойства является id,пате, то есть в нем задано два поля, то добавляются два параметра: originaljd и original_name.
376 Часть II Размещение данных на сайте Конфигурирование параметров во время выполнения При использовании компонента ObjectDataSource с новыми элементами управления ASP.NET 2.0 (например, с элементом управления GridView), их связывание с данны- ми обычно осуществляется автоматически. В случае, если все же придется заняться этим самостоятельно, можно контролировать процесс обновления, используя событие Updating: protected void Updating(object sender, ObjectDataSourceMethodEventArgs e) { Employee emp = (Employee) e.InputParameters[O]; emp.LastName = "Whosthisguy"; } Это событие генерируется непосредственно перед завершением операции обнов- ления. Коллекция InputParameters содержит список параметров, передаваемых мето- ду, который осуществляет обновление. Эта коллекция доступна только для чтения. Точнее, добавлять и Удалять ее элементы нельзя, но, как показывает приведенный код, их разрешается модифицировать Описанная технология используется, в ситуациях, когда по той или иной причине компонент ObjectDataSource не загрузил все данные, необходимые для выполнения операции обновления. Аналогичным образом можно действовать при вставке и уда- лении данных. Компонент SiteMapDataSource Для любого современного и дружественного к пользователю сайта характерно наличие такого элемента, как его карта. Картой сайта называется схема, в кото- рой представлены все его страницы и каталоги. Она используется для отображения логических координат пользователя на сайте при посещении им разных страниц, а также для предоставления ему возможности быстро переходить к нужной точке сайта (рис. 9-11). Рис. 9-11. Web-сайт MSDN Magazine с элементом, отображающим местоположение текущей страницы в иерархии страниц сайта В состав ASPNET 2.0 входит богатая навигационная инфраструктура, позволяю- щая разработчикам документировать структуру сайта. О ней подробно рассказывается в моей книге «Programming Microsoft ASPNET 2-0 Applications: Advanced Topics» (Microsoft Press, 2005). Пока же вам достаточно знать, что карта сайта представляет собой иерархическую информацию, которая может подаваться на вход иерархических
Модель связывания с данными Глава 9 377 компонентов, представляющих источники данных, и в частности компонента SiteMap- DataSource. Его вывод можно связывать с иерархическими элементами управления, такими как Menu, SiteMapPath и Tree View. Отображение информации карты сайта Информация карты сайта может храниться в разной форме; типичным ее источником является XML-файл с именем web.sitemap, находящийся в корневом каталоге при- ложения. Для того чтобы вы лучше поняли назначение карты сайта и ее источников, я приведу несколько примеров. Предположим, что вы создаете сайт и клиент про- сит вывести на каждой его странице последовательность гиперссылок, отражающих местоположение этой страницы на карте сайта. В ASP.NET 1.x вам пришлось бы создавать собственную инфраструктуру для хранения информации карты сайта и вывода указанной последовательности ссылок. (Скорее всего, вы бы записали данные в конфигурационный файл и создали для их отображения пользовательский элемент управления.) Но в ASP.NET 2.0 существует гораздо лучшее решение — эта система содержит готовые средства выполнения указанной задачи. Для начала вы создаете в корневом каталоге приложения XML-файл с именем web.sitemap и описываете в нем связи между страницами, а затем с помощью подходящего элемента управления отображаете информацию из этого файла. Для создания такой цепочки гиперссылок, как на рис. 9-11, используется элемент управления SiteMapPath. Он считывает информацию карты сайта из файла, в котором она хранится, и генерирует необходимую разметку. В простейшем случае компонент SiteMapDataSource вам вообще не потребуется. Но если необходимо сформировать сложное иерархическое представление информации, например выводить ее в виде дерева, то без этого компонента не обойтись. Компонент SiteMapDataSource передает информацию карты сайта иерархическому элементу управления, связанному с данными. Вот небольшой пример — страница с элементом управления Tree View, показанная на рис. 9-12: <%@ Page Language="C#" %> <html> <body> <form runat=”server"> <asp:SiteMapDataSource runat="server" ID='MySiteMapSource” /> <asp:TreeView runat="server” DataSourceId="MySiteMapSource" /> </form> </body> </html> А вот содержимое файла карты сайта, которую вы видите на рисунке: <siteMap> <siteMapNode title="Home" url="default aspx” > <siteMapNode title=”Acknowledgements" url="ack.aspx”/> <siteMapNode title="References" url="ref.aspx" /> <siteMapNode title="Samples"> <siteMapNode title-'Part 1"> <siteMapNode title="Chapter 1" /> <siteMapNode title=”Chapter 2” /> <siteMapNode title="Chapter 3”> j <siteMapNode title-"Dynamic Controls” url=".../dynctls aspx” /> <siteMapNode title="ViewState" url=".../viewstate.aspx” /> ' </siteMapNode> <siteMapNode title=”Chapter 4" />
378 Часть II Размещение данных на сайте </siteMapNode> <siteMapNode title="Part 2‘> <siteMapNode title="Chapter 9"> <siteMapNode title="Site map" url=".../sitemapinfo.aspx” /> </siteMapNode> </siteMapNode> OiteMapNode title="Part 3" url="samples.aspx?partid=3" /> </siteMapNode> </siteMapNode> </siteMap> Рис. 9-12. Информация карты сайта, выведенная с помощью элемента управления TreeView Заметьте, что атрибут url не является обязательным. Если он не задан, узел счита- ется контейнером, предназначенным для вставки в него страниц в будущем, и когда карта сайта выводится на странице, щелкать на таком узле нельзя. Д Примечание Как уже упоминалось, в ASP.NET 2.0 введена новая категория элементов управления, связанных с данными, которой не было в предыдущих версиях этой систе- мы, — иерархические элементы управления. Для них определен и новый базовый класс с минимальным набором функций — HierarchicalDataBoundControl. Элементы управления Tree- View и Menu относятся к упомянутой категории и являются производными от этого класса. Программный интерфейс компонента SiteMapDataSource Свойства компонента SiteMapDataSource перечислены в табл. 9-15. Табл. 9-15. Свойства компонента SiteMapDataSource Свойство Описание Provider Идентифицирует объект провайдера карты сайта, связанный с ком- понентом SiteMapDataSource ShowStartingNode Указывает, должен ли извлекаться и отображаться корневой узел карты; по умолчанию данное свойство имеет значение true SiteMapProvider Возвращает и позволяет задать имя провайдера карты сайта, связан- ного с экземпляром компонента
Модель связывания с данными Глава 9 379 Табл. 9-15. (окончание) Свойство Описание StartFromCurrentNode Указывает, следует ли извлечь лишь поддерево, начинающееся с текущей страницы StartingNodeOffset Возвращает и позволяет задать положительное или отрицательное смещение отображаемого поддерева от начального узла, по умолча- нию данное свойство имеет значение 0 StartingNodeUrl Задает URL корневого узла отображаемого поддерева По умолчанию начальным узлом отображаемого дерева является корневой узел иерархии, но можно изменить эту установку, используя одно из двух взаимоисключа- ющих свойств: StartFromCurrentNode или StartingNodeUrl. Если вы явно задаете URL страницы, которая должна быть началом иерархии, то свойство StartFromCurrentNode следует установить в false. И наоборот, устанавливая это свойство в true, убеди- тесь, что свойство StartingNodeUrl содержит пустую строку. Что касается свойства StartingNodeOffset, то оно используется в дополнение к позиции начального узла, определяемой свойством StartFromCurrentNode или StartingNodeUrl, и позволяет задать положительное или отрицательное смещение от этой позиции. Класс SiteMapDataSource имеет также два взаимоисключающих свойства, позволя- ющие указать провайдер карты сайта, — в свойстве SiteMapProvider провайдер задается по имени, а в свойстве Provider — по ссылке на его объект. Компонент XmlDataSource Компонент XmlDataSource используется для доступа к XML-данным и поддерживает как табличное, так и иерархическое их представление, причем первое является просто списком узлов определенного уровня полной иерархии данных. Отдельный XML-узел представлен экземпляром класса XmlNode, а весь документ — экземпляром класса XmlDocument. Данные компонента XmlDataSource доступны только для чтения. tgb Внимание! Интересной особенностью компонента XmlDataSource является то, что он единственный из встроенных компонентов, представляющих источники данных, реали- зует одновременно два интерфейса: IDataSource и /HierarchicalDataSource. Правда, он реализует только один метод каждого из них — Select, методы Delete, Insert и Update у компонента XmlDataSource отсутствуют. Поэтому он не подходит для Web-приложений, которым необходима возможность не только чтения, но и Записи XML-данных. Программный интерфейс компонента XmlDataSource Свойства компонента XmlDataSource перечислены в табл. 9-16. Табл. 9-16. Свойства компонента XmlDataSource Свойство Описание CacheDuration Срок хранения данных в кэше в секундах CacheExpirationPolicy Указывает, абсолютным или скользящим является срок хранения данных в кэше. В первом случае данные становятся недействитель- ными спустя заданное время после их помещения в кэш, а во вто- ром — спустя заданное время после их последнего использования CacheKeyDependency Определяет имя пользовательского ключа кэша, связанного со все- ми элементами кэша, созданными данным компонентом. Изменив этот ключ, можно очистить кэш компонента Data Содержит блок XML-текста, с которым должен быть связан компонент см. след. стр.
380 Часть II Размещение данных на сайте Табл. 9-16. (окончание) Свойство Описание DataFile Путь к файлу, содержащему данные для компонента EnableCaching Позволяет включать и отключать функцию кэширования Transform Блок XSLT-текста, который будет использоваться для преобразова- ния XML-данных TransformArgumentList Список параметров XSLT-преобразования TransformFile Путь к файлу .xsl, в котором содержится определение XSLT- преобразования XPath XPath-запрос к XML-данным Компонент XmlDataSource принимает XML-данные, содержащиеся в указанном файле или просто заданные в виде строки. Если установлены оба свойства, DataFile и Data, предпочтение отдается первому из них. Заметьте, что свойство Data может быть задано декларативно с помощью одноименного тэга. Более того, содержимое указанного свойства (а это потенциально большой блок текста) сохраняется в состо- янии представления независимо от заданных вами параметров кэширования. Поэтому если связать компонент со статическим текстом, то возникнет вероятность много- кратной пересылки XML-данных между клиентом и сервером в составе состояния представления страницы, а также хранения их в кэше для ускорения доступа. Таким образом, если вы используете свойство Data и включили кэширование, то по крайней мере отключите для компонента функцию управления состоянием представления. Но учтите, что данная мера скажется и на других свойствах компонента, а это может быть нежелательно. Если при включенной функции кэширования изменить значение свойства DataFile или Data, кэш будет очищен О данном изменении уведомляет событие DataSource- Changed. Отображение XML-данных Обычно компонент XmlDataSource связывают с иерархическим элементом управле- ния, таким как TreeView или Menu. (В ASP.NET 2.0 других встроенных иерархических элементов управления нет, но вы можете создать их сами или приобрести разработки сторонних производителей.) Чтобы понять, как работает компонент XmlDataSource, взгляните на приведенное ниже содержимое файла, в котором в XML-форме хранятся записи таблицы Employees из базы данных Northwind: <MyDataSet> <NorthwindEmployees> <Employee> <employeeid>1</employeeid> <lastname>Davolio</lastname> <firstname>Nancy</flrstname> <title>Sales Representative</title> </Employee> <NorthwindEmployees> <MyDataSet> Предположим, что этот файл мы связали с экземпляром компонента XmlData- Source, а его —• с элементом управления TreeView; .
Модель связывания с данными Глава 9 381 <asp:XmlDataSource runat="server" ID="XmlSource" DataFile="employees.xml" /> <asp TreeView runat= "server" DataSourceId="XmiSource'"> </asp:TreeView> Что получится в результате, показано на рис. 9-13. Рис. 9-13. Структура XML-файла, отображенная с помощью элемента управления TreeView Как видите, на странице выводится только структура данных. Для того чтобы ото- бразить сами эти данные, необходимо соответствующим образом сконфигурировать процесс связывания: <asp:TreeView runat="server" DataSourceId=”XmiSource"> <DataBindings> <asp:TreeNodeBinding Depth="3” DataMember="employeeid" TextField="ftinnertext" /> Osp.TreeNodeBinding Depth="3" DataMember="lastname TextField="#innertext" /> Osp.TreeNodeBinding Depth="3" DataMember="firstname" TextField=”#innertext" /> Osp:TreeNodeBinding Depth="3" DataMember="title" TextField="#innertext" /> </DataBinoings> </asp:TreeView> В разделе <DataBindings> элемента управления TreeView определяется раскладка узлов дерева. В узлах <TreeNodeBinding> задается О-базированное значение глубины (атрибут Depth) узла (атрибут DataMember) и указывается, в каком атрибуте содер- жится текст, выводимый в этом узле (TextField). Атрибуту TextField может быть при- своено либо имя атрибута из исходного XML-документа, либо значение #innertext, указывающее, что нужно просто вывести тело узла. На рис. 9-14 представлен вывод элемента управления TreeView, сконфигурированного, как показано выше.
382 Часть II Размещение данных на сайте Рис. 9-14. XML-данные, отображаемые посредством элемента управления TreeView Для того чтобы эффективно использовать элемент управления TreeView, вам нуж- но еще многое узнать о его конфигурировании. Необходимые сведения вы найдете в моей книге «Programming Microsoft ASP.NET 2.0 Applications: Advanced Topics» (Microsoft Press, 2005). Данные, возвращаемые компонентом XmlDataSource, могут быть отфильтрованы с помощью запроса XPath: <asp:xmldatasource runat="server" ID="XmlSource" DataFile="employees.xml” XPath="MyDataSet/NorthwindEmployees/Employee" /> В этом примере выводятся только узлы <Етр1оуее>, без корневого узла, иденти- фицирующего таблицу, к которой они принадлежат. Учтите, что выражения XPath чувствительны к регистру символов. Примечание Компонент XmlDataSource автоматически кэширует данные, поскольку его свойство EnableCaching по умолчанию установлено в true. Заметьте, что для длительности кэширования при этом задано значение 0, то есть данные остаются в кэше бесконечно (точнее до тех пор, пока в их XML-источник не будут внесены изменения). Трансформация XML-данных Класс XmlDataSource поддерживает трансформацию данных с использованием XSLT- преобразований (Extensible Stylesheet Language Transformation — трансформация с использованием расширяемого языка таблицы стилей). Файл с описанием необхо- димого преобразования задается в свойстве TransformFile, но преобразование можно задать непосредственно в свойстве Transform. Аргументы преобразования задаются в свойстве TransformArgumentList. XSLT-трансформация применяется в тех случаях, когда структура XML-документа не отвечает требованиям кода, который будет выполнять обработку данных. Заметьте, что после преобразования данные компонента XmlDataSource доступны только для чтения и их нельзя изменить или сохранить в исходном документе.
Модель связывания с данными Глава 9 383 Заключение Связывание элементов управления с данными в ASP.NET может осуществляться тремя разными способами: путем классического связывания с источником данных, как это делалось в ASP.NET 1.x, с помощью компонентов, представляющих источники данных, д также с использованием выражений связывания с данными. Выражения связывания с данными имеют то важное отличие, что задаются декларативно и, как правило, для элементов управления, использующих шаблоны. Они представляют вычисляемые значения, которые можно связывать с произвольными свойствами. В ASP.NET 2.0 поддержка выражений была расширена и вышла за рамки связывания с данными. Теперь поддерживаются еще и пользовательские выражения, вычисляемые при за- грузке страницы, а не только после инициирования процесса связывания с данными. Модель связывания с данными ASP.NET 1.x основывалась на применении пере- числимых коллекций, которые связывались с элементами управления посредством свойства DataSource и еще нескольких свойств. В ASP.NET 2.0 было введено новое семейство элементов управления — компоненты, представляющие источники данных. Благодаря тому, что они реализованы как элементы управления, эти компоненты можно декларативно включать в состав страниц. По этой же причине они могут поль- зоваться преимуществами инфраструктуры выполнения страниц ASPNET, такими как поддержка состояния представления и кэширование. Компоненты, представляющие источники данных, принимают параметры, подготавливают команды, выполняют их и возвращают результаты. Выполняемые ими команды типичны: выборка данных, вставка, обновление, удаление, агрегирование. Наиболее важной особенностью компонентов, представляющих источники данных, является их тесная интеграция с новыми интеллектуальными элементами управ- ления, связанными с данными, — GridView и DetailsView. Они содержат логику для автоматического выполнения стандартных операций и избавляют разработчика от кодирования рутинных действий. Компоненты, представляющие источники данных, можно связывать и со старыми элементами управления, которые специально для этого были несколько модифицированы, однако, в таком случае используются не все возможности указанных компонентов. Итак, компоненты, представляющие источники данных, обеспечивают возмож- ность декларативного программирования, не требующего написания программного кода. Однако нужно понимать, что это просто средства и что они подходят не для каждой задачи. В следующей главе мы продолжим изучение темы связывания элементов управ- ления с данными и рассмотрим ее с другой перспективы — исследуем сами элементы управления, связанные с данными. Только факты В ASP.NET 2.0 все элементы управления, связанные с данными, поддерживают два механизма связывания: классический, действующий в ASP.NET 1.x и основанный на использовании перечислимых коллекций данных, а также новый, основанный на применении компонентов, представляющих источники данных. Компоненты, представляющие источники данных, — это обычные серверные эле- менты управления; они не имеют пользовательского интерфейса, зато интеллек- туально взаимодействуют с элементами управления, у которых он есть, предостав- ляют им данные и обрабатывают команды пользователя. Компоненты, представляющие источники данных, упрощают разработку страниц за счет сокращения кода, который приходится писать программисту.
384 Часть II Размещение данных на сайте Существовавшие в ASP.NET 1.x элементы управления, связанные с данными, был | модифицированы для поддержки новых компонентов, представляющих источники данных, но полной переработке, необходимой для использования всех преимуществ этих компонентов, они не подвергались. Зато в ASP.NET 2.0 введены два новых элемента управления, GridView и DetailsView, специально предназначенные для работы с такими компонентами. Наряду с выражениями связывания с данными в ASP.NET 2.0 могут использоваться новые выражения (так называемые $-выражения); с их помощью можно, в част- ности, динамически, но декларативным способом, конфигурировать компоненты, представляющие источники данных. $-выражения отличаются от #-выражений тем, что обрабатываются во время ком- пиляции и включаются в состав исходного кода страницы. Наиболее интересным из компонентов, представляющих источники данных, яв- ляется ObjectDataSource, который может служить мостом между слоями представ- ления и доступа к данным, даже если они удалены друг от друга. Этот компонент использует возможности существующих слоев приложения и превращает его до- менную архитектуру в по-настоящему многоуровневую. • Компонент XmlDataSource является одновременно и табличном, и иерархическим, но предоставляемые им данные доступны только для чтения.
Глава 10 I ! Таблицы, связанные с данными Элементы управления, связанные с данными, играют огромную роль в разработке при- ложений ASP.NET. Такие элементы позволяют связать отдельные свойства или даже весь свой пользовательский интерфейс с одним или более столбцами, прочитанными из совместимого с .NET источника данных. Основы работы элементов управления, связанных с данными, были рассмотрены в главе 9, а теперь детально изучим два таких элемента, весьма популярных у разработчиков приложений ASP.NET, DataGrid из ASP.NET 1.x и GridView из ASP.NET 2.0. Оба элемента выводят многоколоночную таблицу, могут использовать шаблоны, заданные разработчиком страницы, обладают гибко настраиваемым пользовательским интерфейсом и поддерживают как чтение, так и запись данных. Имея продвинутый программный интерфейс и богатый набор атрибутов, элементы управления DataGrid и GridView просто генерируют HTML-таблицу с набором гиперссылок для выполнения таких стандартных операций, как сортировка, листание, выделение и редактирование. Поэтому несмотря на возможности настройки табличные элементы управления имеют относительно жесткую структуру — данные, связанные с элементами DataGrid и GridView, всегда выводятся в виде таблицы, хотя содержимое ее ячеек можно до некоторой степени настраивать, используя системные и пользовательские шаблоны. В большинстве управляемых данными приложений ASP.NET 1.x элемент управ- ления DataGnd играет одну из важнейших ролей. Подобно другим элементам управ- ления ASP.NET 1.x, элемент DataGrid полностью поддерживается и в ASP.NET 2.0. Однако здесь его роль не столь значительна, поскольку для его замены разработан новый элемент управления GridView, нередко используемый совместно с другими элементами управления, в частности с элементами DetailsView и Form View, которые мы рассмотрим в следующей главе. В целом элемент GridView обладает теми же воз- можностями, что и DataGrid, но они значительно усовершенствованы и расширены. Мы начнем эту главу с ознакомления с функциями элемента управления DataGrid и обсудим его важнейшие ограничения и недостатки. Затем мы обратимся к элементу управления GridView и подробно изучим его программный интерфейс. При разработке нового приложения ASP.NET 2.0 вопрос о выборе между этими двумя элементами управления не стоит. Однако если вам придется переносить на эту платформу при- ложение, разработанное для ASP.NET 1.x, то желательно будет заменить в нем эле- менты DataGrid элементами GridView (что достаточно просто сделать) — тем самым вы заложите основы для его будущего совершенствования. Элемент управления DataGrid Табличный элемент управления DataGrid может включать столбцы трех основных типов — текстовые, основанные на шаблонах и содержащие командные кнопки. Вы связываете его с данными с помощью свойства DataSource в ASP.NET 1.x или свойства DataSourcelD в ASP.NET 2.0 Простейшее объявление элемента управления DataGrid: <asp:DataGrid runat="server” id="grid" />
386 Часть II Размещение данных на сайте Объектная модель элемента управления DataGrid Элемент управления DataGrid выводит содержимое источника данных в виде таблицы, каждый столбец которой соответствует полю источника данных, а каждая строка — его записи. Поскольку элемент DataGrid поддерживает ряд стилевых и прочих визуаль- ных свойств, в реальных приложениях представляющая его разметка, содержащаяся в файле .aspx, больше и сложнее приведенного выше простейшего объявления эле- мента. Но в ASP.NET 2.0 ее можно упростить за счет применения темы, которая будет содержать все необходимые визуальные установки. Свойства элемента управления DataGrid Свойства элемента управления DataGrid, за исключением унаследованных им от классов Control и WebControl, описаны в табл. 10-1. Табл. 10-1. Свойства элемента управления DataGrid Свойство Описание AllowCustomPaging Возвращает и позволяет задать значение, указывающее, разрешено ли пользовательское разбиение на страницы. Чтобы эта установка действовала, свойство AllowPaging должно иметь значение true AllowPaging Возвращает и позволяет задать значение, указывающее, разрешено ли разбиение на страницы AllowSorting Возвращает и позволяет задать значение, указывающее, разрешена ли сортировка строк AltematingltemStyle AutoGenerateColumns Возвращает стилевые свойства четных строк Возвращает и позволяет задать значение, указывающее, должен ли элемент управления автоматически создавать столбцы, соответству- ющие полям источника данных; по умолчанию имеет значение true BacklmageUrl Возвращает и позволяет задать URL изображения, выводимого в качестве фона элемента управления Caption Определяет текст заголовка элемента управления. Это свойство введено для повышения доступности элемента управления. В ASP.NET 1х данное свойство не поддерживается CaptionAlign Определяет способ выравнивания заголовка элемента управления. BASP.NET 1х данное свойство не поддерживается CellPadding Возвращает и позволяет задать расстояние в пикселах между грани- цами ячейки и содержащимся в ней текстом CellSpacing Возвращает и позволяет задать расстояние в пикселах по горизонта- ли и по вертикали между двумя последовательными ячейками Columns CurrentPagelndex Возвращает коллекцию объектов DataGridColumn Возвращает и позволяет задать индекс текущей отображаемой страницы DataKeyField Возвращает и позволяет задать ключевое поле источника данных, с которым связан элемент управления DataKeys Возвращает коллекцию значений ключей всех записей, отображае- мых в элементе управления в данный момент DataMember Указывает, с какой таблицей многотабличного источника данных следует связать элемент управления. Используется вместе со свой- ством DataSource. Если последнее содержит объект DataSet, то в свойстве DataMember задается имя объекта DataTable DataSource Возвращает и позволяет задать объект, являющийся источником данных элемента управления
Таблицы, связанные с данными Глава 10 387 Табл. 10-1. (окончание) Свойство Описание DataSourcelD Возвращает и позволяет задать имя компонента, являющегося ис- точником данных элемента управления. В ASP.NET 1.x это свойство не поддерживается Edititemindex Возвращает и позволяет задать индекс редактируемой в данный момент строки таблицы EditltemStyle Возвращает объект представляющий стилевые свойства редактиру- емой в данный момент строки таблицы FooterStyle Возвращает объект, представляющий стилевые свойства нижнего колонтитула таблицы GridLines Возвращает и позволяет задать значение, указывающее, должны ли ячейки таблицы иметь нарисованные границы HeaderStyle Возвращает объект, представляющий стилевые свойства верхнего колонтитула таблицы HorizontalAlign Возвращает и позволяет задать значение, определяющее способ вы- равнивания текста в ячейках таблицы по горизонтали Items ItemStyle Возвращает коллекцию отображаемых в данный момент элементов Возвращает объект, представляющий стилевые свойства элементов таблицы PageCount Возвращает количество страниц, которое необходимо для отображе- ния всех записей, содержащихся в источнике данных PagerStyle Возвращает объект, представляющий стилевые свойства блока листания (раздела с элементами управления, используемыми для перехода между страницами) PageSize Возвращает и позволяет задать количество элементов, выводимых на одной странице Selectedlndex Возвращает и позволяет задать индекс выделенного в данный мо- мент элемента Selectedltem Возвращает объект DataGridltem, представляющий выделенный элемент SelectedltemStyle Возвращает объект, представляющий стилевые свойства выделенно- го в данный момент элемента ShowFooter Указывает, должен ли выводиться нижний колонтитул; по умолча- нию имеет значение false ShowHeader Указывает, должен ли выводиться верхний колонтитул; по умолча- нию имеет значение true UseAccessibleHeader Указывает, должен ли верхний колонтитул элемента управления вы- водиться в формате, отвечающем требованиям доступности, то есть посредством тэгов <th>, а не <td>. В ASP.NET 1jc данное свойство не поддерживается VirtualltemCount Возвращает и позволяет задать виртуальное количество элементов таблицы при пользовательском разбиении на страницы Главными составляющими элемента управления DataGrid являются коллекции Co- lumns и Items, стилевые свойства, а также свойства, предназначенные для связывания с данными. Напомню, что в каждый конкретный момент времени на экране отображается лишь некоторое подмножество записей из источника данных элемента управления — это подмножество и возвращает коллекция Items. Класс ее элементов, DataGridltem, явля- ется специализированной версией известного вам класса Table Row.
388 Часть II Размещение данных на сайте Примечание В ASP NET 2.0 введена группа новых свойств, предназначенных для по- вышения доступности элементов управления для пользователей с ограниченными фи- зическими возможностями. К числу таких свойств относятся Caption, CaptionAlign и Use- AccessibleHeader, позволяющие откорректировать генерируемую элементом управления разметку, чтобы ее правильно воспринимали вспомогательные технологические средства, применяемые такими пользователями. Структура вывода элемента управления DataGrid Вывод элемента управления состоит из нескольких элементов, типы которых пред- ставлены членами перечисления ListltemType. Важнейшие их этих элементов показаны на рис. 10-1. Верхний колонтитул (Header) ip|rirst Namejl-ast Name| 1 Nancy Davolio fignmn. 2 ГЛег 3 Janet Levelling 4 Margaret Peacock 5 Steven Buchanan 6 M'Chaei SDyama 7 Robert King j 8 Laura Callahan 9 Anne Dodsworth 9 records - Previous Next Элемент (Item) Нечетный элемент (Alternating Item) Нижний колонтитул (Footer) Блок листания (Pager) Рис. 10-1. Структура вывода элемента управления DataGrid Пользовательский интерфейс элемента управления DataGrid состоит из логических элементов, перечисленных в табл. 10-2. Каждый элемент имеет собственное стилевое свойство, где задается набор автоматически применяемых графических установок. Табл. 10-2. Логические составляющие элемента управления DataGrid Тип элемента Описание Footer Представляет нижний колонтитул (подвал) таблицы. Этот элемент не может быть связан с источником данных, а его стилевое оформление определяется значением свойства FooterStyle Header Представляет верхний колонтитул (шапку) таблицы, состоящий из за- головков ее столбцов. Этот элемент не может быть связан с источником данных, а его стилевое оформление определяется значением свойства HeaderStyle Item Представляет связанную с данными строку таблицы. Его стилевое оформление определяется значением свойства ItemStyle Altematingltem Представляет четную связанную с данными строку таблицы. Его стиле- вое оформление определяется значением свойства AltematingltemStyle Pager Представляет блок листания. Этот элемент не может быть связан с ис- точником данных, а его стилевое оформление определяется значением свойства PagerStyle. Блок листания можно разместить под таблицей, над ней либо и там, и там Selectedltem Представляет выделенную в данный момент строку таблицы (неважно, четную или нечетную). Его стилевое оформление определяется значени- ем свойства SelectedltemStyle < ,
Таблицы, связанные с данными Глава 10 389 Когда приходит время создания каждого из этих элементов, генерируется событие ItemCreated, в обработчике которого можно выполнить те или иные связанные с ним действия. Записи источника данных и отображаемые строки Источником данных элемента управления DataGrid может быть либо перечислимая коллекция объектов, содержащих данные, либо, в ASP.NET 2.0, компонент, представ- ляющий источник данных. Каждой записи источника данных потенциально соответ- ствует строка таблицы, которую выводит элемент управления, однако на практике это не всегда так. Все те строки, которые отображаются в данный момент, содержатся в коллекции Items. Каждый ее элемент является экземпляром класса DataGridltem (расширенной версии класса TableRow) и имеет свойство Dataltem, в котором содер- жится ссылка на объект, представляющий запись в источнике данных. Заметьте, что в коллекции Items находятся только те элементы таблицы, которые связаны с данны- ми, а нижний и верхний колонтитулы, как и блок листания, отсутствуют. Индексные свойства элемента управления DataGrid служат для доступа к ото- бражаемым строкам, а не к записям в источнике данных. Так, при выборе элемента с индексом 1 выбирается второй из отображаемых элементов данных, но в источнике данных индекс этого элемента может быть каким угодно в зависимости от того, какая страница выводится в настоящий момент. Абсолютный индекс элемента в источнике данных хранится в свойстве DataSetlndex объекта DataGridltem. Такой способ доступа к записи не всегда удобен, поскольку, например, когда нужно извлечь не только ее, но и связанные с ней записи дочерней таблицы, важен прежде всего не порядковый номер этой записи, а значение ее ключа. В подобных ситуациях можно воспользоваться коллекцией DataKeys и свойством DataKeyField. Свойству DataKeyField при конфигурировании элемента управления DataGrid вы присваиваете имя ключевого поля таблицы данных. На этапе связыва- ния с данными элемент управления извлекает из их источника значения ключевых полей отображаемых записей и помещает эти значения в коллекцию DataKeys. А уж из этой коллекции нетрудно извлечь значение ключа по индексу выделенной строки. Давайте рассмотрим следующее объявление элемента управления DataGrid, в котором выводится информация о служащих компании: <asp:DataGrid runat="server" id="grid" DataKeyField="employeeid” ... > Для того чтобы узнать идентификатор выбранного пользователем служащего (на- пример, для получения связанной с этим служащим информации из другой таблицы), достаточно выполнить следующий простой оператор: // Помещаем в переменную empID ключ выделенной строки int empID = grid.DataKeys[grid.Selectedlndex]; События элемента управления DataGrid Элемент управления DataGrid не имеет заслуживающих упоминания методов, однако для него определен ряд интересных событий — они описаны в табл. 10-3. События CancelCommand и UpdateCommand генерируются при редактирова- нии строки. О том, как оно осуществляется, я расскажу немного позже. Событие CancelCommand сигнализирует о том, что пользователь хочет отменить изменения, а событие UpdateCommand, напротив, о его намерении их сохранить. Остальные со- бытия —- EditCommand, DeleteCommand и SortCommand — также уведомляют о том, что пользователь задал определенную команду с помощью встроенных кнопок или гиперссылок элемента управления.
390 Часть II Размещение данных на сайте Табл. 10-3. События элемента управления DataGrid Событие Описание CancelCommand Пользователь дал команду отмены изменений, внесенных в теку- щую редактируемую им строку DeleteCommand Пользователь дал команду удаления текущей строки EditCommand Пользователь дал команду перевода текущей строки в режим ре- дактирования ItemCommand Пользователь щелкнул одну из командных кнопок элемента управ- ления ItemCreated Создан новый элемент таблицы ItemDataBound Элемент таблицы связан с данными PagelndexChanged Пользователь дал команду перехода к другой странице данных SelectedlndexChanged Пользователь выделил другую строку SortCommand Пользователь дал команду сортировки строк, щелкнув заголовок столбца UpdateCommand Пользователь дал команду сохранения изменений в отредактиро ванной им строке В дополнение к собственным событиям элемент управления DataGrid генерирует все стандартные события элементов управления Web, включая события Load, Init, PreRender и DataBinding. В частности, модифицировать сгенерированную элементом управления разметку можно сделать в обработчике события PreRender. А точкой входа процесса связывания элемента управления с данными является событие DataBinding, которое генерируется непосредственно перед началом этого процесса независимо от того, с каким объектом осуществляется связывание — перечислимой коллекцией или компонентом, представляющим источник данных. Примечание Способ обработки событий, уведомляющих о командах пользователя, яв- ляется главным отличием элемента управления DataGrid от нового элемента управления GridView. Если первый лишь уведомляет страницу о намерениях пользователя, то второй самостоятельно обрабатывает команды пользователя, автоматически производя необходи- мые вызовы компонента, представляющего источник данных. И хотя элемент управления DataGrid также поддерживает компоненты, представляющие источники данных, эта под- держка ограничивается способностью отображать предоставляемые ими данные — другой связи между этими объектами не существует. Связывание элемента управления DataGrid с данными По умолчанию в элементе управления DataGrid автоматически отображаются все столбцы источника его данных. Однако вы можете изменить поведение этого элемен- та, установив его свойство AutoGenerateColumns в false, и тогда будут отображаться только те столбцы, которые вы явно включите в коллекцию Columns. Элемент управления DataGrid поддерживает несколько типов столбцов, различа- ющихся способом представления данных. При явном заполнении коллекции Columns типы столбцов тоже нужно задавать явно; в противном случае всем им назначается простейший из типов — BoundColumn. Поддерживаемые элементом управления типы столбцов описаны в табл. 10-4. Заметьте, что свойство Auto Generate Columns и коллекция Columns не являются взаимоисключающими элементами. Если свойство установлено в true и коллекция не пуста, то таблица содержит столбцы, определенные пользователем, а также сгене- рированные автоматически.
Таблицы, связанные с данными Глава 10 391 Табл. 10-4. Типы столбцов Тип столбца Описание BoundColumn Содержимое столбца связано с полем источника данных, его ячей- ки содержат чистый текст ButtonColumn Каждая ячейка столбца содержит кнопку, текст которой может быть статическим или извлекаться из источника данных. Всем кнопкам назначается одно и то же имя команды EditCommandColumn Особый столбец, содержащий кнопки с именем команды Edit, служащие для перевода строк в режим редактирования. В этом режиме элементы строки выводятся не как литеральные значения, а как текстовые поля HyperLinkColumn Каждая ячейка столбца содержит гиперссылку, текст которой может быть статическим или извлекаться из источника данных. Целевые URL также могут быть связаны с данными. Щелчок та- кой ссылки приводит к переходу на страницу с заданным URL TemplateColumn Содержимое ячеек столбца выводится с использованием задан- ного шаблона ASP.NET. С помощью шаблона можно даже опреде- лить требуемое поведение ячеек Обычно связывание столбцов с данными осуществляется с помощью тэга <Columns> серверного элемента управления <asp:datagrid>, как в следующем примере: <asp:DataGrid runat="server" id=”grid" .. > <Columns> <asp:BoundColumn runat="server" DataField="employeeid" HeaderText="ID" /> <asp:BoundColumn runat="server" DataField="firstname" HeaderText="First Name" /> <asp:BoundColumn runat="server” DataField="lastname" HeaderText="Last Name" /> </Columns> </asp:DataGrid> Если вы хотите выполнить связывание программным способом, то создайте новый столбец как объект требуемого класса, заполните его свойства и добавьте этот объект в коллекцию Columns'. BoundColumn bo = new BoundColumn(); bc.DataField = "firstname"; be.HeaderText = "First Name"; grid.Columns.Add(be); Порядок отображения столбцов таблицы определяется порядком их добавления в элемент управления DataGrid. га Примечание Коллекция Columns не сохраняет своего содержимого в состоянии представ- ления, и после возврата формы она пуста. Поэтому динамически добавляемые столбцы нужно заново создавать при каждом возврате формы. Столбцы, связанные с данными Классы всех типов столбцов являются производными от одного класса DataGrid- Column и имеют несколько общих свойств, таких как текст заголовка, текст подвала, стиль элементов и флаг видимости. Эти общие для всех столбцов свойства описаны в табл. 10-5.
392 Часть II Размещение данных на сайте Табл. 10-5. Свойства, общие для всех столбцов Свойство Описание FooterStyle Возвращает стилевые свойства нижнего колонтитула столбца FooterText Возвращает и позволяет задать статический текст, выводимый в нижнем колонтитуле столбца HeaderlmageUrl Возвращает и позволяет задать URL изображения, выводимого в заго- ловке столбца HeaderStyle Возвращает стилевые свойства заголовка столбца HeaderText Возвращает и позволяет задать статический текст, выводимый в заголов ке столбца ItemStyle Возвращает стилевые свойства элементов, выводимых в ячейках столбца SortExpression Возвращает и позволяет задать выражение сортировки данных, осущест- вляемой по щелчку заголовка столбца Visible Возвращает и позволяет задать флаг видимости столбца Класс BoundColumn представляет столбец, связанный с полем источника даннщх. Важнейшим из свойств, которые нужно задать для определения столбца, является идентифицирующее это поле свойство DataField. В свойстве DataFormatString при желании можно задать формат выводимого текста, а свойство Readonly, позволяющее сделать данные столбца доступными только для чтения, действует лишь при условии, что таблица содержит столбец с кнопками перехода в режим редактирования. Ниже приведен код, с помощью которого в таблицу добавляются два столбца, для каждого из них заданы заголовок и поле источника данных Кроме того, для второго столбца определена строка форматирования, чтобы содержащиеся в нем Значения выводились как денежные суммы с выравниванием по правому краю. <asp boundcolumn runat="server" datafield="quantityperunit” headertext="Packaging" /> <asp:boundcolumn runat="server" datafield=”unitprice" headertext="Price” DataFormatString="{0:c}"> <itemstyle width="80px" horizontalalign="right" /> </asp:boundcolumn> Графические установки столбца задаются с помощью дочернего стилевого элемента <itemstyle>. Столбцы гиперссылок Столбец, в ячейках которого выводятся гиперссылки, представлен классом BoundCol- umn. Для каждой из гиперссылок разработчик может определить текст и URL, Оба эти значения при желании можно связать с полями источника данных. В таком случае в свойстве DataTextField объекта BoundColumn задается имя поля, содержащего текст гиперссылок, а в свойстве DataNavigateUrlField — имя поля, содержащего их URL. Свойство DataNavigateUrlFormatString определяет формат URL, а в свойстве target задается целевой фрейм. Используемые совместно свойства DataNavigateUrlField и DataNavigateUrlFormatString позволяют перенаправить пользователя к определенной странице, передав в строке HTTP-запроса информацию, которая идентифицирует выбранную пользователем строку таблицы: <asp:hyperlinkcolumn runat="server" datatextfield="productname" headertext="Product" datanavigateurlfield="productid"' datanavigateurlformatstring=”productin*:o aspx?id={0}" target="ProductView">
Таблицы, связанные с данными Глава 10 393 <itemstyle width="200px” /> </asp:tiyperlinkcolumn> Гиперссылки, выводимые в этом столбце, будут указывать на одну и ту же стра- ницу (productinfo.aspx), открываемую во фрейме ProductView. Однако для каждой из ссылок на этой странице будет выводиться своя информация — подробные сведения о выбранном пользователем товаре (эта страница должна будет прочитать из строки HTTP-запроса значение параметра id и извлечь из базы данных информацию о то- варе с таким идентификатором). За правильное построение конечного URL отвечает класс столбца. [у] Примечание Как видите, задавая определенным образом значения свойств DataNavi- gateUrlField и DataNavigateUrlFormatString, можно определять для гиперссылок параме- тризированные URL. Однако таким способом может быть определен только один пара- метр — значение поля, указанного в свойстве DataNavigateUrlField. Если же требуется задать больше параметров, можно заменить ссылочный столбец шаблонным или вос- пользоваться элементом управления GridView. Столбцы кнопок Столбец класса ButtonColumn содержит определяемые пользователем командные кнопки Функционально он подобен столбцу гиперссылок, но отличается от него тем, что возврат формы всегда осуществляется той же странице. И хотя надписи на кнойках мо!ут Извлекаться из поля источника данных, чаще все кнопки такого столбца содержат один и тот же статический текст. Назначением кнопок, выводимых в столбце типа ButtonColumn, является выпол- нение определенного действия, связанного с выбранной пользователем строкой. Все кнопки ассоциируют с определенным сценарием, который выполняет возврат формы и инициирует вызов серверного обработчика события ItemCommand. В нем, используя имя команды (свойство CommandName), вы можете определить, в каком именно кно- почном столбце (если их несколько) щелкнул пользователь, и по значению свойства Itemindex объекта DataGridltem идентифицировать текущую строку таблицы. Ссылка на объект DataGridltem передается обработчику события ItemCommand. Особым типом кнопочного столбца является столбец с именем команды Select. Ког- да пользователь щелкает содержащуюся в нем кнопку, элемент управления DataGrid автоматически перерисовывает выделенную строку, используя другие визуальные установки — те, что заданы в свойстве SelectedltemStyle. Обработчик события Item- Command в этом случае писать не нужно: <asp:ButtonColumn runat=”server" text="Select" CommandName="Select" /> СтиЛ1 выделенной строки (а в каждый момент времени может быть выделена только одна строка) задается так: <selecteditemstyle backcolor="cyan" /> О том, что пользователь выделил другую строку, сигнализирует событие Select- edlvdexChanged. Однако прежде чем оно будет сгенерировано, приложение обработает событие ItemCommand. Когда поток выполнения достигнет обработчика события SelectedlndexChanged, свойство Selectedlndex будет уже содержать индекс новой вы- деленной строки. Столбцы с шаблонами Если определить для столбца таблицы шаблон, то в нем можно разместить элементы управления, связанные с любыми полями источника данных, да еще и оформить их с использованием стилевых HTML-атрибутов, например вывести одни поля курсивом,
394 Часть II Размещение данных на сайте а другие полужирным шрифтом. Если несколько столбцов имеют один и тот же ша- блон, его нужно повторить для каждого из них. Столбец с шаблоном объявляется с помощью тэга <TemplateColumn>, тело которого может содержать до четырех разных шаблонов: ItemTemplate, EditltemTemplate, Head- erTemplate и FooterTemplate. Как и столбцы других типов, столбец с шаблоном может иметь заголовок и с ним можно связать выражение сортировки. Однако поле источ- ника данных для такого столбца не задается. Связывание столбца с одним или более полями осуществляется посредством выражений связывания с данными (см. главу 9). Для вычисления такого выражения и приведения результата к нужному типу ис- пользуется метод Eval. В качестве примера ниже представлено определение столбца типа TemplateColumn, который имитирует поведение столбца BoundColumn, связанного с полем lastname источника данных: <asp:templatecolumn runat="server" headertext="Last Name"> <itemtemplate> <asp:label runat="server" Text=’<%# DataBinder.Eval(Container.Dataltem, "lastname") %>’ /> </itemtemplate> </asp:templatecolumn> С помощью вызова DataBinder.Eval (или просто Eval в ASP.NET 2.0) можно об- ратиться к любому полю той записи в источнике данных, которая связана с текущей строкой элемента управления. Кроме того, вы можете скомбинировать значения не- скольких полей, составив из них выражение, чего нельзя сделать с помощью обычного столбца, связанного с данными, или столбца кнопок. Работа с элементом управления DataGrid Элемент управления DataGrid — это не просто средство отображения статических данных, а довольно мощный в функциональном отношении компонент, позволяющий организовать по команде пользователя переход между страницами данных, сортировку строк и их редактирование. Однако взаимодействие между элементом управления DataGrid и хост-страницей ограничивается обменом уведомлениями в форме событий возврата формы, Элемент управления DataGrid дает странице знать о команде пользователя и предоставляет ей право реагировать на эту команду по своему усмотрению. По такой схеме осущест- вляется большинство операций, за исключением выделения элемента. Как уже упо- миналось, при добавлении в элемент управления DataGrid столбца с кнопками типа Select и правильном определении стиля выделенного элемента щелчок такой кнопки вызывает возврат формы и автоматическое изменение внешнего вида текущей строки. На все остальные команды пользователя, в частности на команды перехода между страницами, сортировки и переключения в режим редактирования строки, элемент DataGrid реагирует только возвратом формы и генерированием события. Очевидно, что перечисленные операции весьма типичны и их поддерживает мно- жество страниц. Так что если решитесь пользоваться элементом управления DataGrid, то будьте готовы к тому, что вам придется писать гораздо больше рутинного, пере- носимого из страницы в страницу кода, чем при использовании нового элемента управления GridView. Переход между страницами данных элемента управления DataGrid В реальных приложениях в окне браузера редко умещаются все записи, с которыми связан табличный элемент управления. В таких случаях полный набор записей обыч- но разбивают на небольшие части и выводят постранично, что гораздо удобнее для
Таблицы связанные с данными Глава 10 395 пользователя. Конечно, можно просто сгенерировать очень длинную Web-страницу, которую пользователь будет прокручивать в окне браузера. Но, учитывая, что в та- ком случае вам, возможно, придется за один раз пересылать клиенту сотни записей, этот подход непродуктивен — чтобы увидеть первую группу записей, пользователю придется дожидаться загрузки полного их набора. Поэтому в элементе управления DataGrid предусмотрена возможность постраничного вывода данных с переходом к следующей или предыдущей странице по команде пользователя (для чего и предна- значен упоминавшийся выше блок кнопок листания). Для организации такой работы элементу управления прежде всего нужно знать, сколько строк будет выводиться на странице и какая функциональность требуется от блока листания. Основываясь на этих сведениях и отслеживая индекс текущей страницы, элемент управления может извлекать из источника строки следующей или предыдущей страницы и обновлять свой пользовательский интерфейс. Когда индекс текущей страницы данных изменяется, для приложения генерируется событие PagelndexChanged. Заметьте, однако, что обеспечить связывание с элементом управления строк новой страницы данных должна хост-страница. Это верно в любом случае: и когда элемент DataGrid связан с компонентом, представляющим источник данных, и когда он свя- зан с классическим перечислимым объектом. Для того чтобы элемент DataGrid осу- ществлял переход между страницами, необходимо написать обработчик его события PagelndexChanged. Что вы будете делать в этом обработчике, зависит от конкретного источника данных. Например, следующий код подходит для элемента управления DataGrid, который связан с компонентом, представляющим источник данных: protected void grid_Page!ndexChanged(object sender, DataGridPageChangedEventArgs e) { grid.CurrentPagelndex = e.NewPagelndex; // Эту операцию необходимо повторять каждый раз, когда // нужно инициировать обновление данных элемента grid.DataSourcelD = "SqlDataSourcel"; } Обновление значения свойства DataSourcelD (даже если ему присваивается то же значение) заставляет элемент управления DataGrid обновить свои данные, заново загрузив их из источника. Если этот элемент связан с перечислимым объектом, вы просто присваиваете его свойству DataSource новую группу строк. Вообще говоря, постраничное отображение данных — довольно затратная функция, и ее применение отрицательно сказывается на масштабируемости приложения. Если предоставить элементу управления возможность выполнять ее более или менее авто- матически, необходимо кэшировать данные. Компонент, представляющий источник данных, облегчает эту задачу (как рассказывалось в главе 9, для включения кэши- рования достаточно установить его свойство EnableCaching в true). Но кэширование большого объема данных со своей стороны представляет серьезную проблему, когда это делается для каждого пользователя. Элемент управления DataGrid поддерживает и пользовательское разбиение данных на страницы — альтернативный и более экономный подход, поскольку с элементом управления связывается ровно столько записей, сколько умещается на странице: protected void grid_Page!ndexChanged(object sender, DataGridPageChangedEventArgs e)
396 Часть II Размещение данных на сайте { grid.CurrentPagelrdex =? e.NewPagelndex; grid.DataSource = GetRecordsInPage(grid.CurrentPagelndex); } protected object GetRecordsInPage(int pagelndex) { // Извлекаем и возвращаем страницу данных } Как вы увидите далее, элемент управления GridView не поддерживает явно поль- зовательское разбиение на страницы. Но вместе с тем, он и не мешает этому, если данная функция поддерживается компонентом, представляющим источник данных, или слоем доступа к данным. Сортировка строк У элемента управления DataGrid есть свойство AllowSorting, позволяющее вк почать и отключать поддержку сортировки строк. Для выполнения сортировки пользователь должен щелкнуть заголовок столбца, по значениям которого он хпчет отсортировать данные. Однако само это действие не производит никакого эффекта, если вы не на- писали обработчик события SortCommand. Вог пример такого обработчика, позволя- ющего отсортировать строки по значениям выражения, которое связано со столбцом через свойство SortExpression\ protected void grid_SortCommand(object sender, DataGridSortCommandEventArgs e) { SqlDataSourcel.SelectCommand += " ORDER BY " + e.SortExpression; grid.DataSourcelD = "SqlDataSourceT"; } Сортировка данных — потенциально длительная операция, и ее выполнение может значительно повлиять на масштабируемость приложения, поэтому исключительно важно понимать, как она осуществляется. ASRNET 1.x позволяет реализовать в об- работчике события SortCommand любую желаемую логику сортировки В частности, можно отсортировать данные в памяти, используя метод Sort объекта DataView (что делается очень медленно), а можно возложить эту работу на СУБД, которая спра- вится с ней быстрее. Однако задержка, связанная со взаимодействием с сервером базы данных, может не только свести выигрыш во времени на нет, но даже привести к увеличению времени отклика приложения на команды пользователя Существует и еще одно решение, но оно применимо лишь в отдельных случаях, держать в кэше заранее отсортированные копии набора данных. Как видите, выбор есть, но хотелось бы подчеркнуть главное: на каком бы решении вы ни остановились, вы должны хо- рошо понимать, что делаете. В ASP.NET 2.0 компоненты, представляющие источники данных, скрываю! не- которые детали процесса сортировки. Так, если компонент сконфигурирован для получения данных в виде объекта DataSet (а такова установка по умолчанию), сорти- ровка производится в памяти и инициируется вызовом метода Sort. Данный подход не особенно эффективен, и применять его нежелательно (кроме случаев, когда вы имеете дело лишь с несколькими записями). Если компонент, представляющий ис- точник данных, действует посредством объектов ч-’ сния данных и хранимых процедур, сортировка может осуществляться прямо на сервере базы данных, и тогда компонент будет получать упорядоченные данные от СУБД. . ...... ч,. ><•••
Таблицы, связанные с данными Глава 10 397 Вывод из всего сказанного таков: сортировка — дело тонкое, и только путем тща- тельного профилирования работы приложения и индивидуального подбора средств и технологий можно добиться оптимального результата. А для этого необходимо хорошо знать особенности внутреннего функционирования элементов управления, вовлеченных в данный процесс. Редактирование строк Элемент управления DataGrid выводит данные, первоначально доступные только для чтения. Когда пользователь хочет модифицировать определенную строку, он выделя- ет ее и дает команду перевода этой строки в режим редактирования. Это, пожалуй, наиболее эффективная форма редактирования данных через Web, обеспечивающая максимальную гибкость. Однако элемент управления DataGrid поддерживает и более простую модель редактирования данных, называемую редактированием на месте. Она напоминает способ редактирования данных электронной таблицы Microsoft Of- fice Excel. Когда пользователь совершает действие, которое служит сигналом к на- чалу редактирования, видимая часть таблица перерисовывается и подобно ячейке электронной таблицы Excel выбранная для редактирования строка выводится иным способом — с текстовыми полями вместо надписей. Одновременно пользовательский интерфейс элемента управления DataGrid дополняется двумя кнопками, позволяю- щими зафиксировать либо отменить внесенные изменения. Редактирование на месте организовать несложно, хотя оно уместно не для каж- дого приложения. Этот метод редактирования исключительно удобен, когда нужно модифицировать содержимое относительно небольшой таблицы, не применяя при этом никакой особой валидационной или бизнес-логики. Главную роль в организации редактирования на месте играет класс EditCommand Column, представляющий столбец кнопок, с помощью которых пользователь ини- циирует редактирование выбранной строки. По щелчку такой кнопки выполняется возврат формы и строка переводится в режим редактирования. Что происходит в этом режиме с каждым из столбцов — зависит от его типа. Например, столбцы кно- пок и гиперссылок игнорируются; в столбцах, связанных с данными, отображаются поля ввода; а столбцы, имеющие шаблоны, выводятся с использованием шаблона <EditItemTemplate >. Как и для выполнения листания и сортировки, для перехода в режим редактирова- ния необходим пользовательский код. Вам нужно написать три обработчика событий: EditCommand, переводящий таблицу в режим редактирования, CancelCommand, воз- вращающий ее в режим просмотра, и UpdateCommand, необходимый для сохранения изменений и обновления таблицы на экране. Обработчики событий EditCommand и CancelCommand относительно просты, чего нельзя сказать об обработчике события UpdateCommand. Главными задачами обработчика события UpdateCommand являются извлечение введенных пользователем данных и сохранение изменений. В новом элементе управ- ления GridView обе операции жестко закодированы, выполняются автоматически при участии компонента, представляющего источник данных, и конфигурируются автором страницы во время разработки. ©Внимание! В этом разделе, посвященном элементу управления DataGrid, я не вдавался в детали функционирования данного элемента и намеренно не рассказывал, как для него реализуются функции сортировки, листания и редактирования данных. Дело в том, что элементы управления DataGrid и GridView структурно существенно различаются Их внешняя функциональность схожа: оба выводят таблицы данных, поддерживают листание, сортировку, редактирование и шаблоны. Однако реализованы эти операции по-разному,
398 Часть II Размещение данных на сайте и по-разному осуществляется связывание двух элементов управления с данными. Ины- ми словами, в основу двух элементов управления положены принципиально различные концепции А поскольку элемент управления GridView является более новым, более ин- теллектуальным и функционально богатым, то элемент управления DataGrid оставлен в ASP.NET 2.0 только из соображения обратной совместимости, поэтому изучать его во всех деталях не обязательно. Если у вас имеются созданные ранее приложения ASP.NET, которые вам предстоит поддерживать и дальше, то все, что нужно знать об элементе управления DataGrid, вы, скорее всего, и так знаете. Если же вы будете разрабатывать новые приложения и вы- водить на их страницах табличные данные, то элемент управления DataGrid вам не по- надобится. Назначение настоящего раздела — помочь тем, кто, так сказать, находится посредине, сделать выбор между двумя элементами управления и объяснить, почему в Microsoft было решено создать новый элемент управления, функционально повторяющий уже имеющийся. Элемент управления GridView часто используется совместно с одним из двух других элементов управления, FormView и DetailsView, с которыми вы познакомитесь в следующей главе. Элемент управления GridView Итак, на смену использовавшемуся в ASP.NET 1.x элементу управления DataGrid при- шел элемент управления GridView с теми же базовыми возможностями, но значительно расширенными и усовершенствованными. Как уже упоминалось, элемент DataGrid в ASP.NET 2.0 по-прежнему поддерживается. Он является мощным и гибким, и был бы всем хорош, если бы не такой серьезный недостаток, как необходимость писать большое количество пользовательского кода, причем даже для выполнения относительно про- стых и весьма типичных операций листания, сортировки, редактирования и удаления данных. Элемент управления GridView был разработан с целью устранения этого недо- статка и обеспечения автоматического двунаправленного связывания с применением минимума пользовательского кода. Он тесно связан с семейством новых компонентов, представляющих источники данных, и способен непосредственно обновлять их данные, лишь бы сами компоненты поддерживали такую возможность. Описанное бескодовое двунаправленное связывание с данными — самая приме- чательная особенность нового элемента управления GridView, и по сравнению с эле- ментом управления DataGrid его преимущества в том, что он позволяет определять более одного ключевого поля, поддерживает новые типы столбцов и предоставляет расширенные возможности оформления с использованием стилей и шаблонов. Кроме того, у него больше событий и он позволяет обрабатывать их либо отменять. Объектная модель элемента управления GridView Элемент управления GridView выводит содержимое источника данных в виде таблицы, каждый столбец которой представляет поле источника, а строка — его запись. Класс этого элемента объявлен так: public class GridView : CompositeDataBoundControl, ICallbackContainer, ICallbackEventHandler Базовый класс обеспечивает связывание с данными и выполнение элементом управления GridView функций контейнера именования, а интерфейсы ICallbackCon- tainer и ICallbackEventHandler — эффективную поддержку постраничного отображения данных и их сортировки. Указанные функции элемент управления выполняет по- средством клиентских вызовов с использованием новой технологии обратного вызова сценария (позже я разъясню, что это значит). Давайте начнем наше исследование элемента управления GridView с его программного интерфейса.
Таблицы, связанные с данными Глава 10 399 Свойства элемента управления GridView Элемент управления GridView обладает большим количеством свойств, которые можно условно разделить на следующие категории: поведение, визуальные установки, стили, состояние и шаблоны. В табл. 10-6 перечислены свойства, определяющие поведение этого элемента управления. Табл. 10-6. Поведенческие свойства элемента управления GridView Свойство Описание AllowPaging Позволяет указать, должно ли выполняться разбиение на страницы AllowSorting AutoGenerateColumns Позволяет указать, разрешена ли сортировка строк Позволяет указать, должны ли столбцы автоматически создаваться для каждого поля источника данных; по умолчанию имеет значение true AutoGenerateDeleteButton Позволяет указать, должен ли элемент управления со- держать столбец с кнопками, предназначенными для удаления строк AutoGenerateEditButton Позволяет указать, должен ли элемент управления содер- жать столбец с кнопками, предназначенными для перево- да строк в режим редактирования AutoGenerateSelectButton Позволяет указать, должен ли элемент управления со- держать столбец с кнопками, предназначенными для выделения строк DataMember Позволяет указать, с какой таблицей многотабличного источника данных связан элемент управления. Использу- ется совместно со свойством DataSource. Если последнее содержит объект DataSet, то в свойстве DataMember за- дается имя объекта DataTable DataSource Возвращает и позволяет задать объект, являющийся ис- точником данных элемента управления DataSourcelD Позволяет задать имя компонента, являющегося источ- ником данных элемента управления EnableSortingAndPagingCallbacks Позволяет указать, должны ли операции сортировки и перехода между страницами выполняться с использо- ванием технологии обратного вызова сценария; по умол- чанию имеет значение false RowHeaderColumn Позволяет задать имя столбца, содержащего заголовки строк. Это свойство введено для повышения доступности элемента управления SortDirection Возвращает направление сортировки строк элемента управления SortExpression Возвращает текущее выражение сортировки строк эле- мента управления UseAccessibleHeader Позволяет указать, должен ли верхний колонтитул эле- мента управления выводиться в формате, отвечающем требованиям доступности, то есть посредством тэгов <th>, а не <td> Свойство SortDirection определяет направление сортировки, а свойство SortExpres- sion возвращает выражение, по которому в настоящий момент отсортированы строки элемента управления. Оба эти свойства устанавливаются встроенным механизмом сор- тировки, когда пользователь щелкает заголовок одного из столбцов. Этот механизм включается и отключается с помощью свойства AllowSorting. Еще одно свойство,
400 Часть II Размещение данных на сайте EnableSortingAndPagingCallbacks, включает и отключает способность элемента управ- ления использовать для сортировки и перехода к другой странице технологию об- ратного вызова сценария, позволяющую обойтись без выполнения возврата формы и обновления всей страницы. Каждая строка элемента управления GridView относится к определенному типу; на- бор предопределенных типов строк у этого элемента такой же, как у DataGrid: верхний и нижний колонтитулы, строка, четная строка, блок листания. Эти элементы статич- ны в том смысле, что они не меняются в течение всего жизненного цикла элемента управления. Элементы других типов существуют или активны в течение короткого периода времени, необходимого для выполнения определенной операции, — это редак- тируемая строка, выделенная строка и элемент EmptyData. Последний представляет тело таблицы в ситуации, когда она связана с пустым источником данных. Примечание Элемент управления GridView имеет несколько свойств, предназначенных для обеспечения его доступности: UseAccessibleHeader, Caption, CaptionAlign и RowHea- derColumn. Когда задано значение свойства RoWHeaderColumn, определяющего заголо- вочный столбец, все ячейки этого столбца выводятся с применением стандартного стиля заголовка (полужирный шрифт). При этом ShowHeader, Headerstyle и другие связанные с заголовками свойства на указанном столбце не отражаются. С перечнем стилевых свойств элемента управления GridView вы можете озна- комиться в табл. 10-7. Каждое из них возвращает ссылку на определенный объект, в свойствах которого содержатся визуальные установки определенного элемента. Изменяя значения этих свойств, вы настраиваете стиль данного элемента. Табл. 10-7. Стилевые свойства элемента управления GridView Свойство Описание AltematingRowStyle Возвращает объект, представляющий стилевые свойства четных строк EditRowStyle Возвращает объект, представляющий стилевые свойства редактируе- мой строки таблицы FooterStyle Возвращает объект представляющий стилевые свойства нижнего колонтитула таблицы HeaderStyle Возвращает объект, представляющий стилевые свойства верхнего колонтитула таблицы EmptyDataRowStyle Возвращает объект, представляющий стилевые свойства пустой строки, выводимой, когда элемент управления GridView связан с пустым источником данных PagerStyle Возвращает объект, представляющий стилевые свойства блока ли- стания элемента управления GridView RowStyle Возвращает объект, представляющий стилевые свойства строки та- блицы SelectedRowStyle Возвращает объект, представляющий стилевые свойства выделенной в данный момент строки В табл. 10-8 описаны свойства, определяющие внешний вид элемента управления GridView, а в табл. 10-9 — свойства, позволяющие задавать для него шаблоны. Объект PagerSettings объединяет в себе все визуальные свойства блока листания. Многие их них знакомы разработчикам, имевшим дело с элементом управления Da- taGrid. Однако есть в составе класса PagerSettings и новые свойства, связанные с новы- ми кнопками (которые предназначены для перехода к первой и последней страницам данных), а также с использованием изображений вместо текстовых ссылок. (Чтобы вы- вести графические кнопки в элементе управления DataGrid, нужно очень исхитриться.)
Таблицы, связанные с данными Глава 10 401 Табл. 10-8. Свойства, определяющие внешний вид элемента управления GridView Свойство Описание Backimagc Url Caption Определяет URL фонового изображения элемента управления Определяет текст заголовка элемента управления. Это свойство введе- но для повышения доступности элемента управления . CaptiOrAlign CellPadding Определяет способ выравнивания заголовка элемента управления Позволяет задать расстояние в пикселах между границами ячейки и содержащимся в ней текстом CellSpacing Позволяет задать расстояние в пикселах по горизонтали и по вертика- ли между двумя последовательными ячейками GridLines Позволяет задать значение из одноименного перечисления, опреде- ляющее стиль границ ячеек таблицы (вертикальные, горизонтальные, и те и другие, никакие) HorizontalAlign Позволяет задать значение, определяющее способ выравнивания тек- ста в ячейках таблицы по горизонтали Err.ptyDataText Позволяет задать текст, выводимый в элемент управления, когда он связан с пустым источником данных PagerSettings Возвращает объект, посредством которого можно задать свойства кно- пок блока листания ShowFooter Позволяет указать, должен ли выводиться нижний колонтитул; по умолчанию имеет значение false ShowHeader Позволяет указать, должен ли выводиться верхний колонтитул; по умолчанию имеет значение true Табл. 10-9. Свойства, определяющие шаблоны элемента управления GridView Свойство Описание vmptyDataTemplate Определяет шаблон для вывода элемента управления, когда он свя- зан с пустым источником данных. Если свойства EmptyDataTemplate и EmptyDataText установлены одновременно, преимущество имеет первое из них. Если же не установлено ни одно из указанных свойств, PagerTewplate рендеринг элемента управления просто не осуществляется Определяет шаблон для вывода блока листания. Это свойство имеет преимущество перед свойством PagerSettings, когда они оба установлены Последняя группа свойств, связанная с внутренним состоянием элемента управ- ления, представлена в табл. 10-10. Табл. 10-10. Свойства элемента управления GridView, связанные с его внутренним состоянием Свойство Описание BottomPagerRow Возвращает объек* GridViewRow, представляющий нижний блок ли • стания элемента управления Columns Возвращает коллекцию объектов DataGridCohimn, представляющих столбцы таблицы. Если столбцы сгенерированы автоматически, эта коллекция пуста DataKeyNames Возвращает массив имен полей первичного ключа отображаемого на- бора записей DataKeys Возвращает коллекцию объектов DataKey, которые представляют зна- чения полей первичного ключа (заданных в свойстве DataKeyNames) всех записей, отображаемых в элементе управления в данный момент Editindex Возвращает и позволяет задать 0-базированный индекс редактируемой строки таблицы см. след. стр.
402 Часть II Размещение данных на сайте Табл. 10-10. (окончание) с Свойство Описание FooterRow Возвращает объект GridViewRow, который представляет верхний колонтитул HeaderRow Возвращает объект GridViewRow, который представляет нижний колонтитул PageCount Возвращает количество страниц, необходимое для отображения всех содержащихся в источнике данных Pagelndex Возвращает и позволяет задать 0-базированный индекс текущей страницы данных PageSize Возвращает и позволяет задать количество записей, выводимых на одной странице Rows Возвращает коллекцию объектов GridViewRow, представляющих ото- бражаемые в данный момент строки данных SelectedDataKey Возвращает объект DataKey, представляющий ключ выделенной строки Selectedlndex Возвращает и позволяет задать 0-базированный индекс выделенной строки SelectedRow Возвращает объект GridViewRow, представляющий выделенную строку SelectedValue Возвращает значение первого из полей выделенной строки, составляю- щих первичный ключ TopPagerRow Возвращает объект GridViewRow, представляющий верхний блок листания Вместе с элементом управления GridView в ASP.NET была введена новая объектная модель источника данных, и наилучшим образом этот элемент управления работает именно тогда, когда он через свойство DataSourcelD связан с компонентом, пред- ставляющим источник данных. У элемента управления GridView есть и классическое свойство DataSource, но если выполнять связывание с данными через него, некоторые функции элемента управления, такие как автоматическое обновление данных и ли- стание, будут недоступны. События элемента управления GridView У элемента управления GridView нет никаких методов, кроме DataBind, но во многих случаях вам не придется вызывать и его. Когда вы задаете в свойстве DataSourcelD компонент, представляющий источник данных, процесс связывания с данными ини- циируется автоматически. В ASP.NET 2.0 многие компоненты, включая и класс Page, генерируют парные со- бытия: делается-сделано. Между такими двумя событиями выполняются ключевые операции жизненного цикла элемента управления: первое генерируется непосред- ственно перед началом операции, второе — сразу по ее завершении. Класс GridView не является исключением из этого правила. Список его событий приведен в табл. 10-11. События RowCreated и RowDataBound эквивалентны событиям ItemCreated и Item- DataBound элемента управления DataGrid — они действуют так же, как в ASP.NET 1.x. То же верно и в отношении события RowCommand, эквивалентного событию ItemCom- mand элемента управления DataGrid. События — исключительно мощный инструмент программирования. Перехваты- вая событие RowUpdating, вы можете узнать, какая строка обновляется, и проверить ее новые значения. Кроме того, обработчик этого события можно использовать для HTML-кодирования возвращенных клиентом данных перед сохранением их в источ- нике данных, что является эффективным способом отражения атак внедрением.
Таблицы, связанные с данными Глава 10 403 Табл. 10-11. События, генерируемые элементом управления GridView Событие Описание PagelndexChanging, PagelndexChanged Оба события генерируются, когда пользователь щелкает кнопку блока листания: одно до, а другое после обработки элементом управления этой команды RowCancelingEdit Генерируется, когда пользователь щелкает кнопку отмены измене- ний, внесенных в текущую строку при ее редактировании, но перед выходом строки из режима редактирования RowCommand RowCreated RowDataBound RowDeleting, RowDeleted Генерируется, когда пользователь щелкает любую кнопку Генерируется при создании строки Генерируется при связывании строки с данными Оба события генерируются, когда пользователь щелкает кнопку удаления строки: одно до, а другое после выполнения элементом управления этой команды RowEditing Генерируется, когда пользователь щелкает кнопку редактирования строки, перед переводом этой строки в режим редактирования RowUpdating, RowUpdated Оба события генерируются, когда пользователь щелкает кнопку обновления строки: одно до, а другое после выполнения элементом управления этой команды SelectedlndexChanging, SelectedlndexChanged Оба события генерируются, когда пользователь щелкает кнопку выделения строки: одно до, а другое после выполнения элементом управления этой команды Sorting, Sorted Оба события генерируются, когда пользователь щелкает заголовок столбца для выполнения сортировки: одно до, а другое после вы- полнения элементом управления этой команды Простейший способ связывания элемента управления GridView с данными Ниже продемонстрирован простейший способ связывания элемента управления Grid- View с данными. Благодаря использованию компонента, представляющего источник данных, выполнение этой операции не требует написания пользовательского кода. <asp:ObjectDataSource ID=”MySource" runat="server" TypeName="ProAspNet20.DAL.Customers" SelectMet hod="LoadAl1"> </asp:Obj ectDataSou rce> <asp:GridView runat=”server" id="grid" DataSourceID="MySource” /> Установкой свойства DataSourcelD инициируется процесс связывания элемента управления с данными, в ходе которого выполняется запрос к источнику данных и заполняется пользовательский интерфейс элемента управления. Еще раз подчеркну, что вам не нужно писать для этого ни строчки кода. (Разумеется, вы отвечаете за на- писание метода LoadAll и слоя доступа к данным.) По умолчанию элемент управления GridView автоматически генерирует столбцы, представляющие поля источника данных, но в этот процесс также можно вмешаться. Связывание элемента управления GridView с данными Если ни одно из двух свойств, определяющих источник данных элемента управления GridView, не задано, его рендеринг не осуществляется. Но когда выполняется связы- вание с пустым компонентом, представляющим источник данных, можно задать ша- блон EmptyDataTemplate, чтобы получить более дружественный пользователю вывод: <asp:gridview runat=”server" datasourceid="MySource"> <emptydatatemplate>
404 Часть II Размещение данных на сайте <asp:label runat="server"> 'з There’s no data to show in this view. </asp:label> </emptydatatemplate> </asp:gridview> Как это будет выглядеть, показано на рис. 10-2. Когда источник данных не пуст, значение свойства EmptyDataTemplate игнорируется. Рис. 10-2. Элемент управления GridView, связанный с пустым источником данных При использовании объявленного набору столбцов свойство AutoGenerateColumns обычно устанавливают в false. Однако это не обязательно, поскольку таблица может содержать одновременно и объявленные, и автоматически сгенерированные столбцы. Объявленные столбцы в таком случае выводятся первыми. Заметьте, что в случае автоматического генерирования столбцов коллекция Columns пуста. Конфигурирование столбцов Свойство Columns содержит коллекцию объектов класса DataControlField. Они по- добны объектам DataGridColumn элемента управления DataGrid, но их класс имеет более обобщенное имя, поскольку объекты DataControlFieldмогут использоваться и в других элементах управления, связанных с данными, причем не обязательно в таких, которые выводят данные в виде столбцов. (Например, элемент управления DetailsView использует объекты DataControlField для вывода строк.) Определять столбцы можно как декларативно, так и программно. В последнем случае просто создаются необходимые объекты DataControlField, которые затем добав- ляются в коллекцию Columns. Например, следующий код включает в состав элемента управления столбец, связанный с данными: BoundField field = new BoundFieldO; field.DataField = "companyname"; field.HeaderText = "Company Name"; grid.ColumnFields.Add(field); Столбцы данных выводятся в том порядке, в каком объекты DataControlField до- бавлялись в коллекцию. Для статического объявления столбца в файле .aspx исполь- зуется тэг <Columns>‘. <columns> <asp:boundfield datafield="customerid" headertext="ID" /> <asp:boundfield datafield= "ccmpanyname" headertext="Company Name" /> </columns> В табл. 10-12 перечислены производные от DataControlField классы, Представля- ющие разные типы столбцов элемента управления GridView.
£ Таблицы, связанные с данными Глава 10 405 Табл. 10-12. Классы, представляющие разные типы столбцов элемента управления GridView Класс Описание BoundField Значение выводится в виде чистого текста (тип столбца задается по умолчанию) ButtonField Значение выводится в виде командной кнопки ссылочного или обычного кнопочного типа CheckBoxField Значение выводится в виде флажка, такие столбцы обычно приме- няются для представления значений булева типа CommandField Подобен классу ButtonField, представляет командную кнопку типа Select, Delete, Insert или Update. С элементом управления GridView данный класс используется редко, поскольку он разрабатывался для другого элемента управления — DetailsView. (В элементах управле- ния GridView и DetailsView используются объекты одних и тех же классов, производных от DataControlField.) HyperLinkField Представляет значение поля в виде гиперссылки; по щелчку на ней браузер выполняет переход по заданному URL ImageField Значение поля выводится как атрибут Src HTML-тэга <img>. Связанное поле должно содержать URL изображения TemplateField В ячейках столбца выводится пользовательский контент. Данный тип столбца используется в случаях когда требуется создать поль- зовательский столбец. Шаблон, который вы связываете со столбцом, может содержать любое сочетание полей, связанных с данными, литералов, изображений и прочих элементов управления Важнейшие свойства, присущие столбцам всех типов, перечислены в табл. 10-13. Табл. 10-13. Общие свойства столбцов элемента управления GridView Свойство Описание AccessibleHeaderText Данное свойство введено для повышения доступности элемента управления; оно содержит текст, предназначенный для чтения спе- циальными технологическими средствами FooterStyle FooterText HeaderlmageUrl Возвращает стилевой объект нижнего колонтитула столбца Возвращает текст нижнего колонтитула столбца Возвращает и позволяет задать URL изображения, размещаемого на заголовке столбца HeaderStyle HeaderText InsertVisible Возвращает стилевой объект заголовка столбца Возвращает текст заголовка столбца Указывает, должно ли поле быть видимо, когда родительский эле- мент управления находится в режиме вставки; это свойство не отно- сится к элементам управления GridView ItemStyle ShowHeader SortExpression Возвращает стилевой объект ячеек столбца Указывает, подлежит ли выводу заголовок столбца Возвращает и позволяет задать выражение, используемое для сорти- ровки строк по щелчку заголовка столбца. Обычно этому строковому свойству присваивается имя связанного поля данных Перечисленные свойства являются лишь подмножеством полного набора свойств классов столбцов. У каждого из этих классов есть и собственные свойства, имеющие смысл только для этого типа столбцов. Полное описание программного интерфейса столбцов элемента управления GridView вы найдете в документации MSDN.
406 Часть II Размещение данных на сайте Столбцы данных Поле источника данных, выводимое в виде чистого текста такими элементами управ- ления, как GridView или DetailsView, представляет класс BoundField. Чтобы указать, с каким полем связан объект этого класса, нужно присвоить имя поля свойству DataField. При желании вы также можете задать для поля строку пользовательского форматирования, присвоив ее свойству DataFormatString. В свойстве NullLhsplayText задается альтернативный текст, выводимый, когда поле содержит значение null. Уста- новив свойство ConvertEmptyStringToNull в true, вы даете объекту BoundField указание трактовать пустые строки как значение null. Можно сделать так, чтобы рендеринг объекта BoundField не осуществлялся, уста- новив свойство Visible в false, или сделать поле доступным только для чтения, при- своив значение true его свойству Readonly. Для вывода надписи в разделе верхнего или нижнего колонтитула, присвойте соответствующий текст свойству HeaderText или FooterText. Вместо текста в верхнем колонтитуле можно вывести изображение, задав его URL в свойстве HeaderlmageUrl. Столбцы с кнопками Кнопочное поле используется для размещения в столбце таблицы активных элемен- тов. Обычно с помощью таких кнопок инициируются действия над текущей строкой. По щелчку кнопки выполняется возврат формы и генерируется событие RowCommand. Пример таблицы с кнопочным столбцом показан на рис. 10-3. Рис. 10-3. Таблица с кнопочным столбцом Далее приведена разметка, с помощью которой сформирована показанная на ри- сунке таблица.
Таблицы, связанные с данными Глава 10 407 <asp:GridView ID=”GridView1" runat="server“ DataSourceID="SqlDataSource1" AutoGenerateColumns="false" AllowPaging=”true" 0nRowCommand="GridView1_RowCommand"> <HeaderStyle backcolor="gray" font-bold="true” height="200%" /> <PagerStyle backcolor="gray" font-bold="true" height="200%" /> <PagerSettings Mode="NextPreviousFirstLast" /> <Columns> <asp:BoundField datafield="productname" headertext="Product" /> <asp:BoundField datafield="quantityperunit" headertext="Packaging" /> <asp:BoundField datafield="unitprice" headertext=”Price" DataFormatString="{0:c}"> <itemstyle width= 80px” horizontalalign="right" /> </asp BoundField> <asp:ButtonField buttontype="Button” text="Add" CommandName="Add’ /> </Columns> </asp:GridView> Информация о товаре выводится с использованием нескольких объектов Bound- Field. Кнопки в последнем столбце служат для добавления товаров в корзину для покупок. Когда пользователь щелкает такую кнопку, выполняется возврат формы и на сервере генерируется событие RowCommand. Если таблица содержит несколько кнопочных столбцов, узнать, какую именно кнопку щелкнул пользователь, можно из свойства CommandName. Атрибуту CommandName тэга ButtonField присваивается строка, уникально идентифицирующая кнопку в рамках элемента управления. Вот пример обработчика события RowCommand'. void GridViewlJtowConrnand(object sender, GridViewCommandEventArgs e) if (e CommandName.Equals( Ado")) { // Получаем индекс строки, в которой щелкнул пользователь int index = Convert.Tolnt32(e.CommandArgument); // Добавляем товар в корзину для покупок AddToShoppingCart(index); } } Все кнопки в кнопочном столбце обычно имеют одну и ту же надпись; ее текст при- сваивается свойству Text объекта ButtonField. Если же вы хотите связать текст кнопок с определенным полем текущей строки данных, задайте имя этого поля в свойстве DataTextField. Стиль кнопки можно выбирать из нескольких вариантов: обычная кнопка, ги- перссылка или изображение. Для вывода кнопок-изображений воспользуйтесь такой разметкой: <asp:buttonfield buttontype="Image" CommandName="Add" ImageUrl= 7proaspnet20/images/cart.gif" /> Чтобы при наведении на кнопку указателя мыши появлялась всплывающая под- сказка, нужно написать обработчик события RowCreated. Как это сделать, я расскажу чуть позже.
408 Часть II Размещение данных на сайте Столбцы с гиперссылками Поле гиперссылки позволяет пользователю перейти по другому URL, при этом кон- тент страницы, на которую указывает URL, обычно выводится прямо на хост-странице в отдельном фрейме. Текст и URL гиперссылки могут извлекаться из связанного источника данных. URL можно задать двумя способами: непосредственно связать с полем источника данных или жестко закодировать, сделав строку запроса настраи- ваемой. Непосредственное связывание применяется, когда URL хранятся в одном из полей источника данных. Имя этого поля в таком случае задается в свойстве DataNavi- gateUrlFields объекта HyperLinkField. Если же поля с URL в источнике данных нет, параметризованный URL жестко кодируют в свойстве DataNavigateUrlFormatString. <asp:HyperLinkField DataTextField=”productname" Heade rT ext="Product” DataNavigatellrlFields="productid" DataNavigatell rlFormatSt ring="p roductinfо.aspx? id={0}" Target="ProductView" /> Когда пользователь щелкает ссылку, браузер направляет серверу НТТР-запрос с URLproductinfo.aspx?id=xxx, где ххх — значение поля productid. Если запрос содер- жит более одного параметра, задайте в свойстве DataNavigateUrlFields разделенный запятыми список полей, значения которых будут подставляться на места параметров. (Я уже рассказывал об аналогичной функции элемента управления DataGrid, который также поддерживает HTTP-запросы, но только с одним параметром.) Текст гиперссылки также можно форматировать, и строка его форматирования тоже может содержать параметр, обозначаемый символами подстановки {0}, — она задается в свойстве DataTextFormatString. Пример таблицы со столбцом гиперссылок вы видите на рис. 10-4. Рис. 10-4. Элемент управления GridView со столбцом гиперссылок Совет В свойстве target столбца гиперссылок, идентифицирующем целевое окно или фрейм, можно задавать следующие стандартные обозначения- self -parent, _new. Кро- ме того, браузеры Microsoft Internet Explorer и Firefox поддерживают установку search, соответствующую Web-панели, расположенной в левой части окна браузера (рис. 10-5).
ТаЬлицы связанные с данными Гласс 10 409 Рис. 10-5. Web-панель и элемент управления GridView со столбцом флажков Столбцы с флажками Типы столбцов, которые мы рассматривали до сих пор, привычны разработчикам приложений ASP.NET 1.x. Они лишь переименованы, но в целом поведение столбцов описанных типов очень напоминает поведение их аналогов из элемента управления DataGrid. А вот столбцы типа CheckBoxField являются нововведением ASP.NET 2.0, то есть у элемента управления DataGrid их аналога нет. Подобный столбец в этом элементе управления можно было создать с помощью шаблона. Столбец типа CheckBoxField связан с данными булева типа, которые в нем пред- ставлены в виде установленных или снятых флажков. Булевы значения в базе данных SQL Server хранятся в столбцах типа Bit (а в других СУБД — в столбцах аналогич- ных типов). Если же элемент управления связан с пользовательской коллекцией, то источником данных для столбца типа CheckBoxField может быть свойство типа bool. Попытка связать его с данными других типов приведет к исключению. В частности, исключение будет сгенерировано в случае, если вы свяжете такой столбец с цело- численным свойством, предполагая, что значение 0 будет интерпретироваться как false, а все остальные значения — как true. Столбцы изображений В столбце типа ImageField выводятся изображения. Ячейка такого столбца содержит тэг <img>, в атрибут Src которого должен подставляться URL изображения. Этот URL может содержаться в поле источника данных, заданном в свойстве DatalmageUrl- Field или составляться динамически с использованием параметра. Во втором случае параметризованный URL задается в свойстве DatalmageUrlFormatString, а поле, из которого извлекается значение параметра, — в свойстве DatalmageUrlField. В качестве
410 Часть il Размещение данных на сайте альтернативы можно связать ячейки столбца с внешней страницей (или обработчиком HTTP), которая будет извлекать байты изображения из некоего источника и пере- давать их браузеру, например: <Columns> <asp:ImageField DataImageUrlField="employeeid" DatalmageU rlFo rmatSt ring="showemployeepictu re.aspx? id={0}" DataAlternateTextField=”lastname"> <ControlStyle Width="120px" /> </asp:ImageField> <asp:TemplateField headertext="Employee"> <ItemStyle Width="220px" /> <ItemTemplate> <b><%# Eval("titleofcourtesy") +""+ Eval(“lastname") + ", "+ Eval("firstname") %></b> <br /> <%# Eval("title")%> <hr /> <i><%# Eval("notes")%></i> </ItemTemplate> </asp:templatefield> </Columns> В данном случае ячейки столбца типа ImageField заполняются изображениями, возвращаемыми страницей с таким URL: ShowEmployeePicture.aspx?id=xxx Здесь ххх — значение поля employeeid (заданного в свойстве DatalmageUrlFielrf). ‘ъъ Интересно, что альтернативный текст, выводимый при отсутствии изображения, также можно связывать с данными, для чего используется свойство DataAltemateTextField. На рис. 10-6 показано, как выглядит таблица с определенными выше столбцами. В первом из них содержатся фотографии служащих, а во втором — информация о служащих, выведенная с использованием шаблона. Ниже приведен программный код простейшей страницы, которая извлекает из базы данных и возвращает запрошенное изображение: void Page_Load(object sender, EventArgs e) { int id = Convert.ToInt32(Request.0ueryString["id"]); string connString = string cmdText = "SELECT photo FROM employees WHERE employeeid=@empID"; using (SqlConnection conn = new SqlConnection(connString)) { SqlCommand cmd = new SqlCommand(cmdText, conn); cmd.Parameters.AddWithValue(”@empID", id); byte[] img = null; conn.Open(); try { img = (byte[])cmd.ExecuteScalar(); if (img != null) { Response.ContentType = "image/png";
Таблицы, связанные с данными Глава 10 411 Response.Outputstream.Write(img, EMP_IMG_OFFSET, img.Length); } } catch { Response.WriteFile("/proaspnet20/images/noimage.gif"); } conn.Close(); Рис. 10-6. Элемент управления GridView co столбцом, содержащим изображения Если поле заданной записи в источнике данных содержит null, приведенный код возвращает стандартное изображение. При непосредственном связывании (без ис- пользования внешней страницы или обработчика HTTP) для получения такого же результата нужно задать значение свойства NullImageUrl. Щ Примечание В приведенном коде используется константа EMPJMG_OFFSET, которая в большинстве случаев должна равняться нулю. Однако структура столбца фотографий в таблице Employees базы данных Northwind такова, что данной константе пришлось при- своить значение 78. Подчеркну: оно требуется именно для данной конкретной таблицы. Столбцы с шаблонами На рис. 10-6 представлена таблица, второй столбец которой содержит значение, сфор- мированное путем соединения значений нескольких полей источника данных. Для подобных случаев предназначен класс TemplateField, позволяющий задать шаблон для формирования значений столбца. Вы можете создать отдельные шаблоны для отображения значений в четных и нечетных строках, для редактирования значений,
412 Часть II Размещение данных на сайте а также для заголовка и нижнего колонтитула столбца. Свойства, представляющие эти шаблоны, перечислены в табл. 10-14. Табл. 10-14. Свойства, представляющие шаблоны Свойство Описание шаблона AlternatingltemTemplate Определяет содержимое ячейки четной строки и способ его пред- ставления; если этот шаблон не задан, используется шаблон Item- Template Edititem Template Определяет содержимое ячейки редактируемой строки и способ его представления; этот шаблон должен содержать поля ввода и, возможно, валидаторы FooterTemplate Определяет содержимое нижнего колонтитула столбца и способ его представления HeaderTemplate Определяет содержимое заголовка столбца и способ его представ- ления ItemTemplate Определяет содержимое ячейки столбца по умолчанию и способ его представления В шаблон разрешается включать любые элементы: серверные элементы управ- ления, литералы, выражения связывания с данными, используемые для получения значений полей текущей строки данных (см. главу 9). Не существует ограничений и на количество фигурирующих в шаблоне полей. Однако нужно учитывать, что выражения связывания с данными поддерживаются не для всех шаблонов, а только для тех, которые связаны со строками данных. Если попытаться использовать такое выражение в заголовке или нижнем колонтитуле, будет сгенерировано исключение. Ниже приведен пример определения шаблона для столбца, содержащего информа- цию о товаре. Ячейка такого столбца (рис. 10-7) содержит две строки — с названием товара и информацией о его расфасовке. <asp:templatefield headertext="PrOduct"> <itemtemplate> <b><%# Eval("productname")%></b> <br /> available in <%# Eval("quantityperunit")%> </itemtemplate> </asp:templatefield> Примечание У класса TemplateField имеется также свойство InsertTemplate, но оно никогда не используется с элементами управления GridView, поскольку предназначено для элементов управления Form View. Как уже упоминалось, у элементов управления XXXV/eiv в ASP.NET 2.0 есть ряд общих классов XXXField, к числу которых относится и TemplateField. Поэтому у всех таких классов, и в частности у TemplateField, есть свойства, используемые одними элементами управления XXXView и не используемые другими. Мы поговорим об элементе управления FormView в следующей главе. Переход между страницами данных Некоторые особые функции компонента, представляющего источник данных, элемент управления GridView автоматически выполняет ряд типичных операций, к числу ко- торых относятся сортировка и листание данных, а также их обновление и удаление. Следует, однако, помнить, что не все компоненты, представляющие источники данных, поддерживают полный набор таких операций. С помощью своих булевых свойств (таких как CanSort) они уведомляют клиентский элемент управления о поддержке той или иной операции.
Таблицы, связанные с данными Глава 10 413 Рис. 10-7. Элемент управления GridView со столбцом, с которым связан шаблон Внимание! Если элемент управления GridView связан с источником данных через свой- ство DataSource, то есть действует без помощи компонента, представляющего источник данных, его поведение в отношении операций листания, сортировки, редактирования и других операций практически идентично поведению элемента управления DataGrid. В таком случае элемент управления GridView просто генерирует события, ожидая, что все необходимые действия будут выполнены пользовательским кодом их обработки. В оставшейся части этой главы мы будем предполагать, что элемент управления GridView связан с компонентом, представляющим источник данных. Элемент управления делает реализацию упомянутых типичных операций почти прозрачной для разработчика приложения. Как правило, при использовании этого элемента управления приходится писать совсем немного кода, а иногда он и вовсе не нужен. Но не стоит забывать мудрую пословицу: «Не все то золото, что блестит». Иными словами, следует иметь в виду, что чем меньше кода вы пишете, тем больше полагаетесь на существующую инфраструктуру, позволяя системе принимать важные решения вместо вас. Постраничное отображение данных и их сортировка относятся к ключевым операциям Web-приложений. И хотя зачастую стандартные действия элемента управления GridView оказываются вполне приемлемыми, вы сможете эф- фективнее выявлять и устранять возникающие проблемы, зная, что происходит за сценой. Листание без программного кода Для современных приложений исключительно важна возможность постраничного отображения потенциально большого объема данных. Эффективный механизм пере- хода между страницами позволяет приложению взаимодействовать с базой данных, не блокируя ресурсы без необходимости. Чтобы активировать такой механизм для элемента управления GridView, достаточно установить свойство AllowPaging в значе- ние true. Тогда элемент управления отобразит блок листания и сможет детектировать щелчки его кнопок пользователем.
414 Часть II Размещение данных на сайте Когда пользователь щелкает кнопку перехода к другой странице, выполняется воз- врат формы, но элемент управления перехватывает данное событие и сам выполняет его обработку. В этом состоит главное отличие программной модели элемента управ- ления GridView от программной модели элемента управления DataGrid, действующей в ASP.NET 1.x. Если вы не хотите выполнять нестандартные действия, обработчик события PagelndexChanged писать не нужно — элемент управления GridView знает, как извлечь и вывести новую страницу данных. Взгляните на следующее объявление этого элемента управления: <asp:GridView ID="GridView1" runat="server" DataSourceID= SqiDataSourcel" AllowPaging="true" /> Данные, содержащиеся в компоненте SqiDataSourcel, автоматически разделя- ются на страницы и так, постранично, выводятся в элементе управления GridViewl (рис. 10-8). Элемент управления выводит блок листания с несколькими стандартными ссылками: первая, предыдущая, следующая, последняя страница, — и по щелчку на одной из этих ссылок автоматически выбирает нужное подмножество строк. Рис. 10-8. Постраничное отображение данных в элементе управления GridView В стандартном пользовательском интерфейсе элемента управления GridView отсут- ствует индикатор номера страницы. Добавить такую надпись несложно — достаточно написать соответствующий обработчик события PagelndexChanged. protected void Gr±dView1_PageIndexChanged(object sender, EventArgs e) { ShowPagelndexO; } private void ShowPagelndexO CurrentPage.Text = (GridViewl.Pagelndex + 1).ToString(); } Заметьте, что обработчик события PagelndexChanged не осуществляет ни свя- зывания с данными, ни выбора страницы, как аналогичный обработчик элемента управления DataGrid. И если вам ничего не нужно делать после перехода к очередной странице, то вы можете не писать его вовсе. Какова же цена использования этого чудесного механизма? Элемент управления GridView на самом деле не знает, как извлечь очередную страницу данных. Он просто просит связанный с ним компонент, представляющий
Таблицы, связанные с данными Глава 10 415 источник данных, вернуть строки определенной страницы. За разбиение на страницы и выборку нужного подмножества строк полностью отвечает указанный компонент. Когда таблица связана с компонентом SqlDataSource, для автоматического разбиения необходимо, чтобы связывание осуществлялось с полным набором данных этого компонента. А когда таблица связана с компонентом ObjectDataSource, возможность разбиения на странице зависит от функциональных характеристик бизнес-объекта, к которому вы подключаетесь. Давайте сначала рассмотрим случай связывания с компонентом SqlDataSource. Его свойство DataSourceMode обязательно должно быть установлено в DataSet (зна- чение по умолчанию). Это означает, что при каждом возврате формы из базы данных извлекается полный набор данных, который может быть очень велик, а на странице при этом выводится немного записей. Например, возможна ситуация, когда для вы- вода таблицы из 10 строк из базы данных извлекается 1000 записей, и, подчеркну еще раз, это происходит каждый раз, когда пользователь щелкает кнопку перехода к другой странице данных. Чтобы исправить ситуацию, можно включить для компо- нента SqlDataSource кэширование данных, установив его свойство EnableCaching в true. Тогда данные будут извлекаться единожды — при обработке первоначального запроса страницы определенным пользователем, а затем, когда он будет переходить от странице к странице, нужные данные компонент станет считывать из кэша. Пока данные остаются в кэше, листание осуществляется практически без издержек. Точнее, без издержек на обращение к серверу базы данных, поскольку при такой технологии работы возникают издержки другого рода: в памяти Web-сервера постоянно находится блок данных. Поэтому данное решение подходит лишь для случаев, когда выводит- ся относительно небольшой набор записей, причем такой, с которым работают все пользователи. Л Совет Если вы решите осуществлять листание на уровне базы данных, то лучшее, что * вы можете сделать, это реализовать необходимый код в виде хранимой процедуры и связать ее со свойством SelectCommand компонента SqlDataSource. Кэширование в этом случае, естественно, нужно отключить. Перекладывание нагрузки по разбиению на страницы на слой доступа к данным Как рассказывалось в главе 9, компонент ObjectDataSource реализует лишь общую инфраструктуру разбиения данных на страницы, а за непосредственное выполнение соответствующих операций отвечают бизнес-слой и слой доступа к данным. Это означает, что бизнес- объект, который служит источником отображаемых дан- ных, должен иметь встроенные функции разбиения на страницы. Вы конфигуриру- ете компонент ObjectDataSource таким образом, чтобы он правильно вызывал метод упомянутого объекта, отвечающий за выдачу данных: задаете в свойствах StartRowIn- dexParameterName и MaximumRowsParameterName имена параметров, в которых этому методу передается начальный индекс и размер страницы. (Напомню, что метод, отве- чающий за выдачу данных, имеет две перегруженные версии, и указанные параметры есть только у второй из них, первая же возвращает весь набор данных.) И еще одно действие необходимо выполнить для того, чтобы элемент управления GridView мог постранично отображать данные, предоставляемые компонентом Object- DataSource, — присвоить свойству EnablePaging этого компонента значение true’. <asp:ObjectDataSource ID="0bjectDataSource1" runat="server" EnablePaging="true" TypeName="ProAspNet20.DAL.Customers" Sta rtRowIndexPa ramete rName=”firstRow"
416 Часть II Размещение данных на сайте MaximumRowsPa ramete rName="totalflows” ‘.ц.6 SelectMethod=" LoadByCount ry ” > fд <SelectParameters> <asp:ControlParameter Name="country" ControlID=”Countries"r PropertyName="SelectedValue" /> </SelectParameters> </asp:Obj ect DataSou rce> 1! <asp:GridView ID="GridView1" runat="server" AutoGenerateColumns-"false" DataSourceID="0bjectDataSource1" AllowPaging="true" 0nPageIndexChanged=”GridView1_PageIndexChanged”> <PagerSettings Mode="NextPreviousFirstLast" /> <Columns> <asp:BoundField DataField="id" HeaderText="ID" /> <asp:BoundField DataField="companyname" HeaderText="Company” /> <asp BoundField DataField="contactname” HeaderText="Contact /> </Columns> • ' </asp:GridView> <-•. <b>Page: </b><asp: Label runat="server" ID="CurrentPage" /> u-ч...» В приведенном коде явно задан только один параметр, без которого метод Load- ByCountry не может работать; его значение берется из расположенного на этой же странице раскрывающегося списка Countries. Два параметра, связанных с разбиением на страницы, элемент управления GridView задает сам. В частности, значение параметра, определяющего размер страницы, берется из свойства PageSize этого элемента управления, а индекс первой строки страницы, подлежащей отображению, получается путем умножения размера страницы на ее индекс. Вот прототип метода LoadByCountry: public static Customercollection LoadByCountry(st ring country) { LoadByCountry(country, -1, 0); } public static Customercollection LoadByCountry(st ring country, int totalRows, int firstRow) { // Извлечение заданного подмножества строк } Таким образом, эффективность алгоритма разбиения данных на страницы опреде- ляется не компонентом ObjectDataSource, а тем бизнес-объектом, с которым он связан. В демонстрационном коде метод LoadByCountry выполняет исходный запрос, получает объект чтения данных, связанный с полным их набором, а затем удаляет все записи, не попадающие в заданный диапазон. Такая реализация является компромиссом между простотой и эффективностью. Для практического применения она подходит не в каж- дом случае, но для целей демонстрации вполне хороша. Потребление памяти ограниче- но одной записью, но на базу данных ложится нагрузка по извлечению полного набора. Алгоритмы разбиения данных на страницы Элемент управления GridView не имеет свойства AllowCustomPaging, как элемент Da- taGrid, однако и он дает возможность определить пользовательский алгоритм раз- биения на страницы. Такой алгоритм обычно реализуют для того, чтобы извлекать данные постранично и тем самым минимизировать затраты на их кэширование. В иде- але можно попросить базу данных разбить результаты запроса на страницы, но лишь немногие СУБД поддерживают данную функцию. Поэтому разработано несколько альтернативных решений, каждое из которых имеет свои достоинства и недостатки.
Таблицы, связанные с данными Глава 10 417 Одно из решений заключается в создании временных таблиц для извлечения необходимого подмножества записей. Вы пишете хранимую процедуру и передаете ей в качестве параметров размер и индекс страницы. Альтернативой могут служить вложенные команды SELECT с предложением ТОР: первая из них, внутренняя, будет извлекать все записи от начала таблицы и до требуемой страницы включительно, а вторая, внешняя, менять порядок записей на обратный и отбрасывать ненужные записи. Но и предложение ТОР поддерживается не всеми СУБД. Еще один подход, основанный на динамическом построении SQL-кода, описан в следующем постинге Web-журнала: http://weblogs.sqlteam.com/jeffs/archive/2004/03/22/1085.aspx. Если вы можете работать в сотрудничестве с администратором базы данных, то попросите его добавить в таблицу специальный столбец для индексации записей. В таком случае слой доступа к данным должен гарантировать, что в этом столбце будут содержаться последовательные значения. Конфигурирование блока листания Когда свойство AllowPaging установлено в true, элемент управления выводит блок листания. Его конфигурация задается с помощью'тэгов <PagerSettings> и <PagerStyle> или соответствующих им свойств. Страничный блок элемента управления может содержать кнопки перехода к первой и последней страницам, а также изображения вместо обычных ссылок. (Создать кнопки с изображениями можно и в элементе управления DataGrid, но для этого приходится писать много кода.) Блок листания может работать в двух основных режимах: номеров страниц или кнопок относитель- ной навигации. В первом случае он содержит ссылки в виде последовательных чисел, представляющих номера страниц, а во втором — кнопки для перехода к первой, преды- дущей, следующей и последней страницам. Режим работы блока листания задается в его свойстве Mode, поддерживаемые значения которого Перечислены в табл. 10-15. Табл. 10-15. Режим работы блока листания Режим Описание NextPrevious Выводятся кнопки для перехода к предыдущей и следующей стра- ницам NextPreviousFirstLast Выводятся кнопки для перехода к первой, предыдущей, следующей и последней страницам Numeric Выводятся числовые ссылки, соответствующие номерам страниц NumericFirstLast Выводятся числовые ссылки, соответствующие номерам страниц, плюс кнопки для перехода к первой и последней страницам С помощью пар свойств XXXPageText и xxxPagelmageUrl, где XXX — это First, Last, Next или Previous, можно задавать надписи и URL кнопок либо ссылок. На рис. 10-9 показана демонстрационная страница с пользовательскими надписями на навигаци- онных кнопках. В зависимости от размера таблицы и установок браузера страница может не уме- щаться целиком в его окне. На такой случай лучше разместить на ней два блока листания — над таблицей и под ней. Это делается с помощью атрибута Position тэга <PagerSettings>: <PagerSettings Position="TopAndBottom" /> Другие значения этого атрибута служат для вывода только верхнего или только нижнего блока листания.
418 Часть II Размещение данных на сайте /' При необходимости стандартный блок листания элемента управления GridView можно заменить пользовательским (рис. 10-10). Для этого в определение элемента управления нужно включить тэг <PagerTemplate>'. <PagerTemplate> <asp:Button ID="BtnFirst" runat="server" commandname=”First" Text="First" /> <asp:Button ID="BtnPrev" runat="server" commandname="Prev" Text="«" /> <asp Button ID="BtnNext" runat="server" commandname="Next” Text="»" /> <asp:Button ID="BtnLast" runat=”server" commandname="Last" Text="Last" /> </PagerTemplate> Рис. 10-9. Элемент управления GridView с двумя страничными блоками Для реагирования на щелчки пользовательских кнопок пишется обработчик со- бытия RowCommand, в котором явно задается индекс страницы: void GridView1_RowCommand(object sender, GridViewCommandEventArgs e) { if (e.CommandName == "Last") GridViewl.Pagelndex = GridViewl.PageCount - 1; if (e.CommandName == "First") GridViewl.Pagelndex = 0. if (e.CommandName == "Next") GridViewl.Pagelndex ++; if (e.CommandName == Prev") GridViewl.Pagelndex —; } Как видите, этот код очень прост; он предлагается лишь в целях демонстрации и в реальном приложении будет несколько сложнее — как минимум, он может делать недоступными кнопки перехода к первой или последней странице, когда пользователь уже на них находится.
Таблицы, связанные с данными Глава 10 419 Dl Paging with 0bj^ tDatdSouri e - Microsoft Intel n. « 1”" :|X J* »"* J*"*» Ioob И* 1* О Back - О ’ Й Jl A I jP search Favorites .ja&aSai.*.__ ] jg) http://fc>cailost:62^6/ProAspNet20/5aniples/ChlQ/Gr Select a country: | [All] ID Company ALFKI Alfreds Futtarkista ANATR Ana Trujillo Empar idudos у helados Ana Trujillo ANTON Antonio Moreno Taquerfe AROUT Around the Hom BERGS Berglunds snabbkdp BLAUS Blauer See Delicatessen BLONP Blondesddsl pAre etfils BOLID Bdlido Comidas preparadas BONAP Bon app* BOTTM Bottom-Dollar Markets Contact Maria Anders Antonio Moreno Thoma* Hardy Christine Berglund Henna Moo* Fridirique Citeaux Martin Sommer Laurence Lebihan Elizabeth Lincoln * j local intranet Рис. 10-10. Элемент управления GridView с пользовательским блоком листания Сортировка данных Мы уже говорили о том, что сортировка данных — дело тонкое, и при неправильной реализации эта операция может оказаться весьма дорогостоящей. Обычно лучше всего сортировать данные прямо в базе данных, поскольку встроенный код сорти- ровки, используемый СУБД, в высшей степени оптимизирован. Помните об этом, когда будет рассматриваться инфраструктура сортировки элемента управления GridView и компонентов, представляющих источник данных. Элемент GridView не имеет собственного алгоритма сортировки — для выполнения этой задачи он об- ращается к компоненту, представляющему источник данных, а будучи связанным с перечислимым объектом, просто генерирует событие сортировки и предоставляет странице его обрабатывать. Сортировка данных без программного кода Для включения функции сортировки данных нужно установить свойство AllowSort- ing элемента управления GridView в true. Тогда щелчок заголовка столбца элемент управления будет воспринимать как команду отсортировать данные по значениям этого столбца. При желании можно связать со столбцом выражение сортировки, за- дав его в свойстве SortExpression. Такое выражение представляет собой разделенную запятыми последовательность имен столбцов. Каждое имя столбца может сопро- вождаться ключевым словом DESC или ASC, определяющим порядок сортировки (по убыванию и возрастанию соответственно). По умолчанию сортировка выполняется по возрастанию значений. В следующем примере создается таблица, поддерживающая сортировку по значениям поля productname источника данных: <asp:GridView runat="server" id="MyGridView" DataSourceID="MySource" AllowSorting="true" AutoGenerateColumns="false"> <Columns> <asp:BoundField datafield="productname" headertext="Product" so rtexp ression="p roductname" /> <asp:BoundField datafield="quantityperunit" headertext="Packaging" /> </Columns> </asp:GridView>
420 Часть II Размещение данных на сайте Как и в случае с листанием, для сортировки данных, выводимых в элементе управ- ления GridView, не требуется писать ни строчки программного кода. Достаточно пра- вильно сконфигурировать инфраструктуру сортировки этого элемента, и она будет работать безо всякого вмешательства с вашей стороны. Элемент поддерживает со- ртировку в обоих направлениях: когда пользователь щелкает заголовок столбца, по значениям которого уже отсортированы строки, направление сортировки меняется на противоположное. И только если вам потребуется реализовать более сложные функ- ции, скажем, вывести в заголовке столбца значок, отражающий текущее направление сортировки, придется написать немного кода. (Такой пример я приведу чуть ниже.) Как и в случае с листанием, реализация сортировки определяется объектом, пред- ставляющим источник данных или являющимся таковым. Давайте посмотрим, ч;то происходит, когда элемент управления GridView связан с компонентом SqlDataSource. В этом случае (рис. 10-11) вам достаточно установить свойство AllowSorting элемента управления в true и задать выражения сортировки для тех столбцов, заголовки кото- рых должны реагировать на щелчки мыши. Рис. 10-11. Поддерживающий сортировку элемент управления GridView, связанный с компонентом SqlDataSource Когда пользователь щелкает заголовок столбца, элемент управления GridView за- прашивает у компонента SqlDataSource отсортированные соответствующим образом данные. Как уже упоминалось, по умолчанию этот компонент получает данные в виде объекта DataSet. Он извлекает их из этого объекта и формирует объект DataView, после чего вызывает его метод Sort. Описанная технология прекрасно работает, но сортировка осуществляется не самым быстрым образом. Возможно, для вашего при- ложения такой скорости будет достаточно, но все равно помните, что сортировка выполняется в памяти сервера. В сочетании с кэшированием данных сортировка и разбиение на страницы в памяти сервера приемлемы для совместно используемых и относительно небольших наборов данных.
Таблицы, связанные с данными Глава 10 421 Существует ли возможность получения от сервера базы данных заранее отсорти- рованных данных? Существует, но для этого прежде всего нужно присвоить свойству DataSourceMode компонента SqlDataSource значение DataReader, иначе сортировка бу- дет осуществляться в памяти. Далее нужно написать хранимую процедуру, с помощью которой будут извлекаться данные. Кроме того, следует присвоить свойству SortParam- eterName компонента SqlDataSource имя параметра хранимой процедуры, в котором будет передаваться выражение сортировки. Очевидно, что хранимая процедура должна динамически составлять текст команды с предложением ORDER BY. Вот как, например, можно модифицировать хранимую процедуру CustOrderHist из базы данных Northwind, чтобы она работала с произвольными выражениями сортировки: CREATE PROCEDURE CustOrderHistSorted 5©CustomerlD nchar(5), ©SortedBy varchar(20)=’total' AS SET QUOTED_IDENTIFIER OFF IF ©SortedBy = '' BEGIN SET ©SortedBy = ’total' END EXEC ( ’SELECT ProductName, Total=SUM(Quantity) ' + ’FROM Products P, [Order Details] 0D, Orders 0, Customers C ' + WHERE C CustomerlD = '” + ©CustomerlD + ' + ’AND C.CustomerlD = 0.CustomerlD AND O.OrderlD = OD.OrderlD ’ + AND OD.ProductID = P.ProductID GROUP BY ProductName ’ + ‘ORDER BY ' + ©SortedBy) GO После выполнения всех перечисленных действий достаточно написать разметку, чтобы работа по сортировке данных была возложена на СУБД: <asp:SqlDataSource ID=”SqlDataSource1” runat=”server” DataSou rceMode=”DataReade r” ConnectionString='<%$ Connectionstrings:LocalNWind %>' So rtPa ramete rName="So rtedBy" SelectCommand="CustOrderHistSorted" SelectCommandType="Sto redP rocedu re"> <SelectParameters> <asp:ControlParameter Cont rolID="CostList" Name="CustomerID" PropertyName='SelectedValue" /> </Select Pa ramete rs> </asp SqlDataSource> Учтите, что сортировка данных в базе данных несовместима с кэшированием. Поэтому установите свойство EnableCaching в false, иначе будет сгенерировано ис- ключение,- Как результат при каждом возврате формы будет производиться обращение к базе данных. Если вы будете работать в режиме DataSet и со включенным кэшированием, то получите записи из базы данных один раз, отсортированными по умолчанию, а по- следующие операции сортировки будут выполняться в памяти. Если же в режиме DataSet вы отключите кэширование, по команде сортировки будет производиться обращение к базе данных, иными словами, результат будет таким же, как при ис- пользовании объекта DataReader. Но смысла в этом нет никакого, поскольку при отключенном кэшировании применение объекта DataReader является значительно более эффективным решением.
422 Часть II Размещение данных на сайте В общем случае при наличии у компонента SqlDataSource свойства SortParameter- Name возможна сортировка данных, выводимых любыми элементами управления (например, элементом Repeater и специализированными элементами управления), которые используют данные и не нуждаются в их разбиении на страницы и кэши- ровании. Перекладывание нагрузки по сортировке на слой доступа к данным Ну а как осуществляется сортировка данных при использовании элемента управления ObjectDataSource? В этом случае вся нагрузка по сортировке ложится на слой досту- па к данным или бизнес-слой приложения, а компонент, представляющий источник данных, лишь инициирует соответствующую операцию посредством программного интерфейса бизнес-объекта. Давайте модифицируем метод LoadByCountry, код ко- торого мы рассматривали при изучении технологии разбиения данных на страницы, добавив в него еще один параметр для выражения сортировки: public static Customercollection LoadByCountry( string country, int tctalRows, int firstRow, string sortExpression) { Customercollection coll = new CustomerCollection(); using (SqlConnection conn = new SqlConnection(Connectionstring)) { SqlCommand cmd; cmd = new SqlCommand(cmdLoadByCountry, conn); cmd.Paramete rs.AddWithValue(”@count ry", count ry); if (!String.IsNullOrEmpty(sortExpression)) cmd.CommandText += " ORDER BY " + sortExpression; conn.0pen(); SqlDataReader reader = cmd.ExecuteReader(); HelperMethods.FillCustomerList(coll, reader, totalRows, firstRow); reader. CloseO; conn.Close(); } return coll; } Константа cmdLoadByCountry представляет SQL-команду или хранимую процеду- ру, используемую для извлечения данных. Как видите, в данной реализации метода к существующей команде просто добавляется предложение ORDER BY. Это не самое лучшее в мире решение, но наша цель им достигается — нагрузка по сортировке данных перекладывается на слой доступа к данным, а с него — на СУБД. Для вызова приведенной версии метода LoadByCountry свойству SortParameterName компонента ObjectDataSource нужно присвоить имя того параметра указанного метода, в котором задается выражение сортировки, а именно sortExpression: <asp:ObjectDataSource ID="ObjectDataSource1" runat="server” EnablePaging=”true” ТуpeName="ProAspNet20.DAL.Oust ome rs" SortParameterName="sortExpression" StartRowIndexParameterName="firstPow" MaximumRowsParameterName="totalRows” SelectMet hod="LoadByCount ry"> <SelectPa ramete rs> </SelectParameters> </asp:ObjectDataSou rce>
Таблицы, связанные с данными Глава 10 423 Преимущество описанного подхода заключается в том, что вы полностью контро- лируете механизм сортировки и сами решаете, где, когда и как она будет выполняться. Например, вы можете включить код сортировки в состав слоя доступа к данным, но это будет узкоспециализированный код, выполняющий исключительно сортировку данных, поскольку вся необходимая инфраструктура уже реализована в ASP.NET. । Примечание Следует упомянуть еще об одной примечательной функции элемента управ- ления GridView — предоставляемой им возможности прервать при необходимости опера- цию сортировки. Для ее реализации нужно создать обработчик события Sorting, извлечь в нем объект, полученный в качестве аргумента (он имеет тип G rid Vie wSortEvent Args), и установить его свойство Cancel в true. Вывод индикатора направления сортировки Элемент управления GridView не содержит визуального элемента, по которому пользо- ватель мог бы определить текущий порядок сортировки, будь то столбец, по которому выполнена сортировка, или ее направление. Создание такого индикатора — один из немногих случаев, когда потребуется написать программный код: <script runat="server”> void GridView1_RowCreated (object sender, GridViewRowEventArgs e) { if (e.Row.RowType == DataControlRowType.Header) AddGlyph(MyGridView, e.Row), } void AddGlyph(GridView grid, GridViewRow item) { Label glyph = new Label(); glyph.EnableTheming = false; glyph.Font.Name = "webdings"; glyph.Font.Size = Fontunit.Small; glyph.Text = (grid.SortDirection==SortDirection.Ascending ?"5" :"6"); // Поиск столбца, по которому отсортированы данные for(int i=0; i<grid.Columns.Count; i++) { string colExpr = grid.Columns[i].SortExpression; if (colExpr != "" && colExpr == grid SortExpression) item.Cellsfi].Controls.Add (glyph); } </script> Идея заключается в следующем: вы пишете обработчик события RowCreated и до- жидаетесь момента создания верхнего колонтитула таблицы. Затем вы создаете новый элемент управления Label, в котором будет выводиться символ-индикатор. Шрифт и текст элемента управления Label задаются таким образом, чтобы в нем выводился графический символ, указывающий направление сортировки На роль та- ких индикаторов подходят символы и ▼ из шрифта Microsoft Webdings (здесь они соответствуют символам 5 и 6 обычного текстового шрифта). Выводить этот символ нужно в заголовке столбца справа от его названия. При желании индекс столбца можно сохранять в состоянии представления, вы- полняя эту операцию в обработчике события Sorting. В качестве альтернативы можно определять его динамически, сравнивая текущее выражение сортировки элемента управления GridView (свойство SortExpression) с выражениями сортировки столбцов. После определения индекса столбца следует извлечь соответствующую ячейку табли- цы и добавить к представляющим ее элементам управления элемент Label-. item.Cells[1].Controls.Add (glyph);
424 Часть II Размещение даннык на сайте Результат показан на рис. 10-12. Учтите, что если со страницей связана тема, то шрифт элемента управления Label, который важен для правильного вывода индикато- ра, может изменяться. Во избежание этого отключите для этого элемента управления поддержку тем, установив его свойство EnableTheming в false. Рис. 10-12. Усовершенствование функции сортировки элемента управления GridView Листание и сортировка с использованием технологии обратного вызова сценария Операции листания и сортировки требуют выполнения возврата формы с последу- ющим полным обновлением страницы. Поскольку страница обычно содержит много графики, это довольно дорогостоящие операции. А ведь было бы неплохо, если бы элемент управления GridView мог сам обратиться к Web-серверу, получить новый набор данных и обновить свой интерфейс, не вынуждая к обновлению всю страницу. Тогда время отклика соответствующих команд существенно бы уменьшилось. А такое действительно возможно благодаря введенной в ASP.NET 2.0 технологии обратного вызова сценария. Данную технологию я подробно описал в книге «Program- ming Microsoft ASP.NET 2.0 Applications: Advanced Topics» (Microsoft Press, 2005). Чтобы ею воспользоваться, нужно установить булево свойство EnableSortingAndPaging- Callbacks элемента управления GridView в true. Данная технология работает с Microsoft Internet Explorer, а также с другими браузерами, включая Firefox, Netscape версии 6.x и выше, Safari 1.2 и последние версии Opera. SqlDataSource и ObjectDataSource в сравнении Несколько приведенных ниже соображений помогут вам сделать выбор между компонентами SqlDataSource и ObjectDataSource. Прежде всего следует помнить, что эти компоненты — наиболее популярные, но не единственно возможные ис- точники данных для элементов управления. Более того, применение компонен- тов, представляющих источники данных, — не единственный способ связывания элементов управления с данными в ASP.NET 2.0. Так что SqlDataSource и Object- DataSource — просто два инструмента из предлагаемого ASPNET ассортимента
Таблицы, связанные с данными Глава 10 425 и пользоваться ими следует только в тех случаях, когда именно они лучше всего отвечают вашим нуждам. Компонент SqlDataSource оптимизирован для автономной работы с данными, и лучше всего он работает тогда, когда связан с объектом DataSet. Только в этом случае доступны реализованные в нем функции разбиения данных на страницы и кэширования, и лишь функция сортировки может использоваться также с объ- ектом чтения данных. Если объект DataSet подходит для вашего приложения, компонент SqlDataSource станет хорошим выбором. С ним вы получите готовые, хотя и не всегда эффективные в реальных приложениях, решения для ряда типич- ных функций, требующие лишь декларативного кода. Иными словами, встроенная функциональность компонента SqlDataSource хороша в отдельных случаях, но едва ли она может служить основой работы всего слоя доступа к данным. Если вам потребуется больший контроль над выполнением операций сортиров- ки и листания данных (например, вы захотите сами разбивать данные на страницы или делегировать сортировку серверу базы данных), можно остановиться на объек- те ObjectDataSource. В таком случае следует вначале спроектировать и реализовать полнофункциональный слой доступа к данным и, возможно, бизнес-слой. Здесь вы реализуете все те функции, которые необходимы элементу управления, ото- бражающему данные, — разбиение на страницы, сортировку данных и их кэширо- вание. Заметьте, что кэширование не поддерживается, если вместо контейнерных классов ADO.NET применяются пользовательские коллекции, но реализация пользовательского слоя кэширования — не такая уж сложная задача. Когда вы пользуетесь компонентом ObjectDataSource, то сами отвечаете за реализацию ключевых функций работы с данными, как это было с элементом управления DataGrid в ASP.NET 1.x. При этом вы просто размещаете допол- нительный код в файле отделенного кода страницы, а включаете его в состав слоя доступа к данным. Это тоже программный код, но роль его совершенно иная! В довершение компонент ObjectDataSource полностью поддерживает пользова- тельски^ с ущностные классы и пользовательские коллекции. Клагодаря поддержке родовых типов, появившейся в .NET Framework 2.0, создавать пользовательские коллекции стало очень просто, и соответственно упростилось создание пользова- тельского слоя доступа к данным, состоящего из специализированных объектов, разработанных для конкретной предметной области и конкретного применения. Редактирование данных Главным достоинством элемента управления GridView (и главным преимуществом перед элементом управления DataGrid) является способность обновлять данные в их источнике. Элемент управления DataGrid лишь предоставляет инфраструктуру для редактирования данных. Он содержит необходимые элементы пользовательского Интерфейса и генерирует события, сигнализирующие об изменении пользователем значения определенного поля данных, но не записывает эти изменения в источник данных. Поэтому использовавшим его разработчикам приходилось писать огромное количество кода лишь для того, чтобы выполнить такую рутинную операцию, как сохранение изменений. С появлением элемента управления GridView ситуация в корне изменилась. Те- перь, если только источник данных поддерживает их обновление, элемент упрайле- ния може’ автоматически выполнять эту операцию, предоставляя таким образом разработчикам готовое решение. О своей способности обновлять данные по запросу
426 Часть II Размещение данных на сайте элемента управления компонент, представляющий источник данных, сообщает через булево свойство CanUpdate. Подобно элементу управления DataGrid элемент управления GridView выводит столбцы командных кнопок, с помощью которых пользователь может перевести же- лаемую строку в режим редактирования или удалить. Но если при использовании элемента управления DataGrid нужно было явно создавать столбец кнопок редактиро- вания как экземпляр специального класса EditCommandColumn, то элемент управления GridView упрощает эту задачу и автоматически создает столбцы кнопок редактиро- вания и удаления строк. Редактирование данных на месте и их обновление Для включения поддержки функции редактирования на месте, то есть изменения отображаемых в элементе управления GridView строк данных, нужно установить его свойство AutoGenerateEditButton в true-. <asp:gridview runat="server" id="GridView1" datasourceid="MySource" autogeneratecolumns="false" autogenerateeditbutton="true"> </asp:gridview> Тогда в элементе управления автоматически появится дополнительный столбец с кнопками для перевода строк в режим редактирования, который вы видите на рис. 10-13. Рис. 10-13. Элемент управления GridView, поддерживающий редактирование на месте Если пользователь хочет прекратить редактирование и отменить внесенные из- менения, он щелкает кнопку отмены, которая в режиме редактирования выводится в том же автоматически сгенерированном столбце вместе с кнопкой обновления дан- ных. По щелчку кнопки отмены строка возвращается в исходное и доступное только для чтения состояние, а свойство Editindex получает значение -1, каковое оно имеет по умолчанию; это значение говорит о том, что ни одна строка в данный момент не редактируется. Ну а что происходит по щелчку кнопки обновления? Элемент управления GridView генерирует событие RowUpdating и затем в собственном его обработчике проверяет значение свойства CanUpdate компонента, представляющего источник данных. Если это свойство возвращает значение false, выбрасывается ис-
Таблицы, связанные с данными Глава 10 427 ключение, поскольку указанное значение свидетельствует о том, что компонент не поддерживает обновление данных. Предположим, что элемент управления GridView связан с компонентом SqlData- Source. Для того чтобы по команде пользователя внесенные им изменения сохраня- лись в источнике данных, элемент GridView и компонент SqlDataSource должны быть сконфигурированы таким образом: <asp:sqldatasource runat="server” ID=”EmployeesSource" Connections!ring=”<%$ ConnectionStrings:LocalNWind %>" SelectCommand="SELECT employeeid, firstname, lastname FROM employees" UpdateCommand="UPDATE employees SET firstname=@firstname, lastname=@lastname WHERE employeeid=@original_employeeid"> </asp:sqldatasou rce> <asp:gridview runat="server" id="GridView1" datasourceid="EmployeesSource” AutoGene rateColumns="false" DataKeyNames="employeeid" AutoGenerateEditButton=”true”> <columns> <asp:boundfield datafield=”firstname” headertext="First" /> <asp:boundfield datafield=”lastname" headertext="Last" /> </columns> </asp:gridview> В атрибуте UpdateCommand задается SQL-команда, предназначенная для обновле- ния данных в источнике. В ней можно использовать любое необходимое количество параметров. Если при этом придерживаться определенного соглашения об именова- нии, разрешение имен параметров будет производиться автоматически. Имена пара- метров, представляющих обновляемые поля (как firstname в нашем примере), должны соответствовать именам полей, заданных в свойстве DataField столбцов элемента GridView. Имя параметра, используемого в предложении WHERE для идентификации обновляемой записи, должно соответствовать значению свойства DataKeyNames, то есть этим параметром должен быть ключ текущей записи. К имени указанного па- раметра по умолчанию применяется строка форматирования original_{0}, которая, напомню, задается в свойстве OldValuesParameterFormatString. Об успешном завер- шении операции обновления элемент управления GridView сигнализирует странице посредством события RowUpdated. Примечание Элемент управления GridView собирает значения из полей ввода теку- щей строки и формирует словарь пар имя-значение, представляющих новые данные строки. Кроме того, элемент GridView генерирует событие RowUpdating, что позволяет программисту проверить введенные пользователем значения, прежде чем передавать их объекту, представляющему источник данных. Прежде чем инициировать обновление данных, элемент управления GridView автоматически вызывает метод PageJsValid. Если этот метод возвращает значение false, операция отменяется. Такая проверка произво- дится на случай применения пользовательского шаблона с валидаторами. Если элемент управления GridView связан с компонентом ObjectDataSource, со- бытия развиваются немного иначе. У связанного с данным компонентом бизнес-объ- екта должен присутствовать метод, который вызывается для обновления данных и имеет необходимое для выполнения этой операции число параметров. Значения могут передаваться этому методу по отдельности, в разных параметрах, или все вместе, объединенные в некоторую структуру. Для профессионально разработанного слоя до- ступа к данным предпочтительнее второе решение. Далее продемонстрирован пример конфигурирования элемента управления GridView и компонента ObjectDataSource.
428 Часть II Размещение данных на сайте <asp:ObjectDataSource ID="CustomersSource" runat="server" TypeName="ProAspNet20.DAL.Customers" SelectMethod="LoadAll" UpdateMethod="Save" Data0bjectTypeName="ProAspNet20.DAL.Customer”> </asp:Obj ectDataSou rce> <asp:GridView ID="GridView1" runat="server” DataSourceID="CustomersSource" DataKeyNames="id" AutoGenerateColumns=”false"> AutoGenerateEditButton="true" <Columns> <asp:BoundField DataField="companyname” HeaderText="Company" /> <asp:BoundField DataField="street” HeaderText="Address" /> <asp BoundField DataField=”city" HeaderText="City" /> </Columns> </asp:GridView> Метод Save может иметь следующие прототип и реализацию: public static void Save(Customer cust) { using (SqlConnection conn = new SqlConnection(ConnectionString)) SqlCommand cmd = new SqlCommand(cmdSave, conn); cmd.Parameters.AddWithValue(’@id", cust.ID), cmd Parameters.AddWithValue("©companyname", cust.CompanyName); cmd Parameters.AddWithValue(”@city", cust City): cmd.Parameters.AddWithValue("©address”, cust.Street); conn.0pen(); cmd. ExecuteNonQue ry(); conn.CloseO; return; ’f } } Используемая SQL-команда (или хранимая процедура) — это самый обыкновен- ный оператор UPDATE с набором предложений SET. В атрибуте DataObjectTypeName задается имя класса, к которому должен принадлежать параметр метода, вызываемого объектом ObjectDataSource. J Примечание Если свойство DataObjectTypeName установлено, все методы объекта, представляющего источник данных, должны либо не иметь параметров, либо принимать объект заданного типа, причем независимо от того, как вы заполняете коллекцию параме- тров метода, декларативным или программным способом. Свойство DataObjectTypeName имеет приоритет перед коллекцией параметров. Удаление отображаемых записей С точки зрения элемента управления GridView операция удаления записей мало чем отличается от операции их обновления. В обоих случаях элемент GridView обраща- ется за выполнением операции к компоненту, представляющему источник данных. Включение поддержки удаления записей осуществляется посредством свойства Auto- GenerateDeleteButton, которое вам нужно установить в true. Тогда элемент управления GridView выведет столбец с кнопками удаления. По щелчку такой кнопки будет вы- зван метод, выполняющий удаление, и ему будет передан словарь пар имя-значение, составляющих ключ удаляемой записи. Вот пример конфигурирования компонента ObjectDataSource, поддерживающего обновление и удаление записей:
Таблицы, связанные с данными Глава 10 429 <asp:sqldatasource runat= server" ID="EmployeesSource" ConnectionString='<%$ Connectionstrings:LocalNWind %>" SelectCommand="SELECT employeeid, firstname, lastname FROM employees" UpdateCommand="UPDATE employees SET firstname=@firstname, lastname=@lastname WHERE employeeid=@o riginal_employeeid” DeleteCommand="DELETE employees WHERE employeeid=@original_employeeid" /> Элемент GridView не выводит никакого диалогового окна с предложением поль- зователю подтвердить свое намерение — он сразу приступает к делу: вызывает метод Page.IsValid на случай применения пользовательского шаблона с валидаторами и генерирует событие RowDeleting, дающее возможность проконтролировать допусти- мость операции. Операция удаления записи может завершиться неудачей вследствие нарушения определенных ограничений, действующих в базе данных. Так, например, запись не может быть удалена, если существуют связанные с ней дочерние записи и действует ограничение, запрещающее удаление записей в такой ситуации. В подобном случае выбрасывается исключение. Если элемент управления GridView связан с компонентом ObjectDataSource, то для удаления записи необходимо, чтобы бизнес-объект, являющийся источником данных, имел следующие два метода: public static void Delete(Customer cust) { Delete(cust.ID); } public static void Delete(string id) { using (SqlConnection conn = new SqlConnection(ConnectionString)) { SqlCommand cmd = new SqlCommand(cmdDelete, conn); cmd Parameters.AddWithValue("@id", id); conn.Open();‘ cmd. ExecuteNonQueryO; conn.Clbse();’ return; } Перегружать метод Delete не обязательно, но во многих случаях это целесообразно, а кроме того, с его перегрузкой слой доступа к данным становится более гибким и простым в использовании. Вставка новых записей В своей нынешней форме элемент управления GridView не поддерживает вставку строк в источник данных. Такова реализация этого элемента управления, и данное ограничение не имеет никакого отношения к возможностям и характеристикам ком- понента, представляющего источник данных. Фактически все такие компоненты под- держивают вставку данных. Но, как вы увидите в следующей главе, разработчики ASP.NET предполагали, что эта операция будет выполняться посредством элементов управления DetailsView и FormView. В ASPNET 1.x типичйой практикой была модификация нижнего колонтитула или блока листания элемента управления DataGrid — в нем предусматривалось место для
430 Часть II Размещение данных на сайте полей ввода и кнопки, с помощью которых выполнялось добавление новой записи. При использовании элемента управления GridView такое тоже возможно, причем он даже упрощает задачу, предоставляя свойство PagerTemplate, в котором может быть задан шаблон блока листания. Модифицировать содержимое нижнего колонтитула можно путем написания обработчика события RowCreated (ниже я расскажу о нем подробнее.) Заметьте, однако, что если элемент управления GridView связан с пустым источником данных, нижний колонтитул не выводится. А если вы захотите, чтобы ваши пользователи могли добавлять новые строки в пустую таблицу? В таком случае вам поможет шаблон EmptyDataTemplate'. <emptydatatemplate> <asp:label ID="Label1” runat="server"> There s no data to show in this view. <asp:Button runat="server" ID="btnAddNew" CommandName="AddNew" Text="Add New Record" /> </asp:label> </emptydatatemp Late> Для перехвата щелчка кнопки btnAddNew нужно написать обработчик события RowCommand'. void Gridview1_RowCommand(object sender, GridViewCommandEventArgs e) { if (e.CommandName == 'AddNew") { •• } } Дополнительные возможности Для завершения знакомства с элементом управления GridView давайте рассмотрим два типичных сценария, реализованных во многих приложениях: получение инфор- мации, связанной с выбранной пользователем строкой, и различное представление строк данных. В первом сценарии в элементе управления GridView выводится некий список элементов, пользователь выбирает один из них, после чего с использованием данного элемента выполняются определенные действия. Для этой цели можно при- менить кнопочный столбец. Во втором сценарии структура и внешний вид строк таблицы автоматически модифицируются в зависимости от их содержимого — напри- мер, для того, чтобы строки с отрицательными значениями определенного столбца выделялись на общем фоне. Выполнение операции над заданной строкой Давайте вернемся к задаче, о которой упоминалось ранее в этой главе при обсужде- нии кнопочных столбцов. Представьте, что у вас есть приложение для электронной коммерции. На одной из его страниц выводится список товаров с кнопками для их добавления в корзину для покупок. Чтобы создать такие кнопки, в элемент управ- ления GridView нужно добавить кнопочный столбец и написать обработчик события RowCommand'. void GridView!JtowCommand(object sender, GridViewCommandEventArgs e) { if (e.CommandName.Equals("Add")) { // Получаем индекс выбранной строки int index = Convert.Tolnt32(e.CommandArgument); // Создаем новую покупку и добавляем ее в корзину
Таблицы, связанные с данными Глава 10 431 AddToShoppingCart(index); } На этом мы тогда остановились. Теперь давайте продвинемся немного вперед и напишем код метода AddToShoppingCart. Каково назначение этого метода? Обычно он извлекает некоторую информацию о выбранном товаре и записывает ее в струк- туру данных, представляющую корзину. В нашем примере роль корзины выполняет пользовательская коллекция ShoppingCart'. public class ShoppingCart : List<ShoppingItem> { public ShoppingCartO { } } Shoppzngltem — это пользовательский класс, представляющий покупаемый товар. Он содержит несколько свойств — идентификатор товара, его название, цену и коли- чество, приобретаемое пользователем. Объект корзины сохраняется в состоянии сеанса и доступен посредством свойства МуShoppingCart, определенного на уровне страницы: protected ShoppingCart MyShoppingCart { get { object о = Session[ ShoppingCart"]; if (o == null) { InitShoppingCartO; return (ShoppingCart) Session["ShoppingCart"]; } return (ShoppingCart) o; } } private void InitShoppingCartO { ShoppingCart cart = new ShoppingCartO; Session["ShoppingCart"] = cart; } Задача метода AddToShoppingCart — создать объект Shoppingitem и заполнить его информацией о выбранном продукте. Как ее получить? Элемент управления GridView передает индекс выделенной строки в свойстве CommandArgument структуры GridViewCommandEventArgs. Эта информация нам не- обходима, но для достижения цели ее недостаточно — нужно еще преобразовать по- лученный индекс в ключ товара, а еще лучше — в его индекс в наборе данных, чтобы можно было извлечь объект элемента данных, выводимого в выделенной строке. В свойстве DataKeyNames элемента GridView содержатся имена ключевых полей данных. Значения этих полей сохраняются в состоянии представления для того, чтобы при обработке событий обратного вызова, таких как RowCommand, их легко можно было извлечь. Реализованное в виде строкового массива свойство DataKeyNames элемента GridView является аналогом свойства DataKeyField элемента управления DataGrid'. <asp:GridView ID="GridView1" runat="server" DataSou rceID="SqlDataSou reel" DataKeyNames=”productid,productname,unitprice" ... />
432 Часть II Размещение данных на сайте Сколько полей нужно задавать в свойстве DataKeyNames? С добавлением каждого из них будет увеличиваться объем состояния представления. Вместе с тем, если за- дать только поля первичного ключа, для извлечения всех необходимых данных вам потребуется выполнять запрос. Выбор оптимального решения зависит от конкретной задачи. В нашем простом случае нужно сделать копию данных о товаре, уже кэши- руемых в памяти Web-сервера. Поэтому нет никакой необходимости выполнять за- прос для получения уже имеющихся данных. Для заполнения объекта Shoppingitem достаточно знать идентификатор товара, его название и цену: private void AddToShoppingCart(int rowindex) { DataKey data = GridViewl,DataKeys[rowindex]; Shoppingitem item = new Shoppingltem(); item.NumberOfItems = 1; item.ProductID = (int) data.Vainest"productid"]; item ProductName = data.Values["productname"].ToStnng(); item.UnitPrice = (decimal) data.Vainest"unitprice"]; MyShoppingCart.Add(item); ShoppingCartGrid.DataSource = MyShoppingCart; ShoppingCartGrid. DataBindO; Значения перечисленных в свойстве DataKeyNames полей упаковываются в массив DataKeys, хорошо знакомый разработчикам, которые пользовались элементом управ- ления DataGrid. Это массив объектов DataKey, представляющий собой нечто вроде упорядоченного словаря. Для доступа к сохраненным значениям полей используется коллекция Values. Чтобы отобразить содержимое корзины для пользователя, можно связать ее со вторым элементом управления GridView (см. рис. 10-3). ©Внимание! Строки таблицы связываются с элементами данных (записями из источника данных) только при рендеринге элемента управления. Событие обратного вызова, такое как RowCommand, генерируется раньше. Поэтому свойство Dataltem объекта GridViewRow, соответствующего строке, в которой щелкнул пользователь, содержит значение null, когда вы обращаетесь к нему из обработчика события RowCommand. Вот почему нам нуж- ны свойства DataKeyNames и DataKeys. Выделение строки Для реализации универсального механизма выделения указанной пользователем строки можно воспользоваться столбцом со специальными командными кнопка- ми, присвоив значение true свойству AutoGenerateSelectButton элемента управления GridView. Чтобы пользователь видел, какую строку он выделил, определите стиль выделенной строки: <asp:GridView ID="GridView1" runat="server" ... > <SelectedRowStyle BackColor="cyan" /> </asp GridView> Когда пользователь щелкает кнопку выделения строки, генерируется событие SelectedlndexChanged и обновляются некоторые свойства, такие как Selectedlndex, SelectedRow и SelectedDataKey. Точнее, при выделении строки сначала генерируется событие RowCommand, а потом SelectedlndexChanged. Однако к моменту наступления события RowCommand никакие свойства еще не обновлены.
Таблицы, связанные с данными Глава 10 433 Приведенный ранее код добавления выделенного товара в корзину можно пере- писать так: protected void GridViewl SelectedIndexChanged(object sender, EventArgs e) { AddToShoppingCart(); } private void AddToShoppingCartO { DataKey data = GridViewl.SelectedDataKey; Shoppingitem item = new Shoppingltem(); item.NumberOfItems = 1: item.ProductID = (int) data.Values["productid”]; item. ProductName = data. Valuesfproductname’']. ToSt ring(); item.UnitPrice = (decimal) data.Values[”unitprice"]; MyShoppingCart.Add(item); ShoppingCartGrid.DataSource = MyShoppingCart; ShoppingCartGrid DataBindO; } Как видите, индекс строки нам не потребовался, поскольку нужный объект DataKey предоставляется свойством SelectedDataKey. Обновленный вариант нашей страницы показан на рис. 10-14 Рис. 10-14. Добавление в корзину выделенного элемента Модификация строк Хотите знать, для чего может понадобиться пользовательский рендеринг строк та блицы? Взгляните еще раз на рис. 10-14. Пользователь только что добавил в корзину товар, которого уже нет на складе. А ведь лучше было бы делать подобные строки
434 Часть II Размещение данных на сайте недоступными или хотя бы просто модифицировать их с учетом условий времени выполнения. Давайте посмотрим, как это можно сделать Для решения этой задачи мы воспользуемся двумя событиями элемента управле- ния GridView: RowCreated и RowDataBound. Первое генерируется при создании строки таблицы, будь то верхний или нижний колонтитул, элемент, четный элемент или блок листания. Второе событие генерируется при связывании только что созданной строки с элементом данных (записью в источнике данных). Событие RowDataBound генерируется не для всех строк таблицы, а только для связанных с данными; верхнему и нижнему колонтитулам, а также блоку листания оно ни к чему. Давайте попробуем сделать недоступной кнопку выделения строки для тех строк, чье поле Discontinued возвращает значение true (рис. 10-15). Для этого необходимо написать обработчик события RowDataBound, поскольку модифицировать нужно уже связанную с данными строку: void GridView1_RowDataBound(object sender, GridViewRowEventArgs e) { if (e.Row.RowType == DataControlRowType.DataRow) { object dataltem = e.Row.Dataltem; bool discontinued = (bool) DataBinder.Eval(dataltem, "discontinued"); e.Row.Enabled = !discontinued; } } Рис. 10-15. Строки отсутствующих на складе товаров недоступны В общем случае вы начинаете с того, что проверяете тип строки. Строго говоря, в обработчике события RowDataBound эта проверка не обязательна, поскольку ука-
Таблицы, связанные с данными Глава 10 435 занное событие генерируется только для строк данных. Элемент данных извлекается посредством свойства Dataltem объекта GridViewRow. После этого вы получаете зна- чение нужного поля и применяете требуемую логику. Возможно, что тип связанного со строкой объекта данных не будет вам известен заранее. Но метод Data.Binder.Eval, являющийся универсальным аксессором, использует технологию рефлексии и дей- ствует независимо от типа объекта. Если вы хотите сделать недоступной всю строку (все ее элементы управления), то можете установить свойство Enabled ее объекта в false. Для доступа же к конкретному элементу управления потребуется вот такая цепочка обращений: ((WebControl)e.Row.Cells[O].Controls[0]).Enabled = !discontinued; Этот оператор срабатывает благодаря тому, что кнопка выделения строки всегда является первым элементом управления первой ячейки строки данных. Получив до- ступ к объектной модели строки элемента управления GridView, вы можете сделать с этой строкой все, что пожелаете. Заключение В этой главе мы рассмотрели табличные элементы управления ASP.NET — DataGrid и новейший элемент GridView. Пожалуй, в наше время редко можно встретить Web- приложение, в котором не выводилось бы ни одной таблицы, и уж во всяком случае такое маловероятно в приложении, связанном с данными. Если выводимые данные имеют табличную структуру, то есть состоят из строк и столбцов, то табличное представление является для них наиболее естественным. Табличный элемент управления позволяет выделять и редактировать строки, может отображать их постранично, если все они не умещаются в окне браузера, и позволяет пользователю сортировать их, щелкая заголовки столбцов. Разработчик может изме- нять способ представления строк таблицы, задавая пользовательские шаблоны, со- стоящие из HTML- и ASP.NET-разметки. Специальная группа событий сигнализирует приложению о важных моментах жизненного цикла элемента управления. Табличные элементы управления позволяют организовать редактирование строк данных прямо внутри таблицы, без использования отдельной формы, как в электрон- ных таблицах Excel (это называется редактированием на месте), тем самым избавляя вас от лишней работы. Почему в ASP.NET 2.0 поддерживаются два очень похожих друг на друга таблич- ных элемента управления? Ответ прост: элемент управления GridView предлагается для использования в новых приложениях, а элемент управления DataGrid оставлен ради обеспечения обратной совместимости. У элемента управления GridView более эффективная внутренняя архитектура, и он умеет использовать преимущества но- вой модели связывания с данными, введенной в ASP.NET 2.0. Главным недостатком модели связывания с данными, действовавшей в ASP.NET 1.x, было то, что она тре- бовала написания большого количества кода, причем преимущественно рутинного, повторяемого для каждого элемента управления. Для устранения этого недостатка и была введена новая модель связывания с данными, основанная на использовании компонентов, которые представляют источники данных. Эти компоненты облада- ют функциями, предназначенными не только для элемента управления GridView, но и для других новых элементов управления, DetailsView и FormView, о которых я рас- скажу в следующей главе. Они заполняют еще один пробел в инструментарии системы ASP.NET 1.x, предназначенном для работы с данными, предоставляя интеллектуаль- ный пользовательский интерфейс для отображения отдельных записей.
436 Часть II Размещение данных на сайте Только факты В ASP.NET имеются два табличных элемента управления: DataGrid и GridView. Первый функционирует так же, как в ASP.NET 1.x. Второй введен в ASP.NET 2.0, имеет более эффективную архитектуру и работает с новой моделью связывания с данными. Элемент управления GridView поддерживает больше типов столбцов, включая столбцы флажков и изображений. В элементе управления GridView реализованы встроенные функции постранич- ного отображения данных, их сортировки и редактирования, основанные на ис- пользовании компонентов, представляющих источники данных. Когда элемент управления GridView связан с перечислимым объектом данных (традиционный метод связывания с данными, применявшийся в ASP.NET 1.x), он ведет себя по- добно элементу управления DataGrid. Будучи связанным с компонентом SqlDataSource, элемент управления GridView для выполнения сортировки данных в памяти и разбиения их на страницы использует возможности объекта DataSet. Когда элемент управления GridView связан с компонентом ObjectDataSource, не- обходим полнофункциональный слой доступа к данным, содержащий собственную логику сортировки, разбиения на страницы, а в некоторых случаях и кэширования. Модель событий элемента управления GridView по сравнению с элементом управ- ления DataGrid была значительно расширена: в ней появились пары событий, сигнализирующие о начале и завершении ключевых операций, другие события, а также реализована возможность отмены текущей операции. Для извлечения информации о строке, в которой щелкнул пользователь, можно воспользоваться свойством CommandArgument структуры данных события. Вы смо- жете извлечь из него индекс строки, а затем с помощью новой коллекции DataKeys получить доступ к важнейшим полям элемента данных. При работе с элементом управления DataGrid доступно было только поле первичного ключа, причем лишь одно, и вам приходилось выполнять запрос к источнику данных, чтобы извлечь другую необходимую информацию.
Глава 11 J Отображение отдельных записей Табличная форма вывода данных, без сомнения, очень удобна, но она подходит не для всех случаев. Существует множество сценариев, когда пользователь в каждый конкрет- ный момент времени имеет дело только с одной записью. Но в ASP.NET 1.x для таких ситуаций не было предусмотрено специальных средств, и разработчик делал все сам: извлекал запись из базы данных, связывал ее поля с размещенными в форме элемен- тами управления и создавал кнопки для навигации по записям. Одним из типичных сценариев, требующих вывода содержимого одной записи, является формирование двух взаимосвязанных представлений, главного и подчиненного. Обычно пользователь выбирает главную запись в списке или таблице, а приложение выводит связанную с ней информацию из других полей той же записи или из связанных с ней записей другой таблицы базы данных. В ASP.NET 2.0 выполнение этой задачи автоматизирова- но — здесь введен элемент управления DetailsView, прекрасно дополняющий элементы управления DataGrid и GridView и способный работать в тандеме с каждым из них. Элемент управления DetailsView DetailsView — это связанный с данными элемент управления, выводящий на странице одну запись и необязательные кнопки для перехода между записями. Он подобен форме Microsoft Office Access и обычно используется для обновления и вставки за- писей в сценариях с главным и подчиненным представлениями. Элемент управления DetailsView может быть связан с любым компонентом, пред- ставляющим источник данных. Он способен выполнять переход от записи к записи, а также обновлять, удалять и вставлять записи в источнике данных, поддерживающем эти операции. В большинстве случаев для их выполнения не требуется писать ни строчки кода Для настройки пользовательского интерфейса элемента управления DetailsView достаточно выбрать необходимые поля данных и стили подобно тому, как это делается с элементом управления GridView. Учтите, что хотя элемент управления DetailsView обычно используется для мо- дификации и вставки записей, он не проверяет, соответствуют ли введенные данные схеме их источника. Не предоставляет он и элементов пользовательского интерфей- са, определяемых схемой данных (например, раскрывающегося списка для выбора значений внешнего ключа или специализированных шаблонов для редактирования данных определенных типов). Объектная модель элемента управления DetailsView Для отдельной записи из источника данных элемент управления DetailsView является тем же, чем элемент управления GridView для страницы записей. Оба они позволяют выбрать подмножество полей, которые будут доступны только для чтения либо и для чтения, и для записи. Вывод элемента управления DetailsView настраивается с ис- пользованием шаблонов и стилей. По умолчанию поля выводятся по вертикали, одно под другим, а слева от них отображаются их имена Иными словами, поля источника данных соответствуют строкам таблицы, формируемой этим элементом управления.
438 Часть II Размещение данных на сайте DetailsView является составным элементом и действует как контейнер именования и связывания. Подобно элементу управления GridView он поддерживает технологию обратного вызова сценария, реализуемую с использованием интерфейсов ICallback- ContainernlCallbackEventHandler. Вот как выглядит объявление класса этого элемента управления: public class DetailsView : CompositeDataBoundControl, IDataltemContaine г, ICallbackContainer, ICallbackEventHandler, INamingContainer Типичный пример вывода элемента управления DetailsView показан на рис. 11-1. Главными составляющими его пользовательского интерфейса являются верхний ко- лонтитул, строки полей, блок листания, командный блок и нижний колонтитул. Кроме того, над ним может выводиться заголовок в виде надписи. Рис. 11-1. Элемент управления DetailsView Свойства элемента управления DetailsView Все свойства элемента управления DetailsView можно разделить на такие категории: поведение, внешний вид, стили, состояние и шаблоны. Поведенческие свойства этого элемента перечислены в табл. 11-1. Табл. 11-1. Поведенческие свойства элемента управления DetailsView Свойство Описание AllowPaging Позволяет указать, поддерживает ли элемент управления на- вигацию AutoGenerateDeleteButton Позволяет указать, содержит ли командная панель кнопку уда- ления записи; по умолчанию имеет значение false AutoGenerateEditButton Позволяет указать, содержит ли командная панель кнопку ре- дактирования записи; по умолчанию имеет значение false
Отображение отдельных записей Глава 11 439 Табл. 11-1. (окончание) Свойство Описание AutoGeneratelnsertButton Позволяет указать, содержит ли командная панель кнопку вставки записи; по умолчанию имеет значение false AutoGenerateRows Позволяет указать, должен ли элемент управления генериро- вать строки автоматически; по умолчанию имеет значение true (автоматически выводятся все поля записи) DataMember Позволяет указать, с какой таблицей источника данных должен быть связан элемент управления. Используется совместно со свойством DataSource-. если в нем задан объект DataSet то свой- ство DataMember содержит имя объекта DataTable DataSource Возвращает и позволяет задать объект, служащий источником данных элемента управления DataSourcelD Позволяет задать идентификатор компонента, представляюще- го источник данных элемента управления DefaultMode Позволяет задать режим отображения элемента управления по умолчанию (только чтение, вставка, редактирование). Допустимыми значениями являются элементы перечисления DetailsViewMode EnablePagingCallbacks Позволяет указать, должен ли переход между записями выпол- няться с помощью технологии обратного вызова сценария PagerSettings Возвращает ссылку на объект PagerSettings, позволяющий уста- новить свойства блока листания Свойство DefaultMode определяет начальный рабочий режим элемента управле- ния; в этот же режим элемент управления возвращается после редактирования или вставки записи. Вывод, генерируемый элементом управления DetailsView, представляет собой та- блицу, строки которой соответствуют полям отображаемой записи. Кроме них таблица содержит несколько дополнительных строк — верхний и нижний колонтитулы, блок листания и командную панель, на которой располагаются автоматически генериру- емые кнопки. Пользовательский интерфейс элемента управления конфигурируется с помощью свойств, описанных в табл. 11-2. Табл. 11-2. Визуальные свойства элемента управления DetailsView Свойство Описание BacklmageUrl Позволяет задать URL фонового изображения Caption Позволяет задать текст заголовка элемента управления CaptionAlign Позволяет указать способ выравнивания заголовка элемента управления CellPadding Позволяет задать расстояние в пикселах между границами ячеек и содержащимся в них текстом CellSpacing Позволяет задать расстояние в пикселах по горизонтали и по вертикали между смежными ячейками EmptyDataText Позволяет задать текст, выводимый в элементе управления, когда он связан с пустым источником данных FooterText Позволяет задать текст нижнего колонтитула Gridlines Позволяет задать стиль разделительных линий элемента управления HeaderText Позволяет задать текст верхнего колонтитула HorizontalAlign Позволяет указать способ выравнивания элемента управления на странице по горизонтали
440 Часть II Размещение данных на сайте Перечисленные свойства воздействуют на элемент управления как целое. Кроме того, вы можете задать стили для разных типов его элементов. Каждое из стилевых свойств, описанных в табл. 11-3, возвращает объект TableltemStyle, в свойствах кото- рого и задаются необходимые установки. Табл. 11-3. Стилевые свойства элемента управления DetailsView Свойство Описание AltematingRowStyle Определяет стилевые свойства полей данных, расположенных в чет- ных строках CommandRowStyle EditRowStyle Определяет стилевые свойства командной панели Определяет стилевые свойства строк элемента управления в режиме редактирования EmptyDataRowStyle Определяет стилевые свойства строки, которая выводится, когда ис- точник данных пуст FieldHeaderStyle FooterStyle Определяет стилевые свойства имен полей Определяет стилевые свойства нижнего колонтитула элемента управ- ления HeaderStyle Определяет стилевые свойства верхнего колонтитула элемента управления InsertRowStyle Определяет стилевые свойства строк элемента управления в режиме вставки PagerStyle RowStyle Определяет стилевые свойства блока листания Определяет стилевые свойства строк данных Элемент управления может отображаться в одном из трех режимов, которым соот- ветствуют три члена перечисления DetailsViewMode\ Readonly, Insert или Edit. Режим Readonly активен по умолчанию, в нем пользователь может лишь просматривать содержимое записи. Для того чтобы ее отредактировать или добавить новую запись, пользователь должен щелкнуть соответствующую кнопку на командной панели. По умолчанию кнопки вставки, удаления и редактирования записей не выводятся их вывод нужно явно включать посредством свойств AutoGenerateXXXButton. Другие свойства, связанные с состоянием элемента управления, перечислены в табл. 11-4. Табл. 11-4. Свойства, связанные с состоянием элемента управления DetailsView Свойство Описание BottomPagerRow Возвращает объект DetailsViewRow, представляющий нижнии блок листания элемента управления CurrentMode Возвращает значение, идентифицирующее текущий режим работы элемента управления, — оно принадлежит к перечислению Details- ViewMode Dataltem Возвращает объект данных, представляющий текущую отображае- мую запись J. -МЛ > DataKey Возвращает объект DataKey, содержащий значения ключевых полей текущей записи, заданных в свойстве DataKeyNames DataltemCount Dataltemlndex DataKeyNames Возвращает общее число записей в их источнике Возвращает и позволяет задать индекс текущей отображаемой записи Возвращает и позволяет задать массив имен полей первичного ключа отображаемых записей; эти поля используются для идентификации записи, над которой выполняется операция обновления или удаления Fields Возвращает коллекцию объектов DataControlField, использовавшихся для генерирования коллекции Rows
Отображение отдельных записей Глава 1Т 441 Табл. 11-4. (окончание) Свойство Описание FooterRow Возвращает объект DetailsViewRow, представляющий нижний колон- титул элемента управления HeaderRow Возвращает объект Details View Row, представляющий верхний колон- титул элемента управления PageCount Возвращает общее число записей в их источнике Pagelndex Возвращает О-базированный индекс текущей записи, отображаемой в элементе управления Rows Возвращает коллекцию объектов DetailsViewRow, представляющих строки данных элемента управления SelectedValue Возвращает значение первого из полей выделенной строки, составля- ющих первичный ключ TopPagerRow Возвращает объект DetailsViewRow, представляющий верхний блок листания элемента управления Если стандартный способ рендеринга элемента управления вас не устраивает, то с помощью свойств-шаблонов, перечисленных в табл. 11-5, можно изменить пользо- вательский интерфейс отдельных его элементов. Табл. 11-5. Свойства элемента управления DetailsView, предназначенные для определения шаблонов Свойство Описание EmptyDataTemplate Определяет шаблон, используемый для вывода элемента управления, когда он связан с пустым источником данных. Если свойства Empty- DataTemplate и EmptyDataText установлены одновременно, преимуще- ство отдается первому из них FooterTemplait Определяет шаблон для вывода нижнего колонтитула элемента управления HeaderTemplate Определяет шаблон для вывода верхнего колонтитула элемента управления. Это свойство имеет преимущество перед свойством HeaderText, когда они оба установлены PagerTemplate Определяет шаблон для вывода блока листания. Это свойство имеет преимущество перед свойством PagerSettings, когда они оба установ- лены Как видите, в списке шаблонов элемента управления DetailsView нет шаблонов для вывода текущей записи. Поэтому, если вам потребуются такие свойства, как Insert- Template или ItemTemplate, воспользуйтесь другим элементом управления — FormView. Он очень сходен с элементом управления DetailsView, но настраивается гибче — его вывод всецело определяется шаблонами. У элемента управления DetailsView имеется всего один метод — ChangeMode, пред- назначенный для переключения в заданный режим отображения: public void ChangeMode(DetailsViewMode newMode) Данный метод используется самим элементом управления, когда пользователь щелкает ту или иную командную кнопку. События элемента управления DetailsView Элемент управления DetailsView генерирует ряд событий (табл. 11-6), позволяющих разработчику выполнять в разные моменты его жизненного цикла пользовательский код. Модель событий данного элемента подобна модели событий элемента управления GridView, в частности, в ней также определены пары событий делается-сделано.
442 Часть II Размещение данных на сайте Табл. 11-6. События элемента управления DetailsView События Когда происходят ItemCommand Пользователь щелкает тот или иной элемент пользовательского ин- терфейса; сюда относятся не только стандартные командные кнопки элемента управления (кнопки удаления, вставки и редактирования), щелчки которых он обрабатывает сам, но и пользовательские кнопки, определенные в составе шаблонов ItemCreated ItemDeleting, ItemDeleted После создания всех строк Пользователь щелкает кнопку удаления записи: одно событие гене- рируется до, а другое после выполнения элементом управления этой команды Iteminserting, Itemlnserted Пользователь щелкает кнопку вставки записи: одно событие генери- руется до, а другое после выполнения элементом управления этой команды ItemUpdating, ItemUpdated Пользователь щелкает кнопку обновления записи: одно событие гене- рируется до, а другое после выполнения элементом управления этой команды ModeChanging, ModeChanged PagelndexChanging, PagelndexChanged Элемент управления переключается в другой режим работы: одно со- бытие генерируется до, а другое после переключения Пользователь щелкает кнопку перехода к другой записи: одно собы- тие генерируется до, а другое после выполнения элементом управле- ния этой команды Событие ItemCommand генерируется только в том случае, если щелчок не обрабо- тан стандартным методом. Обычно оно означает, что пользователь щелкнул кнопку, определенную в одном из шаблонов. Перехватывать и обрабатывать его для кнопок вставки, удаления и редактирования не нужно. Простейший способ связывания элемента управления DetailsView с данными Организовать просмотр записей с помощью элемента управления Details View исклю- чительно просто. Вы размещаете этот элемент в форме, связываете его с компонентом, представляющим источник данных, и добавляете несколько декларативных установок. Ниже демонстрируется минимальная конфигурация элемента: <asp:DetailsView runat="server” id="RecordView" DataSou rceID="MySou rce" Heade rT ext=”Employees"> </asp:DetailsView> Когда свойство AllowPaging установлено в true, элемент Details View выводит блок листания, позволяющий пользователю переходить от записи к записи. Разумеется, это срабатывает только в том случае, когда с элементом управления связано более одной записи. А вот более реалистичный пример определения элемента управления DetailsView (результат обработки этой разметки показан на рис. 11-2 ): <asp:ObjectDataSource ID="RowDataSource" runat="server" TypeName="ProAspNet20.DAL.Employees" SelectMethod="LoadAll”> </asp:Obj ectDataSou rce> <asp:DetailsView ID=”RecordView" runat="server" DataSourceID="RowDataSource" AllowPaging="true" Heade rText=" No rt hwind Employees" AutoGenerateRows="false"> <PagerSettings Mode="NextPreviousFirstLast" />
Отображение отдельных записей Глава 11 443 <Fields> <asp:BoundField DataField="firstname" HeaderText="First Name" /> <asp:BoundField DataField="lastname" HeaderText=”Last Name" /> <asp:BoundField DataField="title” HeaderText="Title" /> <asp:BoundField DataField="birthdate" HeaderText="Birth" DataFormatString="{0:d}" /> </Fields> </asp:DetailsView> Рис. 11-2. Элемент управления DetailsView, в котором выводятся результаты выполнения запроса Связывание элемента управления DetailsView с данными По умолчанию в элементе управления DetailsView выводятся все поля записи, с кото- рой он связан, и соответствующие им вложенные элементы управления генерируются автоматически. Если же свойство AutoGenerateRows этого элемента установить в false, то будут выводиться лишь те поля, которые явно перечислены в коллекции Fields. Как и элемент управления GridView, элемент DetailsView может одновременно содержать объявленные и автоматически сгенерированные поля. В таком случае объявленные поля выводятся первыми, а сгенерированные автоматически не добавляются в кол- лекцию Fields. Элемент управления DetailsView поддерживает тот же набор типов полей, что и GridView (см. главу 10). На тот случай, если источник данных окажется пустым, можно задать шаблон EmptyDataTemplate, чтобы вывод элемента управления был более дружественным пользователю: <asp:DetailsView runat="server" datasourceid="MySource"> <EmptyDataTemplate> <asp label runat="server"> There s no data to show in this view </asp:label> </EmptyDataTemplate> </asp:DetailsView> Но если вы просто хотите вывести текстовое сообщение, задайте его в свойстве EmptyDataText — и вам меньше работы, и страница будет отображаться быстрее. Когда источник данных элемента управления не пуст, значения свойств EmptyDataTemplate и EmptyDataText игнорируются.
444 Часть II Размещение данных на сайте Поля, выводимые в элементе управления, можно определять не только декларатив но (с помощью тэга <Fields>), но и программно. Для этого нужно создать необходимые объекты, сконфигурировать их и добавить в коллекцию Fields'. BoundField field = new BoundFieldO; field.DataField = "companyname"; field.HeaderText = "Company Name"; detailsViewl.Fields.Add(field); На странице поля выводятся в том порядке, в каком они заданы в коллекции Fields или перечислены в тэге <Fields>. Примечание Если вы программно добавляете поля в элемент управления, знайте, что они не будут автоматически сохраняться в состоянии представления и при следующем возврате формы исчезнут. (Как вы, вероятно, помните, точно так же обстоит дело с элементами управления GridView и DataGrid, описанными в предыдущей главе.) Так что код добавле- ния полей нужно помещать в обработчик события Page_Load, чтобы он выполнялся при каждом возврате формы. Если добавление полей производится на условной основе, после выполнения этой задачи надо записать в состояние представления пользовательский флаг. Потом (при возврате формы) нужно будет проверять его в обработчике события Page_Load и добавлять или не добавлять поля в зависимости от того, установлен ли этот флаг. Управление отображением полей Как и табличные элементы управления, элемент DetailsView может отображать все поля источника данных или только некоторое их подмножество. Я уже упоминал, что автоматическое генерирование строк элемента управления, представляющих поля источника данных, можно отключить путем установки свойства Auto Generate Rows в false. После этого вы объявляете необходимое количество полей в элементе <jFieZds'> <asp:DetailsView> <Fields> <asp:BoundField DataField="firstname" HeaderText="First Name" /> <asp:BoundField DataField="lastname" HeaderText="Last Name" /> <asp:BoundField DataField="title" HeaderText="Position" /> </Fields> </asp:DetailsView> В атрибуте HeaderText задается текст надписи, выводимой слева от значения поля. Стиль этой надписи задается в свойстве FieldHeaderStyle. Например, следующий код позволяет отобразить надпись полужирным шрифтом: <FieldHeaderStyle Font-Bold="true" /> Для того чтобы выводимые данные были более читабельными, нужно выбирать тип строки элемента управления DetailsView, наиболее соответствующий типу данных. Например, значения булева типа лучше всего выводить в строках типа CheckBoxField, тогда как URL — в строках типа HyperLinkField. Особенно это важно для редактирова- ния данных. По умолчанию в режиме редактирования или вставки значение вводится в текстовое поле, что удобно для многих типов данных, однако не для всех. Напри- мер, если пользователю нужно отредактировать дату, то для такого случая больше подходит элемент управления Calendar. Его можно было бы задать в шаблоне, но элемент управления DetailsView не поддерживает шаблонов строк данных. В подобных ситуациях следует прибегнуть к элементу управления Form View. Переход от записи к записи Хотя в каждый конкретный момент времени в элементе управления DetailsView вы- водится только одна запись, он нередко бывает связан с группой записей, например
Отображение отдельных записей Глава 11 445 с таблицей источника данных. В сценарии с главным и подчиненным представлениями этот элемент связан с одной записью, а когда он просто используется как средство просмотра, добавления или редактирования записей, его связывают с таблицей ис- точника данных и выводят в нем блок листания — панель с набором кнопок для перехода между записями. По умолчанию блок листания не отображается, и чтобы элемент управления его выводил, нужно установить свойство AllowPaging в true. (Но и тогда переход между записями, разумеется, будет возможен только при условии, что их больше одной.) Возможности конфигурирования блока листания элемента DetailsView точно такие же, как у элемента GridView'. здесь могут выводиться ссылки для перехода к следующей и предыдущей записям, дополнительно — к первой и последней, а в качестве альтер- нативы — числовые ссылки для перехода к конкретным страницам (роль страницы в данном случае играет одна запись). Соответствующие установки задаются в свойстве PagerSettings, а свойство PagerStyle определяет внешний вид блока листания. Механизм листания элемента управления DetailsView использует свойство Page- lndex, в котором содержится индекс текущей записи источника данных. Когда поль- зователь щелкает ту или иную навигационную кнопку, значение свойства Pagelndex изменяется, элемент управления повторяет операцию связывания с данными и об- новляет свой пользовательский интерфейс. Общее число записей в источнике данных возвращает свойство PageCount. Переход к другой записи сопровождается парой со- бытий: PagelndexChanging и PagelndexChanged. Событие PagelndexChanging позволяет выполнить пользовательский код до того, как значение свойства Pagelndex изменится, то есть до перехода элемента управления к другой записи. Этот переход можно отменить, установив свойство Cancel второго аргумента обработчика события в true'. void PageIndexChanging(object sender, DetailsViewPageEventArgs e) e.Cancel = true; } Заметьте, что в обработчике события PagelndexChanging доступны все данные теку- щей записи, но о той записи, к которой будет выполнен переход, вы не знаете ничего, кроме индекса. Получение значений полей текущей записи осуществляется так же, как в случае с элементом управления GridView — посредством свойства DataKey. DataKey data = DetailsView-!. DataKey; string country = (string) data.Values["country”]; if (country == "Mexico" || country == "USA" || country == "Brazil") { } Чтобы иметь возможность пользоваться свойством DataKey в обработчиках со- бытий, связанных с данными, нужно при определении элемента управления задать в свойстве DataKeyNames разделенный запятыми список полей, значения которых вы хотите сохранять в состоянии представления и сделать доступными посредством структуры DataKey. <asp:DetailsView ID="DetailsView1” runat="server" DataKeyNames="id, country"
446 Часть II Размещение данных на сайте Учтите, что в свойстве DataKeyNames можно задавать имена только открытых свойств типа связанных данных. Иными словами, в нашем примере, если элемент управления DetailsView связан с объектом DataSet или DataTable, то id и country долж- ны быть полями записи, а если он связан с пользовательской коллекцией, тогда они должны быть именами свойств. К сожалению, не существует простого способа доступа к следующей записи в об- работчике события PagelndexChanging. Лучшее, что вы можете сделать, это кэшировать набор данных, связанный с элементом управления DetailsView, получать ссылку на кэшируемые данные и выбирать запись, соответствующую новому индексу. ^7] Примечание С переходом между записями в элементе управления DetailsView связаны те же проблемы, что и с межстраничным переходом в элементах управления DataGrid и GridView. Если элемент управления связан с объектом SqlDataSource, то лучше от- казаться от кэширования его данных; если же элемент управления связан с объектом ObjectDataSource, то желательно использовать такие бизнес-объекты, которые сами вы- полняют переход от записи к записи. Использование технологии обратного вызова сценария Обычно листание реализуется путем обработки серверных событий и требует полного обновления страницы. Однако это не единственная возможность: элемент управления DetailsView обладает свойством EnablePagingCallbacks, позволяющим включить режим листания с использованием клиентской функции обратного вызова. Когда применяется технология обратного вызова сценария, возврат формы на сервер не производится, а данные новой записи запрашиваются путем параллельно- го вызова сервера. Обработав этот вызов, сервер возвращает браузеру данные, и тот активизирует функцию обратного вызова для их получения и обновления пользова- тельского интерфейса элемента управления. Чтобы все это было возможно, браузер должен поддерживать объектную модель DHTML. Сама по себе технология обратного вызова сценария довольно сложна, но для ее активизации достаточно установить свойство EnablePagingCallbacks в true. Создание главного и подчиненного представлений В ASP.NET 1.x задача реализации главного и подчиненного представлений не такая уж трудоемкая, но все же требует написания некоторого количества программного кода. А вот в ASP.NET 2.0 эта задача практически автоматизирована. Достаточно со- единить два элемента управления: подчиненный (например, DetailsView) и главный (табличный либо списочный, например, GridView или DropDownList), — так, чтобы пользователь выбирал в главном элементе управления одну из записей, и эта запись тут же становилась источником данных подчиненного элемента управления. Давайте посмотрим, как это можно сделать. Развернутое представление выбранной записи Типичная страница с главным и подчиненным представлениями содержит два элемен- та управления: главный (например, GridView) и подчиненный (например, DetailsView). Каждый из них связан со своим источником данных. Задача состоит в том, чтобы связать подчиненный элемент управления с записью, выбранной в главном элементе управления. Вот какой должна быть конфигурация главного элемента управления GridView при условии, что он связан с компонентом ObjectDataSource, поддержива- ющим листание: <asp:ObjectDataSou rce ID="Custome rsDataSou rce" runat="se rve r" EnablePaging="true"
Отображение отдельных записей Глава 11 447 Sta rtRowIndexPa ramete rName=”fi rstRow" MaximumRowsParameterName="totalRows" TypeName="ProAspNet20.DAL.Customers" SelectMethod="LoadAll"> </asp:Obj ectDataSou rce> <asp:GridView ID="GridViewl” runat="server" DataSou rceID="CustomersDataSou rce" DataKeyNames="id" AllowPaging="True" AutoGenerateSelectButton="True" AutoGenerateColumns="False"> <PagerSettings Mode="NextPreviousFirstLast" /> <Columns> <asp:BoundField DataField=”CompanyName" HeaderText="Company" /> <asp:BoundField DataField="Country" HeaderText=”Country" /> </Columns> </asp:GridView> В таблице выводится столбец с кнопками для выбора записи, однако обрабатывать событие SelectedlndexChanged вам не нужно. Следующая разметка содержит определение подчиненного представления нашей страницы, основанного на элементе управления DetailsView: <asp:ObjectDataSource ID="RowDataSource” runat="server" TypeName="ProAspNet20.DAL.Customers" SelectMethod="Load"> <Select Pa ramete rs> <asp:ControlParameter Name="id" ControlID="GridView1" PropertyName="SelectedValue" /> </Select Pa ramete rs> </asp:Obj ectDataSou rce> <asp:DetailsView ID="DetailsView1" runat="server" HeaderText="Customer Details" EmptyDataText="No customer currently selected" DataSou rceID="RowDataSou rce" AutoGenerateRows="False" AutoGenerateInsertButton="True" AutoGenerateDeleteButton="True" AutoGenerateEditButton="True"> <Fields> <asp:BoundField DataField="ID" HeaderText=”ID" /> <asp:BoundField DataField="CompanyName" HeaderText="Company" /> <asp:BoundField DataField="ContactName" HeaderText="Contact" /> <asp:BoundField DataField="Street" HeaderText="Address" /> <asp:BoundField DataField=”City" HeaderText="City" /> <asp:BoundField DataField="Country” HeaderText="Country" /> </Fields> </asp:DetailsView> Здесь элемент управления DetailsView связан co значением, возвращаемым ме- тодом Load класса Customer слоя доступа к данным. Этот метод получает в качестве аргумента идентификатор клиента, который извлекается из свойства SelectedValue элемента управления GridView. Когда пользователь выделяет строку в таблице, зна- чение свойства SelectedValue изменяется (см. главу 10), выполняется возврат формы и элемент управления DetailsView обновляет свой пользовательский интерфейс,
448 Часть II Размещение данных на сайте получая данные выделенной строки. Как видите, для этого не понадобилось писать ни строчки программного кода. На рис. 11-3 показана страница, на которой в таблице не выделена ни одна строка. (Теперь вы понимаете, для чего нужен шаблон EmptyDataTemplateV) А на рис. 11-4 представлена та же страница с выделенной в таблице записью. Рис. 11-3. Страница с главным и подчиненным представлениями данных — ни одна запись не выделена Рис. 11-4. Страница с главным и подчиненным представлениями данных — отображается дополнительная информация из выделенной записи * Заметьте, что метод Load вызывается каждый раз, когда страница формируется на сервере, в том числе при ее первоначальной загрузке, когда ни одна строка в таблице еще не выбрана. В этом случае метод также получает значение свойства SelectedValue,
Отображение отдельных записей Глава11 449 которым в такой ситуации является null. Что тогда происходит? Это зависит от ре- ализации метода Load. Если он проверит свой аргумент на равенство значению null и выполнит соответствующие этому обстоятельству действия, все будет нормально и страница выведет шаблон, заданный для пустой строки. В противном случае вы можете получить от инфраструктуры ADO.NET исключение времени выполнения, сообщающее о неудачной попытке извлечения данных из источника. Вот хорошая схема метода Load, предоставляющего запрошенные у компонента данные: public static Customer Load(string id) { if (String.IsNullOrEmpty(id)) return null; } Кэширование данных Приведенная схема страницы с главным и подчиненным представлениями очень проста, причем для создания такой страницы в Microsoft Visual Studio .NET 2005 не нужно писать ни строчки кода. Вам остается лишь убедиться, что страница работает так, как требуется. Можно ли желать лучшего? Давайте рассмотрим такой способ связывания элементов управления с данными немного подробнее. Предположим, что на странице выводится таблица со списком клиентов, кото- рый она получает от компонента, представляющего источник данных. Очевидно, что этот список где-то кэшируется. Если используется компонент SqlDataSource, можно управлять кэшированием с помощью соответствующих свойств. При использовании компонента ObjectDataSource, как в приведенном выше примере, кэширование не про- изводится, если вы не дали методу Load или в общем случае слою доступа к данным указание кэшировать данные. Все данные, связанные с таблицей, извлекаются из базы данных при каждом возврате формы, в частности, при сортировке данных в таблице или при переходе к другой странице. Когда пользователь выделяет определенную строку таблицы, производится свя- зывание элемента управления DetailsView с соответствующей записью в источнике данных, которая извлекается с помощью другого запроса. Выполнение этого запроса не всегда является обязательным. В частности, без него можно обойтись, если главный и подчиненный элементы управления связаны с одной таблицей источника данных (в нашем примере это таблица Customers) и притом главный элемент управления уже содержит все данные, необходимые подчиненному элементу. У нас метод LoadAll, по- средством которого заполняется таблица, возвращает коллекцию результатов запроса SELECT [поля] FROM customers. Зачем же элементу управления DetailsView выполнять еще один запрос, возвращающий уже имеющиеся данные (при условии, конечно, что эти данные кэшируются)? Как вы уже знаете, компонент ObjectDataSource не поддерживает кэширование данных, если для их получения не используются контейнеры данных ADO.NET. Кроме того, кэширование позволяет ускорить работу приложения лишь в том случае, когда данных немного — скажем, несколько сотен записей. Если компонент, представ- ляющий источник данных, не поддерживает кэширование, его можно реализовать в бизнес-объектах. Таким образом, при использовании компонента SqlDataSource или ObjectDataSource с объектом ADO.NET стоит включать кэширование, но с учетом объ- ема кэшируемых данных. И в любом случае следует пользоваться профилировщиком SQL Server (или другим подобным средством, предоставляемым вашей СУБД) для контроля количества извлекаемых из базы данных записей.
450 Часть li Размещение данных на сайте Редактирование данных Элементы управления, подобные DetailsView, особенно полезны в тех случаях, когда основной работой пользователя является обновление, удаление и добавление данных. На командной панели элемента управления Details View имеются все необходимые для этого кнопки. Вы даете элементу указание создать эти кнопки, устанавливая в true свойства AutoGenerateEditButton, AutoGenerateDeleteButton и AutoGeneratelnsertButton. Редактирование текущей записи Как и в случае с элементом управления GridView, все операции с данными элемента управления DetailsView осуществляет связанный с ним компонент, представляющий источник данных. Для этого компонента нужно определить необходимые команды, а также задать в свойстве DataKeyNames ключ, идентифицирующий текущую запись: <asp:SqlDataSource runat="server" id="SqlDataSource1" Connections!ring="<%$ Connectionstrings:LocalNWind %>” SelectCommand="SELECT * FROM customers" UpdateCommand="UPDATE customers SET companyname=@companyname, contactname=@contactname, city=@city, country=@country WHERE customerid=@original_customerid" DeleteCommand="DELETE customers WHERE customerid=@original_customerid” /> <asp:DetailsView ID="0etailsView1" runat="server" DataKeyNames="custome rid" DataSourceID="SqlDataSource1" AllowPaging=”True" AutoGenerateRows="False" HeaderText="Customers" AutoGenerateEditButton="True" AutoGenerateDeleteButton=”True"> <PagerSettings Mode="NextPreviousFirstLast" /> <Fields> <asp:BoundField DataField="CompanyName" HeaderText="Company" /> <asp:BoundField DataField="ContactName" HeaderText="Contact" /> <asp:BoundField DataField="City" HeaderText="City" /> <asp:BoundField DataField="Country" HeaderText=”Country" /> </Fields> </asp:DetailsView> Компонент SqlDataSource выполняет обновление и удаление записей с помощью определенных вами SQL-команд или хранимых процедур (см. главу 9). Остальную работу делает элемент управления DetailsView. Когда пользователь щелкает кнопку редактирования или удаления текущей записи, элемент управления вызывает ком- понент, представляющий источник данных. На рис. 11-5 показан интерфейс элемента управления DetailsView в режиме редакти- рования записи. Заметьте, что стандартный набор кнопок на командной панели в этом режиме заменен парой кнопок, служащих для подтверждения и отмены изменений. Связывая элемент управления DetailsView с компонентом ObjectDataSource, убеди- тесь, что для последнего правильно заданы методы обновления и удаления записей: <asp:ObjectDataSource runat="server" id="0bjectDataSource1" TypeName="ProAspNet20.DAL. Customers" SelectMethod="LoadAll"
Отображение отдельных записей Глава 11 451 DeleteMethod="Delete" UpdateMethod="Save" Data0bjectTypeName="ProAspNet20 DAL.Customer" /> Рис. 11-5. Элемент управления DetailsView режиме редактирования записи Свойство DataKeyNames должно содержать имя открытого свойства, представля- ющего ключ для идентификации обновляемой или удаляемой записи. Удаление текущей записи Хотя элемент управления DetailsView позволяет осуществить пред- и постобработку команды удаления записи, генерируя для этого события ItemDeleting и ItemDeleted, выполнить в нем такое типичное действие, как предоставление пользователю воз- можности отменить случайно инициированную операцию удаления, не так-то просто. В отличие от других элементов управления, связанных с данными, элемент управления DetailsView не предоставляет событий и свойств для изменения своего стандартного поведения. Вы могли предположить, что обработчик события ItemCreated — подхо- дящее место для перехвата события создания командной панели и связывания поль- зовательского сценария с кнопкой удаления записи. Это действительно подходящая (и единственная) точка входа механизма, создающего элемент управления. Однако в нем почему-то не предусмотрена возможность выполнения каких-либо действий над создаваемым элементом, в том числе и добавления диалогового окна для под- тверждения операции удаления записи. Событие ItemCreated генерируется при создании строки элемента управления DetailsView, но оно не содержит никакой информации об этой строке. Более того, у элемента управления не существует прямого свойства для доступа к командной панели, подобного свойствам для доступа к блоку листания, нижнему и верхнему колонтитулам. Поэтому вам придется прибегнуть к небольшому трюку. Если включить трассировку кода и в отладчике заглянуть в коллекцию Rows, то нетрудно выяснить, что в ней содержится столько же элементов, сколько строк данных выводится на экран, плюс еще один. Этот дополнительный элемент — и есть командная панель. Следовательно, получить к ней доступ можно с помощью такого кода: protected void DetailsView1_ItemCreated(object sender, EventArgs e) { if (DetailsView!.FooterRow != null)
452 Часть II Размещение данных на сайте { int commandRowIndex = DetailsViewl.Rows.Count-1; DetailsViewRow commandRow = DetailsViewl.Rows[commandRowIndex]; } } Чтобы доступ к командной панели осуществлялся только при условии ее нали- чия, выполняется проверка свойства FooterRow на равенство значению null. Строка нижнего колонтитула создается всегда, независимо от того, выводится она или нет, и происходит это после того, как будут созданы все строки данных. Командной панели соответствует последний элемент коллекции Rows, имеющий тип DetailsViewRow — особый тип строки таблицы. Данная строка содержит ячейку (внутренний объект типа DataControlFieldCell), в которой и выводятся кнопки редактирования, удаления и вставки записей. Опять же с помощью средства трассировки можно выяснить, что это не просто кнопки, а экземпляры класса DataControlLinkButton, производного от LinkButton. Доступ к указанной ячейке можно получить так: DataControlFieldCell cell; cell = (DataControlFieldCell) commandRow.Controls[0]; Мы почти у цели. Осталось лишь перебрать в цикле дочерние элементы управления ячейки и получить ссылки на командные кнопки. Как отличить кнопку удаления от, например, кнопки редактирования? И что, если какая-то из кнопок не выводится? Все кнопки на командной панели обладают свойством CommandName, в котором содер- жится уникальное имя операции: Delete, Edit или New. Взгляните на следующий код: protected void DetailsViewl ItemCreated(object sender, EventArgs e) { if (DetailsViewl.FooterRow != null) { int commandRowIndex = DetailsViewl.Rows.Count-1; DetailsViewRow commandRow = DetailsViewl.Rows[commandRowIndex]; DataControlFieldCell cell; cell = (DataControlFieldCell) commandRow.Controls[0]; foreach(Control ctl in cell.Controls) { LinkButton link = ctl as LinkButton; if (link != null) { if (link.CommandName == "Delete") { link.ToolTip = "Click here to delete"; link.OnClientClick = "return confirm('Do you really want to delete this record?');”; > I else if (link.CommandName == "New") { link.ToolTip = "Click here to add a new record"; } else if (link.CommandName == "Edit") { » link.ToolTip = "Click here to edit the current record”; } ) } } }
Отображение отдельных записей Глава 11 453 После получения ссылки на кнопку можно выполнить над этой кнопкой необ- ходимые действия, например, связать с ней всплывающую подсказку или сцена- рий на языке JavaScript, выводящий диалоговое окно для подтверждения операции (рис. 11 -6). Рис. 11-6. Запрос подтверждения перед удалением текущей записи Вставка новой записи Процесс добавления новой записи во многом подобен процессу редактирования или удаления. Вы даете элементу управления DetailsView указание разместить на панели команд кнопку для вставки записи и определяете для компонента, представляющего источник данных, команду вставки: <asp:SqlDataSource ID="SqlDataSource1" runat="server” EnableCaching="t rue" Connections!ring='<%$ Connectionstrings:LocalNWind %>’ SelectCommand="SELECT employeeid, firstname, lastname, title, hiredate FROM employees" Inser!Command="INSERT INTO employees (firstname, lastname, title, hiredate) VALUES (©firstname, @las!name, ©title, ©hiredate)" /> <asp:DetailsView ID="DetailsView1" runat="server" AllowPaging="!rue" DataSourceID="SqlDataSoureel" AutoGenerateInsertButton=”true" HeaderText="Employee Details" > <PagerSettings Mode="NextPreviousFirstLast" /> </asp:DetailsView> Результат показан на рис. 11-7. При написании команды вставки особое внимание уделяйте первичному ключу. В частности, в приведенной выше команде значение первичного ключа (поля emplo- yeeid) не задано, поскольку база данных автоматически генерирует значения указан- ного поля. Это самый лучший случай. При работе с таблицей, в которой ключи строк определяются пользователем, необходимо реализовать валидационный механизм,
454 Часть II Размещение данных на сайте гарантирующий уникальность ключа вставляемой записи. Соответствующий код лучше всего поместить в слой доступа к данным, связанный с элементом управления DetailsView через компонент ObjectDataSource. К теме валидации вводимых данных я еще вернусь. Рис. 11-7. Элемент управления DetailsView в режиме вставки записи Поля с шаблонами Элемент управления DetailsView не поддерживает шаблонов редактирования и вставки записи, с помощью которых можно было бы полностью изменять его пользовательский интерфейс в соответствующих режимах. Выбор у вас небольшой: либо согласиться на стандартный пользовательский интерфейс (вертикальный список пар заголовок-зна- чение), либо обратиться к другому элементу управления — элементу Form View или специализированному элементу управления. Разработчики элемента DetailsView стре- мились сделать его максимально простым и эффективным, из-за чего в некоторых случаях ему недостает гибкости и открытости для подключения пользовательского кода. Как вы уже видели на примере с удалением записей, для преодоления ограни- чений этого элемента управления зачастую можно найти обходные пути, но заставить его играть по вашим правилам непросто. Пусть нельзя задать шаблон редактирования или вставки для всего элемента управ- ления, зато вы можете задать набор шаблонов для отдельного поля, добавив это поле в элемент управления в виде объекта класса TemplateField. Данный класс вам уже знаком — он поддерживается и табличными элементами управления: <asp:TemplateField HeaderText="Country"> <ItemTemplate> <asp:literal runat="server" Text='<%# Eval("country") %>' /> </ItemTemplate> <EditItemTemplate> <asp:dropdownlist ID="DropDownList1" runat="server" datasou rceid="Count riesDataSou rce' selectedvalue='<%# Bind("country") %>' /> </EditItemTemplate> </asp:TemplateField> Согласно этому определению содержимое поля Country в режиме просмотра выво- дится в литеральной форме, а в режиме редактирования — в виде раскрывающегося
Отображение отдельных записей Глава 11 455 списка. Список связан с компонентом, представляющим источник его данных. Опе- ратор Bind, с помощью которого получается выделенное в списке значение, подобен оператору Eval, но отличается от него двусторонним действием — он не только воз- вращает прочитанные из источника данные но и записывает измененное значение в источник данных (см. главу 9). На рис. 11-8 показана демонстрационная страница с элементом управления DetailsView, в котором в режиме редактирования одно поле выводится в виде раскрывающегося списка, а другое — в виде календаря. Рис. 11-8. Поля элемента управления DetailsView, выводимые с использованием шаблонов Поддержка валидации Используя шаблоны, можно определять не только особые способы рендеринга полей, но и валидационные элементы управления. Однако если требуется только валидация, а стандартный способ рендеринга поля вас вполне устраивает, можно прибегнуть к другому решению: использовать обычные поля BoundField и связать с ними валида- торы программным способом. Создается обработчик события ItemCreated элемента управления DetailsView. protected void DetailsView1_ItemCreated(object sender, EventArgs e) { if (DetailsViewl.CurrentMode == DetailsViewMode.Readonly) return; if (DetailsViewl FooterRow == null) // Строки данных отсутствуют return; AddRequiredFieldValidator(0, "First name required"); AddRequiredFieldValidator(1, "Last name required");
456 Часть И Размещение данных на сайте В этом обработчике мы сначала убеждаемся, что элемент управления находится в режиме редактирования и все его строки данных уже созданы, а затем действуем исходя из предположения, что нам известны позиции модифицируемых строк. (Это вполне разумное предположение, поскольку мы не разрабатываем универсальное решение, а имеем дело с одной конкретной страницей ASP.NET.) Метод AddRequiredFieldValidator принимает индекс поля, значение которого под- лежит валидации, и сообщение, которое должно быть выведено в случае, если это поле окажется пустым. Он создает и инициализирует экземпляр валидатора, а затем добавляет его в соответствующую ячейку: void AddRequiredFieldValidator(int rowindex, string msg) { // Извлекаем нужную строку данных const int DataCelllndex = 1; DetailsViewRow row = DetailsViewl.Rows[rowindex]; // Извлекаем вторую ячейку - в первой содержится надпись DataControlFieldCell cell; cell = (DataControlFieldCell) row.Cells[DataCellIndex]; // Создаем и инициализируем валидатор RequiredFieldValidator req = new RequiredFieldValidator(); req.Text = String.Format("<span title='{0}'>*</span>", msg); // Получаем идентификатор элемента управления // TextBox и связываем с ним валидатор string ctllD = cell.Controls[0].UniqueID; int pos = ctllD LastlndexOf("$"); if (pos < 0) return; string temp = ctllD.Substring(pos +1); req.ControlToValidate = temp, // Вставляем валидатор в ячейку cell.Controls.Add(req); } Извлекая строку данных, которую нужно связать с валидатором, вы получаете ссылку на ее вторую ячейку. Каждая строка данных элемента управления DetailsView содержит две ячейки: в первой выводится имя поля, а во второй — его значение. В режиме ре- дактирования или вставки во второй ячейке содержится элемент управления TextBox. Валидационному элементу управления (в данном примере элементу Required- FieldValidator) необходимы определенные поведенческие установки (в частности вы- водимое сообщение), а также идентификатор проверяемого элемента управления. Но как определить идентификатор текстового поля, которое генерируется автоматически? Для этого нужно получить ссылку на него и прочитать значение свойства UniquelD. Элемент управления DetailsView — это контейнер именования, а значит, его имя нужно добавлять к именам содержащихся в нем элементов. Например, внутренний элемент TextBox с именем cltO1 вне его известен под именем DetailsViewl$cltO1, где DetailsViewl — идентификатор элемента управления DetailsView. Валидатору нужно передавать внутренний идентификатор элемента управления. Поэтому приведенный код ищет в его идентификаторе последнее вхождение символа $ и удаляет его и все, что ему предшествует, а оставшуюся часть идентификатора присваивает свойству ControlToValidate валидатора.
Отображение отдельных записей Глава 11 457 Вот и все. Вы добавили в состав элемента управления DetailsView новый элемент управления, которому не нужно взаимодействовать с остальными его элементами, за исключением текстового поля. Результат показан на рис. 11-9. Рис. 11-9. Элемент управления DetailsView с валидатором Приведенный код выводит рядом с незаполненными полями символы звездочки, а текст сообщения отображается в виде всплывающей подсказки, для чего в вывод элемента управления вставляется тэг <span>. Вы, конечно, можете сконфигурировать валидатор по собственному усмотрению. Валидация без валидаторов Мы рассмотрели два сценария валидации данных, редактируемых с помощью элемента управления DetailsView. В первом используются шаблоны, в которые явно включаются валидационные элементы управления, а во втором — обычные связанные с данными поля, валидаторы же добавляются программным способом. Но существует более простое и естественное решение задачи проверки введенных данных — использование событий. Вы пишете обработчик события ItemUpdating {Iteminserting в случае вставки или ItemDeleting в случае удаления записи), проверяете в нем новые значения и отменя- ете операцию, если что-то оказывается не так. Например, следующий код позволяет убедиться, что поле title содержит одну из двух жестко закодированных строк: void DetailsView1_ItemUpdating(object sender, DetailsViewUpdateEventArgs e) { string title = (string) e.NewValues["title"]; if (!title.Equals("Sales Representative") && I title Equals("Sales Manager")) { e.Cancel = true; } }
458 Часть II Размещение данных на сайте В словаре данных New Values объекта, полученного в качестве аргумента события, содержатся новые значения полей, отредактированные пользователем, а в словаре OldValues — исходные значения этих полей. Чем такое решение отличается от реше- ния, основанного на использовании валидаторов? Событие Item Updating (и другие ему подобные) генерируется на сервере при возврате формы. Валидаторы же могут перехватывать ошибки ввода еще на клиенте. Однако, как гласит золотое правило валидации, никогда не следует полагаться только на клиентскую валидацию. Опреде- ленная часть валидационного процесса всегда должна осуществляться на сервере, поэтому вопрос о производительности в данном случае снимается. Зато решение на основе событий легче реализовать и оно идеально для тех случаев, когда страни- ца создается на скорую руку и вы не хотите возиться со сложным, основанным на шаблонах, пользовательским интерфейсом. В то же время валидационные средства ASP.NET предоставляют полный ассортимент валидационных функций, в том числе обеспечивают возможность серверной валидации (см. главу 4). Принцип валидации без валидаторов может быть применен к любому элементу управления Vie®-типа: DetailsView, FormView или GridView. Элемент управления FormView Элемент управления Form View можно считать расширенной версией элемента управ- ления DetailsView, поддерживающей все возможные виды шаблонов. Как и DetailsView, он выводит записи по одной, извлекая их из заданного источника, и может генери- ровать кнопки для навигации по записям. В отличие от элемента управления Details- View, элемент управления Form View не использует поля источника данных и требует, чтобы разработчик определял все поля с помощью шаблонов. Элемент управления Form View поддерживает все базовые операции с данными, разумеется, при условии, что их поддерживает и источник данных. Обратите внимание: в элементе управления Form View все элементы пользователь- ского интерфейса формируются на основе шаблонов, так как отсутствует встроенный механизм рендеринга. Объектная модель элемента управления Form View Элементы управления Form View и DetailsView отличаются друг от друга не визуально, а функционально. У элемента управления FormView имеются свойства ItemTemplat- е, EditltemTemplate и InsertltemTemplate, которых нет у элемента DetailsView. В то же время в элементе управления FormView не предусмотрена встроенная командная панель, на которой выводятся кнопки для выполнения стандартных операций над записями. Поскольку всю графическую структуру этого элемента формируете вы сами, то сами должны и включить в его шаблон необходимые кнопки. Класс элемента управления Form View объявляется следующим образом: public class FormView : CompositeDataBoundControl, IDataltemContainer, INamingContainer Как видите, у него тот же базовый класс, что и у элемента управления DetailsView, и он реализует те же интерфейсы, за исключением интерфейсов, предназначенных для поддержки технологии обратного вызова сценария. Члены элемента управления FormView Свойства элемента управления FormView в большинстве своем совпадают со свой- ствами элемента управления Details View, что не удивительно, учитывая сходство этих
Отображение отдельных записей Глава 11 459 элементов. Поэтому за сведениями о свойствах элемента управления Form View вы можете обратиться к табл. 11-1-11-5. Программный интерфейс элемента управления Form View отличают только свойства-шаблоны и соответствующие им стили. Поддерживаемые шаблоны Как вы уже поняли, вывод элемента управления FormView целиком и полностью основывается на шаблонах. Это означает, что при использовании данного элемента вы как минимум должны задать шаблон для отображения записи данных. Список поддерживаемых шаблонов, используемых для этой цели, приведен в табл. 11-7. Табл. 11-7. Свойства-шаблоны элемента управления FormView Свойство Описание EditltemTemplate Шаблон для редактирования записи InsertltemTemplate Шаблон для ввода данных новой записи ItemTemplate Шаблон для просмотра записи Кроме того, элемент управления FormView поддерживает все шаблоны элемен- та DetailsView: HeaderTemplate, FooterTemplate, Empty DataTemlate и PagenTemplate (см. табл. 11-5). Поддерживаемые операции Поскольку пользовательский интерфейс элемента управления FormView определя- ется автором страницы, не приходится ожидать, что элемент сам будет реагировать на щелчок определенной кнопки и действовать соответственно. У него имеется не- сколько открытых методов (табл. 11-8), с помощью которых инициируются различные операции. Табл. 11-8. Методы элемента управления FormView Метод Описание ChangeMode Изменяет рабочий режим элемента управления; этому методу переда- ется значение из перечисления Form ViewMode, определяющее целевой режим: Readonly, Edit или Insert Deleteltem Удаляет текущую запись элемента управления из источника данных Insertitem Вставляет текущую запись элемента управления в источник данных. В момент вызова этого метода элемент управления должен находиться в режиме вставки, в противном случае будет выброшено исключение Updateitem Обновляет текущую запись элемента управления в источнике данных. В момент вызова метода элемент управления должен находиться в ре- жиме редактирования, в противном случае будет выброшено исключение Методам Insertitem и Updateitem передается значение булева типа, указывающее, должна ли производиться валидация. Если оно имеет значение true, активизируются валидационные элементы управления, определенные вами в составе шаблона. Если такие элементы управления не найдены, валидация не производится. Методы Insert- Item и Updateitem предназначены для инициирования соответствующих операций и вызываются элементами управления текущего шаблона. Им не нужно передавать ни вставляемой записи, ни новых значений, ни ключа удаляемой записи. Элемент управ- ления FormView сам отлично знает, как извлечь необходимую информацию (он делает это так же, как элемент управления DetailsView). Наличие методов Deleteltem, Insertitem и Updateitem предоставляет вам возможность определить собственный пользовательский интерфейс удаления, вставки и добавления
460 Часть II Размещение данных на сайте записей и подключить его к стандартной модели связывания с данными, действующей в ASP.NET. Связывание элемента управления FormView с данными Давайте рассмотрим процесс конфигурирования элемента управления Form View с по- мощью шаблонов на примере несложной демонстрационной страницы. Каждый ша- блон элемента Form View должен содержать все необходимые для выполнения своей задачи элементы пользовательского интерфейса и командные кнопки. Сам элемент предоставляет только блок листания и таблицу-контейнер. Верхний и нижний колонтитулы и блок листания Элемент управления Form View выводит данные в виде HTML-таблицы со строками верхнего и нижнего колонтитулов и необязательной строкой с кнопками для пере- хода между страницами. Подобно элементу DetailsView элемент управления FormView поддерживает шаблоны верхнего и нижнего колонтитулов, однако у него нет таких простых и удобных свойств, как HeaderText и FooterText: <asp:FormView ID="FormView1" runat="server” AllowPaging=”true” DataSou rceID="Custome rsDataSou rce"> <PagerSettings Mode="NextPreviousFirstLast" /> <HeaderTemplate> <h1>Customer Viewer</h1> </Heade rT emplate> <FooterTemplate> <h3>Courtesy of "Programming ASP NET"</h3> </FooterTemplate> * </asp:FormView> , Блок листания элемент управления может вывести сам согласно установкам» за- данным вами в свойствах PagerSettings и Pager Style, или создать на основе вашего шаблона, заданного в свойстве PagerTemplate. Вывод данных Ниже приведена типичная структура разметки элемента управления Form View, вклю- чаемой в состав страницы .aspx: <asp:FormView ID="FormViewT’ runat="server” DataSourceld='CustomersDataSouroe" AllowPaging="true”> <ItemTemplate> </ItemTemplate> <EditItemTemplate> </EditItemTemplate> < InsertltemTemplate > </InsertItemTemplate> </asp:FormView> А вот разметка элемента, показанного на рис. 11-10: <asp:FormView runat="server" id=”FormView1" DataKeyNames="employeeid"
Отображение отдельных записей Глава 11 461 DataSourceID="MySource" AllowPaging="true”> <ItemTemplate> <table style="border:solid 1px black;" width="100%"> <tr> <td bgcolor="yellow" width="50px" align="center”> <b><%# EvalC’id") %></b></td> <td bgcolor="lightyellow" > <b><%# Eval("companyname") %></bx/td> </tr> </table> <table style="font-family Verdana;font-size:8pt;"> <tr> <td><b>Contact</bx/td> <tdx%# Eval("contactname") %x/td> </tr> <tr> <tdxb>City</b></td> <tdx%# Eval ("city") %></td> </tr> <tr> <td valign="top"xb>Country</bx/td> <tdx%# Eval ("country”) %></td> </tr> </table> <br /> <asp’Button ID="EditButton runat='server" CommandName="Edit" Text="Edit" /> </ItemTemplate> </asp:FormView> Рис. 11-10. Элемент управления FormView Вся разметка, которую вы помещаете в шаблон ItemTemplate, выводится в ячейке таблицы, распространяющейся на два столбца. Как уже упоминалось, весь вывод элемента управления Form View представляет собой таблицу.
462 Часть II Размещение данных на сайте Если вы хотите, чтобы данные элемента выводились в виде таблицы, — пожалуйста, ничто не мешает определить вложенную таблицу, как это сделано в примере. Кнопка редактирования добавляется в виде классического элемента <asp '.Button > с именем команды Edit. Это имя команды служит для элемента управления Form View индикатором необходимости переключиться в режим редактирования и вывести свое содержимое с использованием шаблона EditltemTemplate, если таковой задан. Вы можете задавать любые командные кнопки с любыми именами команд и надписями, а если имена команд не совпадают со стандартными, вызывать из их обработчиков метод ChangeMode или другие методы элемента управления Form View. Функция Eval Как можно вставить в шаблон поле данных? Это делается с помощью выражения связывания с данными, содержащего вызов функции Eval: <td><%# Eval("city") %></td> ASRNET 2.0 поддерживает два способа вызова этой функции (см. главу 9): сокра- щенный (продемонстрированный выше) и полный, тогда как ASP.NET 1.x — только второй из них. Полная форма вызова функции Eval такова: <%# DataBinder.Eval(Container Dataltem, "city")%> Для разбора и вычисления выражения, заданного во втором параметре, статическая функция Eval класса DataBinder использует технологию рефлексии. Эта функция рабо- тает с объектом Container.Dataltem, который представляет текущую запись источника данных, выводимую в элементе управления. Обычно в выражении связывания с данными задается имя одного из полей этой записи, но у функции Eval есть перегруженная версия, принимающая в дополнение к объекту данных и имени поля строку форматирования. В ASP.NET 2.0 определена подобная функция с более компактным синтаксисом вызова — это функция Eval класса TemplateControl, наследуемого всеми страницами ASP.NET. Она является функцией экземпляра, принимает только выражение связы- вания с данными (имя поля) и необязательную строку форматирования. Эта функция самостоятельно определяет, над каким объектом выполняется операция, и затем вы- зывает функцию DataBinder.Eval. Функция Eval может использоваться только для однонаправленного связывания, когда данные считываются, но не записываются обратно в источник. А для реализации двунаправленного связывания в ASP.NET 2.0 определена еще одна функция — Bind, которую мы рассмотрим позже. Редактирование данных Для того чтобы пользователь имел возможность редактировать запись, с которой связан элемент управления, необходимо определить шаблон EditltemTemplate. С его помощью в форме можно разместить любые элементы управления, в том числе и валидаторы. Вы отнюдь не ограничены применением текстовых полей и, выбирая подходящие элементы, вольны следовать своему воображению. Как осуществляется извлечение значений для обновления связанной записи? Для этого применяется технология двунаправленного связывания, то есть вместо функции Eval в шаблоне используется функция Bind. Шаблон редактирования Следующий элемент разметки наглядно демонстрирует различие между шаблонами просмотра и редактирования записи:
Отображение отдельных записей Глава 11 463 <asp:TextBox runat="server" Text='<%# Bind("companyname") %> /> В нем определяется текстовое поле, двунаправленным способом связанное с полем companyname записи источника данных. А вот пример полного шаблона, предназначенного для редактирования записи; он содержит несколько обычных текстовых полей и связанный с данными раскры- вающийся список: <EditItemTemplate> ctable style="border:solid 1px black;” width=”100%"> <tr> <td bgcolor=”yellow" align="center"> <b><%# Eval("id") %></b></td> <td bgcolor="lightyellow"> <asp:textbox runat="server" text='<%# Bind("companyname”) %>’ /> </td> </tr> </table> <table style="font-family:Verdana;font-size:8pt;"> <tr> <td><b>Contac t</bx/td> <td><%# EvalC'contactname”) %x/td> </tr> <tr> <td><b>Add ress</bx/tdxtd> <asp:textbox runat=”server" text='<%# Bind("street") %>’ /> </td> </tr> <tr> <td><b>City</b></td> ctdxasp:textbox runat="server" text='<%# BindC'city") %>' /x/td> </tr> <tr> <td valign="top 'xb>Country</b></td> <tdxasp:dropdownlist runat="server" datasourceid="CountriesDataSource" selectedvalue=’<%# Bind ("country”) %> /x/td> </tr> </table> <br /> <asp:Button runat="server" CommandName="Update" Text="Update" /> <asp Button runat="server" CommandName="Cancel" Text="Cancel" /> </EditItemTemplate> С помощью функции Eval вы заполняете свойства элементов управления, не уча- ствующие в процессе обновления, а когда требуется двунаправленное связывание, применяете функцию Bind с таким же синтаксисом. Раскрывающиеся списки обычно связываются с данными через свойство SelectedValue, текстовые поля — через свойство Text, а элемент управления Calendar — через свойство SelectedDate. Как заполняется раскрывающийся список? Для этого используется еще один компонент, представ- ляющий источник данных. На рис. 11-11 показан вывод, генерируемый на основе приведенной выше разметки. Имейте в виду, что шаблон, используемый для редактирования записи, должен со- держать кнопки для сохранения или отмены изменений. Именами их команд должны быть соответственно Update и Cancel', надписи могут быть любыми. При желании
464 Часть II Размещение данных на сайте можно изменить и имена команд, но тогда вам придется самостоятельно обрабатывать событие ItemCommand объекта Form View и вызывать в нем метод Updateitem. Чтобы пользователь мог удалять записи, следует добавить кнопку с именем команды Delete. Кроме того, для выполнения обеих операций необходимо сконфигурировать соот- ветствующим образом компонент, представляющий источник данных. Рис. 11-11. Элемент управления FormView в режиме редактирования Чтобы команда обновления или удаления сработала, должно быть установлено свойство DataKeyNames элемента управления Form View — ведь не зная, какие поля об- разуют ключ, элемент управления не сможет найти в источнике данных нужную запись. Функция Bind Как работает функция Bind? Она сохраняет значение заданного свойства элемента управления в коллекции, доступной для элемента управления FormView. Когда при- ходит время обновить данные в источнике, элемент Form View автоматически извлекает значения из этой коллекции и использует их для составления списка параметров, передаваемых команде обновления записи. Аргументом метода Bind должно быть имя параметра команды или метода, с помощью которых осуществляется обновление данных. Если этот последний принимает объект, аргументом метода Bind должно быть имя одного из свойств объекта — как в примере, где метод Save (заданный в свойстве UpdateMethod компонента ObjectDataSource) принимает экземпляр класса ProAspNet20.DAL.Customer. Если аргумент метода Bind не соответствует параметру команды или метода обновления, генерируется исключение. Шаблон вставки При редактировании новой записи, предназначенной для вставки в источник данных, используется шаблон, заданный в свойстве InsertltemTemplate элемента управления FormView. Для того чтобы пользовательский интерфейс этого элемента управления был согласованным, шаблон вставки не должен существенно отличаться от шаблона редактирования. Но в то же время следует понимать, что это две разные операции и к ним предъявляются различные требования. Шаблон вставки должен содержать значения элементов управления по умолчанию, а в тех элементах, для которых они не предусмотрены, должны выводиться нейтральные или пустые значения.
Отображение отдельных записей Глава 11 465 Для того чтобы пользователь мог начать операцию вставки, ему необходимо предоставить кнопку с именем команды New. По щелчку такой кнопки элемент управления FormView автоматически перейдет в режим вставки записи и выведет контент, определенный в шаблоне InsertltemTemplate. В этом шаблоне (также как в шаблоне редактирования) должны присутствовать кнопки с именами команд Update и Cancel. Когда функция не поддерживается Элементы управления DetailsView и FormView поддерживают ряд стандартных опера- ций, таких как вставка, редактирование и удаление записи. Сами эти операции должны быть реализованы в компоненте, представляющем источник данных. А что происходит, если для элемента управления FormView поддержка функции редактирования вклю- чена, но ее не поддерживает компонент, представляющий источник данных? В таком случае выбрасывается исключение NotSupportedException. Конечно, трудно представить себе команду разработчиков, которая включит в эле- мент управления Form View кнопку Edit, но при этом свяжет его с источником данных, не поддерживающим редактирование. И тем не менее в надежном и стабильном прило- жении должна выполняться проверка на наличие поддержки запрашиваемой функции: if (е.CommandName == "Edit") { IDataSource obj = (IDataSource) FindControl(FormView1.DataSourcelD); DataSourceView view = obj.GetView("DefaultView"); if (!view.CanUpdate) { Response.Write("Sorry, you can’t update"); return; } else FormViewl.Updateltem(); J u Этот код обращается к компоненту, представляющему источник данных, и получает ссылку на используемый им по умолчанию объект представления (см. главу 9). Затем он проверяет, поддерживается ли необходимая функция. Приведенный код можно помещать в обработчик события ItemCommand любого элемента управления View- типа: DetailsView, FormView или GridView. Заметьте, что все встроенные компоненты ASP.NET, представляющие источники данных, имеют только одно представление, с именем DefaultView. Заключение В ASPNET 2.0 набор доступных разработчику элементов управления, поддержи- вающих связывание с данными, богаче и полнее, чем в ASP.NET 1.x. В вашем рас- поряжении не только кардинально переработанный табличный элемент управления GridView, но и два элемента, предназначенных для работы с единственной записью. В ASP.NET 1.x подобные элементы управления отсутствуют. Элементы управления DetailsView и Form View — как две стороны одной монеты. Оба они служат для отображения и редактирования содержимого одной записи данных, имеют гибко настраиваемый пользовательский интерфейс, поддерживают операции обновления, вставки и удаления записей. Будучи связанными с компонентом, пред- ставляющим источник данных, оба элемента управления могут работать с ним без вмешательства разработчика, поэтому не нужно писать ни строчки программного кода.
466 Часть II Размещение данных на сайте (Я, конечно, не говорю здесь о коде слоя доступа к данным, а лишь о коде элемента управления.) Элементы управления DetailsView и Form View отличаются прежде всего поддерж- кой шаблонов. Первый хотя и богаче функционально, поддерживает лишь шаблоны отдельных полей, а не целой записи, и имеет довольно богатый набор стилевых и ви- зуальных свойств. Чтобы создать собственную форму для редактирования и вставки за- писей, следует воспользоваться элементом управления Form View. Однако в этом случае придется забыть о стандартном рендеринге — данный элемент не имеет собственного пользовательского интерфейса и выводит только то, что определено в шаблонах, и по- этому каждый элемент выходной разметки вам придется генерировать самостоятельно. Настоящей главой завершается вторая часть книги, посвященная доступу к дан- ным. Далее вас ожидает подробное знакомство с инфраструктурой ASPNET, обеспе- чивающей функционирование страниц и приложения в целом. Только факты Элементы управления DetailsView и FormView в каждый конкретный момент ото- бражают одну запись данных, но при этом могут содержать кнопки для навигации по записям. Оба эти элемента прекрасно подходят для создания страниц с главным и подчи- ненным представлениями. Вывод элемента управления DetailsView имеет фиксированную табличную струк- туру и формируется из нескольких структурных элементов: заголовка, верхнего колонтитула, строк, представляющих поля, блока листания, командной панели и нижнего колонтитула. Структурными элементами вывода элемента управления Form View являются заголо- вок, верхний и нижний колонтитулы, блок листания, а также область отображения записи. Содержимое данной области полностью определяется шаблонами, создавая которые, вы фактически создаете пользовательскую форму для работы с записью. По умолчанию элемент управления DetailsView использует для вывода полей данных текстовые поля. Однако для любого из полей можно определить пользовательский шаблон и в нем вывести любую разметку, в том числе и любой набор элементов управления. Такой шаблон позволяет, например, отобразить календарь для выбора даты или раскрывающийся список для выбора значения внешнего ключа. Оба элемента управления, DetailsView и Form View, поддерживают базовые операции ввода-вывода — вставку, обновление и удаление записей. Будучи связанными с компонентами, представляющими источники данных (такими как ObjectDataSource или SqlDataSource}, эти элементы управления выполняют указанные операции с их помощью. Элементы управления DetailsView и FormView поддерживают двунаправленное связывание с данными, при котором данные не только автоматически считываются из источника, но и записываются обратно после изменения пользователем. Валидация данных при редактировании и добавлении записей осуществляется двумя способами. Если используется шаблон, можно включить в его состав вали- дационные элементы управления и проверять введенные данные и на сервере, и на клиенте. Если же шаблон не используется, тогда вам остается только перехваты- вать события, сгенерированные элементами управления DetailsView и FormView. В обработчиках этих событий можно модифицировать данные и даже отменять операцию обновления или вставки.
Часть III Инфраструктура ASP.NET
Глава 12 Контекст запроса HTTP Выполнение входящего запроса HTTP рабочим процессом ASP.NET - сложная про- цедура, состоящая из серии шагов. Поступивший запрос назначается для выполнения ISAPI-расширению aspnet_isapi.dll, которое пропускает его через исполняющий кон- вейер ASP.NET. Точкой входа этого конвейера является класс HttpRuntime. Каждый запрос назначается отдельному экземпляру этого класса, который руководит вы- полнением запроса и генерирует ответ для браузера. Подготавливая свой экземпляр к работе, класс HttpRuntime выполняет ряд инициализационных задач, и в первую очередь создает объект-оболочку, инкапсулирующий специфическую для HTTP ин- формацию о запросе. Это объект класса HttpContexfr, он передается по конвейеру и используется различными модулями, которым требуется доступ к его внутренним рабочим компонентам, в частности объектам Request, Response и Server. Вначале мы рассмотрим процесс инициализации приложения ASP.NET, а затем перейдем к изучению различных объектов, образующих контекст его выполнения. Если вы разрабатывали приложения для классической ASP, то часть материала этой главы будет для вас не нова. Например, вы встретите здесь своих «старых знакомых»: объекты Request и Response. Хотя в ASP.NET они более мощные и обладают большим набором функций, чем аналогичные объекты ASP, это все же низкоуровневые сред- ства, и необходимость в обращении к ним возникает лишь в редких случаях. Частое же обращение к этим объектам, как правило, является признаком того, что приложе- ние плохо спроектировано и вы не оптимально используете основные программные средства ASP.NET. Инициализация приложения После создания контекста запроса класс HttpRuntime подготавливает для его выпол- нения объект приложения ASP.NET — экземпляр класса HttpApplication (см. главу 3). Этот класс генерируется на основе содержимого файла global.asax; он отвечает за об- работку всех HTTP-запросов, которые адресованы страницам, расположенным в его виртуальной папке. Во время выполнения приложение ASP.NET представлено своей виртуальной папкой и, возможно, файлом globaLasax. Имя этой виртуальной папки является чем-то вроде ключа, используемого исполняющей средой HTTP для идентификации при- ложения, которому надлежит обработать поступивший запрос. В файле global.asax (если 1аковой имеется) содержатся установки и код, необходимые для обработки событий уровня приложения, генерируемых исполняющей средой ASP.NET или за- регистрированными модулями HTTP. Конкретный объект класса HttpApplication отвечает за весь жизненный цикл за- проса и может использоваться повторно лишь после того, как выполнение запроса полностью завершится. Если ни один объект HttpApnhcation не доступен (либо потому что приложение еще не разу не запускалось, либо потому что все имеющиеся объекты заняты), создается новый такой объект и помещается в пул.
Контекст запроса HTTP Глава 12 469 Свойства класса HttpApplication Хотя у класса HttpApplication имеется открытый конструктор, приложения никогда не создают его объекты явно — это задача исполняющей среды ASP.NET. Как уже упоминалось, экземпляры класса HttpApplication объединяются в пул и могут ис- пользоваться для выполнения многих запросов, но лишь поочередного — объект класса HttpApplication никогда не обрабатывает несколько запросов одновременно. Для параллельного выполнения нескольких запросов к одному приложению исполь- зуются разные экземпляры класса HttpApplication. В табл. 12-1 перечислены свойства этого класса. Табл. 12-1. Свойства класса HttpApplication Свойство Описание Application Экземпляр класса Http Applicationstate. Представляет глобальное совместно используемое состояние приложения и функционально эквивалентен вну- треннему объекту ASP Application Context Экземпляр класса HttpContext. Инкапсулирует всю специфическую для HTTP информацию о текущем запросе и открывает доступ к своим внутрен- ним объектам (например, Application и Request) как свойствам Modules Возвращает коллекцию модулей, связанных с текущим приложением Request Экземпляр класса HttpRequest. Представляет текущий запрос HTTP и функ- ционально эквивалентен внутреннему объекту ASP Request Response Экземпляр класса HttpResponse. Представляет ответ клиенту и функциональ- но эквивалентен внутреннему объекту ASP Response Server Экземпляр класса HttpServerUtility. Предоставляет вспомогательные методы для обработки запроса и функционально эквивалентен внутреннему объекту ASP Server Session Экземпляр класса HttpSessionState. Управляет данными, связанными с кон- кретным пользователем, и функционально эквивалентен внутреннему объек- ту ASP Session User Объект, реализующий интерфейс IPrincipal', представляет пользователя, от которого поступил запрос Если объектом HttpApplication управляет инфраструктура ASP.NET, то как в та- ком случае используются преимущества открытого и весьма богатого программного интерфейса этого объекта? Свойства, переопределяемые методы и события класса HttpApplication доступны из файла globaLasax, о котором я расскажу вам подробнее в одном из следующих разделов. Модули HTTP приложения Свойство Modules класса HttpApplication возвращает набор компонентов уровня прило- жения, реализующих специальные сервисы. Модулем HTTP называется класс, реализу- ющий интерфейс IHttpModule. Это управляемый аналог фильтра ISAPI — перехватчик запросов, обладающий способностью модифицировать контекст обрабатываемого запроса. В состав Microsoft .NET Framework входит ряд стандартных модулей, перечислен- ных в табл. 12-2. В дополнение к ним вы можете определять пользовательские модули. О том, как и для чего это делается, подробно рассказано в моей книге «Programming Microsoft ASP.NET 2.0 Applications: Advanced Topics» (Microsoft Press, 2005).
470 Часть 111 Инфраструктура ASP.NET Табл. 12-2. Модули ASP.NET Модуль Описание Anonymousldentification Назначает анонимному пользователю временную учетную за- пись. В ASP.NET 1.x данный модуль не установлен FileAuthorizacion Проверяет, имеет ли удаленный пользователь разрешения Micro- soft Windows NT на доступ к запрошенному ресурсу FormsAuthentication Разрешает приложению использовать метод аутентификации Windows Forms OutputCache PassportAuthentication Profile Предоставляет сервис кэширования вывода страниц Служит оболочкой для аутентификационных сервисов Passport Предоставляет сервис пользовательских профилей. В ASP.NET 1.x данный модуль не установлен RoleManager Предоставляет сервис управления ролями. В ASP.NET 1.x данный модуль не установлен SessionState UrlAuthorization Предоставляет сервис управления состоянием сеанса Предоставляет URL-базированный авторизационный сервис для доступа к заданным ресурсам WindowsAuthentication Позволяет приложениям ASP.NET использовать аутентифика- цию Windows и I IS Список используемых по умолчанию модулей, доступных всем приложениям, определен в файле machine.config. Кроме того, можно определить в файле web.config список модулей, предназначенных для использования с определенным приложением. О конфигурировании ASP.NET и ее приложений рассказывается в моей книге «Pro- gramming Microsoft ASPNET 2.0 Applications: Advanced Topics» (Microsoft Press, 2005). Методы класса HttpApplication Методы класса HttpApplication можно разделить на две группы — управляющие методы и обработчики событий. Управляющие методы этого класса перечислены в табл. 12-3. Табл. 12-3. Управляющие методы класса HttpApplication Метод Описание CompleteRequest Устанавливает внутренний флаг, указывающий ASP-NET, что она должна пропустить все последующие шаги конвейера и сразу вызвать метод EndRequest. Используется главным образом моду- лями HTTP Dispose Переопределяемый метод, который после выполнения запроса очищает переменные экземпляра для всех зарегистрированных модулей. После выполнения данного метода объекты Request, Response, Session и Application больше недоступны GetVaryBy CustomString Переопределяемый метод, позволяющий указать, что кэширова- ние вывода всех страниц приложения должно осуществляться на основе пользовательских строк (о кэшировании вывода подробно рассказывается в главе 14) Init Переопределяемый метод, который после связывания всех мо- дулей с приложением выполняет пользовательский инициали- зационный код. Его можно применять для конфигурирования объекта, который будет использоваться на протяжении всего про- цесса обработки запроса. К моменту его вызова объекты Request, Response, Session и Application еще недоступны
Контекст запроса HTTP Глава 12 471 Заметьте, что методы Init и Dispose отличаются от обработчиков событий Ap- plication JStart и Application_End. Метод Init выполняется для каждого запроса, по- лучаемого Web-приложением, тогда как событие Application_Start генерируется для приложения только однажды. Вызов метода Init служит индикатором того, что для обслуживания поступившего запроса создан новый экземпляр класса HttpApplica- tion, а событие Application_Start сигнализирует о создании первого экземпляра этого класса для запуска приложения и обслуживания первого адресованного ему запроса. Аналогичным образом вызов метода Dispose сигнализирует о завершении обработки очередного запроса, но не о завершении работы всего приложения — этот момент отмечается событием Application_End. Примечание Жизненный цикл любого ресурса, созданного методом Init, ограничен вы- полнением текущего запроса. Такие ресурсы должны уничтожаться не позднее, чем в методе Dispose. Если же вам требуются постоянные данные, которые существуют на протяжении работы всего приложения, воспользуйтесь объектами, входящими в состав состояния сеанса или состояния приложения. Дополнительную информацию о таких объек- тах вы найдете в главах 13 и 14. Помимо управляющих методов, перечисленных в табл. 12-3, у класса HttpAppli- cation есть еще несколько методов, предназначенных для регистрации асинхронных обработчиков событий уровня приложения. Эти методы не представляют интереса для пользовательских приложений и вызываются только модулями HTTP для обе- спечения перехвата событий, генерируемых в ходе выполнения запроса. События класса HttpApplication С моделью событий класса HttpApplication вы можете ознакомиться в табл. 12-4. Здесь представлены события, которые могут перехватываться и обрабатываться модулями HTTP и пользовательскими приложениями. Табл. 12-4. События класса HttpApplication События Когда происходят AcquireRequestState, PostAcquireRequestState Обработчик запроса получил связанную с запросом информа- цию состояния. Второе из событий в ASP-NET 1.x не генериру- ется AuthenticateRequest, PostAuthenticateRequest AuthorizeRequest, PostAuthorizeRequest BeginRequest Disposed Модуль защиты идентифицировал пользователя. Второе из событий в ASP.NET 1х не генерируется Модуль защиты авторизировал запрос пользователя. Второе из событий в ASP.NET 1х не генерируется Конвейер HTML начал обработку запроса Объект HttpApplication уничтожен в результате вызова метода Dispose EndRequest Error Это последнее событие конвейера HTTP Выброшено необработанное исключение PostMapRequestHandler Найден обработчик HTTP, который будет выполнять запрос. В ASP.NET 1х данное событие не генерируется PostRequestHandlerExecute Обработчик HTTP завершил выполнение запроса; к этому моменту текст ответа уже сгенерирован PreRequestHandlerExecute Непосредственно перед тем, как обработчик HTTP приступит к выполнению запроса см. след. стр.
472 Часть III Инфраструктура ASP NET Табл. 12-4. (окончание) События Когда происходят PreSendRequestContent Непосредственно перед отправкой текста ответа клиенту PreSendRequestHeaders Непосредственно перед отправкой клиенту заголовков HTTP ReleaseRequestState, PostReleaseRequestState Обработчик HTTP уничтожает информацию состояния текуще- го запроса. Второе из событий в ASP.NET 1зс не генерируется ResolveRequestCache, PostResolveRequestCache Обработчик HTTP разрешает запрос с использованием данных из кэша вывода. Второе из событий в ASP.NET 1.x не генериру- ется UpdateRequestCache, PostUpdateRequestCacae Исполняющая среда ASPNET сохраняет ответ на текущий запрос в кэше вывода для использования при разрешении последующих запросов. Второе из событий в ASP.NET 1jc не генерируется Для асинхронной обработки перечисленных событий в приложении используются методы, имена которых формируются по стандартному шаблону AddOnXXXAsync, где XXX — имя события. Если же то или иное событие нужно обрабатывать в синхронном режиме, следует определить в файле globaLasax процедуру обработки этого события с такой сигнатурой: public void Application_XXX(object sender, EventArgs e) { // Здесь выполняется необходимая обработка } Разумеется, обозначение XXX здесь также соответствует имени события. Обработ- чикам событий, перечисленных в приведенной выше таблице, не передаются никакие связанные с ними данные. Поэтому при определении этих обработчиков можно ис- пользовать следующий упрощенный синтаксис: public void Application_XXX() { И Здесь выполняется необходимая обработка } В дополнение к событиям из табл. 12-4 в файле globaLasax можно обрабатывать события Application_Start и Application_End. Первое из них ASP.NET генерирует не- посредственно перед тем, как в приложении в первый раз происходит событие Ве- ginRequest. Событие EndRequest генерируется в конце обработки каждого запроса, а событие Application_End происходит вне контекста запроса, когда приложение за- вершает свою работу. События приложения генерируются в описанной ниже последовательности. 1. BeginRequest Конвейер HTTP ASP.NET начинает обработку запроса. Это со- бытие достигает приложения после события Application_Start. 2. AuthenticateRequest Необходимо аутентифицировать запрос. Все внутренние аутентификационные модули ASP.NET подписываются на это событие и произ- водят попытку поиска учетной записи пользователя. Если ни один из указанных модулей не сгенерировал объект Identity, представляющий аутентифицированного пользователя, вызывается внутренний (используемый по умолчанию) аутентифи- кационный модуль, который генерирует объект Identity, представляющий неаутен- тифицированного пользователя. Это делается для обеспечения согласованности, чтобы в дальнейшем не приходилось особым образом обрабатывать пустые ссылки на аутентификационный объект.
Контекст запроса HTTP Глава 12 473 3. PostAuthenticateRequest Запрос аутентифицирован, и вся доступная информа- ция сохранена в свойстве User объекта HttpContext. 4. AuthorizeRequest Сейчас начнется авторизация запроса. Обычно это событие обрабатывается кодом приложения, предназначенным для выполнения пользова- тельской авторизации на основе бизнес-логики или других требований приложения. 5. PostAuthorizeRequest Запрос авторизирован. 6. ResolveRequestCache Исполняющая среда ASP.NET проверяет, можно ли раз- решить запрос путем возврата предыдущей страницы, хранящейся в кэше. Если подходящая страница найдена, запрос разрешается из кэша и цикл его выполнения сокращается до вызова зарегистрированных обработчиков события EndRequest. 7. PostResolveRequestCache Запрос не может быть разрешен из кэша и процесс продолжается. Создается обработчик HTTP, соответствующий заданному в запросе URL. Если запрошена страница .aspx, то создается экземпляр ее класса. 8. PostMapRequestHandler Успешно создан обработчик HTTP, соответствующий запрошенному URL. 9. AcquireRequestState Модуль, перехватывающий это событие, извлекает ин- формацию состояния запроса. Для того чтобы это было возможно, обработчик HTTP должен поддерживать состояние сеанса в той или иной форме и должен существовать допустимый идентификатор сеанса. 10. PostAcquireRequestState Получена информация состояния (например, объект Application или Session). И. PreRequestHandlerExecute Это событие генерируется непосредственно перед выполнением обработчика запроса. Этот обработчик генерирует вывод для клиента. 12. PostRequestHandlerExecute Обработчик сгенерировал текст ответа. 13. ReleaseRequestState Обработчик готов уничтожить информацию состояния и завершить свою работу. Это событие используется модулем, который отвечает за управление состоянием сеанса, для обновления данных состояния, если это необходимо. 14. PostReleaseRequestState Состояние сеанса, измененное в ходе обработки запро- са, сохранено, и фильтрация ответа произведена (о том, для чего она выполняется, я расскажу немного позже). 15. UpdateRequestCache Исполняющая среда ASP.NET определяет, должен ли сге- нерированный и отфильтрованный зарегистрированными модулями вывод быть сохранен в кэше для повторного использования при выполнении будущих иден- тичных запросов. 16. PostUpdateRequestCache Страница сохранена в кэше, если это требовалось. 17. EndRequest Данное событие генерируется на последнем шаге конвейера. Управ- ление передается обратно объекту HttpRuntime, который отвечает за отправку ответа клиенту. В этот момент текст ответа еще не отправлен. Существуют еще два недетерминированных события, которые могут быть сге- нерированы в ходе выполнения запроса: PreSendRequestHeaders и PreSendRequest- Content. Первое из них уведомляет объект HttpApplication о том, что сейчас кли- енту будут отправлены заголовки HTTP. Обычно оно генерируется после события EndRequest, хотя это не всегда так. Например, если буферизация отключена, данное событие генерируется, как только какая-то часть контента оказывается готовой к от- правке клиенту. С помощью события PreSendRequestContent, то с его помощью объект HttpApplication узнает о готовности к отправке тела ответа. Ну и если уж описывать
474 Часть III Инфраструктура ASP.NET все недетерминированные события, то для полноты картины следует упомянуть о со- бытии Error, уведомляющем об ошибке. Файл global.asax Файл global.asax используется Web-приложениями для обработки событий уровня приложения, генерируемых исполняющей средой ASP.NET или зарегистрированными модулями HTTP. Наличие файла global.asax не является обязательным, и если в папке приложения его нет, исполняющая среда ASP.NET считает, что обработчики событий приложения или модулей не зарегистрированы. Если вы создаете такой файл, то его следует поместить в корневую папку приложения. У каждого приложения может быть только один файл global.asax, и одноименные файлы, находящиеся в подкаталогах приложения, просто игнорируются. Заметьте, что Microsoft Visual Studio .NET 2005 знает об этом, и потому не предлагает файл globaLasax в числе кандидатов на создание, когда такой файл уже имеется в составе проекта. Компиляция файла global.asax При запуске приложения файл global.asax подвергается синтаксическому анализу, на его основе генерируется исходный код класса, который затем компилируется. По- лученная в результате компиляции сборка, как и все остальные динамически генери- руемые сборки, помещается во временную папку. Ниже демонстрируется структура кода на языке С#, генерируемого ASP.NET на основе содержимого файла global.asax: namespace ASP { public class global_asax : System.Web.HttpApplication { // // Исходный код файла global.asax размещается здесь как // есть. Поэтому если поместить в файл global.asax, например, // следующий код, будет сгенерирована ошибка компиляции: // int i; // i = 2; // Операторы не могут размещаться вне методов // } } Класс получает имя ASP.global_asax и является производным от базового класса HttpApplication. В большинстве случаев при развертывании приложения в его состав включают исходный файл global.asax, но иногда его создают как файл класса и ком- пилируют в отдельную сборку либо включают в состав сборки проекта. Необходимо, чтобы исходный код класса соответствовал приведенным выше правилам, в частности, этот класс обязательно должен быть производным от HttpApplication. Сборка с от- компилированной версией файла global.asax при развертывании приложения должна помещаться в его подкаталог Bin. Заметьте, что даже если вы изолируете логику файла global.asax в предкомпилиро- ванной сборке, этот файл все равно будет вам необходим — код в него при этом вклю- чать не нужно, а только ссылку на сборку (подробнее о синтаксисе файла global.asax я расскажу в следующем разделе): <%@ Application Inherits="ProAspNet.Global" %> Развертывание предкомпилированной версии файла globaLasax позволяет защитить его исходный код от атак извне. Но и в исходном виде этот файл достаточно защищен, поскольку все запросы на его получение перенаправляются IIS, поэтому внешние
Контекст запроса HTTP Глава 12 475 пользователи не имеют возможности ни загрузить этот файл, ни просмотреть его исходный код. Такое перенаправление осуществляется с помощью следующей строки из файла machine.config: <add verb="*" path="*.asax" type="System.Web.HttpForbiddenHandler" /> ASP.NET регистрируется в IIS как обработчик запросов на получение ресурсов .asax, но для обслуживания этих запросов он вызывает HTTP-обработчик HttpForbid- denHandler В результате при попытке получить ресурс .asax пользователь видит лишь сообщение об ошибке, показанное на рис. 12-1. Рис. 12-1. Блокирование попытки прямого доступа к ресурсу .asax /fY Совет Поместив в файл web.config строку, аналогичную приведенной выше, можно за- '' блокировать прямой доступ к ресурсам любого типа, специфического для вашего приложения. Но для этого необходимо убедиться, что IIS перенаправляет запросы ресурсов данного типа ASP.NET. Иными словами, вначале вы должны зарегистрировать модуль aspnetjs- api.dll как обработчик ресурсов данного типа в метабазе IIS, а уж затем просить ASP. NET блокировать обращения к ним. Регистрация обработчиков ресурсов в метабазе IIS осуществляется с помощью менеджера IIS, вызываемого из модуля администрирования, входящего в состав Панели управления Windows. Когда в файл globaLasax работающего приложения вносятся изменения, испол- няющая среда ASP.NET обнаруживает это и перезапускает приложение. Чтобы его остановить, исполняющая среда дожидается завершения всех выполняющихся в этот момент запросов и генерирует событие Application End. Когда поступает следующий запрос к данному приложению, файл globaLasax компилируется заново и генерируется событие ApplicationJStart. Синтаксис файла globaLasax Синтаксисом файла globaLasax определяются четыре элемента: директивы прило- жения, блоки программного кода, серверные тэги <object> и серверные директивы включения файлов. Директивы приложения В файле globaLasax может присутствовать три вида директив: ©Application, ©Import и ©Assembly. Директивы ©Import и ©Assembly были описаны в главе 3: первая из них
476 Часть III Инфраструктура ASP.NET импортирует в приложение пространство имен, а с помощью второй с приложением во время компиляции связывается заданная сборка. Что касается директивы ©Application, то для нее определен ряд атрибутов, в том числе Description, Language и Inherits. Атрибут Description может содержать любой текст, представляющий собой описание поведения приложения. Он предназначен для целей документирования и анализатор ASP.NET его игнорирует. В атрибуте Language задается используемый в файле язык программирования, а в атрибуте Inherits — имя класса отделенного кода, наследуемого классом приложения. Это может быть любой класс, производный от HttpApplication. Содержащая его сборка должна находиться в папке Вт приложения. Блоки программного кода В файле global.asax может содержаться код, заключенный внутрь тэга <script>. Так же как в файлах страниц, этот тэг должен иметь атрибут runat-* "server". В атрибуте language задается используемый язык. <script language="C#" runat="server"> </script> Если атрибут language не задан, ASP.NET использует компилятор языка, задан- ного в конфигурационном файле (по умолчанию это Microsoft Visual Basic .NET). Исходный код может загружаться и из внешнего файла, в таком случае виртуальный путь к файлу задается в атрибуте Src: <script language="C#" runat="server" src="global.aspx.cs” /> Ссылка на файл разрешается с использованием метода Server.MapPath, то есть начиная от физического корневого каталога Web-приложения. Во время исполь- зования кода из внешнего файла любой код, который находится в блоке <script>, игнорируется. Заметьте, что в ASP.NET в отношении блока <script> действуют определенные синтаксические правила: атрибут runat является обязательным, а при отсутствии содержимого блока обязателен также атрибут Src. Серверные тэги <object> Серверный тэг <object> предназначен для создания новых объектов декларативным способом. Этот тэг может принимать следующие три формы: <object id="...” runat="server” scope="..." class=”.. " /> <object id="...” runat="server" scope="..." progid="... ” /> <object id="..." runat="server" scope="...” classid="..." /> В первом случае создается объект класса, имя которого задается в атрибуте class и должно быть уточнено именем содержащей его сборки. В двух других случаях создается COM-объект, определяемый программным идентификатором (progid) или 128-битовым идентификатором класса (CLSID). Как нетрудно догадаться, атрибуты classid, progid и class являются взаимоисключающими. Если включить в серверный тэг <object> более одного из них, будет сгенерирована ошибка компиляции. Объявленные таким способом объекты загружаются при запуске приложения. Атрибут scope определяет область видимости объявляемого объекта. Его допу- стимые значения перечислены в табл. 12-5. Если иное не задано, серверный объект доступен только в рамках конвейера HTTP, обрабатывающего текущий запрос (уста- новка Pipeline). Две другие установки увеличивают время жизни объекта.
Контекст запроса HTTP Глава 12 477 Табл. 12-5. Допустимые значения атрибута scope тэга <object> Значение Описание Pipeline Значение по умолчанию; объект доступен только в контексте текущего запро- са HTTP Application Объект добавляется в коллекцию StaticObjects объекта Application и может использоваться всеми страницами приложения Session Объект добавляется в коллекцию StaticObjects объекта Session и может ис- пользоваться всеми страницами, запрашиваемыми в рамках текущего сеанса Серверные директивы включения файлов Директива # include позволяет вставить содержимое заданного файла в тот файл, в котором она находится, и может использоваться не только в файле global.asax, но и в файлах страниц .aspx. Данную директиву необходимо обрамлять символами ком- ментария HTML, чтобы она не была принята за чистый текст, подлежащий выводу в исходном виде: <!-- «include file="filename" —> <! — «include virtual="filename" --> Для директивы it include определены два взаимоисключающих атрибута: file и virtual. Если используется атрибут file, имя файла уточняется путем относительно того катало- га, в котором содержится текущий файл с директивой ttinclude. Если же задан атрибут virtual, имя файла уточняется полным путем от виртуального каталога Web-сайта. Статические свойства Если вы определяете в файле global.asax статические свойства, они становятся до- ступными для чтения и записи всем страницам приложения: <script language="C«" runat="server"> public static int Counter = 0; </script> Определенное в этом примере свойство Counter действует подобно свойству объек- та Application — оно видимо всем страницам и сеансам. Учтите, что параллельный доступ к такому свойству не сериализуется; но вместе с тем вы получаете строго типизированный непосредственно доступный глобальный элемент, обращение к ко- торому осуществляется намного быстрее, чем извлечение такой же информации из универсальной коллекции, подобной тем, что входят в состав объекта Application. Для доступа к определенному вами свойству из кода страницы можно использовать уточнитель ASPglobal asaxr. Response.Write(ASP.global_asax.Counter.ToString()); Если вам не нравится префикс ASP.global asax, можете назначить ему псевдо- ним при условии, что вы программируете на С#. Для этого нужно добавить в файл страницы или в файл ее отделенного кода (иными словами, в тот файл, откуда будет осуществляться доступ к глобальному свойству) следующий оператор: using Globals = ASP.global.asax; Приведенный оператор создает псевдоним для класса ASPglobal asax (или класса с другим именем, который вы определили в файле global.asax). В данном случае для доступа к глобальным свойствам будет использоваться псевдоним Globals, например: Response. Write(Globals. Counter. ToStringO);
478 Часть III Инфраструктура ASP.NET О Внимание! Файл globaLasax можно использовать для обработки любых событий, ге- нерируемых модулями, которые вызываются для обработки запроса. Обработчики этих событий должны иметь имена, отвечающие такой схеме: ИмяМодуля_ИмяСобытия. Имя используемого модуля определяется в разделе <httpModules> конфигурационного файла. Выявление ошибок и аномалий Когда происходит та или иная ошибка, профессионально выполненное приложение выводит для пользователя дружественную страницу с сообщением о случившемся. Однако это лишь половина дела. Вторая его половина заключается в отправке со- общения системному администратору, и по возможности в реальном времени. Как вы уже видели в главе 5, в этом может помочь событие Error объекта HttpApplication. Напишите обработчик этого события в файле globaLasax, и система будет вызывать его в тех случаях, когда то или иное исключение останется необработанным — будь то исключение в пользовательском коде, коде компонента или самой ASP.NET. В обработчике события Application Error вы первым делом получаете информацию об ошибке, а затем выполняете те действия по ее обработке, которые отвечают нуждам приложения, например, отправляете администратору уведомление по электронной почте, вносите запись в журнал событий Windows или делаете дамп необходимой информации в текстовом файле. Необходимую для этого информацию можно по- лучить с помощью метода Server.GetLastError, который возвращает объект Exception, представляющий необработанное исключение, и объекта Request, содержащего инфор- мацию HTTP-запроса. Кроме того, в обработчике события Application_Error доступны состояние сеанса и состояние приложения. Ниже демонстрируется код определенного в файле globaLasax обработчика события Application Error, записывающий сведения об аномалиях в работе приложения в журнал событий Windows. Результат выполнения этого кода показан на рис. 12-2. Он извле- кает последнее исключение и записывает в журнал всю доступную связанную с ним информацию. Заметьте, что метод ToString объекта-исключения возвращает больше информации, чем свойство Message, — он выводит данные трассировки стека. <%@ Import Namespace="System.Diagnostics" %> <%@ Import Namespace="System.Text" %> <script language="C#" runat="server"> void Application_Error(object sender, EventArgs e) // Получаем URL запроса string url = Request.Path; // Получаем объект Exception, содержащий описание ошибки Exception error = Server.GetLastError(); // Формируем сообщение в формате: [Error occurred. XXX at url] StringBuilder text = new StringBuilder(”Error occurred "); text.Append(e r ro r.ToSt ri ng()); text.Append(" at ”); text.Append(url); // Записываем сообщение в журнал событий Event Log log = new EventLogO; log.Source = "ProAspNet20 Log"; log.WriteEnt ry(text.IoSt ring(), EventLogEnt ryType.Error); } </script>
Контекст запроса HTTP Глава 12 479 Рис. 12-2. Информация об ошибке в приложении ASP.NET в окне утилиты Event Viewer Вашему коду не обязательно создавать источник события. Если источник, задан- ный в свойстве Source, отсутствует, он будет создан автоматически перед записью сообщения в журнал, о чем позаботится метод WriteEntry. Windows поддерживает три файла журналов: Application (события приложений), Security (события системы безопасности) и System (события драйверов устройств). Какой из журналов должен использоваться для записи сообщений, можно указать в свойстве Log класса EventLog, по умолчанию это журнал Application. ©Внимание! Для создания новых журналов событий используется статический метод CreateEventSource класса EventLog. Заметьте, что приложения ASP.NET не создают новых журналов событий, поскольку учетная запись ASP.NET (ASPNET или NETWORK SERVICE) не имеет необходимых для этого разрешений Поэтому, если вам нужно, чтобы при- ложение записывало сообщения в собственный журнал, создайте его при инсталляции приложения. Класс HttpContext Объект класса HttpContext, содержащий полную информацию об обрабатываемом HTTP-запросе, передается от класса к классу по цепочке выполнения, как показано на рис. 12-3. Он создается объектом HttpRuntime при конфигурировании механизма обработки запроса.
480 Часть III Инфраструктура ASP NET HTML Рис. 12-3. Объект HttpContext инкапсулирует всю информацию запроса и передается по конвейеру HTTP, сопровождая запрос на всех этапах его обработки Свойства класса HttpContext Класс HttpContext является точкой входа целой группы системных объектов, к которым относятся знакомые вам объекты классической ASP, а также введенные в ASP.NET объекты Cache и User. В табл. 12-6 приведен список свойств класса HttpContext. Табл. 12-6. Свойства класса HttpContext Свойство Описание AllErrors Возвращает массив объектов Exception, каждый из которых пред- ставляет ошибку, происходящую при выполнении запроса Application Возвращает экземпляр класса HttpApplicationState, содержащий глобальное совместно используемое состояние приложения Applicationinstance Возвращает и позволяет задать для текущего запроса объект HttpAp- plication, точнее, объект класса, определенного в файле globaLasax
Контекст запроса HTTP Глава 12 481 Табл. 12-6. (окончание) Свойство Описание Cache Возвращает связанный с текущим запросом объект ASP.NET Cache Current Возвращает связанный с текущим запросом объект HttpContext CurrentHandler Возвращает обработчик текущего запроса. В ASP.NET 1х данное свойство не поддерживается Error Возвращает первое исключение, сгенерированное при обработке текущего запроса Handler Возвращает и позволяет задать для текущего запроса обработчик HTTP IsCustomErrorEnabled Указывает, включена ли для текущего запроса пользовательская обработка ошибок IsDebuggingEnabled Items Указывает, выполняется ли запрос в режиме отладки Возвращает и позволяет задать коллекцию пар имя -значение (точнее, хэш-таблицу), которая для модулей и обработчиков HTTP может служить средством совместного доступа пользовательским данным и объектам на протяжении жизненного цикла запроса PreviousHandler Возвращает объект IHttpHandler родительского обработчика Profile Возвращает объект, представляющий профиль текущего пользовате- ля. В ASP.NET 1 х данное свойство не поддерживается Request Возвращает экземпляр класса HttpRequest, представляющий теку- щий запрос HTTP Response Возвращает экземпляр класса HttpResponse, отправляющий клиенту данные ответа HTTP Server Возвращает экземпляр класса HttpServerUtility, предоставляющего вспомогательные методы для обработки Web-запросов Session Возвращает экземпляр класса HttpSessionState, управляющий дан- ными, связанными с конкретным сеансом SkipAuthorization Позволяет указать, должен ли модуль, осуществляющий авториза- цию на основе URL, пропустить для текущего запроса этап автори- зационной проверки; по умолчанию имеет значение false. Свойство используется главным образом модулями аутентификации, которым нужно перенаправлять пользователя на страницу, к которой разре- шен анонимный доступ Timestamp Возвращает объект DateTime, представляющий начальный штамп времени текущего запроса Trace Возвращает объект TraceContext текущего запроса User Возвращает и позволяет задать объект IPrincipal, представляющий пользователя, от которого поступил запрос Свойство Current, которое возвращает объект HttpContext, связанный с текущим запросом, является одним из наиболее часто используемых статических членов класса HttpContext. Свойство Items позволяет любому пользовательскому модулю или об- работчику HTTP добавлять в объект HttpContext собственную информацию, которая становится доступной для страницы. Эта информация сохраняется до тех пор, пока выполняется запрос.
482 Часть III Инфраструктура ASP.NET Методы класса HttpContext С методами класса HttpContext вы можете ознакомиться в табл. 12-7. Табл. 12-7. Методы класса HttpContext Метод Описание AddError Добавляет в коллекцию AllErrors объект, представляющий исключение ClearError Удаляет все ошибки текущего запроса GetAppConfig Возвращает запрошенную конфигурационную информацию, связанную с текущим приложением. Эта информация извлекает- ся из файла machine.config и главного файла web.config приложе- ния. В ASP.NET 2.0 данный метод помечен как устаревший GetConfig Возвращает запрошенную конфигурационную информацию, связанную с текущим приложением Информация собирается на уровне запрошенного URL с учетом всех дочерних файлов web. config, определенных в подкаталогах. В ASPNET 2.0 данный метод помечен как устаревший GetGlobalResourceObject Загружает глобальный ресурс. В ASPNET 1 х данный метод не поддерживается GetLocalResourceObject Загружает локальный ресурс, связанный с конкретной страни- цей. В ASP.NET 1 х данный метод не поддерживается GetSection Возвращает запрошенную конфигурационную информацию, связанную с текущим запросом. В ASPNET 1х данный метод не поддерживается RewritePath Переопределяет URL и строку запроса в текущем объекте Request. Предназначен главным образом для внутреннего ис- пользования В ASP.NET 2.0 метод GetConfig заменен методом GetSection\ он помечен как уста- ревший, и пользоваться им более не следует. Если у вас имеется старый код, в котором вызывается данный метод, просто измените его имя — новый метод обладает точно таким же прототипом. Также помечен как устаревший метод Get Арр Config — он заме- нен статическим членом GetWebApplicationSection нового класса WebConfigurationMan- ager, имеющим тот же прототип. Далее описано несколько наиболее примечательных методов класса HttpContext. Замена URL Метод RewritePath на лету заменяет URL текущего запроса, перенаправляя таким образом пользователя на другую страницу Однако пользователь об этом ничего не знает, поскольку в адресной строке его браузера отображается исходный URL запроса. Замена URL происходит на сервере, и, что особенно важно, в контексте исходного за- проса. Пользоваться методом RewritePath следует осторожно, и вызывают его обычно из файла global.asax. Если вызвать данный метод в контексте возврата формы, могут возникнуть проблемы, связанные с состоянием представления. Следующий код считывает из URL запроса, такого как page.aspx?id=1234, значе- ние атрибута id и по нему определяет URL реальной страницы, извлекаемый из базы данных или конфигурационного файла. protected void Application„BeginRequest(object sender, EventArgs e) { HttpContext context = HttpContext.Current; object о = context Request["id"];
Контекст запроса HTTP Глава 12 483 if (о != null) { int id = (int) о; string url = GetPageUrlFromld(id); context.RewritePath(url); } } Загрузка ресурсов программным способом В главе 2 рассказывалось о том, как с помощью выражений осуществляется связы- вание в ASPNET 2.0 свойств элементов управления с глобальными или локальными ресурсами. Но упомянутые выражения, $ Resources для глобальных и meta:resourcekey для локальных ресурсов, можно использовать только во время разработки. А что, если вы хотите программным способом генерировать текст, в состав которого входят зна- чения из файлов ресурсов? Для этой цели классы Page и HttpContext предоставляют два метода: GetGlobalResourceObject и GetLocalResourceObject. Метод GetGlobalResourceObject извлекает глобальный ресурс, то есть ресурс, кото- рый определен в файле .resx, находящемся в специальной папке App GlobalResources. Метод GetLocalResourceObject делает то же самое в отношении файла .resx из папки App LocalResources. Вот как они вызываются: msgl.Text = (string) HttpContext.GetGlobalResourceObject "Test", "MyString"); msg2.Text = (string) HttpContext.GetLocalResourceObject( "/ProAspNet20/Samples/Ch02/ResPage.aspx", "PageResourcel Title"); В первом параметре методу GetGlobalResourceObject передается имя файла .resx без расширения, а во втором — имя извлекаемого ресурса. Что касается метода GetLocal- ResourceObject, то ему в первом параметре передают виртуальный путь к странице, а во втором — также имя ресурса. Объект Server В состав объекта HttpContext, содержащего всю наличествующую информацию о за- просе, входит несколько популярных системных объектов, к числу которых относятся Server, Request и Response. Это «старые знакомые» разработчиков приложений ASP, обогатившиеся в ASP.NET новыми функциями. Они по-прежнему остаются одним из базовых ресурсов в арсенале разработчика и поэтому заслуживают того, чтобы вы побольше о них узнали. Давайте начнем с объекта Server. Функциональность объекта Server в ASP.NET реализуется классом HttpServerUti- lity, экземпляр которого создается, когда ASP.NET начинает обработку запроса, и со- храняется как составная часть контекста запроса. Вспомогательные методы, предо- ставляемые этим классом, могут использоваться модулями и обработчиками HTTP, к числу которых относятся global.asax, страницы и Web-сервисы. Все эти методы доступны через свойство Server объекта HttpContext. Создатели ASP.NET старались, где это возможно и уместно, максимально приблизить ее к ASP в отношении стиля программирования, поэтому у некоторых других наиболее часто используемых объек- тов ASP.NET также имеется свойство Server. Благодаря этому автор страницы может, например, поместить в ее программный код вызов ServenMapPath, и это не приведет к ошибке компиляции. Свойства класса HttpServerUtllity У класса HttpServerUtility имеются два свойства: MachineName, возвращающее имя ком- пьютера, и ScriptTimeout, возвращающее и позволяющее задать максимальное время
484 Часть III Инфраструктура ASP.NET обработки запроса в секундах. По умолчанию свойство ScriptTimeout имеет значение 90, однако когда страница выполнятся с атрибутом debugs true, этому свойству при- сваивается значение, представляющее практически бесконечное время ожидания: this.Server.ScriptTimeout = 30000000; Свойство ScriptTimeout явно и автоматически устанавливается в конструкторе динамически создаваемого класса, представляющего страницу Методы класса HttpServerUtility Методы класса HttpServerUtility — это вспомогательные функции, которые могут при- годиться на разных этапах обработки запроса. С их помощью, например, создаются экземпляры СОМ -компонентов, обрабатываются ошибки, а также выполняется ко- дирование и декодирование контента и строк, передаваемых через URL. С методами класса HttpServerUtility вы можете ознакомиться в табл. 12-8. Табл. 12-8. Методы класса HttpServerUtility Метод Описание ClearError CreateObject CreateObjectFrom Clsid Удаляет последнее исключение, выброшенное при обработке запроса Создает экземпляр заданного СОМ-компонента Создает экземпляр СОМ-компонента, идентифицированного зна- чением CLSID (строковым идентификатором класса) Execute Передает управление заданной странице. Эта дочерняя с граница выполняется как подпрограмма, а ее вывод может быть временно сохранен в объекте записи текста или сразу выведен в буфер ответа родительской страницы GetLastError HtmlDecode Возвращает последнее выброшенное исключение Декодирует строку, к которой было применено HTML-кодирова ние. Например, строку &/t; этот метод преобразует в < HtmlEncode Кодирует строку, подлежащую отображению в браузере, заменяя в ней символы, использование которых в HTML-разметке не допу- скается. Например, строку < этот метод превращает в &/(;, посколь- ку символ < имеет в HTML специальное значение и в литеральных строках должен быть представлен специальной последовательнос- тью &lt; MapPath Возвращает физический путь, соответствующий заданному вирту- альному пути Web-сервера Transfer Выполняет серверную переадресацию останавливает выполне- ние текущей страницы и передает управление заданной странице. От метода Execute он отличается тем, что по окончании выпол нения заданной страницы не происходит возврат управления ис- ходной странице UrlDecode Декодирует строку, полученную сервером в составе URL, закодиро- ванную для пересылки через HTTP. Декодированная строка может быть просто возвращена методом или помещена в объект записи данных UrlEncode Кодирует строку для пересылки клиенту через HTTP в составе URL. Закодированная строка может быть просто возвращена мето- дом или помещена в объект записи данных UrlPathEncode Кодирует только путь, указанный в URL, и возвращает закодиро- ванную строку. Этот метод не меняет содержимого строки запроса
Контекст запроса HTTP Глава 12 485 HTML- и URL-кодирование выполняются для того, чтобы пересылаемый браузеру текст был им правильно воспринят. В частности, при HTML-кодировании опреде- ленные символы (>, & и кавычки) заменяются эквивалентными HTML-последова- тельностями Scgt;, Stamp; и Stquot;). Также кодируются пробелы, отступы, знаки препинания, иными словами, все символы, которые не разрешены к использованию в HTML-потоке. Что касается кодирования URL, то его цель — откорректировать текст, передаваемый в строках URL. При этом заменяются те же самые символы, что и при HTML-кодировании, но другими последовательностями. В ASP.NET 2.0 введено два новых статических метода, предназначенных для ко- дирования и декодирования отдельного токена: UrlTokenEncode принимает байтовый массив с данными в кодировке Base64 и преобразует его в URL-кодированный токен, a UrlTokenDecode выполняет обратное преобразование. Использование результатов выполнения другой страницы Метод Execute позволяет использовать внешнюю страницу как подпрограмму, получая сгенерированные ею данные. По достижении потоком выполнения вызова ServerExecute, управление передается заданной странице, а выполнение текущей страницы приоста- навливается. Результаты, сгенерированные вызванной страницей, обрабатываются способом, который зависит от используемой перегруженной версии метода Execute (табл. 12-9). Табл. 12-9. Перегруженные версии метода Execute Метод Описание Execute(stnng); Принимает URL страницы и автоматически включает текст ответа в состав вывода основной страницы Execute (string, TextWriter); Помещает текст ответа в объект записи текста Execute(string, bool); Подобен предыдущему, но позволяет указать, сле- дует ли сохранить коллекции QueryString и Form-, по умолчанию имеет значение true Execute(IHttpHandler, TextWriter, bool); Принимает обработчик HTTP, которому должен быть передан текущий запрос, и помещает ответ в объект записи текста Execute(string, TextWriter, bool); Помещает текст ответа в объект записи текста; позволяет указать, нужно ли сохранить коллек- ции QueryString и Form-, по умолчанию имеет значение true В ASP.NET 1.x поддерживаются только первые две перегруженные версии метода Execute из перечисленных в этой таблице. Имейте в виду, что когда задан объект TextWriter, в нем накапливается сгенери- рованный дочерней страницей вывод, с тем чтобы исходная страница впоследствии могла использовать его по своему усмотрению. Пример реализации такого решения приведен на рис. 12-4: исходная страница вывела полужирным шрифтом текст, ко- торый сгенерировала сама, а обычным шрифтом отобразила текст, сгенерированный дочерней страницей. Исходный код главной страницы из нашего примера таков: void Page_Load(object sender, EventArgs e) {
486 Часть III Инфраструктура ASP.NET Response.Write("Response generated before Execute is called<hr>"); Server.Execute("child.aspx"); Response.Write("<hr>Response generated after the call to Execute."); } Рис. 12-4. Текст ответа, сгенерированного страницей, которая была вызвана с помощью метода Execute, кэшировался в объекте записи текста Внутренняя реализация метода Execute представляет для нас интерес. Главная и дочерняя страниц! выполняются одним и тем же объектом HttpApplication, как будто это один и тот же запрос, а метод Execute лишь осуществляет нечто вроде переключения контекста. Он получает от фабрики приложения объект обработчика HTTP, необходимый для обслуживания нового запроса, кэширует обработчик ис- ходного запроса и заменяет его новым обработчиком. При этом дочерняя страница наследует контекст родительской, и когда выполнение дочерней страницы заверша- ется, изменения, внесенные ею в объекты Session и Application, становятся видимыми родительской странице. Внимание! ASP.NET явно вызывает метод Execute, не применяя снова логику аутен- тификации и авторизации. Если политика безопасности требует, чтобы для доступа к ресурсу клиент был правильно авторизирован, приложение должно само позаботиться о повторной авторизации. Для этого вместо метода Execute нужно вызвать метод Re- sponse.Redirect. Тогда браузер сгенерирует новый запрос, который будет аутентифици- рован и авторизирован IIS и ASP.NET как обычно. Существует и альтернативный способ проверки наличия у пользователей разрешений на вызов страниц: определить роли и перед вызовом метода Execute проверить, к какой из них принадлежит конкретный пользователь. Серверная переадресация Метод Transfer отличается от метода Execute тем, что после вызова заданной страницы выполнение текущей страницы не приостанавливается, а прекращается вовсе, и новая страница обрабатывается так, как если бы она была запрошена изначально. У метода Transfer есть следующие перегруженные версии: public void Transfer(string); public void Transfer(string, bool); public void Transfer(IHttpHandler, bool); В строковом параметре задается целевой URL, а в булевом вы указываете, что делать с коллекциями Query String и Form. Если он имеет значение true, эти коллекции со- храняются, в противном случае они очищаются и целевая страница не имеет к ним доступа (этот вариант является рекомендуемым). В ASP.NET 2.0 можно также прямо
Контекст запроса HTTP Глава 12 487 указать подлежащий вызову обработчик HTTP. При этом необходимо учитывать те же соображения безопасности, что и при работе с методом Execute. Имейте в виду, что если главная страница содержит код, следующий за вызовом метода Transfer, этот код никогда не будет выполнен — ведь данный метод просто переадресует запрос, и из него нет пути назад. Однако метод Transfer исключительно эффективен по двум причинам: во-первых, в отличие от метода Response.Redirect он не требует перехода на клиент и обратно, а во-вторых, новый запрос обслуживается тем же объектом HttpApplication. Таким образом, издержки переадресации при вызове метода Transfer минимальны. Позднее связывание СОМ-объектов Класс HttpServerUtility позволяет создавать СОМ-объекты и подключать их к при- ложению методом позднего связывания, причем делается это практически так же, как в ASP. Создание COM-объекта выполняется с помощью методов CreateObject и CreateObjectFromClsid — первый получает progID класса, а второй — его CLSID. Так, следующий код создает экземпляр СОМ-компонента с использованием его CLSID: // Только в VB (в нестрогом режиме) можно вызвать методы // переменной типа Object, не являющиеся членами класса Object. // Приведенный код можно переписать и на С#, но едва ли это имеет смысл. Dim strClsid As String = "42754580-16b7-11ce-80eb-00aa003d7352" Dim comObj As Object = Server.CreateObject(strClsid) Присваивание объекта переменной типа Object называется поздним связывани- ем—в отличие от раннего связывания, при котором создаваемый объект присваивается строго типизированной переменной. При позднем связывании переменная может со- держать ссылку на любой объект, но функциональные возможности такого объекта оказываются ограниченными. Так, объекты, подключенные к приложению методом раннего связывания, можно использовать везде, где приемлемы объекты их типа, поскольку они функционируют значительно быстрее, чем объекты, подключенные методом позднего связывания. Кроме того, строгая типизация упрощает разработку кода и его сопровождение, делает его более читабельным. Из соображений обратной совместимости можно иногда создавать и поздно свя- зываемые экземпляры COM-компонентов. Нужно сказать, что COM-объекты часто используются в приложениях ASP.NET (и .NET в целом), и без них бывает невозможно обойтись. Наилучший способ их интеграции в приложение .NET заключается в соз- дании управляемых оболочек — особых классов, которые экспортируют библиотеку типов COM-класса как класс .NET. Обычно Visual Studio .NET автоматически создает такую управляемую оболочку, когда вы включаете в проект ссылку СОМ-компонент. Примечание Существует также утилита с интерфейсом командной строки, с помощью которой можно сгенерировать сборку класса, используя пространство имен, язык или имя файла, отличные от тех, которые Visual Studio .NET задает по умолчанию. Эта ути- лита — Type Library Importer (tlbimp.exe) — расположена в инсталляционной папке Visual Studio .NET. Хотя это и не особенно эффективное решение, метод Server.CreateObject может ис- пользоваться для создания экземпляра СОМ-компонента путем позднего связывания. Идеальным языком для позднего связывания является Visual Basic .NET. Однако имейте в виду, что позднее связывание поддерживается только при условии, что управляющему параметру Strict присвоено в Visual Basic значение Off (значение по умолчанию).
488 Часть III Инфраструктура ASP.NET Следующий код показывает, как заполнить объект ADO.NET Recordset, используя стиль программирования ASP: <%@ Page Language-”VB” AspCompat="trde" %> <script runat="server"> Sub Page_Load(sender as object, e as EventArgs) Dim rs As Object = Server.CreateObjectC'ADODB.Recordset") rs.Open("SELECT firstname, lastname FROM employees", "PROVIDER=sqloledb;DATABASE=northwind;SERVER=localhost; UID=sa; ”) Dim sb As StringBuilder = New StringBuilder("") While Not rs.EOF sb.Append(rs.Fields("lastname").Value.ToSt ring()) sb.Append("', ") sb.Append(rs.Fields("fi rstname").Value.ToSt ring()) sb.Append("<br>") rs.MoveNext End While Response.Write(sb.ToSt ring()) End Sub </script> Обратите внимание на атрибут AspCompat директивы ©Page. СОМ- компоненты, разработанные в расчете на однопоточную апартаментную модель (SingleThreaded Apartment, STA) выполнения, могут создаваться только теми страницами ASP.NET, в которых атрибут AspCompat установлен в true, поскольку в ASP.NET по умолчанию действует многопоточная апартаментная модель выполнения кода (MultiThreaded Apartment, МТА). Прежде чем предпринимать попытку создания объекта, метод Cre- ateObject выясняет, какова потоковая модель COM-компонента. Если страница уже работает в режиме совместимости с ASP, то есть атрибут AspCompat установлен в true, объект создается независимо от потоковой модели COM-компонента. Если же атрибут AspCompat установлен в false (значение по умолчанию), метод CreateObject считывает информацию о потоковой модели COM-компонента из реестра. Если там указана потоковая модель apartment (STA) или никакой потоковой модели не задано, выбрасывается исключение. В противном случае объект успешно создается. Применение атрибута AspCompat с библиотекой ADO не является обязательным, поскольку эта библиотека поддерживает и апартаментную модель, и модель свобод- ных потоков. Примечание Если вы хотите использовать в странице ASP.NET COM-компонент, напи- санный на Visual Basic 6.0, обязательно установите атрибут AspCompat в true, поскольку такие компоненты ориентированы на модель выполнения STA. В проп ihom случае, как уже упоминалось, будет выброшено исключение. Заметьте, что если ваш код создает COM-объект посредством управляемой оболочки (а не с помощью функции CreateObject), исполняющая среда не может обнаружить апар- таментную природу компонента и не выбрасывает исключение. Управляемая оболочка уберегает вас от исключения времени выполнения, но не избавляет от необходимости устанавливать атрибут AspCompat. Значение атрибута AspCompat С позиций производительности выполнение STA-компонентов в многопоточной апар- таментной среде (МТА), каковой является ASP.NET, очень нежелательно. Во из- бежание этого необходимо использовать атрибут AspCompat. Давайте посмотрим,
Контекст запроса HTTP Глава 12 489 почему такое выполнение столь отрицательно сказывается на производительности приложения. Обычно для обработки HTTP-запросов ASP.NET использует пул потоков МТА. Поскольку в МТА объект не привязан к конкретному потоку и может выполняться в любом из них, возможно параллельное выполнение любого количества методов объекта. В то же время однопоточные апартаментные COM-компоненты (каковы- ми являются все COM-компоненты, созданные с использованием Visual Basic 6) должны выполняться в том потоке, в котором они были созданы, и только один их метод может выполняться в каждый конкретный момент времени. Если не принять специальных контрмер, при выполнении STA-компонента в МТА-среде, скорее всего, будет происходить переключение потоков. Ну а теперь примите во внимание, что STA-компоненту, выполнение которого было приостановлено, может быть выделен поток для продолжения работы только при условии, что в пуле имеется именно тот поток, который годится для обслуживания данного STA-компонента, — и вы поймете, что переключение потоков для STA-компонентов чревато значительным снижением производительности и даже взаимоблокировками. Установив атрибут AspCompat в true, вы даете ASP.NET указание использовать для выполнения COM-объекта пул потоков STA на постраничной основе. Тогда и вызывающий поток, и вызываемый компонент будут находиться в одном апарта- менте, с чем связаны некоторые дополнительные затраты. По сути дела страницы ASP.NET, содержащие COM-компоненты STA, лучше работают в STA-режиме, чем в режиме МТА, несмотря на то что при других обстоятельствах последний является более производительным. Поскольку атрибут AspCompat обрабатывается после создания экземпляра класса страницы, следует избегать создания экземпляров COM-компонентов STA в кон- структоре страницы. Иначе страница будет выполняться в режиме МТА независимо от значения атрибута AspCompat, что приведет к снижению производительности. Установка атрибута AspCompat в true имеет еще и то преимущество, что СОМ- компоненту становятся доступными внутренние объекты ASP (в частности, Object- Context, Request и Response). ASP.NET создает неуправляемые внутренние объекты ASP и передает их используемым страницей СОМ-компонентам. Объект Response В ASP.NET информацию ответа HTTP инкапсулирует объект класса HttpResponse. Этот объект создается при подготовке конвейера HTTP к обслуживанию запроса и связывается с объектом HttpContext, ассоциированным с данным запросом; объект HttpResponse доступен через свойство Response объекта HttpContext. Хотя пользо- вательскому коду ASP.NET никогда не приходится вызывать конструктор класса HttpResponse, интересно все же взглянуть на его объявление: public HttpResponse(TextWriter writer); Как видите, этот конструктор принимает объект записи текста, в котором будет накапливаться текст ответа. Все вызовы метода Response. Write (и подобных ему мето- дов вывода) в конечном счете разрешаются как внутренние вызовы этого объекта. Свойства класса HttpResponse Свойства класса HttpResponse описаны в табл. 12-10. Некоторые из них вы будете устанавливать для конфигурирования важнейших полей ответа HTTP, таких как тип контента, набор символов, срок хранения страницы и код состояния.
490 Часть III Инфраструктура ASP.NET Как видите, многие свойства класса HttpResponse устарели и поддерживаются только ради обратной совместимости с приложениями классической ASP.NET. В не- которых случаях свойство лишь переименовано (как Buffer Output), в других оно заменено более универсальным и мощным API (как свойства, управляющие кэши- рованием и устареванием страниц). Табл. 12-10. Свойства класса HttpResponse Свойство Описание Buffer Позволяет указать, должен ли текст ответа буферизироваться и отправляться только в конце обработки запроса. Это свойство устарело и поддерживается только для обратной совместимости с приложениями классической ASP. В приложениях ASP.NET вместо него следует пользоваться свойством BufferOutput BufferOutput Позволяет включать и отключать буферизацию текста ответа. По умолчанию данное свойство имеет значение true, то есть бу- феризация включена Cache Возвращает объект HttpCachePolicy, представляющий заданную для страницы политику кэширования. Этот объект можно ис- пользовать для установки отдельных HTTP-заголовков ответа, связанных с кэшированием CacheControl Позволяет задать HTTP-заголовок Cache-Control. Допустимыми значениями являются Public, Private и No-Cache. Данное свойство устарело, и вместо него следует пользоваться свойством Cache Charset Возвращает и позволяет задать строку, определяющую набор символов ответа HTTP. Если данное свойство установлено в null, заголовок Content-Type в ответ не включается ContentEncoding Возвращает и позволяет задать объект типа Encoding, определяю- щий кодировку символов выходного потока ContentType Возвращает и позволяет задать строку, идентифицирующую MIME-тип выходного потока. По умолчанию данное свойство имеет значение text/html Cookies Возвращает коллекцию типа HttpCookieCollection, содержащую набор сгенерированных сервером экземпляров класса Http Cookie. Все cookie в этой коллекции будут переданы клиенту с использо- ванием HTTP-заголовка set-cookie Expires Возвращает и позволяет задать число минут, спустя которое кэшируемая браузером страница должна считаться устаревшей. Это свойство поддерживается лишь для обратной совместимости с ASP, и вместо него следует пользоваться свойством Cache ExpiresAbsolute Возвращает и позволяет задать абсолютные дату и время, по на- ступлении которых кэшируемая браузером страница станет уста- ревшей. Это свойство поддерживается лишь для обратной совмес- тимости с ASP, и вместо него следует пользоваться свойством Cache Filter Возвращает и позволяет задать фильтрационный объект Stream, которому будет перенаправляться весь вывод HTTP IsClientConnected IsRequestBeingRedirected Указывает, является ли клиент все еще подключенным к серверу Указывает, был ли запрос переадресован. В ASP.NET 1х данное свойство не поддерживается Output Возвращает объект записи текста, в который направляется выво- димый текст Outputstream Возвращает объект Stream, в который направляются выводимые двоичные данные
Контекст запроса HTTP Глава 12 491 Табл. 12-10. (окончание} Свойство Описание RedirectLocation Status Возвращает и позволяет задать строку для заголовка Location Возвращает и позволяет задать возвращаемую клиенту строку с информацией состояния ответа. Это свойство поддерживается лишь для совместимости с ASP, и теперь вместо него следует пользоваться свойством StatusDescription StatusCode Возвращает и позволяет задать целочисленный код состояния HTTP для ответа клиенту; значение по умолчанию — 200 StatusDescription Возвращает и позволяет задать строку состояния HTTP, содержа- щую описание общего состояния возвращаемого клиенту ответа; значение по умолчанию — ОК SuppressContent Позволяет указать, должен ли контент HTTP быть отправлен клиенту. По умолчанию данное свойство имеет значение false, и контент отправляется; если установить его в true, будут отправ- лены только заголовки HTTP Определение политики кэширования ответа У объекта класса HttpResponse имеются три свойства, содержащие отдельные установ- ки кэширования страницы на клиенте. В свойствах Expires и ExpiresAbsolute задается абсолютный или относительный срок, в течение которого страница будет оставаться действительной, так что браузер сможет предоставлять ее, не обращаясь к серверу. Свойство CacheControl содержит HTTP-заголовок Cache-Control, управляющий кэ- шированием страницы. Все три свойства являются устаревшими и поддерживаются для обратной совместимости с приложениями классической ASP. В ASP.NET все установки кэширования объединены в объекте класса HttpCache- Policy. У этого объекта двойная роль: в его состав входят методы для установки НТТР- заголовков, управляющих кэшированием на клиенте, и методы для управления кэ- шированием вывода страницы ASP.NET на сервере. (В этой главе мы рассмотрим заголовки HTTP, а о кэшировании вывода поговорим в главе 14.) Для установки параметров кэширования страницы на клиенте используются методы SetCacheability и SetExpires класса HttpCachePolicy. Метод SetCacheability позволяет вклю- чать и отключать кэширование, а метод SetExpires определяет срок службы кэширован- ной копии страницы — ему передаются абсолютные дата и время как объект DateTime. Примечание В случае конфликта политик кэширования ASP.NET применяет наиболее строгую установку. Например, если страница содержит два элемента управления, один из которых присваивает заголовку Cache-Control значение public, а второй — private, при отправке ответа клиенту будет использоваться установка private как более огра- ничивающая. Установка фильтра ответа В ASP.NET был введен новый функциональный компонент, называемый фильтром ответа. Это объект класса, основанного на классе Stream, связанный с объектом HttpResponse. Он отслеживает и фильтрует весь вывод страницы. Если присвоить свойству Filter экземпляр фильтрующего класса, то вывод, направляемый в объект записи текста, будет проходить через указанный вами фильтр. Пользовательский фильтр вызывается из метода Flush объекта HttpResponse перед отправкой текста ответа клиенту. С помощью такого фильтра можно внести послед- ние коррективы в сгенерированную разметку, исправить в ней что-то или сделать ее более компактной.
492 Часть III Инфраструктура ASP.NET Для того чтобы разработать фильтр ответа, нужно создать новый потоковый класс и переопределить в нем несколько методов. У этого класса должен иметься конструктор, принимающий объект типа Stream. Выше я сказал, что класс фильтра основан на классе Stream. Теперь уточню: он является скорее оболочкой класса Stream, чем производным от него классом. Если вы попытаетесь присвоить свой- ству Response.Filter экземпляр, скажем, класса MemoryStream или FileStream, будет выброшено исключение. Ниже показано, как создать потоковый класс, действующий как фильтр ответа. Для простоты я сделал этот класс производным от MemoryStream. Можно сделать его производным и от Stream, но тогда придется переопределять больше методов, в частности методы CanRead, CanWrite, CanSeek и Read, которые в классе Stream явля- ются абстрактными. Следующий класс переводит символы в тексте ответа в верхний регистр: public class MyFilterStream MemoryStream { private Stream m_Stream; public MyFilterStream(Stream filterstream) { m_Stream = filterstream; } // Этот метод выполняет фильтрацию public override void Write(byte[] buffer, int offset, int count) { // Получаем вывод в виде строки , string buf = UTF8Encoding.UTF8.GetString(buffer, offset, count); // Вносим изменения - заменяем символы нижнего регистра // символами верхнего регистра buf = buf .ToUpperO; // Записываем результирующую строку обратно в выходной поток byte[] data = UTFSEncoding.UTF8 GetBytes(buf.ToStringO); m.Stream.Write(data, 0, data.Length); } } А вот как осуществляется связывание созданного фильтра со свойством Re- sponse.Filter (это код из файла демонстрационной страницы, которую вы видите на рис. 12-5): void Page_Load(object sender, EventArgs e) { Response.Filter = new MyFilterStream(Response.Filter); } Фильтры ответа — полезное средство в арсенале Web-разработчика, но пользо- ваться им нужно умеючи. Ведь если не глядя вносить изменения в сгенерированный страницей вывод, то можно испортить содержимое полей состояния представле- ния или код сценария Учтите также, что включать фильтрацию следует для каждой страницы в отдельности. Если же вам потребуется фильтровать все страницы сайта, то лучше написать модуль HTTP.
Контекст запроса HTTP Глава 12 493 Рис. 12-5. В состав этой страницы входит фильтр ответа, переводящий весь текст в верхний регистр Методы класса HttpResponse С перечнем методов класса HttpResponse вы можете ознакомиться в табл. 12-11. Табл. 12-11. Методы класса HttpResponse Метод Описание AddCacheDependency Добавляет массив зависимостей кэша (если любая из зависи- мостей окажется разрушенной, вывод кэшируемой страницы станет недействительным). Элементами массива могут быть объекты любого класса, производного от CacheDependency. В ASP.NET 1 л данный метод не поддерживается AddCacheltemDependencies Добавляет массив имен элементов кэша ASP.NET (объекта Cache). Когда любой из этих элементов изменится, кэширован- ный вывод страницы станет недействительным AddCacheltemDependency Действует подобно предыдущему методу, но добавляет имя только одного элемента кэша AddFileDependencies Добавляет в коллекцию имен файлов имена группы файлов, от которых зависит текущая страница. Когда в один из этих файлов вносятся изменения, кэшированный вывод текущей страницы становится недействительным AddFileDependency Добавляет в коллекцию имен файлов имя одного файла, от ко- торого зависит текущая страница. Когда в этот файл вносятся изменения, кэшированный вывод текущей страницы становится недействительным AddHeader Добавляет в выходной поток заголовок HTTP. Этот метод под- держивается лишь для совместимости с классической ASP; в ASP.NET следует использовать вместо него метод AppendHeader AppendCookie AppendHeader AppendToLog ApplyAppPathModifier Добавляет элемент в коллекцию cookie Добавляет в выходной поток заголовок HTTP Добавляет в файл журнала IIS пользовательское сообщение Добавляет к заданному виртуальному пути идентификатор сеанса и возвращает результат. Метод обычно вызывается для тех сеансов, в которых не используются cookie, для составления абсолютных значений HREF гиперссылок см. след. стр.
494 Часть III Инфраструктура ASP.NET Табл. 12-11. {окончание) Метод Описание BinaryWrite Записывает двоичный код в выходной поток HTTP; может использоваться только с относительно небольшими файлами (далее в этой главе будет описан подробнее) Clear ClearContent ClearHeaders Close End Удаляет из буферного потока весь контент Вызывает метод Clear Удаляет из буферного потока все заголовки Закрывает сокет соединения с клиентом Отправляет клиенту весь буферизированный текст, завершает выполнение страницы и генерирует соответствующее событие Flush Pics Отправляет клиенту весь буферизированный текст Записывает в выходной поток HTTP-заголовок PICS-Label. (Аббревиатура PICS означает Platform for Internet Content Se- lection — платформа для выбора интернет контента. Так назы вается стандарт определения рейтинга страниц, утвержденный W3C.) Допустима любая строка длиной не более 255 символов Redirect Перенаправляет клиента по другому URL (с заходом на клиент и обратно) RemoveOutputCacheltem Статический метод, который принимает путь в файловой системе и удаляет из кэша все элементы, ассоциированные с этим путем SetCookie TransmitFile Обновляет заданный элемент в коллекции cookie Подобно методам Binary Write и WriteFile записывает содержи мое заданного файла в выходной поток. Данный метод можно использовать независимо от размера файла. В ASPNET 1х ме- тод доступен при условии, что вы установите Service Pack 1 Write Записывает контент в выходной поток. Этот метод может вы- вести строку, один символ или массив символов, а также объект В последнем случае для записи объекта будет вызван его метод ToString WriteFile Записывает содержимое заданного файла (или его часть) в выходной поток. Файл идентифицируется путем или дескрип- тором Win32 (объектом IntPtr). Может использоваться только с относительно небольшими файлами (далее в этой главе он будет описан подробнее) WriteSubstitution Позволяет заменить фрагменты страницы и отправить их в вы- ходной кэш (мы подробно рассмотрим этот метод в главе 14). В ASP.NET 1х данный метод не поддерживается Кэширование вывода У класса HttpResponse есть несколько методов, которые ставят вывод страницы в зави- симость от файлов или элементов кэша. Методы AddFileDependency (AddFileDependen cies) и AddCacheltemDependency (AddCacheltemDependencies) делают вывод страницы зависимым от заданных файлов или кэшированных элементов, так что при изменении этих файлов или элементов вывод страницы становится недействительным. Это простейшая форма управления кэшированием вывода страницы, и хотя она не столь мощная, как API, который мы будем изучать в главе 14, с ней тоже стоит ознакомиться. Преимущества упомянутого API заключаются в том, что он позволяет
Контекст запроса HTTP Глава 12 495 управлять кэшированием вывода страницы, его длительностью и, возможно, место- положением кэшированной копии. В ASP.NET 2.0 введен новый метод AddCacheDependency, который дает вам возмож- ность связать вывод страницы с объектом зависимости, доступным вашему приложе- нию (это может быть даже пользовательский объект). Подробнее о пользовательских объектах зависимостей рассказывается также в главе 14. Передача больших файлов В ASP.NET определено три метода, предназначенных для записи в выходной поток больших блоков данных, — Binary Write, WriteFile и TransmitFile. Метод TransmitFile был введен в ASP.NET 1.x в пакете исправлений и дополнений, который был опи- сан в статье Microsoft Knowledge Base КВ823409, и позднее включен в состав .NET Framework 1.x SP1. Методы WriteFile и BinaryWrite прекрасно подходят для вывода двоичных данных, однако они загружают блок данных (содержимое файла или байтовый массив) в па- мять целиком. Если файл велик, это может вызвать серьезные проблемы и даже при- вести к перезагрузке процесса ASP.NET. Метод TransmitFile весьма элегантно решает эту проблему. Он выводит заданный файл прямо в выходной поток HTTP, без его буферизации в памяти. Таким образом, TransmitFile является наиболее стабильным и надежным из трех методов, но это его преимущество сказывается только на файлах большого размера. Примечание Хотя метод TransmitFile делает процесс загрузки больших файлов более стабильным, чем когда-либо, и устраняет проблему перезагрузки процесса ASP.NET, он не решает другой важной задачи — отслеживания загрузки больших файлов и ее воз- обновления. Если загрузка файла по какой-либо причине завершается неудачей, метод TransmitFile просто начинает ее сначала. Решение указанной задачи описано в статье, которую вы найдете по адресу http://www.devx.com/dotnet/Article/22533. Объект Request В объекте класса HttpRequest, возвращаемом свойством Request объекта HttpContext, объединена вся информация, которая содержится в пакете HTTP, представляющем входящий Web-запрос. Содержимое разных заголовков HTTP, строка запроса, зна- чения, введенные в поля формы, путь и информация URL для простоты и удобства доступа организованы в объекте HttpRequest в виде набора коллекций и других спе- циализированных объектов. Исполняющая среда ASP.NET заполняет объект HttpRe- quest, как только начинает работать над Web-запросом, и делает его доступным через свойство Request объекта HttpContext. Класс HttpRequest имеет множество свойств, и при переходе от ASP к ASP.NET он был расширен более других классов. Свойства класса HttpRequest Свойства класса HttpRequest можно разделить на три категории в зависимости от со- держащейся в них информации: тип запроса, клиентские данные и подключение. Информация о запросе Свойства, определяющие тип полученного сервером запроса, описаны в табл. 12-12. В ASPNET 2.0 идентификаторы анонимных пользователей обычно передаются через cookie (по умолчанию пользователю назначается имя ASPXANONYMOUSy, они используются для идентификации пользователей, чаще всего с целью создания
496 Часть III Инфраструктура ASP.NET их профилей. Идентификатор анонимного пользователя — это GUID, передаваемый в виде чистого текста. Он не играет важной роли в системе аутентификации и защи- ты, а является лишь средством отслеживания доступа к сайту незарегистрированных пользователей. (О профилях рассказывалось в главе 5, а об аутентификации пользо- вателей вы сможете прочитать в главе 15.) Табл. 12-12. Свойства класса HttpRequest, определяющие тип запроса Свойство Описание AcceptTypes Возвращает массив строковых идентификаторов MIME-типов, поддерживаемых клиентом для заданного запроса AnonymousID Содержит идентификатор анонимного пользователя. Этот иден- тификатор является строкой, сгенерированной модулем Апопу- mousldentification, и не имеет ничего общего с идентификатором анонимного пользователя из IIS. В ASP.NET 1х данное свойство не поддерживается Browser Возвращает объект HttpBrowserCapabilities, содержащий инфор- мацию о возможностях браузера клиента ContentEncoding Возвращает и позволяет задать объект Encoding, представляющий набор символов клиента. Если это свойство задано, оно имеет преимущество перед кодировкой, заданной в ASP.NET по умол- чанию ContentLength Возвращает длину полученного от клиента контента в байтах ContentType Возвращает и позволяет задать идентификатор MIME-типа вхо- дящего запроса CurrentExecutionFilePath Возвращает текущий виртуальный путь запроса, даже если кли- ент перенаправлен на другую страницу с помощью метода Execute или Transfer. Свойство FilePath всегда возвращает путь к исходной запрошенной странице FilePath Возвращает виртуальный путь текущего запроса. В случае сер- верной переадресации значение этого свойства не меняется HttpMethod Возвращает строку, идентифицирующую метод HTTP: GET, POST или HEAD RequestType Возвращает и позволяет задать строку, идентифицирующую ко- манду HTTP, которая использовалась для выдачи запроса: GET или POST TotalBytes Возвращает общее количество байтов во входном потоке. Это свойство отличается от свойства ContentLength тем, что включает и длину заголовков UserAgent Возвращает строку, идентифицирующую браузер. Это свойство возвращает необработанное содержимое заголовка пользователь- ского агента Первоначально свойства CurrentExecutionFilePath и FilePath содержат одно и то же значение — запрошенный URL. Однако если происходит серверная переадресация, значение свойства CurrentExecutionFilePath автоматически изменяется, и вы можете получить из него актуальную информацию о целевом URL. Объект HttpBrowserCapabihties содержит полный набор информации о возможно- стях браузера, в том числе о поддержке им компонентов ActiveX, языков сценариев, фреймов, cookie и т. д. Когда поступает очередной запрос, информация пользова- тельского агента используется для идентификации направившего его браузера. За- тем создается и заполняется экземпляр класса HttpBrowserCapabilities. Учтите, что
Контекст запроса HTTP Глава 12 497 информация о возможностях браузера не передается им самим, а считывается из серверного репозитария. Примечание В .NET Framework версии 1.1 и выше свойство Browser может использовать- ся и в приложениях для мобильных устройств. В таком случае это свойство возвращает объект класса MobileCapabilities, производного от HttpBrowserCapabilities. Поэтому, если вас интересуют возможности мобильного браузера, следует привести значение свойства Browser к типу MobileCapabilities. Информация, полученная от клиента В табл. 12-13 перечислены свойства объекта HttpRequest, возвращающие клиентские данные, которые могут быть полезны приложению, — cookie, значения полей формы, содержимое строки запроса. Табл. 12-13. Свойства, представляющие клиентские данные Свойство Описание ClientCertificate Возвращает объект HttpClientCertificate с информацией об установках сертификата безопасности клиента, если таковой сертификат имеется. Данный объект содержит номер сертификата, информацию о том, дей- ствителен ли он и кем выдан Cookies Возвращает коллекцию, представляющую все полученные от клиента cookie Элементами этой коллекции являются объекты HttpCookie Files Возвращает коллекцию файлов, загруженных клиентом на сервер. Она заполняется, когда HTTP-заголовок Content-Type содержит значе- ние multipart/form-data Filter Возвращает и позволяет задать основанный на объекте Stream объект фильтрации, через который проходит весь HTTP-ввод. Отфильтрован- ный ввод считывается из свойства InputStream Form Возвращает коллекцию пар имя-значение, заполненную данными из полей ввода формы; эта коллекция заполняется, когда НТТР-заголовок Content-Type содержит значение application/x-vneif -form-urlencoded или multipart/form -data Headers Возвращает коллекцию пар имя-значение, заполненную значениями всех заголовков запроса InputStream Возвращает объект Stream, представляющий контент (тело) входящего НТТР-запроса Params Возвращает коллекцию пар имя- значение, которая является объедине- нием четырех однотипных коллекций: QueryString, Form, ServerVariables и Cookies QueryString Возвращает коллекцию пар имя-значение, заполненную переменными строки запроса ServerVariables Возвращает коллекцию пар имя-значение, заполненную серверными переменными UserHostAddress UserHostName UserLanguages Возвращает IP-адрес удаленного клиента Возвращает DNS-имя удаленного клиента Возвращает массив строк, идентифицирующих поддерживаемые клиен- том для текущего запроса языки. Список языков считывается из заголов- ка Accept-Language В коллекции Params объединены значения четырех различных, но гомогенных коллекций: QueryString, Form, Cookies и finallyServerVariables — в том порядке, в каком они указаны.
498 Часть III Инфраструктура ASP.NET Информация о подключении Свойства, которые связаны с открытым подключением, представлены в табл. 12-14. Табл. 12-14. Свойства, связанные с подключением Свойство Описание ApplicationPath Возвращает виртуальный путь к текущему приложению IsAuthenticated Указывает, аутентифицирован ли пользователь IsLocal Указывает, является ли запрос локальным. В ASP.NET 1л данное свойство не поддерживается IsSecureConnection Указывает, используются ли для установки подключения протоко- лы Secure Sockets Layer (SSL) и HTTPS Logon Useridentity Возвращает объект, представляющий идентификатор подключив- шегося через И? текущего пользователя в Windows. В ASP.NET 1л данное свойство не поддерживается Path Возвращает виртуальный путь к ресурсу, указанному в текущем запросе Pathinfo Возвращает для запрошенного ресурса дополнительную информа- цию о пути, если таковая имеется. Это свойство возвращает любой текст, следующий за URL PhysicalApplication- Path Возвращает путь к корневому каталогу текущего приложения в файловой системе PhysicalPath Возвращает физический путь к запрошенному ресурсу в файловой системе Raw Url Возвращает URL в том виде, в каком он задан в запросе Url Возвращает объект Uri, представляющий URL текущего запроса UrlReferrer Возвращает объект Uri, представляющий URL предыдущего запро- са, из страницы которого был направлен данный запрос Класс Uri представляет универсальный идентификатор ресурса (Uniform Resource Identifier, URI) — уникальное имя ресурса, доступного через Интернет. Этот класс обеспечивает удобный доступ к частям URI, а также предоставляет свойства и методы для получения информации о хосте, портах и DNS. Серверные переменные, содержащиеся в коллекции ServerVariables, создаются ис- полняющей средой, которая обрабатывает запрос. Часть информации этой коллекции извлекается из объекта запроса HTTP, а другая часть связана с Web-сервером. Данная коллекция создается просто для удобства доступа ко всем этим сведениям. Методы класса HttpRequest Перечень методов класса HttpRequest приведен в табл. 12-15. Табл. 12-15. Методы класса HttpRequest Метод Описание Binary Read Считывает двоичные данные из текущего входного потока. Он по- зволяет задать количество подлежащих чтению байтов и возвра- щает байтовый массив. Данный метод поддерживается только ради обратной совместимости с приложениями ASP, а в приложениях ASP.NET следует считывать данные из потока, связанного со свой- ством Inputstream
Контекст запроса HTTP Глава 12 499 Табл. 12-15. (окончание) Метод Описание MapImageCoordinates MapPath Определяет координаты поля ImageField По заданному виртуальному пути определяет физический путь на Web-сервере Sate As Сохраняет текущий запрос в дисковом файле, с заголовками или без них. Этот метод особенно полезен при отладке Validatelnput Выполняет быструю, не исчерпывающую, проверку входных данных запроса на предмет потенциально опасных значений Сохранение запроса на диске Метод SaveAs создает дисковый файл и сохраняет в нем содержимое НТТР-запроса. Заметьте, что его вывод может направляться только в файл, не в поток и не в объект записи текста. Поскольку по умолчанию ASP.NET не имеет разрешения на запись в файлы, если не принять специальных мер, результатом вызова этого метода станет исключение. Одной из таких мер может быть предоставление ASP.NET полных прав на создаваемый файл (или на всю папку). Ниже демонстрируется пример содержимого файла, созданного методом SaveAs". GET /ProAspNet20/Samples/Ch12/TestFilter.aspx НТТР/1.1 Connection: Keep-Alive Accept: */* Accept-Encoding: gzip, deflate Accept-Language: en-us,it;q=0.5 Authorization: NTLM T1RMTVNTUAADAAAAIAAAA ... BcKIogUCzg4AAAAP Cookie: .ASPXAN0NYM0US=AcW35sC18TwwNDcyYTMxY .. w2 Host: localhost User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; SV1; snprtz|S04739424200867; .NET CLR 1.1.4322; .NET CLR 2.0.50215) UA-CPU: x86 Если сохраняется POST-запрос, в конец этого файла добавляются возвращенные клиентом значения входных полей. Проверка клиентского ввода Золотое правило безопасности Web-приложений гласит: весь пользовательский ввод должен рассматриваться как потенциально опасный, и перед использованием его необходимо фильтровать, удаляя вредоносные значения. В ASP.NET 1.1 Microsoft ввела новый атрибут ValidateRequest директивы @Page, предназначенный для ав- томатического блокирования потенциально опасных данных. Эта новая функция не является серебряной пулей защиты Web-приложений, но позволяет выявлять некоторые из возможных проблем. В общем случае лучше не полагаться на автома- тическую валидацию ввода, а написать собственный мощный и специализированный валидационный слой. Функция автоматической валидации по умолчанию включена — она включается с помощью вызова метода Validationinput объекта HttpRequest, который при необходимо- сти вы можете вызвать и сами. Автоматическая валидация запроса производится путем сверки входных данных с жестко закодированным списком потенциально опасных значений. В ходе этой процедуры проверяется содержимое коллекций Query String, Form и Cookies.
500 Часть III Инфраструктура ASP.NET Заключение В настоящей главе мы рассмотрели ряд объектов, которые являются основой про- граммирования в ASP.NET, в том числе объекты Server, Response и Request. Прило- жение ASP.NET представлено экземпляром класса, который является производным от HttpApplication и создается на основе содержимого файла globaLasax. И класс Http- Application, и файл globaLasax нашли отражение в этой главе. Обсуждая интерфейс объектов, составляющих контекст HTTP-запроса, мы под- робно рассмотрели некоторые важные вопросы программирования, такие как позднее связывание COM-объектов, серверная переадресация запросов и создание фильтров ответов. В следующей главе освещается еще одна обширная тема, связанная с Web- приложениями и ASP.NET — управление состоянием. Сама по себе Web функциони- рует без сохранения состояния, но ASP.NET предоставляет необходимые механизмы для управления состоянием и кэширования страниц. Только факты Любой запрос ASP.NET проходит по конвейеру ее внутренних компонентов, точкой входа которого является класс HttpRuntime. Класс HttpRuntime создает объект класса HttpContext, представляющий контекст запроса. Этот объект сопровождает запрос на протяжении всего его жизненного цикла. В контексте запроса HTTP содержатся ссылки на внутренние рабочие объекты ASP.NET Request, Response и Server, а также на состояние сеанса, кэш ASP.NET, трассировщик и объект, идентифицирующий пользователя. Выполнением запроса управляет извлекаемый из пула объект HttpApplication. Экземпляр его класса создается на основе содержимого файла globaLasax, если таковой имеется. Объект HttpApplication обеспечивает прохождение запроса и контекста HTTP че- рез конвейер зарегистрированных модулей HTTP, которые по очереди получают возможность перехватить запрос, использовать содержащуюся в нем информацию и даже его модифицировать. Модули могут генерировать собственные события, обработчики которых должны размещаться в файле globaLasax. Стандартные внутренние объекты ASP.NET, такие как Request, Response и Server, по- хожи на свои аналоги из ASP, но их возможности шире. Например, объект Response способен управлять загрузкой файлов и фильтровать вывод страницы, а объект Request позволяет управлять входящим запросом и его содержимым.
Глава 13 Управление состоянием Любому реальному приложению для выполнения запросов пользователя необходимо поддерживать свое состояние, и приложения ASP.NET — не исключение. Однако в отличие от приложений других типов им для этого необходимы специальные сер- верные средства. Дело в том, что приложения ASPNET функционируют на базе сети Web, действующей без сохранения состояния. И пока транспортным протоколом Web остается HTTP, разработчики любых Web-приложений будут сталкиваться с одной и той же трудностью — необходимостью искать наиболее эффективный способ со- хранения состояния. Состояние приложения — это своеобразный контейнер, заполняемый любой инфор- мацией, которая должна сохраняться между сеансами работы пользователя. Инфор- мация эта разнородная: например, сведения о предпочтениях пользователя, глобаль- ные установки, рабочие данные, значения счетчиков, таблицы поиска и содержимое корзин для покупок. Все эти данные нужно определенным способом организовать и предоставить средства для доступа к ним, осуществляемого по различным схемам. Следует добавить, что обычно части информации состояния приложения находятся в разных его слоях, с разными уровнями видимости, возможностями программного доступа и различным временем жизни. ASP.NET предоставляет возможность управления состоянием на четырех уровнях: приложения, сеанса, страницы и запроса, и для каждого из этих уровней поддер- живается собственный контейнерный объект. В этой главе мы рассмотрим объекты HttpApplicationState, HttpSessionState и View State, предназначенные для управления информацией состояния приложения, сеанса и страницы, а в следующей главе деталь- но исследуем объект Cache. При этом напомню также, что в главе 12 рассматривался объект HttpContext — главное средство управления состоянием и другой информа- цией, которая сохраняется в течение всего жизненного цикла запроса и вместе с ним передается по исполняющему конвейеру HTTP. Объект контекста запроса HttpCon- text отличается от других объектов состояния тем, что время его жизни ограничено временем выполнения запроса. Состояние приложения Основные характеристики различных объектов состояния приведены в табл. 13-1. Табл. 13-1. Объекты состояния Объект Время жизни Видимость данных Местонахождение Cache Реализует автоматиче- ский механизм сборки мусора и периодиче- ски удаляет наиболее редко используемый контент Глобален для всех сеансов Не поддерживает сценарии Web- фер- мы и Web-сада см. след. стр.
502 Часть III Инфраструктура ASP.NET Табл. 13-1. (окончание) Объект Время жизни Видимость данных Местонахождение HttpApplicationState Создается при полу- чении Web-сервером первого запроса и уничтожается при завершении работы приложения Глобален для всех сеансов Не поддерживает сценарии Web-фер- мы и Web-сада HttpContext Поддерживается на протяжении времени жизни одного запроса Глобален для объ- ектов, вовлеченных в обработку запроса Не поддерживает сценарии Web-фер- мы и Web-сада HttpSessionState Создается, когда определенный поль- зователь направляет Web-серверу первый запрос и поддержива- ется до тех пор, пока этот пользователь не завершит сеанс Глобален для всех запросов пользо- вателя, начавшего сеанс Может быть сконфи- гурирован для ра- боты с Web-фермой или Web-садом ViewState Представляет контекст вызова конкретной сгенерированной страницы Видим только це- почке запросов, на- правляемых поль- зователем в рамках одного сеанса рабо- ты с определенной страницей Может быть сконфи- гурирован для ра- боты с Web-фермой или Web-садом Несмотря на новые имена, объекты HttpApplicationState и HttpSessionState полнос- тью совместимы с системными объектами Application и Session классической ASP и работают с ними практически так же. Для доступа к объектам HttpApplicationState и HttpSessionState в ASP.NET используются свойства с именами Application и Session. { Примечание В этой главе вы познакомитесь с несколькими объектами, на разных уров- нях участвующих в поддержании состояния приложений. Мы не будем подробно рассма- тривать файлы cookie, но имейте их виду как весьма полезное средство сохранения на клиентском компьютере небольших объемов информации. С каждым запросом эта информация отправляется на сервер для прочтения и изменения серверной страницей, а затем вместе с результатами выполнения запроса снова пересылается клиенту. Файлы cookie часто применяются в приложениях для электронной коммерции и других подобных приложениях, имеющих большое количество разовых пользователей. Кроме того, для cookie можно задавать политику устаревания. К недостаткам cookie-файлов относятся их ограниченный размер (его максимальное значение зависит от браузера, но, как правило, не превышает 8 Кбайт) и возможность отключения пользователем их поддержки. В объекте HttpApplicationState содержится словарь, которым могут пользоваться для сохранения своих данных все обработчики HTTP, вызываемые в процессе вы- полнения приложения. В классической ASP к состоянию приложения имели доступ только страницы, а в ASPNET им могут пользоваться все обработчики и модули HTTP. Состояние приложения доступно только для его компонентов — другие работающие в той же системе приложения не могут ни прочитать, ни модифицировать его данные. Экземпляр класса HttpApplicationState создается при получении первого запроса к определенному виртуальному каталогу. Каждое приложение поддерживает соб- ственный такой объект. Наиболее распространенным способом доступа к нему служит обращение к свойству Application объекта Page. Состояние приложения не является общим для всех компьютеров Web-фермы или Web-сада.
Управление состоянием Глава 13 503 Свойства класса HttpApplicationState Класс HttpApplicationState наследует класс NameObjectCollectionBase. Основной его со- ставляющей является коллекция пар ключ-значение, в которых ключ имеет строковый тип, а значение — тип Object. Доступ к элементам коллекции может осуществляться либо по ключу, либо по индексу. Базовый класс NameObjectCollectionBase реализует хэш-таблицу, которая первоначально имеет нулевой размер, но по мере необходимости расширяется. В табл. 13-2 кратко описаны свойства класса HttpApplicationState. Табл. 13-2. Свойства класса HttpApplicationState Свойство Описание AllKeys Возвращает массив строк, которые содержат ключи хранящихся в объекте элементов Contents Возвращает текущий экземпляр класса HttpApplicationState. Внимание! Это свойство возвращает ссылку на объект состояния, а не его копию. Оно под- держивается для совместимости с ASP Count Возвращает текущее количество объектов в коллекции Item Индексное свойство, обеспечивающее доступ к элементам коллекции для чтения и записи. Элемент можно идентифицировать именем или индексом. Аксессор и мутатор этого свойства реализованы как методы Get и Set StaticObjects Возвращает коллекцию объектов, объявленных в файле globaLasax с помо- щью тэга <object>, атрибут scope которого имеет значение Application Заметьте, что статические объекты и реальные данные состояния хранятся в раз- ных коллекциях. Типом возвращаемой свойством StaticObjects коллекции статических объектов является HttpStaticObjectsCollection. Методы класса HttpApplicationState Методы класса HttpApplicationState являются специализированными версиями ме- тодов типичной коллекции пар ключ-значение. Как видно из табл. 13-3, наиболее значительные отличия этого класса от обычной коллекции связаны с механизмом блокировки, который необходим для сериализации доступа к данным состояния. Табл. 13-3. Методы класса HttpApplicationState Метод Описание Add Добавляет в коллекцию новое значение типа object Clear Удаляет все объекты из коллекции Get Возвращает значение элемента коллекции, идентифицированного ключом или индексом GetEnumerator Возвращает объект-перечислитель для перебора элементов коллекции GetKey Lock Возвращает строковый ключ элемента с заданным индексом Блокирует доступ к коллекции для записи — параллельная запись в нее остается невозможной до тех пор, пока не будет вызван метод UnLock Remove Удаляет из коллекции элемент с заданным ключом RemoveAll Вызывает метод Clear RemoveAt Удаляет элемент с заданным индексом см. след. стр.
504 Часть III Инфраструктура ASP.NET Табл. 13-3. (окончание) Метод Описание Set Присваивает элементу с заданным ключом заданное значение. Этот метод поддерживает многопоточность, и доступ к элементу блокируется до завер- шения операции записи UnLock Разблокирует коллекцию, открывая параллельный доступ к ней для записи Заметьте, что метод GetEnumerator унаследован от базового класса коллекции и как таковой ничего не знает о механизме блокировки, реализованном в классе HttpAp- plicationState. Если перебирать элементы управления коллекции с использованием этого метода, каждое возвращаемое значение будет получено путем вызова одного из методов get (аксессоров) базового класса NameObjectCollectionBase. Поэтому для многопоточного выполнения такой способ доступа не подходит, и целесообразнее перебирать элементы коллекции с помощью обычного оператора while и метода Get класса HttpApplicationState. В качестве альтернативы перед прохождением по коллек- ции ее можно заблокировать. Синхронизация доступа Заметьте, что все операции класса HttpApplicationState требуют принятия опреде- ленных мер для синхронизации параллельного доступа, чтобы потоки приложения свободно работали с его данными и можно было не опасаться взаимоблокировок и отказов в доступе. Методы, используемые для записи и удаления данных, такие как Set, Remove и мутатор set свойства Item, перед выполнением своей задачи сами не- явно блокируют данные, препятствуя их параллельной записи другими потоками. Для явного блокирования данных объекта HttpApplicationState можно использовать метод Lock, который гарантирует, что до тех пор, пока текущий поток не вызовет метод UnLock, только он сможет модифицировать состояние представления. Таким образом, вам не нужно обрамлять отдельные вызовы методов Set, Cleat или Remove операторами блокирования и разблокирования, поскольку эти методы сами делают все необходимое для обеспечения безопасной работы в многопоточной среде. Вызов метода Lock приведет лишь к ненужным издержкам, увеличивая уровень внутренней рекурсии. // Эта операция может выполняться в многопоточной среде: Application("MyValue"] = 1; А когда необходимо обезопасить от параллельной записи группу инструкций, без метода Lock не обойтись: // Эти операции выполняются единым блоком, // без вмешательства со стороны параллельного потока Application.Lock(); int val = (int) Applicationf'MyValue"]; if (val < 10) Applicationf’MyValue"] = val + 1; Application. UnLock(); Методы, предназначенные для чтения данных, такие как Get, аксессор get класса Item и даже Count, имеют внутренние синхронизационные механизмы, которые при использовании совместно с методом Lock надежно защищают от параллельного до- ступа для чтения и записи данных: // Операция чтения защищена от параллельного // выполнения чтения или записи
Управление состоянием Глава 13 505 Application.Lock(); int val = (int) Applicationf’MyValue"]; Application.UnLock(); Методы Lock и UnLock всегда следует использовать вместе. Правда, если опустить вызов метода UnLock, вероятность возникновения взаимоблокировки будет невысокой, поскольку Microsoft .NET Framework автоматически снимает блокировки по завер- шении выполнения запроса, по истечении его таймаута, а также при возникновении ошибки, которая останется необработанной. Поэтому при обработке исключения лучше всего использовать блок finally для снятия блокировки — в противном случае произойдет некоторая задержка из-за того, что ASP.NET сама снимает блокировки, завершая выполнение запроса. Затраты на управление состоянием приложения Вместо того чтобы записывать глобальные данные в объект HttpApplicationState, мож- но использовать для этого глобальные члены объекта приложения, определяемые в файле global.asax. Принципы работы с ними описаны в главе 12. По сравнению с элементами коллекции HttpApplicationState эти глобальные члены имеют несколько преимуществ. Во-первых, они строго типизированы, а во-вторых, для доступа к ним не приходится осуществлять поиск в хэш-таблице. Но вместе с тем, как указывалось в главе 12, для глобальных членов класса HttpApplication не предусмотрен встроенный механизм синхронизации, и защищать их вам придется вручную. Для этого следует использовать конструкции языка программирования, такие как оператор lock из C# или оператор SyncLock из Microsoft Visual Basic .NET. Использование памяти Какой бы способ хранения глобального состояния приложения вы не выбрали, с ним будут связаны определенные издержки. Следует помнить, что такие глобальные дан- ные постоянно находятся в памяти, и если программный код приложения не удаляет их оттуда явно, они удаляются лишь по завершении работы приложения. Конечно, рели поместить несколько мегабайтов данных в память, доступ к ним значительно ускорится, но при этом часть такого ценного ресурса, как память, будет занята до конца работы приложения. По этой причине исключительно важную роль в архитектуре хранения глобаль- ных данных приложения играет объект Cache, который мы подробно рассмотрим в следующей главе. В отличие от объекта приложения и объекта HttpApplicationState объект Cache имеет встроенный механизм удаления данных из памяти в случае, когда объем занятой виртуальной памяти превышает некоторый заданный предел. Кроме того, объект Cache выполняет множество других полезных функций. Соб- ственно говоря, он введен именно для решения проблемы использования памяти, как замена объекта Application. Параллельный доступ к данным Еще одной проблемой, связанной с глобальными данными, является необходимость их блокирования при параллельном доступе, с тем чтобы они не могли быть изменены несколькими одновременно работающими потоками. Однако блокирование состояния приложения отрицательно влияет на его производительность и может привести к тому, что потоки будут использоваться не оптимально. Глобальное состояние прило- жения хранится в оперативной памяти и никогда не выходит за пределы компьютера. В мультикомпьютерной и мультипроцессорной средах глобальное состояние при- ложения поддерживается в рамках одного рабочего процесса, функционирующего
506 Часть III Инфраструктура ASP.NET на отдельном компьютере и на отдельном центральном процессоре. В этой связи его трудно назвать по-настоящему глобальным. Более того, способ его хранения нена- дежен, поскольку в работе процесса возможны сбои, в результате которых процесс останавливается и запускается заново. Если приложение предназначено для работы в Web-саду или Web-ферме, целесообразнее отказаться от использования состояния представления в пользу таблиц базы данных. Или, по крайней мере, можно заключить глобальные данные в оболочки в виде интеллектуальных прокси-объектов, которые будут проверять наличие данных и при их отсутствии, какой бы ни была причина, заполнять их контейнеры заново. Приведем простой пример: // Извлечение данных public object GetGlobalData(string entry) { object о = Application[entry]; if (o == null) { // Здесь нужно выполнить повторную // загрузку данных из их источника return Application[entry]; } return о; } Состояние сеанса Класс HttpSessionState реализует модель сохранения и восстановления данных со- стояния сеанса. В отличие от класса HttpApplicationState этот класс не предоставляет своего содержимого для обработки всех запросов к определенному виртуальному каталогу. Данные состояния сеанса доступны только в рамках последовательности запросов, полученных от одного пользователя с момента его подключения к серверу, в рамках одного сеанса работы пользователя с приложением. Существует множество способов работы с состоянием сеанса, которое может поддерживаться не только для одного компьютера, но и для их группы — Web-фермы или Web-сада. По умолчанию состояние сеанса поддерживается рабочим процессом ASP.NET. Объект класса HttpSessionState из ASP.NET используется практически так же, как внутренний объект ASP Session, но он значительно богаче функционально и имеет иную архитектуру. Кроме того, в нем реализовано несколько исключительно полезных возможностей, таких как работа с браузерами, не поддерживающими cookie, а также работа в Web-ферме или Web-саду, и его хостом может служить внешний процесс, включая Microsoft SQL Server. Таким образом, средства управления состоянием сеанса в ASP.NET отличаются беспрецедентной мощностью и надежностью. В ASP.NET 2.0 разработчик может создать пользовательское хранилище данных состояния сеанса. Например, если требуется надежное решение на основе базы данных, но при этом вы используете Oracle, нет необходимости непременно устанавливать еще и SQL Server. Достаточно написать немного дополнительного кода, и состояние сеанса будет сохраняться в базе данных Oracle, причем с использованием той же семантики и тех же классов. Подсистему поддержки состояния сеанса можно расширять и модифицировать двумя способами: путем модификации отдельных ее элементов (например, создать провайдер состояния сеанса, работающий с базой данных Oracle, или модуль, управ- ляющий генерированием идентификаторов) или замены стандартного модуля HTTP,
Управление состоянием Глава 13 507 реализующего эту подсистему, новым модулем. Первый способ значительно проще, но он ограничивает ваши возможности, тогда как второй, хотя и требует написания сложного кода, дает вам полную свободу действий. Модуль HTTP, отвечающий за управление состоянием сеанса Независимо от своей внутренней реализации API управления состоянием сеанса всегда одинаков — это API вашего старого знакомого объекта Session. В классической ASP объекта Session был экземпляром COM-класса; он создавался ISAPI-расшире- нием asp.dll и помещался в область памяти модуля ActiveX Scripting, вызывавшегося для разбора и выполнения сценария .asp. В ASP.NET этот объект представляет собой коллекцию типа HttpSessionState и доступен через свойство Session класса Page. Класс HttpSessionState, не допускающий наследования, реализует интерфейсы ICollection и lEnumerable. Экземпляр этого класса создается в самом начале выполнения каждого запроса, работающего с состоянием сеанса. Коллекция заполняется парами имя-зна- чение, которые считываются из заданного хранилища и добавляются в контекст за- проса (то есть в объект HttpContext). Свойство Session класса Page является простым отражением свойства Session класса HttpContext. Разработчики могут использовать объект Session, не углубляясь в детали его реа- лизации. Всю необходимую работу по извлечению и сохранению данных состояния сеанса выполняет соответствующий модуль HTTP с некоторой помощью со стороны объектов провайдеров. За подготовку состояния сеанса для каждого подключивше- гося к приложению пользователя отвечает модуль ASP.NET SessionStateModule. Он реализует интерфейс IHttpModule и предоставляет приложениям услуги, связанные с управлением состоянием сеанса. Хотя интерфейс IHttpModule включает всего два метода, Init и Dispose, класс Session- StateModule выполняет множество сложных задач, большая часть которых критична для правильного функционирования Web-приложений. Модуль SessionStateModule вызывается при подготовке к работе объекта HttpApplication, который будет обраба- тывать запрос. Он отвечает за генерирование или получение уникального идентифи- катора сеанса, а также за сохранение и восстановление данных состояния посредством провайдера (например, использующего для хранения данных SQL Server или память Web-сервера). Клиентские менеджеры состояния После своего вызова модуль HTTP, управляющий состоянием сеанса, прежде всего считывает установки, хранящиеся в разделе <sessionState> файла web.config, и опреде- ляет, какой клиентский менеджер состояния должен использоваться в приложении. После этого компонент SessionStateModule просит этот менеджер предоставить набор пар имя-значение, составляющих состояние текущего сеанса. Клиентский менеджер состояния (он же провайдер состояния) — это компонент, который выполняет работу по сохранению и восстановлению данных состояния для всех активных сеансов. В ASP.NET 2.0 предусмотрено четыре режима поддержки состояния сеанса. В част- ности, это состояние может: храниться локально в памяти рабочего процесса ASP.NET; поддерживаться внешним, а иногда даже удаленным, процессом, имеющем имя aspnet_state.exe; храниться в базе данных SQL Server; находиться под управлением пользовательского компонента.
508 Часть III Инфраструктура ASP.NET Последняя возможность была введена в ASP.NET 2.0. Краткое описание каж- дого из четырех перечисленных режимов поддержки состояния сеанса приведено в табл. 13-4. Табл. 13-4. Режимы поддержки состояния сеанса Режим Описание Custom Данные каждого сеанса хранятся в пользовательском контейнере. В ASP.NET 1.x данный режим не поддерживается InProc Данные каждого сеанса хранятся в виде «живого* объекта в памяти рабочего процесса ASP.NET (процесса aspnet_wp.exe или w3wp.exe в Microsoft Win- dows Server 2003). Это установка по умолчанию Off Функция управления состоянием сеанса отключена, и ни один клиентский провайдер состояния не активен SQLServer Данные каждого сеанса сериализуются и сохраняются в таблице базы дан- ных SQL Server. Экземпляр SQL Server может функционировать как локаль- но, так и удаленно StateServer Данные каждого сеанса сериализуются и сохраняются в памяти отдельного системного процесса (aspnet_state.exe). Этот процесс может выполняться на другом компьютере. Данные десериализуются и помещаются в словарь сеанса в начале обработки запроса. Если выполнение запроса завершается успешно, данные состояния сериализуются, помещаются в память указанно- го процесса и становятся доступными другим страницам Указанные здесь идентификаторы режимов являются элементами перечисления SessionStateMode. При выборе опции InProc доступ к данным состояния осуществля- ется очень быстро, однако чем больше данных содержит состояние сеанса, тем больше потребляется памяти и может возникнуть риск снижения производительности. Пла- нируя решение на базе внешнего процесса, следует подумать о возможном влиянии на производительность приложения процесса сериализации и десериализации объекта состояния. Позже мы рассмотрим этот момент подробнее. Модуль, управляющий состоянием сеанса, при выборе провайдера руководству- ется установкой, заданной в разделе <sessionState> файла web.config. Он создает и инициализирует нужный провайдер, после чего тот продолжает процесс своей ини- циализации самостоятельно, выполняя действия, определяемые его типом. Напри- мер, провайдер, использующий SQL Server, открывает подключение к нужной базе данных; провайдер, использующий внешний процесс, проверяет заданный порт TCP; провайдер типа InProc сохраняет ссылку на функцию обратного вызова, которая будет использоваться для генерирования события Session_End (Эту тему мы также рас- смотрим позднее.) Чтобы модуль HTTP, управляющий состоянием сеанса, мог взаимодействовать с провайдером состояния, в состав последнего должен входить стандартный набор методов. Общая схема их взаимодействия приведена на рис. 13-1. * Ё J Примечание В ASP.NET 2.0 классы всех провайдеров состояния являются производными от общего базового класса, SessionStateStoreProviderBase, тогда как в ASP.NET 1.x они реализовывали общий интерфейс, IStateClientManager. Этот интерфейс при переходе к версии 2.0 был удален из ASP.NET, став жертвой рефакторинга, которому подверглась эта система, и введения модели провайдеров (см. главу 1). Заметьте, что переход от ин- терфейсов к базовым классам типичен для всей архитектуры .NET Framework 2.0.
Управление состоянием Глава 13 509 web.config состояния сеанса состояния сеанса состояния сеанса состояния сеанса в памяти рабочего процесса ASP.NET (aspnet_wp.exe или w3wp.exe) в памяти внешнего процесса (aspnet_state.exe) в таблице SQL Server (временной или постоянной) в пользовательском контейнере, например в пользовательской таблице SQL Server или Oracle Рис. 13-1. Модуль SessionStateModule и его клиентские менеджеры состояния Создание объекта HttpSessionState Модуль, управляющий состоянием сеанса, отвечает за извлечение состояния сеанса и присоединение его к контексту каждого запроса, выполняемого в рамках этого сеанса. Состояние сеанса становится доступным приложению только после того, как будет сгенерировано и обработано событие HttpApplicationAcquireRequestState, а после события HttpApplication.ReleaseRequestState оно безвозвратно утрачивается. К моменту, когда генерируется событие Session_End, состояние сеанса уже недо- ступно. При обработке события HttpApplicationAcquireRequestState модуль, управляющий состоянием сеанса, создает для запроса объект HttpSessionState (представляющий собой род коллекции). Этому объекту присваиваются идентификатор сеанса и его словарь. Словарем сеанса называется коллекция значений, составляющих состояние сеанса; через свойство Session объекта страницы он доступен всем страницам, запра- шиваемым в рамках этого сеанса. Когда начинается новый сеанс, словарь данных пуст. Если же модуль, управля- ющий состоянием сеанса, обслуживает запрос сеанса, начавшегося ранее, словарь данных заполняется десериализованным контентом, предоставленным текущим про- вайдером состояния сеанса. После выполнения запроса модифицированное им содер- жимое словаря передается обратно провайдеру, который его сериализует и сохраняет в определенном контейнере. Процесс работы с состоянием сеанса проиллюстрирован на рис. 13-2. ..
510 Часть III Инфраструктура ASP.NET Синхронизация доступа к состоянию сеанса Когда Web-страница вызывает свойство Session, она на самом деле обращается к ло- кальной, расположенной в памяти, копии данных. Но что если другая страница, за- прошенная в рамках того же сеанса, попытается одновременно с ней обратиться к состоянию сеанса? Если не принять необходимых мер, может случиться, что парал- лельно выполняющиеся запросы будут работать с несогласованными или неактуаль- ными данными. Во избежание этого модуль, управляющий состоянием сеанса, реализует механизм блокировки чтения и записи и ставит операции доступа к данным состояния в очередь. Страница, которая обращается к состоянию сеанса для записи, блокирует его, делая недоступным другим запросам до тех пор, пока ее запрос не завершится. Налагаемая в этом случае блокировка называется блокировкой для записи (writer lock). Для того чтобы страница могла записывать данные состояния сеанса, в ее директиву ©Page нужно включить атрибут EnableSessionState со значением true. Если же странице достаточно доступа к состоянию сеанса для чтения, ее атрибуту EnableSessionState присваивают значение Readonly, и тогда на состояние сеанса до завершения запроса накладывается блокировка для чтения (reader lock).
Управление состоянием Глава 13 511 Когда страница накладывает блокировку для чтения, одновременно с ней другие страницы могут читать состояние сеанса, но не могут его обновлять. Когда же страница накладывает блокировку для записи, никакие другие страницы не могут обращаться к состоянию сеанса — ни обновлять его, ни даже считывать. Поэтому если, например, два фрейма одновременно пожелают выполнить запись данных состояния сеанса, второму из них придется дождаться, пока первый завершит свою работу (рис. 13-3). Рабочий процесс ASP.NET Рис. 13-3. Синхронизированный доступ к состоянию сеанса /Нь Совет Параллельный доступ к состоянию сеанса на практике осуществляется редко. Он V возможен, когда на странице выводится несколько фреймов, а также когда пользователь одновременно работает с несколькими копиями одной страницы или несколькими стра- ницами одного приложения. Кроме того, параллельный доступ возможен при применении использующих состояние сеанса обработчиков HTTP, которые предоставляют доступ к встроенным ресурсам, например изображениям или файлам CSS. По умолчанию состояние сеанса защищено от параллельного доступа, однако явное объявление способа работы с ним страницы (чтение-запись, только чтение, доступ не требуется) является хорошей мерой оптимизации. Для этого используется атрибут EnableSessionState директивы @Раде. Свойства класса HttpSessionState Класс HttpSessionState определен в пространстве имен System.Web.SessionState. Это родовой класс-коллекция, который реализует интерфейс ICollection^ Свойства класса HttpSessionState описаны в табл. 13-5.
512 Часть III Инфраструктура ASP.NET Табл. 13-5. Свойства класса HttpSessionState Свойство Описание CodePage Возвращает и позволяет задать идентификатор кодовой станицы текуще- го сеанса Contents Возвращает ссылку на объект this. Поддерживается для совместимости с ASP CookieMode Определяет конфигурацию приложения для сеансов, в которых не ис- пользуются cookie (ниже я расскажу о нем подробнее). Имеет тип HttpCookieMode. В ASP.NET 1х данное свойство не поддерживается Count Возвращает текущее количество элементов, составляющих состояние сеанса IsCookieless Указывает, как передается состояние сеанса — в виде cookie или в составе URL. Данное свойство более специализировано, чем CookieMode IsNewSession IsReadOnly Указывает, было ли состояние сеанса создано для текущего запроса Указывает, является ли состояние сеанса доступным только для чтения. (Это так, если атрибут EnableSessionState директивы ©Page имеет значе- ние Readonly) IsSynchronized Возвращает значение false (это свойство описывается далее в настоящей главе) Item Индексное свойство, предоставляющее доступ к данным состояния се- анса для чтения и записи. Значения можно идентифицировать именами или индексами Keys Возвращает коллекцию ключей всех значений, составляющих состояние сеанса LCID Возвращает и позволяет задать идентификатор локализации (Locale Identifier, LCID) текущего сеанса Mode Возвращает значение, идентифицирующее тип используемого клиент- ского менеджера. Допустимые значения этого свойства перечислены в табл. 13-4 SessionlD StaticObjects Возвращает строковый идентификатор сеанса Возвращает коллекцию объектов, объявленных в файле globaLasax с использованием тэга <object>, атрибут scope которого имеет значение Session. Заметьте, что добавлять в эту коллекцию объекты программным способом нельзя SyncRoot Возвращает ссылку на объект this (это свойство рассматривается далее в настоящей главе) Timeout Возвращает и позволяет задать максимальное время в минутах между двумя последовательными запросами сеанса, спустя которое модуль управления сеансом должен его завершить Класс HttpSessionState — это обычный класс-коллекция, реализующий интерфейс ICollection, но способный синхронизировать доступ к своим данным. Как упоминалось ранее, реализованный в компоненте SessionStateModule механизм синхронизации гарантирует, что в каждый конкретный момент времени только один поток будет иметь доступ к состоянию сеанса. Однако поскольку класс HttpSessionState реали- зует интерфейс ICollection, в нем должны быть реализованы свойства IsSynchronized и SyncRoot. Заметьте, что эти свойства не имеют ничего общего с синхронизацией доступа к состоянию сеанса и являются синхронизационными свойствами коллек- ции — они необходимы для поддержания синхронного режима работы класса-коллек- ции (в данном случае класса HttpSessionState). Строго говоря, класс HttpSessionState не синхронизирован, но доступ к состоянию сеанса синхронизирован.
Управление состоянием Глава 13 513 Методы класса HttpSessionState Методы класса HttpSessionState описаны в табл. 13-6. Большинство из них связано с типичными операциями коллекций, за исключением метода Abandon, предназна- ченного для прекращения сеанса. Табл. 13-6. Методы класса HttpSessionState Метод Описание Abandon Устанавливает внутренний флаг, информирующий модуль управления сеансом о необходимости прекратить текущий сеанс Add Включает в состав состояния сеанса новый элемент в виде объекта типа object Clear Удаляет все данные состояния сеанса СоруТо Копирует коллекцию значений состояния сеанса в одномерный массив, начиная с заданного индекса в массиве GetEnumerator Возвращает перечислитель для перебора значений состояния сеанса Remove Удаляет элемент из коллекции состояния сеанса. Элемент идентифициру- ется ключом RemoveAll Вызывает метод Clear RemoveAt Удаляет элемент из коллекции состояния сеанса. Элемент идентифициру- ется позицией В процессе отмены текущего запроса модуль состояния сеанса проверяет значе- ние флага, чтобы выяснить, дал ли пользователь указание прервать сеанс. Если флаг установлен, это свидетельствует о том, что был вызван метод Abandon. В таком случае удаляются cookie ответа и начинается процедура завершения сеанса. Заметьте, что при этом не обязательно будет сгенерировано событие Session_End. Событие Session_End генерируется только тогда, когда управление состоянием сеанса осуществляется в режиме InProc. Кроме того, оно не генерируется, если сло- варь сеанса пуст. Иными словами, чтобы при естественном завершении сеанса или при вызове метода Abandon было сгенерировано событие Session_End, в рамках этого сеанса уже должен завершиться хотя бы один запрос. Работа с состоянием сеанса Теперь, когда вы ознакомились с основами управления состоянием сеанса, углубим- ся в изучение технических аспектов этого процесса Задачу управления состоянием сеанса можно условно разделить на три этапа: назначение идентификатора сеанса, получение данных состояния сеанса от провайдера и включение их в состав контекста страницы. Как упоминалось, выполнением этой задачи управляет модуль состояния сеанса, который прибегает к помощи пары дополнительных компонентов: генератора идентификаторов сеанса и провайдера состояния сеанса. В ASP.NET 2.0 оба они могут быть заменены пользовательскими компонентами, о чем я расскажу позднее. Пока же рассмотрим несколько практических моментов, с которыми вы будете сталкиваться при работе с состоянием сеанса. Идентификация сеанса Каждый активный сеанс идентифицируется 120-битовой строкой, состоящей только из символов, разрешенных к использованию в URL. Идентификатор сеанса гаран- тировано уникален и генерируется случайным образом во избежание конфликтов по данным и атак злоумышленников. Алгоритмически вычислить идентификатор
514 Часть 111 Инфраструктура ASP.NET следующего сеанса по идентификатору предыдущего сеанса практически невозмож- но. В ASP.NET 1.x генератор идентификаторов сеанса был системным компонентом, интегрированным в исполняющую среду и невидимым извне. В ASP.NET 2.0 этот компонент можно заменить пользовательским. К] Примечание Пословица гласит, что не нужно делать что-то лишь потому, что это воз- можно. Это особенно касается заменяемых компонентов подсистемы управления состо- яния сеансом. Эти компоненты имеет смысл заменять пользовательскими лишь тогда, когда для этого имеются веские основания и уверенность, что замена не повлечет за собой снижение уровня безопасности приложения и оно не станет работать хуже. Я еще вернусь к этой теме немного позже. Генерирование идентификатора состояния сеанса Идентификатор состояния сеанса — это 15-байтовое значение (15 байт 8 = 120 бит), сгенерированное с использованием провайдера шифрования Random Number Genera- tor (RNG). Этот провайдер возвращает последовательность из 15 чисел, сгенериро- ванных случайным образом. Каждому из этих чисел ставится в соответствие символ, использование которого допустимо в URL, и полученная таким образом строка ста- новится идентификатором сеанса. Если состояние сеанса не содержит никаких данных, для каждого запроса генери- руется новый идентификатор сеанса и состояние сеанса не сохраняется с помощью провайдера Однако при использовании обработчика события Session_ Start состояние сеанса сохраняется всегда, даже если оно пустое. Поэтому, и особенно если вы не ис- пользуете провайдер типа InProc, обработчик события Session_Start следует писать особо тщательно и только в случае крайней необходимости. Сеансовые cookie Строка SessionlD передается браузеру и возвращается им серверному приложению одним из двух способов: в виде cookie или в составе URL. По умолчанию модуль со- стояния сеанса создает на клиенте cookie, но вы можете выбрать второй подход, что особенно важно, если браузер не поддерживает cookie. Текущее решение определяется конфигурационными установками, заданными в файле web.config. Как известно, cookie — это маленький текстовый файл, сохраняемый Web-стра- ницей на жестком диске клиентского компьютера В ASP.NET cookie представлен экземпляром класса HttpCookie. Обычно cookie характеризуется именем, набором значений и сроком службы. Кроме того, его можно сконфигурировать для использо- вания определенного виртуального пути и передачи через защищенное соединение (например, HTTPS). ©Внимание! В ASP.NET 2.0 для работы с сеансовыми cookie используется функция HTTP- only при условии, что она поддерживается браузером. К числу таких браузеров относится Microsoft Internet Explorer 6.0 с установленным пакетом расширений — SP1 или Windows ХР SP2. Функция HTTP-only предотвращает использование cookie клиентскими сценариями, создавая тем самым барьер для потенциальных межсайтовых сценарных атак с целью похищения идентификаторов сеансов. Когда поддержка cookie включена, модуль управления состоянием создает cookie с заданным именем и сохраняет в нем идентификатор сеанса. Следующий псевдокод демонстрирует процесс создания cookie: HttpCookie sessionCookie; sessionCookie = new HttpCookie("ASP.NET_Se,ssionId ", sessionlD) sessionCookie.Path =
Управление состоянием Глава 13 515 Здесь ASP.NET Sessionld — имя cookie, а в переменной sessionlD содержится его зна- чение. В этом примере cookie связывается с корневым разделом текущего домена. (Свойство Path определяет относительный URL, с которым будет связан cookie.) Сеансовым cookie назначается очень короткий срок службы, но в конце каждого успешно выполненного запроса он устанавливается заново. Свойство Expires объекта HttpCookie определяет время дня на клиенте, с наступлением которого cookie станет недействительным. Если это время не задано явно, как в случае с сеансовыми cookie, свойство Expires получает значение DateTime.MinValue — минимальное значение вре- мени, используемое в .NET Framework. П-fl Примечание Серверный модуль, которому требуется создать на клиенте cookie-файл, добавляет объект HttpCookie в коллекцию Response.Cookies. Все найденные на клиенте cookie, ассоциированные с доменом запроса, передаются на сервер вместе с запросом, и их можно прочитать из коллекции Request.Cookies. Для того чтобы состояние сеанса сохранялось между запросами, клиент должен иметь возможность передать его идентификатор серверному приложению. Как это делается, зависит от конфигурации приложения. Установки, связанные с состоянием сеанса, содержатся в разделе <sessionState> конфигурационного файла. Для того чтобы указать, как должен передаваться между клиентом и сервером идентификатор сеанса, необходимо присвоить атрибуту cookieless одно из описанных в табл. 13-7 значений, которые относятся к перечислимому типу HttpCookieMode. Табл. 13-7. Элементы перечисления HttpCookieMode Значение Описание AutoDetect Использовать cookie только при условии, что браузер их поддерживает UseCookies Использовать cookie для сохранения идентификатора сеанса независимо от того, поддерживает ли их браузер UseDeviceProfile Решение об использовании cookie зависит от возможностей браузера, ко- торые перечислены в разделе профилей устройств в конфигурационном файле UseUri Передавать в URL идентификатор сеанса независимо от того, поддержи- вает ли браузер cookie. Используйте эту опцию, если вы не хотите ис- пользовать cookie в принципе Когда задана установка AutoDetect, ASP.NET запрашивает у браузера информа- цию о том, поддерживает ли он cookie. Если поддерживает, идентификатор сеанса сохраняется в виде cookie, в противном случае он передается в составе URL. При установке UseDeviceProfile реальные возможности браузера не проверяются, а решение принимается на основании серверной информации о возможностях браузера, для чего модуль состояния сеанса обращается к свойству SupportsRedirectWithCookie объекта HttpBrowserCapabilities. Заметьте, что даже если браузер сам по себе поддерживает cookie, пользователь может отключить эту функцию, и в таком случае работа с со- стоянием сеанса не будет выполняться правильно. F. J Примечание В ASP.NET 1 .х выбор установок был не столь богат: там использовался атрибут cookieless раздела <sessionState>, принимавший значения булева типа. Для от- ключения поддержки cookie этот атрибут нужно было установить в true. Предположим, что поддержка cookie отключена, и вы запрашиваете страницу по- средством такого URL: http://www.contoso.com/test/sessions.aspx
516 Часть III Инфраструктура ASP NET После отображения страницы содержимое адресной строки браузера изменяется: http://www.contoso.com/test/(S(5ylg0455mrvws1uz5mmaaii45))/sessions.aspx В данном случае модуль состояния сеанса проверяет значение атрибута cookieless, и обнаружив, что оно равно true, перенаправляет запрос (код состояния HTTP 302) по измененному виртуальному URL, включающему (перед именем страницы) идентифи катор сеанса. Специальный фильтр IS API aspnet_filter.exe выполняет предобработку запроса, удаляет из его URL идентификатор сеанса и сохраняет этот идентификатор в дополнительном HTTP-заголовке AspFilterSessionld для последующей обработки Проблемы, связанные с сеансами, в которых не используются cookie Разработка приложений, сохраняющих состояние, возможна и для браузеров, ко- торые не поддерживают cookie (вообще либо потому, что их поддержку отключил пользователь). Однако в таком случае с сохранением и восстановлением состояния сеанса сопряжены некоторые проблемы. Прежде всего, в начале сеанса и при любом переходе пользователя по абсолютным URL со страниц приложения происходит переадресация запросов. Когда используются cookie, пользователь может очистить адресную строку, перейти к другому приложению и вернуться к исходному, но несмотря на это состояние сеанса будет успешно восстановлено. Если же cookie отключены, при таких действиях пользователя состояние сеанса будет утрачено. Для возврата формы это не проблема, поскольку данная операция реализуется с использованием относительных URL, но переход по любой ссылке на абсолютный URL ведет к утрате состояния текущего сеанса и началу нового. Примером может служить следующий код: <а runat="server" href="/test/sessions.aspx">Click</a> Существует ли способ указания абсолютных URL в ссылках и гиперссылках, при котором в запрос включалась бы информация сеанса? Можно прибегнуть к такому трюку, воспользовавшись методом ApplyAppPathModifier класса Response: <а href='<% =Response.ApplyAppPathModifier("test/page.aspx")%>' >Click</a> Метод ApplyAppPathModifier принимает строку, содержащую относительный URL, и возвращает абсолютный с включенной в него информацией сеанса. Этот трюк осо- бенно полезен, когда нужно перенаправить запрос странице HTTPS, поскольку в таком случае необходимо задавать полный абсолютный адрес. Заметьте, что метод ApplyAppPathModifier возвращает исходный URL, если использование cookie для сохранения состояния сеанса разрешено, а путь является абсолютным. Внимание! В серверных тэгах (помеченных атрибутом runat=server) нельзя использовать блоки кода <%...%>. В предыдущем примере это было возможно, поскольку тэг <а> не имел указанного атрибута. Сеансы, в которых не используются cookie, и безопасность Еще одна проблема, с которой сопряжено сохранение состояния сеанса без использо- вания cookie, связана с безопасностью. Похищение идентификаторов сеанса — один из наиболее распространенных типов атак, открывающих доступ к системе. Вы мо- жете попробовать сами: настройте свое приложение для работы без использования cookie и откройте его страницу, а потом скопируйте URL с идентификатором сеанса из адресной строки браузера и отправьте его другу по электронной почте. Пусть он поместит этот URL в адресную строку браузера и щелкнет кнопку перехода. Этот человек получит доступ к вашему сеансу на все время, пока он будет длиться! Раз- умеется, идентификатор сеанса и без того не очень хорошо защищен (и, вероятно,
Управление состоянием Глава 13 517 иначе просто не может быть). Но для защиты этой информации используется не- предсказуемый генератор идентификаторов сеанса, и когда такой идентификатор сохраняется в cookie, он, по крайней мере, не выводится на экран. Таким образом, если вы сохраняете в состоянии сеанса критичные данные, рекомендуется исполь- зовать протокол Secure Sockets Layer (SSL) или Transport Layer Security (TLS) для шифрования взаимодействия клиента и сервера. Кроме того, следует всегда предоставлять пользователю возможность идентифи- цировать себя при подключении к приложению и явно завершать сеанс с помощью команды, при выполнении которой будет вызываться метод Abandon. Тем самым сократится время, в течение которого злоумышленник сможет использовать иденти- фикатор сеанса пользователя для доступа к данным, хранящимся в состоянии сеанса. Также очень важно так сконфигурировать систему, чтобы недействительные иден- тификаторы сеанса не использовались в ней повторно. Соответствующую установку в ASP.NET 2.0 можно задать в разделе <sessionState>. Конфигурирование управления состоянием сеанса При переходе от ASP.NET 1.x к ASP.NET 2.0 содержимое раздела <sessionState> было значительно расширено. Теперь он выглядит так: <sessionState mode-"Off | InP гос | Stat eSe rver I SQLSe rve r | Custom" tirreout="number of minutes" cookieName="session cookie name" cookieless="http cookie mode" regenerateExpiredSessionId="true|false" sqlConnectionString="sql connection string" sqlCommandTimeout-"number of seconds" allowCustomSqlDatabase="t rue|false" useHostingidentity="true|false" pa rt it ionflesolverType="" sessionIDManagerType="custom session ID generator" stateConnectionString="tcpip=server:port” stateNetworkTimeout="number of seconds customProvider="custom provider name"> <providers> </providers> </sessionState> Атрибуты этого раздела описаны в табл. 13-8. Заметьте, что от ASP.NET 1.x в неиз- менном виде унаследовано только четыре атрибута: mode, timeout, stateConnectionString и sqlConnectionString. Атрибут cookieless также существовал в ASP.NET 1.x, но там он имел булев тип. Все остальные атрибуты введены в ASP.NET 2.0. Табл. 13-8. Атрибуты раздела <sessionState> Атрибут Описание allowCustomSqlDatabase Если этот атрибут равен true, данные состояния сеанса могут храниться в заданной вами таблице базы данных, а не в стан- дартной таблице ASPState Cookieless Определяет, как идентификатор сеанса должен передаваться клиенту cooMeName Имя cookie, если для хранения идентификаторов сеанса используются cookie
518 Часть III Инфраструктура ASP.NET Табл. 13-8. (окончание) Атрибут Описание customProvider Имя пользовательского провайдера состояния сеанса, который должен использоваться для сохранения и восстановления дан- ных состояния mode partitionResolverType Определяет, где должны храниться данные состояния Определяет тип и сборку компонента PartitionResolver, кото- рый будет предоставлять информацию о подключении в режи- ме SQLServer или StateServer. Если этот компонент может быть успешно загружен, значения атрибутов sqlConnectionString и stateConnectionString игнорируются regenerateExpiredSessionld Если этот атрибут равен true, при получении запроса с просро- ченным идентификатором сеанса генерируется новый иденти- фикатор, в противном случае прежний идентификатор снова становится действительным. По умолчанию данный атрибут имеет значение false sessionlDManagerlype Идентифицирует компонент, который будет использоваться в качестве генератора идентификаторов сеанса. По умолчанию данный атрибут имеет значение null sqlCommandTimeout Определяет, как долго SQL-команда может не возвращать результат прежде чем она будет отменена. По умолчанию дан- ный атрибут имеет значение 30 секунд sqlConnectionString state ConnectionString Строка подключения к SQL Server Имя сервера, на котором будет удаленно храниться состояние сеанса, или его адрес и порт stateNetworkTimeout Определяет, как долго сетевое соединение TCP/IP между Web- сервером и сервером состояния может отсутствовать, прежде чем запрос будет отменен. По умолчанию данный атрибут име- ет значение 10 секунд timeout Определяет, как долго сеанс может быть бездействующим, прежде чем будет прекращен По умолчанию данный атрибут имеет значение 20 минут useHostingldentity Указывает, какая учетная запись должна использоваться при доступе к пользовательскому провайдеру состояния или про- вайдеру SQLServer в случае применения метода интегрирован- ной защиты. Значение true (значение по умолчанию) соответ ствует использованию учетной записи хоста, то есть учетной записи ASP.NET в системе Windows. Если же задано значение false, используется учетная запись клиента (то есть процесс ASP.NET имперсонализирует учетную запись клиента) Кроме того, в дочернем разделе <providers> перечисляются пользовательские провайдеры состояния сеанса. ASP.NET позволяет хранить данные состояния сеанса в разных местах, например в памяти Web-сервера или в базе данных SQL Server. За сохранение и восстановление данных отвечает соответствующий провайдер. Мы еще вернемся к этой теме в настоящей главе. Время жизни сеанса Жизненный цикл состояния сеанса начинается, когда в находящийся в памяти словарь добавляется первый элемент. Следующий пример показывает, как можно модифици- ровать элемент в этом словаре. Здесь "МуData " — это ключ, уникально идентифици-
Управление состоянием Глава 13 519 рующий значение. Если такой ключ в словаре уже имеется, существующее значение переопределяется: Session["MyOata"] = "I love ASP.NET"; Словарь Session содержит значения типа object, поэтому для прочтения из него данных нужно приводить их к более конкретному типу: string tmp = (string) Sessionf’MyData"]; Когда страница сохраняет данные в объекте Session, они загружаются в словарь, на- ходящийся в памяти — экземпляр внутреннего класса SessionDictionary (см. рис. 13-2). Другие страницы, выполняющиеся параллельно с ней, не имеют доступа к этому словарю до тех пор, пока ее выполнение не завершится. Событие Sesslon_Start Событие Session Start не связано с конкретным запросом. Оно генерируется, когда модуль управления состоянием обслуживает первый запрос определенного пользова- теля и для этого запроса требуется новый идентификатор сеанса. Исполняющая среда ASPNET может обслуживать серию запросов в контексте одного сеанса, но только для первого из них генерируется событие Session Start. Создание нового идентификатора сеанса и генерирование события SessionJStart происходят, когда при обработке запроса обнаруживается, что словарь сеанса пуст. Архитектура подсистемы управления состоянием сеанса довольно сложна, поскольку должна поддерживать разные провайдеры состояния. Общая схема предполагает, что по завершении выполнения запроса словарь сериализуется и передается про- вайдеру для сохранения. Однако на практике, когда словарь пуст, эта процедура не выполняется, что позволяет немного ускорить процесс. Но, как упоминалось, если в приложении определен обработчик события Session Start, сериализация выполняется и в этом случае. Событие SesslonJEnd Событие SessionEnd сигнализирует о завершении сеанса и используется для выпол- нения соответствующего «уборочного» кода. Заметьте, что это событие генерируется только в режиме InProc, то есть когда данные сеанса хранятся в памяти рабочего процесса ASP.NET. Кроме того, условием генерирования события Session_End является наличие состояния сеанса. Иными словами, необходимо, чтобы завершился хотя бы один за- прос, записавший в него данные. Когда в словарь сеанса добавляется первый элемент, этот элемент помещается в кэш ASP.NET — уже упоминавшийся объект Cache, о ко- тором будет подробно рассказано в следующей главе. Такое поведение характерно для провайдера InProc, другие провайдеры с объектом Cache не работают. Но гораздо интереснее то, что к помещенному в кэш элементу (только одному за сеанс) применяется определенная политика устаревания. Вам предстоит еще многое узнать о кэше и политиках устаревания, а пока достаточно сказать, что срок службы добавленного в кэш элемента является скользящим и равняется таймауту сеанса. Пока запросы обрабатываются в рамках текущего сеанса, скользящий срок службы элемента автоматически возобновляется. Модуль состояния сеанса делает это во время обработки события EndRequest, для чего он просто выполняет чтение из кэша. Когда срок хранения элемента в кэше заканчивается, сеанс завершается по таймауту.
520 Часть III Инфраструктура ASP NET Устаревшие элементы автоматически удаляются из кэша. Для этого модуль со- стояния сеанса указывает функцию обратного вызова, которая активизируется авто- матически и в свою очередь генерирует событие Session_End. । J»' Примечание Элементы в кэше, представляющие состояние сеанса, недоступны извне сборки system.web и невозможен даже их перебор, поскольку они хранятся в системной области кэша. Иными словами, программный доступ к данным состояния другого сеанса исключен, как исключено и их удаление. Почему состояние сеанса иногда утрачивается Значения, помещенные в объект Session, удаляются из памяти либо приложением (программным способом), либо системой, когда сеанс завершается явно или по тай- мауту. Однако иногда случается и так, что состояние сеанса просто утрачивается. Когда это происходит? При работе в режиме InProc состояние сеанса отображается в память домена при- ложения, в котором обрабатывается запрос страницы. Поэтому на состоянии сеанса отражается перезапуск процесса приложения. Рабочий процесс ASP.NET периоди- чески перезапускается, что необходимо для того, чтобы его производительность не снижалась. Когда это происходит, состояние сеанса утрачивается. Частота перезапуска процесса зависит от объема занятой памяти и количества обслуживаемых запросов. Однако оценить ее довольно сложно. Поэтому при разработке приложений следует помнить, что в любой момент состояние сеанса может быть утрачено, и производить необходимую обработку исключений. Не следует рассчитывать и на определенную периодичность операций перезапуска рабочего процесса, поскольку они выполняются нерегулярно. Например, в какой-то момент антивирусное программное обеспечение может пометить файл web.config или global.asax как измененный, что вызовет перезапуск приложения и потерю со- стояния сеанса. То же произойдет, если ваш код изменит штамп времени одного из этих файлов или внесет изменения в одну из специализированных папок, таких как Bin или App_Code. pr,i Примечание Что происходит с состоянием сеанса, когда при выполнении страницы генерируется исключение? Словарь будет сохранен или утрачен? Состояние сеанса не сохраняется, если результатом выполнения страницы оказывается ошибка, то есть метод GetLastError объекта Server возвращает исключение. Однако в обработчике исключения можно восстановить статус-кво, вызвав метод Server.ClearError, и тогда состояние сеанса будет сохранено как обычно. Сохранение состояния сеанса на удаленном сервере Упоминавшаяся ранее проблема потери состояния сеанса в режиме InProc может быть решена путем использования одного из двух предопределенных провайдеров, сохраняющих состояние сеанса вне процесса, — StateServer или SQLServer. В этом случае потребуется дополнительный слой кода, выполняющий сериализацию и де- сериализацию данных состояния, а также их чтение и запись. Необходимость копировать данные состояния из внешнего репозитария в локаль- ный словарь сеанса приводит к снижению производительности процесса управления состоянием на 15-25 %. Заметьте, что это лишь грубая оценка, и на практике влияние на производительность чаще оказывается минимальным, нежели максимальным. Кроме того, во время ее расчета не учитывалась сложность типов сохраняемых данных.
Управление состоянием Глава 13 521 • Внимание! Выбирая провайдер, сохраняющий состояние сеанса вне рабочего процесса । (например, провайдер StateServer или SQLServer), имейте в виду, что перед запуском приложения в эксплуатацию необходимо настроить исполняющую среду. Для этого нужно либо запустить соответствующий сервис Windows для провайдера StateServer, либо скон- фигурировать базу данных для провайдера SQLServer. Когда же сохранение состояния осуществляется в используемом по умолчанию режиме InProc, подготовительные работы не требуются. Сериализация и десериализация состояния В режиме InProc объекты хранятся в состоянии сеанса как «живые» экземпляры класса. Их сериализация и десериализация не требуется, а это означает, что в объекте Session можно хранить любые объекты (включая объекты СОМ) и обращаться к ним без лишних затрат. В случае с провайдерами состояния, хранящими его вне процесса, ситуация не столь радужна. В подобных случаях данные состояния сеанса копируются из их основного места хранения в память домена приложения, который обрабатывает запрос. Для выпол- нения этой задачи необходим слой сериализации-десериализации, работа которого является основным источником издержек, связанных с использованием провайдеров состояния, хранящих его вне процесса. А как это отражается на вашем коде? Прежде всего, вы должны убедиться, что помещаете в словарь сеанса только сериализуемые объекты — в противном случае состояние нельзя будет сохранить. Для сериализации и десериализации различных типов данных ASP.NET использует два метода, характеризующихся разной производительностью. Для базовых типов данных используется оптимизированный внутренний сериализатор, а для остальных типов, включая встроенные и пользовательские классы, ASP.NET использует двоич- ный сериализатор .NET (класс Binary Formatter), который работает довольно медленно. К базовым типам относятся string, DateTime, Guid, IntPtr, TimeSpan, Boolean, byte, char и все числовые типы. Оптимизированный сериализатор — внутренний класс AltSerialization — с помощью объекта Binary Writer записывает один байт, обозначающий тип данных, и затем значе- ние. При чтении класс AltSerialization извлекает вначале этбт первый байт, определяет йо нему тип считываемых данных, а затем вызывает один из специализированных методов класса BinaryReader для чтения данных этого типа. Тип идентифицируется индексом во внутренней таблице (рис. 13-4). Рис. 13-4. Схема сериализации базовых типов, используемая классом AltSerialization
522 Часть III Инфраструктура ASP.NET Примечание Если значения булева и числовых типов данных имеют фиксированный раз- мер, то длина строк может быть разной Как объект чтения определяет размер строки? Метод BinaryReader.ReadString использует тот факт, что в потоке строка всегда предва- ряется значением ее длины в виде семибитового целого числа. Значения типа DateTime сохраняются в виде общего количества тактов и считываются как значения типа Int64. Как я уже сказал, более сложные объекты сериализуются с использованием отно- сительно медленно работающего класса Binary Formatter при условии, что эти объекты допускают сериализацию. И простые, и сложные типы записываются в один поток, но все типы, не являющиеся базовыми, идентифицируются одним идентификатором типа. Приведенный выше оценочный показатель снижения производительности на 15-25 % был получен для базовых типов данных. Чем сложнее тип, тем больше из- держки, и реальную их оценку можно получить только тестированием. С учетом сказанного особое внимание следует уделять эффективному хранению данных. Например, если вам нужно сохранять экземпляр класса с тремя с гроковыми свойствами, возможно, целесообразнее заполнить три разных слота состояния зна- чениями базовых типов, что позволит избежать обращения к классу BinaryFormatter. А еще лучше воспользоваться классом-конвертером, который будет преобразовывать ваш объект в строковый формат и обратно в объектный. Однако все это лишь реко- мендации, и вы сами должны будете подобрать оптимальную стратегию. Внимание! Из-за многопоточного выполнения в классической ASP сохранение объекта ADO Recordset было потенциально опасной операцией. В ASP.NET данная проблема снята, однако это не означает что можно, ни о чем не беспокоясь, сохранять в объекте Session любые объекты. Когда состояние сеанса хранится вне процесса, сохранению объектов Data- Set нужно уделять самое пристальное внимание, учитывая особенности сериализации класса DataSet. Ведь это сложный тип, генерирующий большой объем XML-данных, и в большом приложении, сохраняющем много данных, процесс сериализации может очень отрицательно сказываться на производительности. Легко может оказаться, что для каждого запроса перемещаются мегабайты данных. В ASP NET 1 .х в этой связи можно порекомендовать избегать использования объектов DataSet при сохранении состояния сеанса вне процесса и ограничиться обычными массивами данных, а в ASP.NET 2.0 хорошим решением будет установка нового свойства RemotingFormat объекта DataSet. Сохранение данных сеанса При работе в режиме StateServer все содержимое объекта StateServer сериализуется и сохраняется во внешнем приложении, которым является сервис Microsoft Win- dows NT aspnet_state.exe. Этот сервис вызывается для сериализации и сохранения состояния сеанса по завершении выполнения запроса. Состояние каждого сеанса он хранит в виде байтового массива. Когда начинается обработка очередного запроса, массив, соответствующий его идентификатору сеанса, копируется в поток в памяти и затем десериализуется во внутренний объект состояния сеанса, прикладным про- граммным интерфейсом которого является объект HttpSessionState. Как упоминалось, сложные типы сериализуются и десериализуются посредством системного класса BinaryFormatter, который может работать только с теми классами, которые явно помечены как сериализуемые. Это означает, что COM-объекты, создан- ные программно или объявленные как статические объекты в файле global.asax, не могут использоваться с провайдером, сохраняющим состояние сеанса вне процесса. То же касается несериализуемых объектов. Конфигурирование провайдера StateServer Сохраняя состояние сеанса вне процесса, вы обеспечиваете большую надежность его хранения, а значит, устойчивость работы всего приложения, защищая его от сбоев IIS
Управление состоянием Глава 13 523 и процесса ASP.NET и периодической потери данных в связи с перезапусками этого процесса. Кроме того, отделяя состояние сеанса от страницы, вы облегчаете масшта- бирование приложения и перенесение его на платформу Web-сада или Web-фермы. Как вы уже, наверное, знаете, провайдером состояния сеанса ASP.NET является сервис Windows NT aspnet_state.exe. Обычно его файл находится в инсталляционной папке ASP.NET: %WINDOWS%\Mic rosoft.NET\F ramewo rk\[версия} Прежде чем использовать сервер состояния, нужно убедиться, что он запущен и правильно функционирует (на локальном или удаленном компьютере). Этот сервис является компонентом ASP.NET и устанавливается вместе с ней. Однако по умолча- нию он остановлен, и запускать его следует вручную. Конфигурацию этого сервиса можно изменить в диалоговом окне его свойств, показанном на рис. 13-5. Рис. 13-5. Конфигурирование сервера состояния ASP.NET Необходимо также задать для приложения ASP.NET TCP/IP-адрес компьютера, на котором функционирует сервис состояния сеанса. Ниже показано, какие изменения нужно внести в файл web.config, чтобы включить поддержку удаленного хранения состояния сеанса: <configuration> <system.web> <sessionState mode=”StateServer" stateConnectionString=”tcpip=MyMacbine:42424” /> </system.web> </configuration>
524 Часть III Инфраструктура ASP.NET Заметьте, что атрибут mode чувствителен к регистру. Формат атрибута stateCon- nectionString таков (по умолчанию адресом компьютера является 127.0.0.1, номером порта — 42424): • • Я’ stateConnectionString="tcpip=cepsep порт" Для идентификации сервера можно задать либо его IP-адрес, либо имя компью- тера. Учтите, что в последнем случае поддерживаются не все символы ASCII. Номер порта не является обязательным, и его можно опустить. О Внимание! Сервер состояния не ставит аутентификационного барьера, и поэтому каж- дому, кто имеет доступ к сети, доступны и данные состояния. Для их защиты можно использовать брандмауэр или политики IPSec. Еще одной мерой защиты является из- менение номера порта, для чего нужно отредактировать в реестре элемент Port с ключом HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\aspnet_state\Parameters. Учтите, что одной только записи номера порта в файл web.config недостаточно. Приложение ASP.NET пытается подключиться к серверу состояния сразу после загрузки. Сервис aspnet_state к этому моменту должен быть уже запущен, иначе будет выброшено исключение HTTP (по умолчанию сервис автоматически не запускается). Передача данных между сервисом и приложением осуществляется по технологии .NET Remoting. Примечание Провайдер состояния ASP.NET выполняется от имени ее учетной записи, которую можно изменить и сконфигурировать с помощью утилиты Service Control Manager. Это легковесный и простой сервис, не имеющий никаких дополнительных функций, — он просто хранит данные и прослушивает заданный порт на предмет входящих запросов на обслуживание. Сервис не является кластерным (то есть не предоставляет монитор отказов, необходимый для обеспечения его отказоустойчивости) и не может использо- ваться в кластерном мире, где один сервер принимает эстафету от другого, когда тот выходит из строя. Наконец, имейте в виду, что по умолчанию сервер состояния принимает только локальные запросы, и если он и Web-сервер находятся на разных компьютерах, необходимо активизировать функцию приема удаленных запросов. Для этого указанном ранее разделе реестра нужно модифицировать еще один элемент, AllowRemoteConnection, которому следует присвоить ненулевое значение. Сохранение данных состояния в таблице SQL Server Мы уже говорили, что при сохранении состояния сеанса вне процесса ASP.NET при- ложение более стабильно. Что бы ни случилось с его рабочим процессом, состояние сеанса не утрачивается, а остается доступным для дальнейшего использования. Если сервис приостанавливается, данные сохраняются и автоматически восстанавливаются при возобновлении его функционирования. К сожалению, если провайдер состояния будет остановлен или в его работе произойдет сбой, данные все же окажутся утра- ченными. Поэтому, когда надежность является ключевым требованием, предъявля- емым к приложению, следует отказаться от установки StateServer в пользу режима SQLServer. Производительность и надежность Когда ASP.NET работает в режиме SQLServer, данные сеанса сохраняются в специ- альной таблице базы данных. Поэтому они выживают даже при отказе SQL Server, но ценой дополнительных издержек. Режим SQLServer обеспечивает возможность хранения данных на любом подключенном к сети компьютере, на котором установлен SQL Server версии 7.0 или выше. Механизм сохранения и восстановления данных практически идентичен описанному ранее для режима StateServer. В частности, при- меняется тот же алгоритм сериализации и десериализации данных, но эти операции
Управление состоянием Глава 13 525 выполняются, естественно, еще медленнее. При сохранении данных базовых типов время подготовки объекта Session к работе для очередного запроса обычно на 20 % превышает длительность подготовки этого объекта в режиме InProc, и чем сложнее типы данных, составляющих состояние сеанса, тем это время больше. га Примечание Делая выбор между режимами StateServer и SQLServer, нужно учитывать, что SQL Server поддерживает кластеризацию, что делает основанное на его использовании решение более надежным. Конфигурирование провайдера SQLServer Если вы хотите, чтобы данные состояния сеанса хранились в базе данных SQL Server, внесите в раздел <sessionState> файла web.config следующие изменения: <configuration> <system.web> <sessionState mode="SQLServer" sqlConnectionString="server=127.0.0.1; integrated security=SSPI;" /> </system.web> ^/configuration? В частности, необходимо присвоить атрибуту mode (напомню, что он чувстви- телен к регистру) значение SQLServer и задать строку подключения в атрибуте sqlConnectionString. Заметьте, что этот атрибут должен содержать идентификатор пользователя, пароль имя сервера, однако в него нельзя включать такие токены, как Database и Initial Catalog, если только вы не активизировали опцию работы с пользовательской базой данных (см. табл. 13-8) — alloxoCustomSqlDatabase. В таком случае вы можете указать имя базы данных в атрибуте Initial Catalog или исполь- зовать атрибут attachDBFileName строки подключения к SQL Server Express, задав в нем MDB-файл. га Примечание В ASP.NET 2.0 строка подключения для режимов SQLServer и StateServer может задаваться косвенно путем указания имени элемента раздела <connectionStrings>. Модуль состояния сеанса сначала ищет заданный вами текст в этом разделе (как имя строки подключения) и лишь затем, если соответствие не найдено, пытается использовать этот текст как непосредственно заданную строку подключения. В ASP.NET 2.0 можно установить для режима SQLServer пользовательское значение таймаута в секундах, что полезно, если ваш сервер базы данных отвечает с задержкой. Это значение задается в атрибуте sqlCommandTimeout (См. табл. 13-8). Создание хранилища данных SQL Server В состав ASP.NET входят две пары сценариев, предназначенных для конфигуриро- вания базы данных — создания необходимых таблиц, хранимых процедур, триггеров и заданий. Первые два сценария содержатся в файлах InstallSqlState.sql и Uninstall- SqlState.sql. Они создают базу данных с именем ASPState и несколько хранимых процедур Данные хранятся в двух таблицах базы данных TempDB. В SQL Server эта база данных используется для временных таблиц, хранимых процедур и других временных объектов. Это означает, что при перезапуске компьютера, на котором функционирует SQL Server, данные состояния сеанса утрачиваются. Другие два, InstallPersistSqlState.sql и UninstallPersistSqlState.sql, также создают базу данных ASPState, но таблицы для хранения состояния сеанса помещают в нее же. Все сце- нарии находятся в папке %SYSTEMROOT%\Microsoft NЕТ\Fгamewoгk\[версия]
526 Часть III Инфраструктура ASP NET ©Внимание! В ASP.NET 2.0 описанные файлы сценариев включены лишь ради обратной совместимости, поскольку для создания и удаления объектов SQL Server, необходимых для хранения состояния сеанса, теперь можно пользоваться утилитой aspnet_regsql.exe. Помимо прочего эта утилита поддерживает больше опций, в частности, позволяет создать пользовательскую таблицу базы данных. В ASP.NET 1.x такая утилита тоже была, но имела очень ограниченные возможности — не поддерживала ни постоянных, ни пользо- вательских* таблиц. Создаваемые таблицы называются ASPStateTempApplications и ASPStateTempSessions. На рис. 13-6 показана база данных ASPState в окне утилиты SQL Server Enterprise Manager. Рис. 13-6. База данных ASPState в окне утилиты SQL Server Enterprise Manager Таблица ASPStateTempApplications содержит по одной записи на каждое выполня- ющееся приложение ASP.NET. Ее структура описана в табл. 13-9. Табл. 13-9. Структура таблицы ASPStateTempApplications Столбец Тип Описание Appld int Ключевой столбец, содержащий автоматически сгенериро- ванные идентификаторы приложений, работающих в режиме SQLServer AppName char(280) Для каждого приложения содержит значение свойства Арр- DomainAppId объекта HttpRuntime В таблице ASPStateTempSessions хранятся данные состояния сеанса — по одной строке на каждый активный сеанс. Ее структура описана в табл. 13-10.
Управление состоянием Глава 13 527 Табл. 13-10. Структура таблицы ASPStateTempSessions Столбец Тип Описание LockDate datetime Содержит время, когда сеанс был заблокирован для добавления последнего элемента. Это значение пред- ставлено в формате Universal Time Coordinate (UTC) LockDateLocal datetime Содержит то же значение, что и столбец LockDate, но в виде локального времени системы. В ASP.NET 1jc данное свойство не поддерживается LockCookie int Содержит число, определяющее, сколько раз сеанс был заблокирован (то есть количество обращений к нему) Timeout int Содержит таймаут сеанса в минутах Locked bit Содержит число, определяющее, заблокирован ли сеанс в настоящий момент SessionltemShort varbinary (7000) Содержит данные состояния сеанса, сериализованные провайдером (см. описание провайдера StateServer). Если для хранения данных необходимо более 7000 байт, они записываются в другой столбец — SessionltemLong. Этот столбец может содержать значение null SessionltemLong image Содержит сериализованные данные состояния сеанса, объем которых превышает 7000 байт. Данный столбец может содержать значение null Flags int Содержит флаги операции из перечисления Session- StateActions. В ASP.NET 1.x этот столбец не поддер- живается В столбце SessionltemLong содержится большой блок двоичных данных. Данные хранятся в виде коллекции страниц размером 8 Кбайт, не обязательно расположенных последовательно, хотя для пользователя значение такого столбца представляется единым блоком байтов. При создании объектов SQL Server, необходимых для хранения состояния сеанса, наряду с таблицами создается задание, предназначенное для удаления из базы данных завершившихся сеансов. Это задание (рис. 13-7) называется ASPState^JobDelete- ExpiredSessions, а выполняется по умолчанию каждую минуту. Разумеется, для этого должен быть запущен сервис SQLServerAgent. Рис. 13-7. Задание на удаление завершившихся сеансов
528 Часть III Инфраструктура ASP.NET Возврат к учетной записи хоста В приложении ASP.NET 1.x учетные данные пользователя, используемые для до- ступа к состоянию сеанса, могут быть различными. Если имя и пароль пользователя явно заданы в строке подключения, то аутентификация пользователя производит- ся по ним. В противном случае подключение осуществляется от имени текущего пользователя Windows (используется интегрированная защита) — того, от имени которого выполняется запрос. Однако такого рода решение конфликтует с про- вайдером StateServer, который использует для выполнения своей задачи учетную запись ASP.NET. Более того, возникают проблемы административного характера с интранет-сайтами, в которых применяется метод имперсонализации клиента (то есть ASP.NET функционирует от имени пользователя). В таких случаях приходится предоставлять доступ к базе данных каждой учетной записи, от которой могут по- ступать запросы. Для решения этой проблемы в ASP.NET 2.0 введен атрибут useHostingldentity (см. табл. 13-8). Если он установлен в true, то в отличие от ASPNET 1.x при исполь- зовании провайдера SQLServer и интегрированной защиты запросы к SQL Server выполняются от имени учетной записи ASP.NET. По умолчанию это ASPNET или NETWORK SERVICE либо другая учетная запись, имперсонализируемая рабочим процессом ASP.NET, — та, что задана в разделе <identity> конфигурационного файла. Такое решение значительно упрощает администрирование интранет-сайтов, поскольку только учетной записи ASP.NET предоставляется доступ к защищенным и критичным ресурсам. Атрибут useHostingldentity по умолчанию имеет значение true, то есть перед вызовом провайдера состояния сеанса SQLServer либо пользовательского провайдера выполняется возврат к учетной записи ASP.NET. 1^ Примечание Если для доступа к SQL Server применяется метод интегрированной за- щиты, из соображений безопасности рекомендуется выполнять возврат к учетной записи хоста. В противном случае желательно создать отдельную учетную запись и предоставить ей два разрешения: на выполнение хранимых процедур, предназначенных для работы с состоянием сеанса, и на доступ к соответствующим ресурсам. Состояние сеанса в Web-ферме Приложения ASPNET, предназначенные для работы в аппаратной среде Web-фер- мы или Web-сада, не могут хранить состояние сеанса в памяти своего процесса (то есть не могут работать в режиме InProc). Дело в том, что на каждом компьютере Web-фермы выполняется свой рабочий процесс, и каждый из них будет в таком случае поддерживать собственное состояние сеанса. А в Web-саду несколько ра- бочих процессов выполняются на одном компьютере. Распределить приложение между несколькими рабочими процессами, функ- ционирующими на одном или разных компьютерах, вам поможет отделение со- стояния сеанса от рабочего процесса, иными словами, выбор режима StateServer или SQLServer. Если вы имеете дело с Web-фермой, позаботьтесь о том, чтобы раздел <machine- Кеу> на всех ее серверах был одинаковым. (Подробнее об этом рассказывается в статье Knowledge Base Q313091.) Кроме того, должен быть одинаковым путь к копиям приложения, хранящимся в метабазе IIS. Этот путь используется как идентфикатор приложения, представленного доменом приложения (свойство AppDomainAppId объекта HttpRuntime), и идентифицирует приложение в базе данных состояния ASP.NET (см. статью Knowledge Base Q325056.)
Управление состоянием Глава 13 529 В ASP.NET 2.0 введены компоненты PartitionResolver, помогающие провайдеру состояния сеанса распределить его данные между несколькими серверными узла- ми. Это позволяет масштабировать приложение до большой Web-фермы согласно пользовательской схеме балансирования нагрузки. Компонент PartitionResolver предоставляет строку подключения (настоящую строку, а не ссылку на нее в файле web.config), предназначенную для доступа к состоянию сеанса и имеющую приоритет перед строкой подключения, заданной в разделе <sessionState>. Настройка управления состоянием сеанса Подсистема управления состоянием сеанса ASP.NET изначально разрабатывалась как гибкая и настраиваемая. Однако в ASP.NET 1.x при высокой степени настраи ваемости ей недоставало возможности расширения В ASP.NET 2.0 она была пере- работана и теперь у разработчиков появилась возможность заменять большинство ее функциональных элементов. Таким образом, у вас имеются следующие три воз- можности адаптации подсистемы управления состоянием сеанса к нуждам своего приложения. Вы можете оставить стандартный модуль управления состоянием сеанса, но на- писать пользовательский провайдер состояния сеанса, чтобы сменить хранилище данных (например, использовать СУБД, отличную от SQL Server, или другой на- бор таблиц). При этом у вас появляется возможность переопределить некоторые вспомогательные классы (главным образом коллекции), используемые для пере- несения данных из их хранилища в объект Session и обратно. Вы можете оставить стандартный модуль управления состоянием сеанса, но за- менить генератор идентификаторов сеанса. Алгоритм этого генератора является критической составляющей приложения, поскольку если будет возможно пред- сказать результат его работы, приложение станет уязвимым для атак, основанных на похищении идентификаторов сеанса. Тем не менее и этот аспект работы подси- стемы управления состоянием сеанса заменяем, благодаря чему у вас появляется возможность еще лучше защитить свое приложение. Вы можете отключить стандартный модуль управления состоянием сеанса, заменив его собственным. Хотя и в ASP.NET 1.x это технически осуществимо, прибегать к такой мере следует лишь в крайнем случае. Очевидно, что она дает вам полную свободу управления состоянием сеанса, однако реализовать такое решение очень сложно. Мы даже не будем рассматривать эту тему в настоящей книге. Первое решение — простейшее из всех, и его достаточно для большинства ситуа- ций, в которых требуется изменить работу подсистемы управления состоянием сеанса. Его мы и рассмотрим прежде всего. Создание пользовательского провайдера состояния сеанса Вы уже знаете, что провайдер состояния сеанса — это компонент, отвечающий за предоставление данных, связанных с текущим сеансом. Он вызывается модулем, управляющим состоянием сеанса, когда в процессе выполнения запроса возникает необходимость в этой информации. Провайдер извлекает данные из определенного хранилища и возвращает их модулю. Когда выполнение запроса завершается, тот же модуль снова вызывает провайдер состояния сеанса, чтобы он записал данные в слой их хранения Как упоминалось, ASP.NET поддерживает три провайдера состо- яния, перечисленные в табл. 13-11.
530 Часть III Инфраструктура ASP NET Табл. 13-11. Стандартные провайдеры состояния сеанса Имя Класс Слой хранения InProc InProcSessionStateStore Данные хранятся как «живые» объекты в кэше ASP.NET StateServer OutOfProcSessionStateStore Данные хранятся в сериализованном виде в памяти сервиса Windows с именем aspnet_state.exe SQLServer SqlSessionStateStore Данные хранятся в сериализованном виде в базе данных SQL Server В ASP.NET 2.0 можно написать собственный класс провайдера состояния, который будет работать с желаемым хранилищем данных. Заметьте, что стандартные провай- деры состояния используют различные вспомогательные классы, и в собственном провайдере вы при желании можете заменить и их. Определение провайдера состояния Провайдером состояния называется класс, производный от SessionStateStoreProvider- Base. Важнейшие методы его интерфейса описаны в табл. 13-12. Табл. 13-12. Методы класса SessionStateStoreProviderBase Метод Описание CreateNewStoreData Создает объект SessionStateStoreData, содержащий данные нового сеанса CreateUninitializedltem Создает в источнике данных элемент, представляющий новый, неинициализованный сеанс. Этот метод вызывается, когда приложение, не использующее cookie, для сохранения состо- яния сеанса обращается к устаревшему объекту состояния. В таком случае модуль состояния сеанса генерирует новый идентификатор сеанса и вызывает метод CreateUninitialized- ltem для создания соответствующего ему объекта состояния в хранилище. Это важно для того, чтобы запрос, для которого только что создан новый идентификатор сеанса, не был вос- принят как запрос, состояние сеанса которого устарело Dispose Освобождает все ресурсы (кроме памяти), используемые про- вайдером состояния EndRequest Вызывается используемым по умолчанию модулем состояния сеанса, когда он начинает обработку события EndRequest Getltem Возвращает состояние сеанса с заданным идентификатором, извлекая его из хранилища данных, и блокирует его для чте- ния. Этот метод обслуживает запросы, в которых состояние сеанса только считывается GetltemExclusive Возвращает состояние сеанса с заданным идентификатором, извлекая его из хранилища данных, и блокирует его для за- писи. Этот метод обслуживает запросы, в которых состояние сеанса не только считывается, но и обновляется Initialize Унаследован от базового класса провайдера, выполняет одно- разовую инициализацию InitializeRequest Вызывается используемым по умолчанию модулем состояния сеанса, когда он начинает обработку события AcquireRequestState ReleaseltemExclusive Разблокирует состояние сеанса, заблокированное ранее мето- дом GetltemExclusive
Управление состоянием Глава 13 531 Табл. 13-12. (окончание) Метод Описание Removeitem Удаляет состояние сеанса из хранилища данных. Вызывается, когда сеанс завершается (естественно или принудительно) Resetitem Timeout Сбрасывает установку времени устаревания состояния сеанса. Вызывается при отключении поддержки состояния сеанса для приложения SetAndReleaseltemExclusive SetltemExpireCallback Записывает состояние сеанса в хранилище данных Вызывается используемым по умолчанию модулем состо- яния сеанса для уведомления класса провайдера о том, что вызывающий модуль зарегистрировал обработчик события SessionEnd Классы, наследующие SessionStateStoreProviderBase, работают с используемым по умолчанию модулем состояния сеанса ASPNET и заменяют лишь ту его часть, которая управляет сохранением и извлечением данных Никакая другая функциональность при их подключении не изменяется. Блокировка и устаревание Moiyr ли два запроса к одному и тому же состоянию сеанса выполняться параллельно. Без сомнения. Взгляните на рис. 13-3. Запросы могут поступать практически одно- временно, например, от двух фреймов одной страницы, или при одновременной работе пользователя с двумя экземплярами одной страницы. Во избежание проблем в про- вайдере состояния должен быть реализован механизм блокировки для сериализации доступа. Модуль состояния определяет, какой вид доступа требуется запросу, только чтение или чтение и запись, и вызывает соответственно метод Getltem или Getltem- Exclusive. Реализуя эти методы, разработчик провайдера должен создать механизмы блокировки для чтения и блокировки для записи, обеспечивающие возможность одновременного чтения данных, но не одновременного чтения и записи. Еще одна проблема заключается в том, чтобы вовремя дать знать модулю состояния сеанса о его завершении. Когда в файле global.asax определен обработчик события Session_End, модуль состояния сеанса вызывает метод SetltemExpireCallback, передавая в нем ссылку на функцию обратного вызова, которая имеет следующий прототип: public delegate void SessionStateItemExpireCallback( string sessionlD, SessionStateStoreData item); Провайдер состояния сеанса должен сохранить делегат этой функции и вызвать его по завершении сеанса. Использование функции обратного вызова не являет- ся обязательным, и среди стандартных провайдеров ее поддерживает лишь InProc. Если вы не хотите, чтобы ваш провайдер использовал функцию обратного вызова для уведомления модуля состояния об устаревании состояния сеанса, пусть метод SetltemExpireCallback возвращает значение false. |__| Примечание В провайдере, который должен поддерживать сеансы, работающие без использования cookie, должен быть реализован метод CreateUninitialized, записывающий в хранилище данных пустое (не содержащее данных) состояние сеанса. Такой объект состояния должен содержать идентификатор сеанса, дату его создания и, возможно, идентификаторы блокировок, но никаких данных. В режиме без cookie, когда в запросе указан идентификатор устаревшего сеанса ASP.NET 2.0 генерирует новый идентифика- тор и выполняет переадресацию запроса И если не найдется неинициализированного состояния сеанса с таким идентификатором, новый запрос опять будет распознан как содержащий идентификатор устаревшего сеанса.
532 Часть III Инфраструктура ASP NE'l Замена словаря данных сеанса Состояние сеанса в хранилище данных представляет класс SessionStateStoreData. Именно его объекты возвращают методы Getltem и GetltemExclusive. Этот класс имеет три свойства: Items, StaticObjects и Timeout. В свойстве Items содержится коллекция пар имя-значение, доступная странице через свойство Session. Свойство StaticObjects — это коллекция статических объектов сеанса, объявленных в файле global.asax. Ну а свойство Timeout, как следует из его имени, определяет максимальную длительность сеанса в минутах (по умолчанию — 20 минут). Как только модуль состояния получает связанные с запросом данные состояния, он записывает эти данные (извлекая их из коллекции Items) в новый экземпляр класса HttpSessionStateContainer. Затем этот объект передается конструктору класса HttpSes- sionState и становится контейнером данных, стоящим за свойством Session. Класс SessionStateStoreData используется в определении базового класса провайде- ра состояния, и полностью заменить его вы не можете. Если он по какой-то причине вас не устраивает, создайте производный от него класс. И для модуля, управляющего состоянием сеанса, и для провайдера состояния сеанса контейнер элементов сеанса это просто класс, реализующий интерфейс ISessionStateltemCollection, по умолчанию им является класс SessionStateltemCollection. Его вы можете заменить собственным классом, реализующим указанный интерфейс. /fV Совет При написании провайдера состояния вам могут пригодиться методы класса • SessionStateUtility. Этот класс содержит методы для сериализации и десериализации состояния сеанса с его записью в хранилище данных или чтением оттуда. Кроме того, у класса имеются методы для извлечения словаря данных сеанса и подключения его к контексту HTTP и свойству Session. Регистрация пользовательского провайдера состояния Для того чтобы сделать пользовательский провайдер состояния доступным приложе- нию, его нужно зарегистрировать в файле web.config. Предположим, у вас есть класс провайдера SampleSessionStateProvuler, который вы откомпилировали в сборку MyLib. В этом случае вам нужно поместить в файл web.config такой код: <system.web> <sessionState mode="Custom” customProvider="SampleSessionProvider”> <providers> Odd name="SampleSessionProvider’i' type="SampleSessionStateProvider, MyLib" /> </providers> </sessionState> </system.web> Имя провайдера является произвольным, но обязательным. Чтобы модуль состояния сеанса мог найти ваш провайдер, присвойте атрибуту mode значение Custom. Пользовательский генератор идентификаторов сеанса За генерирование идентификатора сеанса в ASP.NET 2.0 отвечает компонент с именем SessionlDManager. Строго говоря, этот класс не является ни модулем HTTP, ни про- вайдером. Он наследует класс System.Objtct. реализует интерфейс ISessionlDManager, и вы можете заменить его пользовательским классом, реализующим тот же интерфейс. Для того чтобы обоснованно принимать решение о том, .ребуется ли вам пользо- вательский генератор идентификатора сеанса, необходимо знать, что представляет собой стандартный генератор.
Управление состоянием Глава 13 533 Стандартный генератор идентификаторов сеанса К часе SessionlDManager, используемый по умолчанию для получения идентификато- ра сеанса, генерирует вначале массив байтов, содержащий криптографически силь- ную случайную последовательность из 15 значений. Затем он кодирует этот массив, И в результате получается строка из 24 символов, которые допустимо использовать в URL, — это и есть идентификатор сеанса. Как вы уже знаете, идентификатор сеанса может передаваться на клиент либо в виде cookie, либо в составе URL в зависимости от значения атрибута cookieless конфигурационного раздела <sessionState>. Следует заметить, что когда cookie не используются, за включение идентификатора сеанса в состав URL и переадресацию запроса отвечает компонент, играющий роль генератора идентификаторов сеанса. Стандартный генератор перенаправляет браузер по адресу http://www.contoso.com/test/(S(id сеанса))/page.aspx В ASP.NET 1.x этот URL несколько иной, он не содержит разделителей Как выполняется запрос с таким URL? Если cookie не используются, действует малень- кий и простой фильтр IS API (aspnet_filter.dll, имеющийся и в ASP.NET 1.x), который динамически изменяет URL запроса Запрос обрабатывается правильно, но путь в адресной строке не изменяется. Извлеченный из URL идентификатор сеанса поме- щается в заголовок запроса AspFilterSessionld. Пользовательский генератор идентификаторов сеанса Итак, вы уже знаете, что генератор идентификаторов сеанса — это класс, реализую- щий интерфейс ISessionlDManager. Если вы хотите написать его самостоятельно, то вам остается лишь выбрать одну из двух возможностей: создать собственный класс с нуля и реализовать в нем указанный интерфейс или сделать свой класс произво- дным от SessionlDManager и переопределить пару его виртуальных методов, чтобы адаптировать его к своим нуждам. Первое решение обеспечивает вам максимальную гибкость, а второе быстрее и проще реализовать, причем в большинстве случаев оно вполне вас устроит, поскольку позволяет достичь главной цели, ради которой создает- ся пользовательский генератор — реализовать собственный алгоритм формирования идентификаторов сеанса. Давайте начнем с ознакомления с методами интерфейса ISessionlDManager, опи- санными в табл. 13-13. Табл. 13-13. Методы интерфейса ISessionlDManager Метод Описание CreateSessionlD Виртуальный метод, генерирующий уникальный идентификатор сеанса Decode Декодирует идентификатор состояния сеанса, используя метод HttpUti- lity.UrlDecode Encode Кодирует идентификатор состояния сеанса, используя метод HttpUtility. UrlEncode Initialize Выполняет одноразовую инициализацию объекта SessionlDManager InitializeRequest Вызывается для инициализации объекта SessionlDManager данными, связанными с конкретным запросом GetSessionlD Извлекает идентификатор сеанса из контекста текущего НТТР-запроса RemoveSessionlD Удаляет идентификатор сеанса из cookie или из URL SaveSessionlD Записывает созданный идентификатор в ответ HTTP Validate Проверяет, действителен ли идентификатор сеанса
534 Часть III Инфраструктура ASP.NET Если вы планируете создать собственный генератор идентификаторов сеанса, следует учесть приведенные далее соображения. Алгоритм генерирования идентификаторов сеанса критичен для приложения. Если создаваемая на его основе последовательность значений не будет криптографи- чески сильной, злонамеренный пользователь сможет вычислить идентификатор сеанса, и пока этот сеанс будет активен, получить доступ к данным пользователя. (Атаки такого типа называются похищением сеанса.) Хороший пользовательский алгоритм генерирования идентификаторов сеанса может, к примеру, возвращать глобально уникальные идентификаторы (GUID). Вы можете решить, будут ли поддерживаться сеансы без использования cookie. Если не будет, то следует позаботиться о наличии компонента, который будет извлекать идентификатор сеанса из HTTP-запроса и переадресовывать браузер, а также включать идентификатор сеанса в ответ браузеру. Это может быть либо фильтр ISAPI, либо модуль HTTP, выполняющий предобработку запроса и вно- сящий необходимые изменения. Если вы делаете свой класс производным от SessionlDManager, то необходимо переопределить два его метода: CreateSessionlD и Validate. Первый из них возвра- щает строку, содержащую идентификатор сеанса, а второй проверяет, является ли заданный идентификатор действительным. После этого вы регистрируете свой класс в конфигурационном файле: <sessionState sessionIDManagerType="Samples.MyIDManager, MyLib" /> </sessionState> Управление состоянием сеанса и производительность Включая функцию поддержки состояния сеанса, вы идете на значительные затра- ты. В выпуске MSDN Magazine за сентябрь 2005 года содержится статья, в которой описаны лучшие из выработанных командой разработчиков ASP.NET приемов, позволяющих уменьшить влияние этой функции на производительность при- ложения. Первая и главная рекомендация заключается в том, что функцию поддержки состояния сеанса, когда только это возможно, следует отключать Однако для того чтобы состояние сеанса не устаревало, модуль HTTP при этом должен помечать его в хранилище данных как активное. Для серверов состояния, хранящих его вне процесса ASP.NET, это означает лишнее обращение к хранилищу идентификато- ров. Чтобы этого не происходило, пользовательский генератор идентификаторов состояния сеанса должен возвращать пустой идентификатор для всех запросов, которым не требуется информация состояния. (Напишите класс, наследующий SessionlDManager, и переопределите в нем метод GetSessionlD.) Следующая рекомендация состоит в минимизации вероятности соревнования за данные сеанса, для чего нужно избегать использования фреймов и загружае- мых ресурсов, которые предоставляются обработчиками HTTP, использующими состояние сеанса Еще одна рекомендация относится к сериализации и десериализации данных. Всегда используйте простые типы, а сложные классы разделяйте на отдельные элементы и массивы данных. (Разумеется, я не предлагаю перерабатывать все классы слоя доступа к данным, речь идет лишь о данных, сохраняемых в состоянии
Управление состоянием Глава 13 535 сеанса.) В качестве альтернативы можно создать собственный алгоритм сериали- зации, оптимизированный для хранения состояния сеанса. Разделение класса на отдельные свойства, каждое из которых будет храниться в отдельном слоте со- стояния сеанса, предпочтительно не только по причине использования простых типов, но также потому, что такое высоко гранулярное решение минимизирует объем сохраняемых данных, когда в состояние сеанса вносятся изменения. Если изменено только одно свойство, обновляется маленький слот простого типа, а не большой слот, содержащий комплексные данные. Состояние представления страницы Страницы ASP.NET имеют свойство ViewState, предназначенное для формирования контекста вызова и сохранения значений между двумя последовательными запроса- ми к одной и той же странице. Состояние представления — это состояние страницы в последний момент ее обработки на сервере. Обычно оно сохраняется на клиенте и восстанавливается в самом начале обработки запроса. По умолчанию состояние представления сохраняется в составе клиентской страницы в виде скрытого поля и в таком виде «путешествует» между страницей и сервером. Хотя состояние представления отправляется клиенту, оно не содержит никакой информации, предназначенной специально для него. Она относится лишь к странице и некоторым дочерним элементам управления и не используется бра- узером. У использования состояния представления имеются как преимущества, так и не- достатки, которые вам нужно взвесить как можно тщательнее, прежде чем принимать относительно него какое-либо решение. К достоинствам относится то, что состояние представления не потребляет серверных ресурсов, а реализовать и использовать его очень просто. Поскольку оно является частью страницы, то извлекается очень бы- стро. Однако если шире взглянуть на вопрос о производительности страницы, то вы увидите, что здесь не все так радужно. Поскольку состояние представления включается в состав клиентской страницы, объем HTML-кода, передаваемого клиенту, неизбежно увеличивается, причем эти несколько килобайтов дополнительных данных браузеру совершенно не нужны. Сложная страница реального приложения, для которой не предпринималась попыт- ка ограничить или оптимизировать использование состояния представления, может содержать порядка 20 Кбайт таких данных. Подытоживая сказанное, отмечу, что управление состоянием представления явля- ется одной из наиболее важных функций ASPNET, поскольку именно на нем основана модель Web Forms. Однако при бездумном использовании состояние представления может значительно отягощать страницы приложения. Класс StateBag Класс StateBag является основой состояния представления. Он управляет инфор- мацией, которую страницы и элементы управления хотят сохранять между последо- вательными вызовами одного и того же экземпляра страницы. Работает он подобно словарю, а кроме того, реализует интерфейс IStateManager. Базовые классы Page и Control предоставляют доступ к состоянию представления через свойство ViewState. Используя его, можно добавлять и удалять элементы объекта StateBag так же, как это делается с любым словарным объектом: ViewState["FontSize"] = value;
536 Часть III Инфраструктура ASP NET Запись данных в состояние представления можно выполнять только досле того, как в процессе выполнения запроса будет сгенерировано событие Init. Чтение же этих данных возможно на любом этапе жизненного цикла страницы, но до того, как ода войдет в стадию рендеринга, то есть до события PreRender. Свойства состояния представления Со свойствами класса StateBag вы можете ознакомиться в табл. 13-14. Табл. 13-14. Свойства класса StateBag Свойство Описание Count Возвращает количество элементов, хранящихся в объекте состояния Item Индексное свойство, используемое для доступа к элементам данных состояния Keys Возвращает коллекцию ключей элементов данных состояния Values Возвращает коллекцию значений элементов данных состояния Каждый элемент данных, хранящихся в объекте StateBag, представлен объектом класса Stateitem. Такой объект создается неявно, когда с помощью свойства Item или метода Add вы задаете очередное значение. Добавляемые в объект StateBag, элементы доступны через него до тех пор, пока состояние представления не будет сериализовано при рендеринге страницы. Сериализации подлежат те элементы, у которых свойство IsDirty имеет значение true. Методы состояния представления Перечень методов класса StateBag приведен в табл. 13-15. Табл. 13-15. Методы класса StateBag Метод Описание Add Добавляет в коллекцию новый объект Stateitem. Если заданный элемент уже существует, он обновляется Clear Удаляет все элементы текущего состояния представления GetEnumerator Возвращает перечислитель для прохода по коллекции элементов объекта StateBag IsItemDirty Позволяет указать, был ли элемент с заданным ключом модифицирован в процессе обработки запроса Remove Удаляет заданный объект из коллекции элементов объекта StateBag Метод IsItemDifty является средством неявного обращения к свойству IsDirty за- данного объекта Stateitem. ["д,"! Примечание Состояние представления страницы включает содержимое свойства View- State самой страницы плюс содержимое одноименного свойства каждого из ее элементов управления. Общие вопросы управления состоянием представления Если рассматривать вопрос о состоянии представления с позиций архитектуры, то значение его невозможно переоценить, поскольку именно оно является ключевым элементом реализованной в ASP.NET функции автоматического управления состо- яния. Однако с его использованием связано несколько проблем. Более всего разра- ботчиков волнуют проблемы безопасности и производительности. Можно ли считать
Управление состоянием Глава 13 537 состояние представления надежно защищенным и возможны ли его похищение либо модификация злонамеренным пользователем? Как наличие в состоянии представле- ния дополнительной информации влияет на время загрузки страницы? Шифрование и защита Многие разработчики сомневаются в целесообразности использования состояния представления лишь потому, что оно хранится в скрытом поле страницы и потому полностью находится во власти потенциальных злоумышленников. И действительно, хотя данные его зашифрованы, не существует полной гарантии того, что они не будут похищены. Но следует иметь в виду, что все же эти данные защищены гораздо лучше, чем любые другие скрытые поля (скажем, те, которые вы почти наверняка использо- вали в приложениях классической ASP). Кроме того, вопрос о защите наиболее остро стоит для конфиденциальных данных, а данные состояния представления могут вовсе не быть конфиденциальными, так что в этом случае опасность похищения информации менее значительна, чем, к примеру, опасность атаки внедрением. Свободно доступная в скрытом поле с именем VIEWSTATE информация состоя- ния представления по умолчанию кэшируется и сохраняется в кодировке Base64. Для ее декодирования на клиенте потенциальный взломщик должен немало потрудиться. Но главный вопрос, как отмечалось, состоит не в том, можно ли декодировать эту информацию, а в том, имеет ли злоумышленник возможность изменить состояние представления, чтобы на сервер были переданы вредоносные данные. А вот это как раз невозможно, поскольку сервер тут же обнаружит подмену и выбросит исключение. Из соображений производительности состояние представления не шифруется. При необходимости можно включить функцию шифрования, поместив следующую установку в файл web.config: <machineKey validation="3DES" /> Учтите, что когда атрибут validation имеет значение 3DES, хэширование состояния представления не производится, а вместо этого к нему применяется шифрование по алгоритму 3DES. Аутентификационный контроль Как упоминалось в главе 3, директива ©Page содержит атрибут EnableViewStateMac, назначение которого — сделать состояние представления более защищенным посред- ством выявления попыток изменения его данных. Когда этот атрибут установлен в true, при сериализации состояния представления в него добавляется валидационная хэш-строка, которая генерируется с использованием алгоритма и ключа, заданных в разделе <machineKey> конфигурационного файла. Результирующий байтовый мас- сив — результат двоичной сериализации объекта StateBag плюс хэш-значение — сохра- няется в кодировке Base64. По умолчанию для получения хэш-значения используется алгоритм шифрования SHA1, а ключи шифрования генерируются автоматически и сохраняются на компьютере Web-сервера в подсистеме Local Security Authority (LSA), которая является защищенным компонентом Windows NT, Windows 2000, Windows Server 2003 и Windows XP. Она предоставляет сервисы, связанные с защитой, и хранит информацию обо всех аспектах локальной безопасности системы. Если атрибут EnableViewStateMac имеет значение true, при возврате формы из поля состояния представления извлекается хэш-значение, по которому система определяет, не было ли состояние представления изменено на клиенте. Если это так, выбрасы- вается исключение. Благодаря этому, как мы уже сказали, злоумышленник может прочесть состояние представления, но не может его изменить без ключа шифрования,
538 Часть III Инфраструктура ASP.NET хранящегося в LSA-подсистеме Web-сервера. Аббревиатура МАС в имени атрибута EnableViewStateMac означает Machine Authentication Check — аутентификационный контроль уровня компьютера. По умолчанию этот атрибут равен true, но если уста- новить его в false, злоумышленник получит возможность изменять состояние пред- ставления на клиенте и отправлять его модифицированную версию на сервер при возврате формы, так что подмена останется незамеченной ASP.NET. Для включения защиты состояния представления в ASP.NET 1.1 использовалось свойство ViewStateUserKey класса Page. Это свойство содержит определенную поль- зователем строку (обычно идентификатор сеанса), которая известна на сервере, но которую трудно угадать на клиенте. Содержимое этого свойства ASP.NET использует в качестве аргумента хэш-алгоритма, генерирующего МАС-код. Объем данных состояния представления и производительность страницы По моему личному мнению, разработчику страниц ASP.NET следует заботиться о размере состояния представления, но не стоит беспокоиться о потенциальной бреши, которую оно оставляет в системе защиты вашего кода, поскольку она лишь открывает доступ к другим дырам в этой системе, если таковые имеются. Главной вашей заботой должна быть производительность страницы, прежде всего характе- ризующаяся ее временем отклика. Для большой страницы с богатым интерфейсом состояние представления может иметь весьма внушительный объем, измеряемый килобайтами и десятками килобайтов. Эти дополнительные данные передаются с каждым запросом в обоих направлениях, что может сильно замедлять работу при- ложения. Каков приемлемый размер страницы ASP.NET? А размер ее состояния представле- ния? Давайте возьмем в качестве примера демонстрационную страницу, содержащую табличный элемент управления с сотней записей (скажем, из таблицы Customers базы данных Northwind SQL Server). <html> <head runat="server"> <title>Measure Up Your ViewState (2.0)</title> </head> <script language="javascript"> function ShowViewStateSizeO { var buf = document.forms[0]["_VIEWSTATE"].value; alert("View state is " + buf.length + " bytes"); } </script> <body> <form id="form1" runat="server"> <input type="button” value="Show View State Size" onclick="ShowViewStateSize()"> <asp:SqlDataSource ID=”SqlDataSource1" runat="server" SelectCommand="SELECT companyname, contactname, contacttitle FROM customers" ConnectionString=”<%$ Connectionstrings:LocalNWind %>" <asp DataGrid ID="grid" runat="server" DataSou rceID="SqlDataSou reel" /> </form> </body> </html>
Управление состоянием Глава 13 539 В ASP.NET 2.0 общий размер этой страницы — около 20 Кбайт, а ее состояние представления занимает около 11 Кбайт. Если ту же страницу поместить в среду ASP.NET 1.x, результаты будут еще худшими: страница будет иметь размер 28 Кбайт, и из них 19 Кбайт займут данные состояния представления. Из этого примера можно сделать следующие выводы: в ASP.NET 2.0 поле состояния представления более компактно (позже я расскажу, почему); состояние представления занимает большую часть общего объема страницы — при- мерно 60 %. Что можно в этой связи предпринять? Давайте поиграем с числами и опреде- лим разумный и приемлемый размер состояния представления. По возможности следует стремиться к тому, чтобы размер страницы не превышал 30 Кбайт. Для сравнения приведу несколько примеров. Домашняя страница Google имеет размер 4 Кбайт, домашняя страница официального сайта ASP.NET (которая расположена по адресу http://www.asp.net) — 50 Кбайт. Сайт Google разрабатывался не в ASP.NET, так что о его состоянии представления говорить не приходится. А вот состояние представления сайта http://www.asp.net, как ни странно, занимает всего 1 Кбайт. Одна из страниц этого сайта, http://www.asp.net/ControlGallery/default.aspx7Ca- tegory=7&tabindex=0, просто огромна: она имеет размер 500 Кбайт, из которых 120 Кбайт занимает состояние представления. Идеальный размер состояния представления — около 7 Кбайт, совсем хорошо, если вам удастся уменьшить его до 3 Кбайт. Но в любом случае, независимо от абсолютного значения, его размер не должен превышать 30 % размера страницы. Примечание Откуда взялись эти цифры? Я мог бы сказать: «Из моего собственного опыта», — но едва ли такой ответ вас удовлетворит. Поэтому я лучше немного изменю свою рекомендацию: наилучшим размером состояния представления является его наи- меньший возможный размер. На мой взгляд, 30 Кбайт — приемлемый компромисс, по- скольку в большинстве случаев этого достаточно для сохранения наиболее необходимых данных. Очевидно, что если на странице должно выводиться, скажем, 250 элементов, ее размер вполне может оказаться равен 1 Мбайт. В конце концов, размер состояния пред- ставления является проектной характеристикой и зависит от конкретного приложения. И важнее не то, каков он, а то, какой процент от общего размера страницы он составляет. Я бы сказал, что это значение не должно превышать 25-30 %. Но опять же, это значение подсказано здравым смыслом и общими соображениями, а в конкретном случае у вас могут быть мотивы, оправдывающие значительное превышение этого предела. К тому же если поддержку состояния представления можно отключить, это стоит сделать. И если какой-то элемент данных можно в нем не хранить (как, скажем, фиксированный список стран), то опять же так и следует поступить. Разработка Web-форм без использования состояния представления По умолчанию поддержка состояния представления включена для всех серверных элементов управления, однако это не означает, что она действительно требуется каж- дому элементу. Применение этой функции следует тщательно контролировать, чтобы избежать ненужных затрат. Без сомнения, эта функция избавляет вас от написания огромного количества кода и, что еще важнее, упрощает разработку страницы. Однако если окажется, что поддержка состояния представления определенных элементов управления обходится вам слишком дорого, ее можно отключить и заново инициа- лизировать состояние этих элементов управления при каждом возврате формы. От- ключение поддержки состояния представления сэкономит время обработки страницы и ускорит процессе ее загрузки.
540 Ч ггь III Инфраструктура ASP.NET Отключение поддержки состояния представления > Вы можете отключить поддержку состояния представления для всей станицы, исполь- зуя атрибут EnableViewState ее директивы ©Page. Но хотя в общем случае поступать так не рекомендуется, следует иметь такую возможность в виду при создании страниц не осуществляющих возврат формы или не нуждающихся в состоянии представления <% @Page EnableViewState="false" %> Лучшим решением, как правило, является отключение поддержки состояния пред- ставления для отдельных элементов управления страницы, что делается посредством свойства EnableViewState'. <asp:datagrid runat="server" EnableViewState=”false"> </asp:datagrid> Разрабатывая страницу, можно прибегнуть к функции ее трассировки, чтобы про- следить за тем, сколько данных сохраняется в ее состоянии представления. Правда, трассировщик не показывает общего объема состояния страницы, но позволяет со- ставить представление о том, что делает каждый из ее элементов управления. Для примера на рис. 13-8 показаны данные трассировки страницы, содержащей относи- тельно простой элемент управления DataGrid. Как видим, большую часть состояния представления занимают ячейки таблицы. В частности, элемент управления TableCell записывает в состояние представления все составляющие своего пользовательского интерфейса, включая текст, столбец и стилевые атрибуты. Рис. 13-8. Многие элементы управления, в том числе дочерние, записывают что-то в состояние представления | 4Л Примечание Подобно всем остальным элементам управления, связанным с данными, элемент управления DataGrid хранит отображаемые данные в состоянии представления. Но это не единственная причина увеличения его объема: каждая составляющая пользо- вательского интерфейса элемента управления тоже добавляет что-то от себя.
Управление состоянием Глава 13 541 Когда целесообразно отключать поддержку состояния представления Давайте вкратце посмотрим, какова роль состояния представления в функциониро- вании страницы и чем вы жертвуете, отключая его поддержку. Состояние представле- ния —- это текущее состояние страницы и ее элементов управления непосредственно перед рендерингом. При возврате формы состояние страницы, играющее роль контек- ста вызова, восстанавливается из ее скрытого поля, десериализуется и используется для инициализации серверных элементов управления страницы и ее самой. Однако, как отмечалось в главе 3, это только половина дела. После загрузки состояния представления страница считывает возвращенные кли- ентом данные и использует их для обновления большинства установок серверных элементов управления. Таким образом, возвращенные клиентом данные в значитель- ной мере заменяют данные состояния представления. Поэтому определенная его часть передается между клиентом и сервером напрасно — ни там, ни там она не используется. Рассмотрим типичный случай. Предположим, у вас есть страница с серверным текстовым полем. При возврате формы этому полю будет автоматически присвоено значение, введенное в него пользователем. Зачем тогда состояние представления? Для такого элемента управления его можно без колебаний отключить. Вот пример страницы: <% @Page language="c#" %> <form runat="server"> <asp textbox runat="server" enableviewstate="false" id="thelnput" readonly="false” text=”Type here /> <asp:checkbox runat="server" enableviewstate="false' id="theCheck" text=”Check me" /> <asp:button runat="server" text="Click” onclick=”OnPost" /> </form> Очевидно, что состояние такой страницы будет само собой сохраняться, даже если вы отключите его поддержку для двух Элементов управления <- TextBox и CheckBox. Ведь их ключевые свойства, Text и Checked, обновляются в соответствии со значе- ниями, которые задал пользователь. И даже если для них будет восстанавливаться состояние представления, оно будет тут же заменяться указанными значениями. Третьему элементу управления, кнопке, состояние представления тоже ни к чему, так что вы вполне можете отключить для страницы его поддержку. Аналогично не требуется поддержка состояния представления тем элементам управления, свойства которых устанавливаются во время разработки в файле .aspx и в процессе сеанса не изменяются. Вот пример такого элемента: <asp:textbox runat="server" id=‘TextBoxI” Text="Some text” MaxLength="20" ReadOnly="true" /> Если значения свойств элемента управления не изменяются во время выполнения в течение всего жизненного цикла страницы и недоступны для изменения пользовате- лем, то такие значения совершенно не нужно сохранять в состоянии представления. Тогда в каких же случаях состояние представления необходимо странице? Прежде всего, когда определенные свойства элементов управления (отличные от тех, которые может модифицировать пользователь) обновляются при выполнении страницы. Под обновлением в этом случае понимается изменение исходных значений свойств — ис- пользуемых по умолчанию или тех, которые вы присвоили во время разработки в фай- ле .aspx. Рассмотрим такую форму: <script runat="server"> void Page_Load(object sender, EventArgs e)
542 Часть III Инфраструктура ASP.NET { if (!IsPostBack) thelnput.Readonly = true; } </script> <form id="form1" runat=”server”> <asp:textbox runat="server" id="thelnput" text="Am I read-only?” /> <asp button ID=”Button1" runat="server" text="Click" onclick-”OnPost" /> </form> Когда страница загружается в первый раз, ее текстовое поле доступно только для чтения. Затем пользователь щелкает кнопку, выполняется возврат формы, и если состояние представления было сохранено, восстанавливается программно заданное значение свойства Readonly — true, — то есть поле остается доступным лишь для чтения. Однако если поддержка состояния представления отключена, при следую- щем формировании страницы программно заданное значение свойства Readonly не восстанавливается, а принимает значение по умолчанию (false), и поле становится доступным для ввода данных. Вообще говоря, без автоматического восстановления состояния представления можно обойтись в тех случаях (одним из которых является наш пример), когда со- стояние восстановимо иным способом. Но если получить эти сведения динамически нельзя, функция поддержки состояния представления гарантирует правильное вос- становление страницы после возврата формы. jtt. Совет Поддержку состояния представления можно включать и отключать программным » способом как для всей страницы, так и для отдельных элементов управления, используя свойство EnableViewState. Отключение состояния представления может вызвать специфические проблемы, которые трудно диагностировать и исправлять, особенно если вы работаете с эле- ментами управления сторонних производителей или любыми другими элементами, доступа к исходному коду которых не имеете. Некоторые элементы управления ASP.NET могут сохранять в состоянии представления не только те свойства, ко- торые являются официальными элементами их программного интерфейса, но и поведенческие свойства, используемые для внутренних целей и помеченные как защищенные или даже приватные. К сожалению, для таких элементов управления поддержку состояния представления отключать нельзя. Но в ASP.NET 2.0 у этой проблемы появилось решение в виде отдельно определяемого состояния элемента управления. Изменения, внесенные в управление состоянием представления в ASP.NET 2.0 В реализацию функции поддержки состояния представления в ASP.NET 2.0 было внесено два важных изменения, призванных исправить два важных недостатка этой функции или хотя бы уменьшить их эффект. Во-первых, благодаря новому фор- мату сериализации значительно уменьшился размер состояния представления. Его содержимое было поделено на две части: классическое состояние представления и состояние элемента управления. В отличие от классического состояния представле- ния состояние элемента нельзя отключать программно, и оно считается приватным состоянием элемента управления. Такое нововведение позволяет разработчикам эле- ментов управления ASP.NET, особенно тем, кто создает их на продажу, защитить их нуждающиеся в сохранении свойства от потери данных вследствие отключения раз- работчиком страницы поддержки состояния представления.
Управление состоянием Глава 13 543 Формат сериализации На рис. 13-9 показана одна и та же страница, запущенная в системах ASP.NET 1.x и ASP.NET 2.0. Эта страница содержит кнопку, по щелчку которой клиентский сцена- рий извлекает строку состояния представления и выводит в диалоговом окне ее длину: <script language="javascript"> function ShowViewStateSize() { var buf = document.forms[0]["___VIEWSTATE"].value; alert("View state is " + buf.length + " bytes"); } </script> Рис. 13-9. Сравнение размера состояния представления в ASP.NET 1.x и ASP.NET 2.0 Как видим, для одной и той же страницы выводятся разные цифры, показываю- щие, что размер состояния представления в ASP.NET 2.0 значительно уменьшился. Давайте теперь посмотрим, как это стало возможно.
544 Часть III Инфраструктура ASP.NET Вы уже знаете, что содержимое поля состояния представления — это результат сериализации данных, сохраняемых в состоянии представления всеми объектами страницы и ею самой. При обходе дерева элементов управления эти данные нака- пливаются в кортежах, составляемых из особых контейнеров, таких как Pair и Triplet. Это очень простые классы, содержащие соответственно два и три члена типа object. Каждый объект сериализуется в поток согласно следующим правилам. Значения простых типов, такие как строки, даты, байты, символы, значения булева типа и все виды чисел записываются в двоичный поток как есть. Значения перечислимых типов, а также словарные объекты Color, Туре и Unit сериализуются по правилам, определенным для их типов. Для остальных объектов выполняется проверка наличия конвертера типов, пре- образующего их в строки, и если такой конвертер имеется, он используется для сериализации. При отсутствии указанного конвертера для сериализации объекта используется класс Binary Formatter. Если тип не является сериализуемым, выбрасывается ис- ключение. Объекты Pair, Triplet и ArrayList сериализуются рекурсивно. Полученный в результате описанного процесса поток хэшируется или шифруется в зависимости от конфигурационной установки и в кодировке Base64 записывается в текстовое поле. Примерно так все происходило и в ASP.NET 1.x. В чем же различие? А различие состоит в маленькой детали, которая здесь не упомянута. В приложе- нии ASP.NET 2.0 тип каждого сериализованного значения идентифицируется бай- том — непечатным символом в диапазоне от 1 до 50. В приложении ASP.NET 1.x для этого использовались печатные символы, такие как <, >, 1, s, I и некоторые другие. Это изменение и позволило уменьшить размер состояния представления и несколько ускорить процесс десериализации. | мцП Примечание Для сериализации состояния представления в формат ASP.NET 2.0 исполь- зуется новый класс двоичного форматирования — ObjectStateFormatter, выводящий резуль- тат в объект записи двоичных данных. Специально разработанный для сериализации и десериализации графов объектов в ASP.NET 2.0 этот класс является еще одним классом форматирования .NET Framework реализующим интерфейс /Formatter. В ASP NET 2.0 класс ObjectStateFormatter заменил класс LosFormatter, использовавшийся в ASP.NET 1.x. Послед- ний выводит данные в объект записи текста. Изменение типа объекта записи само по себе не является оптимизацией, но позволяет внести другие усовершенствования, например, индексировать строки, а также компактнее хранить данные при записи в двоичной форме значений, не являющихся строковыми (например, чисел и значений булева типа). Состояние элемента управления Для серверных элементов управления типично сохранение информации между воз- вратами формы. Представьте, например, что пользователь щелкнул заголовок столбца элемента управления DataGrid, чтобы изменить порядок сортировки его строк. Эле- мент управления должен сравнить старое и новое выражения сортировки. Если они одинаковые, направление сортировки изменяется. А как элемент управления может определить старое выражение сортировки? Если не сохранить его своевременно в состоянии представления, оно будет утрачено, и элемент управления не сможет пра- вильно выполнять сортировку по команде пользователя. Подобные свойства не предназначены для конфигурирования элементов управ- ления, как стилевые и цветовые свойства, — они связаны с его работой. Но что если
Управление состоянием Глава 13 545 элемент управления будет размещен на странице, для которой отключена поддержка состояния представления. В ASP.NET 1.x это приведет к его неправильной работе. Значения приватных и защищенных свойств, которые должны сохраняться между возвратами формы, просто не следует записывать в состояние представления, обратив- шись к другим средствам хранения данных — кэшу, состоянию сеанса или, возможно, пользовательскому скрытому полю. Но в ASP.NET 2.0 появилось лучшее решение, здесь, как я уже говорил, введено по- нятие состояния элемента управления. Это особый контейнер данных, исполняющий роль защищенной зоны в составе общего состояния представления, поддержка которой не подлежит отключению. Если значения приватных или защищенных свойств соз- данных вами ранее элементов управления сохраняются в состоянии представления, следует сменить их контейнер на состояние элемента управления. Но помните, что состояние элемента управления, как и состояние представления страницы, передается между клиентом и сервером, поэтому его тоже не следует пере- гружать данными — в этом отношении к нему применимо все, что было сказано ранее относительно размера состояния представления. Реализация состояния элемента управления Задача реализации состояния элемента управления возложена на программиста, что одновременно и хорошо, и плохо. Плохо потому, что вам придется самостоятельно вы- полнять сериализацию и десериализацию состояния элемента управления. А хорошо, поскольку вы можете контролировать каждый нюанс работы элемента управления и оптимизировать его код для достижения оптимальной производительности. Инфра- структура страницы инициирует кодирование и сериализацию данных в нужный момент. Состояние элемента управления обрабатывается вместе с информацией со- стояния представления и точно так же подлежит сериализации и записи в кодировке Base64. Более того, оно сохраняется в том же скрытом поле. Корневой объект, сери- ализуемый в поток состояния, — это объект Pair, первый элемент которого содержит состояние элемента управления, а второй — классическое состояние представления. Не существует готового словарного объекта для хранения элементов, образующих состояние представления. Вам больше не нужно помещать данные в фиксированный контейнер, такой как ViewState, и вы можете держать их в приватных и защищенных членах класса элемента управления, благодаря чему ускоряется доступ к этим данным. Например, если требуется отслеживать направление сортировки данных элемента управления, для этого достаточно такой переменной: private int _sortDirection; тогда как в ASP.NET 1.x потребовался бы следующий код: private int _sortDirection { get {return Parse.Int32(ViewState["SortDirection ']);.) set {ViewState["SortDirection"] = value;) } Для восстановления состояния представления класс Page вызывает метод LoadCon- trolState для всех элементов управления, зарегистрированных в объекте страницы как элементы, сохраняющие свое состояние. Типичное поведение этого метода де- монстрирует следующий псевдокод: private override void LoadControlState(object savedState) { // Создаем копию сохраненного состояния.
546 Часть III Инфраструктура ASP.NET // Тип объекта нам известен, поскольку мы Ц сохранили его в методе SaveControlState. objectf] currentstate = (object!]) savedState; if (currentstate == null) return; // Инициализируем приватный или защищенный член, // сохраненный в состоянии представления. Значения // хранятся в том порядке и формате, в каком // мы их записали в методе SaveControlState. _myProperty1 = (int) currentState[O]; _myProperty2 = (string) currentState[1]; 1 Метод LoadControlState получает объект, идентичный тому, который был создан методом SaveControlState. Как разработчик элемента управления, вы хорошо знаете структуру этого объекта и можете извлечь информацию, необходимую для восстанов- ления состояния представления. Например, вы можете использовать массив объектов, каждый слот которого содержит значение определенного свойства элемента управления. А вот псевдокод, который поможет вам составить представление о методе Save- ControlState: private override object SaveControlState() { // Объявляем массив объектов нужного размера object!] stateToSave = new Object!...]; // Заполняем массив значениями локальных свойств stateToSave!0] = jnyPropertyl; stateToSave!1] = _myProperty2; // Возвращаем массив return stateToSave; 1 Вы объявляете новую структуру данных (такую как Pair, Triplet, массив или объект пользовательского типа) и заполняете ее приватными свойствами, которые хотите сохранять между возвратами формы. Завершая свою работу, метод возвращает создан- ный объект исполняющей среде ASP.NET. Затем объект сериализуется и кодируется в поток Base64. Разумеется, класс, в который вы записали данные состояния элемента управления, должен быть сериализуемым. Подробнее о состоянии представления рас- сказывается в моей книге «Programming Microsoft ASP.NET 2.0 Applications: Advanced Topics» (Microsoft Press, 2005). Сохранение состояния представления страницы на сервере Как я уже говорил, существует причина, по которой вы можете предпочесть хранение состояния представления на сервере: чем больше данных оно содержит, тем больше времени занимает загрузка страницы. Скрытое поле страницы вовсе не является един- ственно возможным местом хранения состояния представления, а лишь используемым по умолчанию. Посмотрим, как можно сохранять его на сервере в дисковом файле. Класс LosFormatter Если вы хотите разработать альтернативную схему хранения состояния представле- ния, то вам прежде всего необходимо завладеть строкой, которую ASP.NET сохраняет
Управление состоянием Глава 13 547 в скрытом поле страницы. Эту строку можно затем сохранить в серверном файле и считывать оттуда при обработке страницы. Для сериализации и десериализации данных состояния представления ASP.NET 1.x использует класс LosFormatter. Интерфейс этого класса очень простой и содержит всего два метода. Метод Serialize записывает сериализованное представление данных в объект Stream или TextWriter. public void Serialize(Stream stream, object viewState); public void Serialize(TextWriter output, object viewState); А метод Deserialize формирует объект на основе данных, прочитанных из потока, объекта TextReader или просто строки в кодировке Base64: public object Deserialize(Stream stream) public object Deserialize(TextReader input); public object Deserialize(string input); Класс LosFormatter является точкой входа механизма управления состоянием пред- ставления. Используя его, вы можете быть уверены, что все будет происходить так, как происходит по умолчанию, и, более того, вы будете изолированы от ненужных деталей. Класс ObjectStateFormatter Как упоминалось, класс LosFormatter заменен в ASP.NET 2.0 классом ObjectStateFormatter. Первый из них все еще доступен и поддерживается с целью обратной совместимости. Новый сериализатор записывает данные в объект записи двоичных данных, тогда как класс LosFormatter помещал их в объект записи текста. Классу LosFormatter необхо- димо преобразовывать любые данные к строковому типу, а класс ObjectStateFormatter использует исходную двоичную модель и просто выводит байты. Поэтому результат работы класса ObjectStateFormatter получается вдвое более компактным и на сериа- лизацию и десериализацию затрачивается вдвое меньше времени. Метод Serialize класса ObjectStateFormatter записывает итоговое представление данных в кодировке Base64 в объект Stream либо в строковый объект: public string Serialize(Object graph); public void Serialize(Stream stream, Object graph); Метод Deserialize класса ObjectStateFormatter строит объект на основе потока или строки в кодировке Base64: public object Deserialize(Stream stream); public object Deserialize(string input); Создание страницы без состояния представления Класс Page имеет пару защищенных виртуальных методов, которые исполняющая среда вызывает, когда приходит время сериализации или десериализаци и состояния пред- ставления, — LoadPageStateFromPersistenceMedium и SavePageStateToPersistenceMedium: protected virtual void SavePageStateToPersistenceMedium(object viewState); protected virtual object LoadPageStateFromPersistenceMedium(); Если переопределить их оба, можно загружать и сохранять информацию состоя- ния представления, используя любое подходящее хранилище. Поскольку эти методы определены как защищенные, единственный способ их переопределения заключается в создании нового класса, производного от класса Page. Следующий код позволит вам составить представление о том, что делает метод LoadPageStateFromPersistenceMediurrr. string m_viewState = Request. Formf__VIEWSTATE"], ObjectStateFormatter m_formatter = new ObjectStateFormatter(); StateBag viewStateBag = m_formatter.Deserialize(m_viewState);
548 Часть III Инфраструктура ASP.NET А вот какую структуру имеет класс страницы: public class ServerViewStatePage : Page { protected override object LoadPageStateFromPersistenceMedium() { ... } protected override void SavePageStateToPersistenceMedium(object ViewState) { } } Сохранение состояния представления в файле на Web-сервере Задача метода SavePageStateToPersistenceMedium очень проста — он принимает стро- ковый аргумент, открывает выходной поток и вызывает сериализатор LosFormatter. protected override void SavePageStateToPersistenceMedium(object viewStateBag) { string file = GetFileName(); StreamWriter sw = new StreamWriter(file); ObjectStateFormatter m_formatter = new ObjectStateFormatter(); m_fоrmatter.Serialize(sw, viewStateBag); sw. CloseO; return; } Как выбрать имя файла, чтобы не было конфликтов? Состояние представления связано с запросом страницы, выполняемым в рамках определенного сеанса. Поэто- му для связывания запроса с нужным файлом удобно использовать идентификатор сеанса и URL запроса. В качестве альтернативы можно присвоить файлу имя, сгене- рированное случайным образом, и сохранить его в скрытом поле страницы. Заметьте, что в этом случае нельзя полагаться на скрытое поле _ VIEWSTATE, поскольку, пере- определив методы, вы изменили внутреннюю процедуру создания этого поля. Вызываемая в приведенном ранее коде функция GetFileName присваивает файлу имя следующей структуры: SessionID_URL.viewstate Заметьте, что для создания локального файла из приложения ASP.NET нужно заранее присвоить учетной записи ASP.NET необходимые разрешения. Я предлагаю создать для таких файлов отдельную вложенную папку. Удаление файлов закончив- шихся сеансов организовать непросто, и, пожалуй, наилучшим средством для этого будет сервис Windows. Загрузка состояния представления из файла Web-сервера В нашей реализации метод LoadPageStateFromPersistenceMedium сначала определяет имя файла, в котором находится состояние представления, извлекает из него строку в кодировке Base64, а затем вызывает метод LosFormatter для десериализации дан- ных: protected override object LoadPageStateFromPersistenceMedium() { object viewStateBag;
Управление состоянием Глава 13 549 string file = GetFileNameO; try { StreamReader sr = new StreamReader(file); string m_viewState = sr. ReadToEndO; sr.CloseO; } catch { throw new HttpException("The View State is invalid."); } LosFormatter m__formatter = new LosFormatter(); try { viewStateBag = m_formatter.Deserialize(m_viewState); } catch { throw new HttpException("The View State is invalid ); } return viewStateBag; } Для использования новой возможности (хранения состояния представления на сервере) необходимо изменить родительский класс в файле отделенного кода страницы — он должен быть производным от класса ServerViewStatePage: public partial class TestPage : ServerViewStatePage { } Содержимое файла состояния представления, находящегося во временной папке на компьютере Web-сервера, показано на рис. 13-10. В странице, для которой создан этот файл, включена поддержка состояния представления, но скрытое поле генерируемой ею клиентской страницы пустое. БЖ1 t No ViewStaie Ирге 1 eft on the Server Microsoft Internet I xplorer [Jb . Daw Favorites lock Help О Back ’ fa > ЙГЙ * Favorites 1 AjldryjljShttp://locahost:4263/ProAspNet20/Sanyies/Chl3/5erverVlewstate/TestPage.aspx f1 ,1 Sh'ow View' State 'Size'' [ Just post back... nompanyname Alfreds Fut AnaTrupHc Around the Berglunds s Blauer See Blondesddsl pire et fils Bdlido Comidas preparadas Bon app' Bottom-Bollar Markets contactname \nders tjiHo Moreno Hardy a Berglund contacttitle Sales Representative Owner Owner Sales Representative Order Administrator Sales Representative Рис. 13-10. Содержимое файла состояния представления
550 Часть HI Инфраструктура ASP.NET Заключение Как известно, протокол HTTP не сохраняет состояние, однако Web-приложения в та- ком режиме работать не могут. Функция сохранения состояния критична для их функ- ционирования, в чем они подобны любым другим приложениям. От того, насколько эффективно она будет реализована, зависит масштабируемость приложения. Одной из наиболее интенсивно используемых форм состояния является состояние сеанса — связанное с конкретным пользователем и поддерживаемое до тех пор, пока этот пользователь работает с приложением. Вы можете сохранять данные состояния сеанса в памяти рабочего процесса ASP.NET, внешнего процесса и даже в таблице SQL Server либо в другом хранилище, с которым работает пользовательский провайдер состояния сеанса. Несмотря на различия этих возможностей, все они реализуются посредством одного и того же интерфейса программирования. Более того, состояние сеанса в ASP.NET можно сохранять и в случае работы приложения в Web-саду или Web-ферме. В следующей главе вам предстоит знакомство с еще одним весьма мощным кон- тейнером состояния — объектом Cache. Только факты От внутреннего объекта Session классической ASP.NET объект состояния сеанса ASP.NET 2.0 отличается богатой функциональностью и радикально переработан- ной архитектурой, однако используется практически так же. В ASP.NET 2.0 имеется пять возможностей хранения состояния сеанса: локально в памяти рабочего процесса ASP.NET, в памяти удаленного процесса, в стандартной таблице SQL Server, в заданной вами таблице базы данных и, наконец, в произ- вольном хранилище, с которым будет работать пользовательский провайдер. • В ASP.NET 2.0 подсистема управления состоянием сеанса подверглась рефакто- рингу, и теперь разработчики могут заменять ее функциональные элементы. По умолчанию для всех серверных элементов управления включена поддержка состояния представления. Однако это не означает, что она необходима им всем и всегда. Напротив, ее применение следует контролировать, отключая эту функцию для тех элементов управления, которым она не нужна, чтобы не перегружать кли- ентские страницы. • В ASPNET 2.0 размер состояния представления значительно сокращен в результате введения нового формата двоичной сериализации. Содержимое состояния представления в ASP.NET 2.0 разделено на две части: клас- сическое состояние представления и состояние элемента управления. Последнее нельзя отключить программным способом, оно является приватным состоянием представления элемента управления.
Глава 14 Кэширование в ASP.NET Кэшированием называется служебная функция системы или приложения, которая заключается в сохранении часто используемых данных в некотором промежуточном хранилище (расположенном между приложением и его источником данных) для уско- рения их сохранения и извлечения. В типичном Web-сценарии этим промежуточным хранилищем является память Web-сервера, а источником данных — серверная система управления данными. Очевидно, что система кэширования может быть многоуровне- вой и строиться с учетом требований и характеристик конкретного приложения. ASP.NET поддерживает кэширование двух разных видов информации: данных приложения и вывода страниц. Для кэширования вывода страниц не нужно писать ни строчки кода — достаточно сконфигурировать соответствующим образом при- ложение во время его разработки. Для страниц, которые устаревают не слишком быстро, кэширование вывода — наилучший и простой способ повышения произво- дительности. Желая создать для приложения индивидуальную систему кэширования, можно воспользоваться специальным API ASP.NET, позволяющим хранить данные в глобальном управляемом системой объекте Cache. Это более гибкое решение, но для его реализации вам необходимо будет обучиться правильно применять API кэширо- вания, чтобы приложение получилось надежным и защищенным. Кэширование данных приложения Центральный объект системы кэширования ASP.NET, с которым работает ее API кэ- ширования, — Cache - это не просто контейнер глобальных данных, доступных всем сеансам, как описанный в предыдущей главе объект Application. Внутренний объект ASP.NET Application является просто глобальным контейнером данных приложения; в его состав входят индексное свойство для доступа к данным и вызываемый из при- ложения механизм блокирования, необходимый для сериализации доступа к данным, осуществляемого несколькими потоками одновременно. Что касается объекта Cache, то это более интеллектуальный контейнер, он сам сериализует многопоточный доступ, способен автоматически удалять из кэша неиспользуемые элементы и поддерживает различные виды зависимостей. Кроме того, объект Cache позволяет индивидуально определять для кэшируемых элементов политики устаревания, основанные на при- оритетах, а также на абсолютном или скользящем сроке хранения, и при удалении эле- мента из кэша может активизировать заданную вами функцию обратного вызова. Объект Application поддерживается ради обратной совместимости с приложени- ями классической ASP, которые были разработаны до появления ASP.NET. В новых приложениях вместо него следует использовать объект Cache. Класс Cache Класс Cache определен в пространстве имен System.Web.Caching и является новым элементом в наборе средств управления состоянием ASP.NET. Он действует как ре- позитарий данных и объектов, но это единственное, что объединяет его с классом HttpApplicationState.
552 Часть III Инфраструктура ASPNET Экземпляр класса Cache создается для каждого домена приложений И остается дей- ствительным все время, пока функционирует этот домен. Текущий экземпляр классй Cache можно получить через одноименное свойство объекта HttpContext или Page. J -ь ’*!( Cache и другие классы состояния Несмотря на общую цель — создание глобального репозитария данных приложения ASP.NET — классы Cache и HttpApplicationState существенно различаются. Класс Cache поддерживает многопоточное выполнение и не требует, чтобы вы явно блокировали и разблокировали используемые в приложении данные. Все критичные разделы его вну- треннего кода надежно защищены с помощью синхронизационных конструкций. Еще одно важное отличие класса Cache заключается в том, что данные в нем не обязательно сохраняются в течение всего времени работы приложения: для каждого хранящегося в кэше элемента могут быть определены длительность хранения и приоритет. Любой кэшируемый элемент может быть сконфигурирован так, чтобы спустя определенное число секунд он считался устаревшим и удалялся из памяти. Разрешает- ся также задавать приоритеты элементов, по которым объект Cache будет определять, какие из них следует удалить из памяти, если занята слишком большая ее часть. Поддерживаются различные типы зависимостей элементов: от штампа времени одно- го или более файлов либо каталогов, от изменения других кэшируемых элементов, от изменений в таблицах базы данных, а также от внешних событий. Областью видимости объектов классов Cache и HttpApplicationState является все приложение, то есть они доступны во всех активных сеансах. Однако эти объекты не поддерживают архитектур Web-фермы и Web-сада, поскольку не могут работать вне текущего домена приложения. Д Примечание Когда работа приложения поддерживается более чем одним AppDomain (как в архитектуре Web-фермы), необходимо, чтобы все домены приложения хранили в кэше одни и те же данные, если только кэшируемая информация не является динамической. Это не проблема, поскольку статические значения уровня приложения могут загружаться в кэш при инициализации. Если же кэшируемая информация является динамической, то это уже совсем другая ситуация; в таком случае следует подумать над организацией глобального контейнера, единого для всех компьютеров. Объект Cache обладает уникальной способностью автоматически освобождать память, удаляя из нее все неиспользуемые элементы. В остальном он имеет тот же программный интерфейс, присущий контейнерам словарного типа, что и объекты Application и Session. В отличие от объекта Session, содержащего данные только одного сеанса, объект Cache хранит данные, общие для всех сеансов и всех поль- зователей. [Д Примечание Если вам необходим глобальный репозитарий данных, подобный объекту Session, который способен работать в архитектуре Web-фермы или Web-сада, то увы! — такого объекта в Microsoft .NET Framework не существует Для создания кросс-компью- терного контейнера вам придется обратиться к какому-нибудь удаленному совместно используемому ресурсу, например внешнему сервису или СУБД. Но учтите, что доступ к данным, хранящимся в подобном контейнере, должен сериализоваться, что становится причиной серьезных задержек в работе приложений. Такая схема работы настолько слож- на, что сводит на нет все преимущества кэширования данных. Вам придется решать, что эффективнее: работать с готовыми данными, хранящимися в промежуточном слое, или каждый раз выполнять запросы на получение данных из источника. ASP.NET предостав- ляет готовую инфраструктуру для локального кэширования данных, потому что именно она требуется большинству приложения. Что касается инфраструктуры кэширования данных для Web-фермы, то если вы все же решите, что дело того стоит, вам придется реализовать ее самостоятельно.
Кэширование в ASP.NET Глава 14 553 Свойства класса Cache Класс Cache имеет несколько свойств и полей (табл. 14-1) свойства служат для до- ступа к элементам данных, а поля являются внутренними константами класса, пред- назначенными для конфигурирования политики устаревания элементов. Табл. 14-1. Свойства и открытые поля класса Cache Свойство Описание ’ • Count Возвращает количество элементов в кэше Item Индексное свойство, предоставляющее доступ к элементу кэша по значению ключа NoAbsoluteExpiration Статическая константа, указывающая, что заданный элемент никогда не устаревает NoSlidingExpiration Статическая константа, указывающая, что для заданного элемента отключен режим скользящего устаревания Полю NoAbsoluteExpiration с типом DateTime присвоено значение DateTime.Max- Value — максимальное значение даты-времени, поддерживаемое .NET Framework. По- ле NoSlidingExpiration имеет тип TimeSpan и содержит значение TimeSpan.Zero, ука- зывающее, что режим скользящего устаревания (о нем я расскажу позже) отключен. Свойство Item доступно для чтения и записи и может использоваться для поме- щения в кэш новых элементов. Если заданный в качестве аргумента этого свойства ключ не существует, добавляется новый элемент; в противном случае обновляется значение существующего элемента: Cache["MyItem"] = value; Хранящиеся в кэше данные имеют тип object, а их ключи являются строковыми и чувствительными к регистру. Когда с помощью свойства Item вы добавляете в кэш новый элемент, ему по умолчанию назначается ряд атрибутов. Политика устаревания для него не задана, нет функции обратного вызова, активизируемой при его удалении, а приоритет он имеет нормальный. Поэтому элемент остается в кэше бесконечно, точнее, до тех пор, пока не будет удален программно или пока не завершится работа приложения. Если вы хотите задать для добавляемого элемента другие значения атрибутов, пользуйтесь для его добавления методом Insert класса Cache, имеющим соответствующие аргументы. Методы класса Cache С помощью методов класса Cache можно добавлять, удалять и перебирать элементы кэша. Перечень этих методов приведен в табл. 14-2. Табл. 14-2. Методы класса Cache Метод_______________Описание_____________________________________________________ Add Добавляет в кэш заданный элемент. Метод позволяет определить зависимости, политику устаревания и приоритет элемента, а также за- дать функцию обратного вызова, которая будет активизироваться при его удалении. Если элемент с заданным ключом существует, метод возвращает этот элемент, но не заменяет ert> значение заданным вами Get Возвращарт значение заданного элемента Элемент идентифициру- ется ключом. Если элемент с заданным ключом не найден, метод воз- вращает null (Этот метод йспользуется в реализации аксессора get свойства Item) см. след. стр.
554 Часть III Инфраструктура ASP.NET Табл. 14-2. (окончание) Метод Описание GetEnumerator Insert Возвращает перечислитель для перебора элементов кэша Помещает в кэш заданный элемент. У данного метода есть несколько перегруженных версий, позволяющих задать зависимости, политику устаревания и приоритет, а также функцию обратного вызова, которая будет активизироваться при удалении элемента. Метод имеет тип void и, в отличие от метода Add, заменяет существующий элемент кэша, если элемент с заданным ключом уже существует. (Этот метод ис- пользуется в реализации мутатора set свойства Item) Remove Удаляет из кэша заданный элемент. Элемент идентифицируется клю- чом. Метод возвращает экземпляр удаленного объекта или null, если элемент с заданным ключом не найден Методы Add и Insert не принимают значения null в качестве ключа или значения элемента кэша, и если задать такое значение, будет выброшено исключение. Задавае- мый вами скользящий срок хранения элемента не может превышать одного года, иначе тоже будет выброшено исключение. Наконец, имейте в виду, что для одного и того же элемента не может быть задан и скользящий, и абсолютный срок хранения. Примечание Методы Add и Insert делают одно и то же, но имеют два существенных раз- личия. Если элемент с заданным ключом уже присутствует в кэше, метод Insert заменяет его значение, а метод Add оставляет все как есть, но не выбрасывает исключение. Кроме того, у метода Add только одна сигнатура, а метод Insert имеет несколько перегруженных версий. Внутренняя структура кэша Класс Cache наследует класс Object и реализует интерфейс [Enumerable. Этот интер- фейс служит оболочкой внутреннего класса, который и является настоящим контейне- ром данных. Какой именно класс используется для реализации кэша ASP.NET, зависит от количества центральных процессоров. Если имеется только один центральный процессор, — это класс CacheSingle, в противном случае — класс CacheMultiple. В обо- их случаях элементы кэша хранятся в виде хэш-таблиц, но у класса CacheSingle такая таблица одна, а у класса CacheMultiple их несколько, по одной на каждый центральный процессор. Архитектура объекта Cache представлена на рис. 14-1. Рис. 14-1. Внутренняя структура кэша ASP.NET Каждая хэш-таблица разделена на две части: открытую и приватную. В откры- той части размещаются все элементы, видимые пользовательским приложениям, а в приватной находятся системные данные. Кэш — это ресурс, который интенсивно
Кэширование в ASP.NET Глава 14 555 используется исполняющей средой ASP.NET, поэтому системные элементы отделены от пользовательских данных и приложения не имеют к ним доступа. Объект Cache открывает доступ лишь к пользовательской части кэша. Методы get и set его внутренних классов принимают флаг, указывающий, к какой части кэша осуществляется доступ. При вызове через класс Cache этот флаг всегда соответствует открытой части кэша. Помимо хэш-таблиц система кэширования содержит внутренние компоненты, ре- ализующие необходимый программный интерфейс и различные служебные функции, в частности алгоритм LRU (Least Recently Used — наиболее долго не использовав- шийся), применяемый для удаления элементов из кэша, когда в системе остается мало памяти, а также функции поддержки зависимостей и обратного вызова. Внимание! Когда ASP.NET функционирует на мультипроцессорном компьютере и ее рабочему процессу выделено несколько центральных процессоров, для каждого из них создается свой объект кэша, и эти объекты не синхронизируются. Кроме того, в Web-саду вы не можете рассчитывать на то, что каждый следующий запрос пользователя будет выполняться на том же процессоре. Поэтому система не гарантирует, что состояние кэша ASP.NET для двух последовательных запросов одной и той же страницы будет одинаковым. Работа с кэшем ASP.NET Экземпляр объекта Cache связывается с выполняющимся приложением и существует в течение всего его жизненного цикла. Этот объект содержит ссылки на данные и периодически проверяет, являются ли эти данные действительными. Когда в системе остается мало памяти, объект Cache автоматически удаляет редко используемые и наи- менее ценные кэшируемые элементы. Каждому элементу при его сохранении в кэше присваиваются атрибуты, определяющие его приоритет и политику устаревания, что позволяет программисту управлять механизмом удаления данных из кэша. Добавление в кэш новых элементов Элементы кэша характеризуются атрибутами, значения которых передаются в каче- стве аргументов методам Add и Insert. В частности, элемент, хранящийся в объекте Cache, имеет такие атрибуты. Ключ Чувствительная к регистру строка, используемая для идентификации элемента в хэш-таблице. Если заданный вами ключ равен null, выбрасывается исключение. Если же элемент с заданным ключом уже имеется в таблице, то ре- зультат попытки добавления в кэш нового элемента зависит от используемого метода: метод Insert заменяет старое значение новым, а метод Add оставляет все как есть. Значение Не равное null значение, представляющее кэшируемый элемент. Про- читанный из кэша элемент имеет тип Object, и его необходимо приводить к ис- ходному типу данных. Зависимости Объект типа CacheDependency, определяющий зависимость между элементом кэша с одной стороны и файлами, каталогами, таблицами базы данных или другими объектами — с другой. Когда объект, от которого зависит кэширу- емый элемент, модифицируется, этот элемент помечается как недействительный и автоматически удаляется из кэша. Абсолютное время устаревания Объект типа DateTime, представляющий абсо- лютные дату и время, с наступлением которых элемент автоматически удаляется
556 Часть III Инфраструктура ASP.NET из кэша. Если элемент никогда не устаревает, данному атрибуту следует присвоить константу NoAbsoluteExpiration, представляющую наибольшую возможную дату. Абсолютное время устаревания не изменяется при чтении и записи элемента. Скользящее время устаревания Объект типа TimeSpan, представляющий срок хранения элемента. Когда этому атрибуту присвоено ненулевое значение, при до- бавлении элемента абсолютное время устаревания автоматически устанавливается равным сумме текущего времени и срока хранения элемента. Однако если доступ к элементу будет осуществлен до того, как истечет срок его службы, абсолютное время его устаревания обновится. Таким образом, когда используется скользящее время устаревания, элемент удаляется лишь в случае, если в течение заданного времени к нему не производится ни одного обращения. Учтите, что при исполь- зовании скользящего времени устаревания абсолютное время явно задано быть не может — с точки зрения разработчика это взаимоисключающие параметры. Приоритет Элемент перечисления CacheltemPriority — значение в диапазоне от Low до NotRemovable. По умолчанию используется приоритет Normal. Элементы с наиболее низким приоритетом удаляются первыми Функция обратного вызова Функция, которую объект Cache вызывает после удаления элемента из кэша для уведомления приложения об этом факте. Как упоминалось в главе 13, когда управление состоянием сеанса осуществляется в режиме InProc, данная функция обратного вызова используется для генерирования события SessionJEnd. Тип ее делегата — CacheltemRemoveCallback. Существуют три способа добавления в объект Cache нового элемента: с помощью аксессора set свойства Item, методов Add и Insert. Свойство Item йозволяет задать ключ и значение. Метод Add имеет единственную сигнатуру с аргументами для каждого из перечисленных ранее атрибутов. Что касается метода Insert, то он является наиболее гибким из трех и позволяет задавать атрибуты выборочно: public void Insert(string, object); public void Insert(string, object, CacheDependency); public void Insert(string, object, CacheDependency, DateTime, TimeSpan); public void Insert(string, object, CacheDependency, DateTime, TimeSpan, CacheltemPriority, CacheltemRemovedCallback); Вот какой вызов выполняется на самом деле во время добавления элемента в кэш с помощью свойства Item-. Insert(key, value, null, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, CacheltemPriority.Normal, null); Когда вы вызываете метод Add для добавления в кэш элемента и обнаружива- ется, что элемент с заданным вами ключом уже существует, метод возвращает су- ществующий элемент (в виде значения типа Object) и не выбрасывает исключение. В противном случае, то есть если добавление прошло успешно, метод возвращает значение null. Удаление элементов из кэша Все элементы, для которых заданы политика устаревания или зависимости, автомати- чески удаляются из кэша в том или ином случае. Кроме того, вы можете программно удалить элемент из кэша с помощью метода Remove. Заметьте, что этот метод удаляет любой элемент, включая элементы с наивысшим уровнем приоритета {NotRemovable). Вот как осуществляется его вызов: object oldValue = Cache.Remove( Myltem");
Кэширование в ASP.NET Глава 14 557 Обычно метод возвращает значение, только что удаленное из кэша. Однако если заданный вами ключ не найден, метод возвращает значение null (не выбрасывая ис- ключение). 1 Если с элементом ассоциирована функция обратного вызова, она вызывается сразу после удаления элемента, а для указания причины удаления ей передается значение из перечисления CacheltemRemovedReason (табл. 14-3). Табл. 14-3. Элементы перечисления CacheltemRemovedReason Значение Причина удаления элемента из кэша Dependency Changed Изменился объект, от которого зависит данный элемент Expired Элемент устарел Removed Элемент удален из кэша программным способом, с помощью метода Remove, или же заменен новым элементом с помощью метода Insert или свойства Item Underused Элемент удален для освобождения памяти Отслеживание зависимостей элементов кэша Элемент, добавляемый в кэш с помощью метода Add или Insert, можно связать с груп- пой файлов или каталогов, существующих элементов кэша, таблиц баз данных или внешних событий. Связь между новым элементом и объектами, от которых он зависит, хранится в виде экземпляра класса CacheDependency. Объект CacheDependency может представлять как отдельный предмет зависимости (например, файл или каталог), так и их группу, заданную в виде массива. Класс CacheDependency имеет довольно вну- шительное число конструкторов с разными наборами аргументов (табл. 14-4). Табл. 14-4. Наборы аргументов конструкторов класса CacheDependency Аргументы конструктора Описание String String[] String, DateTime Stringf], DateTime String]], String]] String]], String]], CacheDependency. String]], String]], DateTime String]], String]], CacheDependency, DateTime Путь к файлу, то есть URL файла или имя каталога Массив путей к файлам Путь к файлу и время начала мониторинга Массив путей к файлам и значений времени начала мониторинга Массив путей к файлам и массив ключей кэша Массив путей к файлам, массив ключей кэша и отдельный объект CacheDependency Массив путей к файлам, массив ключей кэша и время начала мони- торинга Массив путей к файлам, массив ключей кэша, отдельный объект CacheDependency и время начала мониторинга Любое изменение объектов, подлежащих мониторингу, делает элемент кэша недей- ствительным, в результате чего он автоматически удаляется. По умолчанию монито- ринг начинается сразу после того, как элемент помещается в кэш, но при желании вы можете указать конкретное время его начала. Интересно, что предметом мониторинга, осуществляемого с помощью объекта CacheDependency, может быть другой объект CacheDependency — в таком случае зависимый элемент кэша становится недействи- тельным при изменении предметов мониторинга объекта CacheDependency, который сам является предметом мониторинга.
558 Часть III Инфраструктура ASP.NET Примечание В ASP.NET 2.0 класс зависимостей кэша был значительно усовершен- ствован. В ASP.NET 1 .х класс CacheDependency нельзя было наследовать, в связи с этим предметами мониторинга могли быть только файлы, каталоги и другие кэшируемые элементы. В ASP.NET 2.0 этот класс допускает наследование, что позволяет определять пользовательские зависимости. Кроме того, для мониторинга таблиц баз данных суще- ствует готовый класс. Ниже показано, как можно ассоциировать элемент кэша со штампом времени файла. Внесение в этот файл любого изменения будет делать элемент кэша недей- ствительным и приводить к его удалению из кэша: CacheDependency dep = new CacheDependency(filename); Cache Insert(key, value, dep); Имейте в виду, что объект CacheDependency принимает имена файлов и каталогов, заданные как абсолютные пути в файловой системе. Определение функции обратного вызова Удаление элемента из кэша — событие, асинхронное по отношению к приложению. Если приложение не знает, что элемент удален, оно может попытаться его прочитать, а в результате получит значение null. Чтобы этого не случилось, можно либо проверять, существует ли нужный вам элемент, либо, если требуется сразу узнавать об удалении элементов кэша, зарегистрировать функцию обратного вызова, которая будет заново загружать в кэш удаленный элемент. Такое решение прекрасно подходит для случаев, когда кэшируемый элемент представляет собой содержимое отслеживаемого файла или результат запроса к отслеживаемым таблицам базы данных. Как только в файл или таблицу вносятся изменения, их старые данные удаляются из кэша и вместо них загружается обновленная информация. Далее приведен код страницы, которая кэширует элемент с именем MyData, связав с ним функцию обратного вызова ReloadltemRemoved. В случае его удаления с указа- нием причины Dependency Changed элемент заново загружается в кэш. Содержимое этого элемента считывается из серверного файла. void Load_Click(object sender, EventArgs e) { AddFileContentsToCache("data.xml"); } void Read_Click(object sender, EventArgs e) { object data = Cachet"MyData”]; if (data == null) { contents.Text = "[No data available]"; return; } contents.Text = (string) data; } void AddFileContentsToCache(string fileName) { string file = Server.MapPath(fileName); StreamReader reader = new StreamReader(file); string buf = reader. ReadToEndO;
Кэширование в ASP NET Глава 14 559 reader.Close(); // Создаем элемент, помещаем в кэш и выводим на экран CreateAndCacheItem(buf, file); contents.Text = Cache["MyData"].ToString(); void CreateAndCacheItem(object buf, string file) { CacheltemRemovedCallback removal; removal = new CacheltemRemovedCallback(ReloadltemRemoved); CacheDependency dep = new CacheDependency(file); Cache.InsertC’MyData”, buf, dep, Cache.NoAbsoluteExpiration, Cache NoSlidingExpiration, CacheltemPriority.Normal, removal); } void ReloadItemRemoved(string key, object value, CacheltemRemovedReason reason) if (reason == CacheltemRemovedReason.DependencyChanged) { // К этому моменту элемент уже удален. Мы получаем // свежие данные и снова помещаем их в кэш if (key == "MyData") AddFileContentsToCache("data.xml"); // Этот код выполняется асинхронно по отношению к приложению, // как только разрушается зависимость. Для его тестирования // вы можете поместить сюда трассировочный код } } void Remove_Click(object sender, EventArgs e) { Cache.Remove("MyData”); } На рис. 14-2 показана демонстрационная страница, предназначенная для тести- рования API кэширования. Если исходный файл изменяется, зависимость разруша- ется, в результате чего элемент с содержимым файла удаляется из кэша, вызывается функция ReloadltemRemoved и новое содержимое файла помещается в кэш. Однако если элемент просто удаляется их кэша, последняя операция не производится, и при попытке прочитать элемент MyData из кэша приложение получает значение null. Подчеркну еще раз, что функция обратного вызова, определенная в файле кода страницы, автоматически вызывается объектом Cache при удалении элемента из кэша. Код этой функции выполняется асинхронно по отношению к странице. Если вы поместите в кэш элемент, зависимый от несуществующего файла, каталога или элемента кэша, этот элемент будет успешно кэширован, но если впоследствии будет создан объект, от которого он зависит, зависимость окажется разрушенной и элемент будет удален их кэша. Иными словами, предметом мониторинга вполне может быть несуществующий объект, и создание этого объекта трактуется как изменение предмета зависимости со всеми вытекающими последствиями.
560 Часть III Инфраструктура ASP.NET Рис. 14-2. Демонстрационная страница, предназначенная для тестирования поведения функции обратного вызова [У| Примечание Как только элемент, для которого определены зависимости, удаляется из кэша мониторинг предметов этих зависимостей прекращается. Для того чтобы задать функцию обратного вызова, которая активизируется при удалении элемента кэша, необходимо объявить переменную типа CacheRemovedltem- Callback и создать экземпляр этого типа, передав его конструктору ссылку на нужную функцию: CacheltemRemovedCallback removal; removal = new CacheltemRemovedCallback(ReloadltemRemoved); Созданный таким образом делегат передается методу, с помощью которого соз дается элемент кэша. CacheDependency dep = new CacheDependency(file); Cache.Insert("MyData", buf, dep, Cache NoAbso]uteExpiration, Cache.NoSlidingExpiration, CacheltemPriority.Normal, removal); Совет Определив функцию обратного вызова как статический метод, вы избавляетесь от необходимости держать в памяти экземпляр класса к которому она принадлежит Статические методы (на языке Microsoft Visual Basic .NET — Shared) вызываются непо- средственно для класса, а не для его экземпляра. Однако создание статической функ- ции обратного вызова влечет за собой другие вопросы, например, как с ее помощью повторно поместить в кэш удаленный оттуда элемент. Ведь для этого вам потребуется доступ к классу страницы, что из статического члена неосуществимо. Для решения этой проблемы можно создать статическое поле, скажем, ThisPage, и в обработчике события Pagejnit присвоить ему объект страницы (используя ключевое слово this в C# или Me в Visual Basic .NET). Тогда вы сможете вызывать методы экземпляра через статический член ThisPage даже из статического члена. Назначение элементам приоритета Каждому элементу в кэше назначается определенный приоритет — значение из пере- числения CacheltemPriority, в диапазоне от Low (низший приоритет) до NotRemovable (высший приоритет). По умолчанию элемент имеет приоритет Normal. Чем выше приоритет элемента, тем больше шансов у него остаться в памяти, когда начнется удаление элементов по причине ее нехватки. Назначить помещаемому в кэш элементу приоритет можно с помощью метода Add или Insert. Элементы перечисления CacheltemPriority описаны в табл. 14-5.
Кэширование в ASP.NET Глава 14 561 Табл. 14-5. Уровни приоритета элементов кэша Константа Значение Описание Low 1 Элементы с таким уровнем приоритета первыми удаляются из кэша, когда сервер освобождает память системы BelouNormal 2 Промежуточный уровень приоритета между Normal и Low Normal 3 Уровень приоритета, назначаемый по умолчанию; в част- ности, он назначается элементам, создаваемым с помощью свойства Item Default 3 То же, что Normal AboveNormal 4 Промежуточный уровень приоритета между Normal и High High 1 5 Когда сервер освобождает память системы, элементы с таким уровнем приоритета удаляются из кэша последними NotRemovable 6 Элементы с таким уровнем приоритета никогда не удаляют- ся из кэша. Пользоваться этой установкой следует осмотри- тельно Проектируя класс Cache, разработчики ASP.NET преследовали две цели: во-первых, обеспечить его эффективность и удобный программный доступ к глобальному репози- тарию данных приложения, а во-вторых, сделать класс достаточно интеллектуальным, чтобы он отслеживал объем свободной памяти системы и по достижении определенной нижней границы удалял кешируемые элементы из памяти. Интеллектуальное удале- ние данных является главным отличием объекта Cache от объекта HttpApplicationState, который сохраняет свое содержимое до завершения работы приложения (если только оно само не удалит какие-либо данные). Процесс удаления из кэша низкоприоритет- ных и редко используемых объектов называется сборкой мусора. Контроль за устареванием данных Элементы автоматически удаляются из кэша в трех случаях: при нехватке памяти (тогда в первую очередь удаляются элементы с наиболее низким приоритетом), из- За изменения объектов, от которых они зависят, а также вследствие устаревания. По умолчанию для добавляемого в кэш элемента ни срок хранения, ни абсолютное время его окончания не задаются. Если вы хотите задать одно из этих двух взаимоисклю- чающих значений, воспользуйтесь методом Add или Insert. Я уже упоминал о двух политиках устаревания: абсолютном и скользящем устаре- вании. При абсолютном устаревании задается время, по достижении которого элемент удаляется из кэша. Если нужно, чтобы он не удалялся никогда, следует задать значение DateTime.MaxValue или его псевдоним Cache.NoAbsoluteExpiration. При скользящем устаревании вы задаете срок хранения элемента в кэше, и этот срок автоматически возобновляется при каждом обращении к этому элементу. Для определения срока хранения используется объект TimeSpan, представляющий в .NET Framework интервал времени. Нулевой интервал представлен значением TimeSpan.Zero, а также статиче- ским полем NoSlidingExpiration класса Cache. Чтобы задать для элемента скользящий срок хранения, равный 10 минутам, нужно добавить этот элемент в кэш с помощью такого оператора: Insert(key, value, null, Cache.NoAbsoluteExpiration, TimeSpan.FromMinutes(10), CacheltemPriority.Normal, null): В результате время устаревания элемента будет установлено равным текущему времени плюс 10 минут. Но пусть вас не сбивает с толку тот факт, что, когда вы
562 Часть III Инфраструктура ASP.NET задаете срок скользящего устаревания, объект Cache автоматически устанавливает абсолютное время. Приведенный вызов и вызов Insert(key, value, null, DateTime.Now AddMinutes(IO), Cache NoSlidingExpiration, CacheltemPriority.Normal, null): принципиально различаются. Хотя в результате обоих вызовов абсолютное время устаревания элемента оказывается одинаковым, в первом случае для него задается политика скользящего, а во втором — абсолютного устаревания, когда абсолютное время устаревания при обращении к элементу не изменяется и он неизбежно удаля- ется из кэша в указанное время. Статистика использования памяти Сразу после своей инициализации объект Cache собирает статистическую ин- формацию о памяти системы и текущем состоянии системных ресурсов. Затем он регистрирует таймер для ежесекундной активизации собственной функции об- ратного вызова. Эта функция периодически обновляет статистику использования памяти и при необходимости активизирует модуль, отвечающий за сборку мусора. Статистика использования памяти собирается с помощью функций Win32, по- зволяющих получить сведения об объеме свободной физической и виртуальной памяти. Объект Cache характеризует состояние системных ресурсов в терминах низкой и высокой нагрузки. Каждое значение соответствует определенному проценту занятой памяти. Обычно низкой нагрузке соответствуют значения 15-40 %, а высокой — 45-65 %. Когда нагрузка на память достигает определенного граничного значения, редко используемые объекты удаляются из нее первыми в порядке их приоритета. Практические вопросы кэширования Эффективное кэширование — критичный фактор успеха Web-приложения. Обычно кэширование осуществляется с целью ускорения доступа к данным за счет их про- межуточного хранения, избавляющего приложение от необходимости обращаться к удаленным компьютерам, выполнять запросы к базе данных и выполнять другие дли- тельные операции Не менее важно кэширование и для записи, особенно в системах, где записывается большой объем данных Адресуя запросы записи промежуточной структуре, находящейся в памяти компьютера, вы отделяете приложение от сервиса, который отвечает за запись в их хранилище данных. Некоторые называют эту техно- логию пакетным обновлением, но на самом деле это не что иное, как разновидность кэширования данных для записи. API кэширования обеспечивает вас средствами, необходимыми для построения надежной стратегии кэширования. Однако с его использованием связано несколько практических вопросов. Кэширование или все-таки выборка? На этот вопрос ответ один: зависит от обстоятельств. В частности, от характеристик приложения и ваших целей. Например, когда для приложения важно малое время от- клика, без кэширования не обойтись. Количество кэшируемых данных и длительность их хранения в кэше — это те два параметра, подбирая которые, вы сможете добиться оптимальной производительности.
Кэширование в ASP.NET Глава 14 563 Поскольку технология кэширования ориентирована прежде всего на повторное использование данных, ту информацию, к которой приложение обращается редко, помещать в кэш не имеет смысла. Помимо частоты использования важно также то, являются данные специфичными для сеанса либо запроса или они используются во всех сеансах (запросах). Если вы имеете дело с данными, которые используются часто и в каждом сеансе, помещайте их в кэш без колебаний. Кэширование — это также и вопрос использования памяти. Она относительно дешева, и поэтому в серверной системе ее объем может быть весьма большим, однако если приложение плохо спроектировано, то ошибок, связанных с нехваткой памяти, все равно не избежать. Вместе с тем даже если кэширование не решает проблемы, оно может применяться как временная мера, позволяющая повысить производительность уже эксплуатирующегося приложения, а тем временем заниматься более основатель- ным его рефакторингом. Следует понимать, что существуют разновидности приложений, которые долж- ны предоставлять пользователям самые актуальные данные. Данные же в кэше по определению статичны и не отражают изменений, внесенных в источник данных после их загрузки. Допустимо ли предоставлять пользователю данные, не обновлявшиеся в течение нескольких секунд, и устроит ли его такое решение? Как правило, ответ на первый вопрос утвердительный, хотя пользователю это, возможно, придется до- казывать. Типичное Web-приложение едва ли может иметь дело с такими данными, которые нельзя кэшировать хотя бы одну-две секунды. Какими бы ни были требо- вания конечного пользователя, для большинства приложений кэширование вполне приемлемо. Системы реального времени и системы с высокой степенью параллелизма (например, системы заказа), разумеется, являются исключениями, но, как правило, кэширование в течение одной-двух секунд позволяет ускорить работу приложения без ухудшения качества сервиса. В целом можно сказать, что кэширование должно применяться всегда, за исключе- нием особых случаев. Как правило, если пользователь утверждает, что ему требуются «живые» данные, можно привести контраргументы, доказывающие, что несколько секунд их кэширования вполне приемлемы, зато позволяют минимизировать вложе- ния в программное и аппаратное обеспечение. Работа напрямую с реальными данными — дорогое удовольствие. Если вы решите действовать таким способом, то вначале убедитесь, что это действительно необходимо. Доступ к кэшированным данным осуществляется быстрее, но не забывайте и о том, что на них расходуется память. Неправильное применение технологии кэширования приводит к ошибкам из-за нехватки памяти, а также к снижению производительности приложения и Web-сервера в целом. Создание объекта-оболочки для объекта Cache Как уже упоминалось, объект Cache не дает гарантии, что когда вы обратитесь к дан- ным, помещенным ранее в кэш, они все еще будут там находиться. Поэтому никогда не следует полагаться на наличие значения, возвращенного методом Get или свойством Item. Безопасной может считаться лишь такая схема работы: object data = Cachet"MyData"]; if (data != null) // Данные имеются, обработайте их
564 Часть III Инфраструктура ASP.NET В этом фрагменте кода намеренно опущена ветвь else. Что нужно делать, когда за- прошенный элемент оказывается равным null? Можно отменить текущую операцию и вывести для пользователя дружественное сообщение, а можно заново загрузить данные. Какое бы решение вы не выбрали, оно не будет универсальным. Когда дело доходит до построения слоя кэширования, лучше всего использовать доменный подход. Иными словами, нужно избегать кэширования отдельных элемен- тов данных, извлекаемых только на основе ключей. Вместо этого следует разработать вспомогательный класс со специфическими для конкретных доменов данных свой- ствами, связанными с кэшируемыми элементами. Вот пример: public static class MyCache protected static class MyCacheEntries { public const string Customers = "Customers"; public static Customercollection Customers { get { object о = HttpContext.Current.CachetMyCacheEntries.Customers]; if (o == null) { HttpContext.Current.Trace Warn("Empty cache--reloading...”); LoadCustomersO; • // Здесь можно выполнить повторное получение данных из кэша, // например, так: // о = HttpContext.Current.Cache[MyCacheEntries.Customers]; // Однако в нашем упрощенном примере просто выводится // трассировочное сообщение. Метод LoadCustomersO повторно // загружает данные в кэш, но свойство Customers возвращает // значение null. Поэтому в демонстрационном приложении для // загрузки списка клиентов нужно два раза подряд быстро // щелкнуть кнопку загрузки. } return (Customercollection) о; } } protected static void LoadCustomersO { // Получаем данные (извлекаем из базы данных) Customercollection coll = ProAspNet20. DAL.Customers. LoadAHO; // Помещаем элемент в кэш (на 5 секунд) HttpContext.Си г rent.Cache.Inse rt(MyCacheEnt ries.Custome rs. coll, null, DateTime.Now.AddSeconds(5), Cache.NoSlidingExpiration); } } В составе класса MyCache определено свойство Customers типа CustomerCollection Его содержимое загружается из демонстрационного слоя доступа к данным, о котором мы говорили в главе 9, и сохраняется в кэше на 5 секунд. Свойство Customers скрывает
Кэширование в ASP.NET Глава 14 565 все детали управления кэшированием и обеспечивает предоставление хост-странице необходимых данных. Если элемент данных отсутствует в кэше, аксессор этого свой- ства выполняет повторную загрузку данных. Примечание Если разместить приведенный код вне класса отделенного кода страницы, вы не сможете обращаться в нем к объекту ASP.NET Cache просто по имени. В отличие от классической ASP, система ASP.NET не имеет внутренних объектов, то есть все си- стемные объекты, с которыми вы в ней можете работать, являются открытыми свойствами определенного класса. В частности, вне класса страницы, который имеет свойство Cache, обращаться к объекту Cache приходится таким образом: HttpContext.Current.Cache, то есть через статическое свойство Current класса HttpContext, возвращающее текущий контекст HTTP (см. главу 12). Вызывающая страница заполняет таблицу хранящимися в кэше данными с по- мощью такого кода: Customercollection data = MyCache.Customers; CustomerList.DataTextField = "CompanyName"; CustomerList.DataValueField = "ID"; CustomerList.DataSource = data; CustomerList.DataBind(); Написание класса-оболочки конкретных помещаемых в кэш данных облегчает реализацию надежного алгоритма доступа к данным, гарантирующего их получение и правильно работающего с каждой конкретной категорией (доменом) данных. В ре- зультате код получается более читабельным и облегчается его сопровождение. Примечание Данный подход потенциально эффективнее, чем использование встроен- ных функций кэширования, которые имеются у компонентов, представляющих источники данных. Во-первых, такой класс инкапсулирует все необходимые данные, а не только связанные с компонентом. Во-вторых, его реализация полностью вам подконтрольна, вы можете задавать приоритеты, функции обратного вызова, создавать сложные зависимости и выбирать имена элементов. В-третьих, данный подход пригоден для любых данных, а не только представленных объектами ADO.NET, как в случае с объектами SqlDataSource и ObjectDataSource. Его можно использовать при разработке собственного слоя доступа к данным, в результате чего вы получите набор классов, поддерживающих кэширование, которые можно будет связывать с компонентами, представляющими источники данных. Но если страницы вашего приложения просты (скажем, их основными элементами являются таблицы или другие связанные с данными элементы управления) и вы используете толь- ко объекты DataSet и DataTable, инфраструктура кэширования, реализованная в составе компонентов, представляющих источники данных, возможно, удовлетворит вашим нуждам. Перебор кэшируемых элементов Хотя в большинстве случаев вы просто обращаетесь к кэшируемым элементам по именам, полезно знать, как выполняется перебор всех хранящихся в кэше открытых элементов. Вы уже знаете, что класс Cache является разновидностью коллекции, эк- земпляр которой создается при запуске приложения. Поэтому его содержимое можно перебирать в цикле for...each. Следующий код показывает, как скопировать текущее содержимое кэша ASP.NET в новый объект DataTable'. private DataTable CacheToDataTableO { DataTable dt = CreateDataTableO; foreach(DictionaryEntry elem in HttpContext.Current.Cache) AddItemToTable(dt, eJem); . return dt; } -«А . /.
566 Часть III Инфраструктура ASP.NET private DataTable CreateDataTableO { DataTable dt = new DataTableO, dt Columns.Add(”Key", typeof(string)); dt.Columns.Add("Value", typeof(string)); return dt; } private void AddItemToTable(DataTable dt, DictionaryEntry elem) { DataRow row = dt NewRowO; row["Key"] = elem. Key. ToStringO; rowfValue"] = elem. Value. ToStringO; dt.Rows.Add(row); } Объект DataTable содержит два столбца — с ключами и со значениями элементов. Для помещения ключа и значения элемента кэша в поля строки таблицы использу- ется метод ToString, то есть строки и числа интерпретируются нормально, а вместо объектов в таблицу обычно помещаются имена их классов. Внимание! При переборе элементов кэша для каждого из них доступны два информаци- онных элемента: ключ и значение. Клиентская страница не имеет возможности прочитать приоритет элемента или узнать, какова политика его устаревания, — возвращаемый при переборе объект DictionaryEntry не содержит такой информации. Если она вам потребу- ется, воспользуйтесь API .NET Reflection. Учтите также, что поскольку объект Cache хранит данные в хэш-таблице, перечислитель возвращает элементы в случайном с позиций приложения порядке — они не упорядочены ни по алфавиту, ни по времени добавления в кэш, а лишь по внутренним хэш-кодам, используемым для их индексации. Очистка кэша В .NET Framework класс Cache не имеет метода для программного удаления из кэша всего его содержимого. Однако написать такой метод легко: public void Clear() { foreach(DictionaryEntry elem in Cache) { string s = elem.Key.ToStringO; Cache.Remove(s); } } Хотя в кэше ASP.NET данные приложения и системы разделены, желательно все же удалять элементы по одному. Если вам нужно поддерживать несколько кэшируемых элементов, лучше создайте для них класс-оболочку и в его состав включите метод, удаляющий все эти данные. Синхронизация доступа к кэшу Когда вы считываете или записываете отдельный элемент кэша, в отношении много- поточного выполнения эти операции надежно защищены. Объект Cache ASP.NET гарантирует, что никакой параллельно выполняющийся поток не вмешается в вашу работу. Но если вам нужно, чтобы несколько последовательных операций над объ- ектом Cache выполнялись подряд без вмешательства со стороны, об этом придется позаботиться самостоятельно. Рассмотрим следующий код:
Кэширование в ASP.NET Глава 14 567 int counter = -1; object о = Cachet"Counter"]; if (o == null) { // Извлекает последнее значение из базы данных // или в звращает значение по умолчанию counter = RetrieveLastKnownValueO; } else { counter = (int) Cachet"Counter"]; counter ++; Cachet"Counter"] = counter; } Операция инкрементирования счетчика, требующая двух обращений к объекту Cache, является атомарной — точнее, она должна выполняться как атомарная, но в данном примере это не так. Если каждое отдельное обращение к объекту Cache гарантировано выполняется как атомарная операция, то между ними к объекту вполне может обратиться другой поток, которому, в частности, ничего не стоит изменить значение элемента Counter. Поэтому когда потенциально возможно со- ревнование за кэшируемое значение, необходимо использовать специальные бло- кирующие конструкции, такие как оператор lock в C# или SyncLock в Microsoft Visual Basic .NET. £5) Внимание! Что именно следует блокировать? Если заблокировать весь объект Cache, то могут возникнуть серьезные проблемы. ASP.NET использует его весьма интенсивно, и его блокировка способна привести к существенному снижению производительности приложения. Однако чаще всего ASP.NET обращается к кэшу не через объект Cache, а через входящий в его состав контейнер данных, CacheSingle или CacheMultiple. Поэтому блокировка объекта Cache на многие компоненты ASP.NET не повлияет. И все же лично я не пошел бы на такой риск: заблокировав объект Cache, вы рискуете заблокировать активные модули и обработчики HTTP, относящиеся к конвейеру выполнения, а также другие страницы и сеансы приложения, которым на самом деле нужны вовсе не те эле- менты данных, доступ к которым вы хотите сериализовать. Лучший способ сериализации доступа заключается в использовании синхронизатора — промежуточного глобального объекта, который блокируется перед входом в тот участок кода, который чувствителен к параллельному выполнению: lock(yourSynchronizer) { // Здесь осуществляется доступ к объекту Cache. Данную // конструкцию следует повторять каждый раз, когда доступ // к кэшу должен выполняться последовательно. 1 Синхронизационный объект должен быть глобальным для приложения. Например, это может быть статический член, определенный в файле global.asax. Кэширование на уровне запроса Хотя в большинстве случаев кэшированию подлежат только глобальные данные, причем те, которые используются многими запросами и страницами, иногда, когда имеет значение даже самый малый прирост производительности, можно кэшировать данные, которые используются единственной страницей. Их тоже помещают в объект Cache, но существует и альтернативное решение. Рабочую информацию, совместно
568 Часть III Инфраструктура ASP.NET используемую несколькими элементами управления и компонентами, участвующими в обработке запроса, можно сохранять в глобальном контейнере, существующем на протяжении выполнения запроса Для этого прекрасно подойдет коллекция Items класса HttpContext (описанная в главе 12), поскольку по завершении выполнения запроса она освобождается и при работе с ней не нужно устанавливать ни явные, ни даже неявные блокировки — ведь контекст запроса у каждого запроса по определе- нию свой. Определение пользовательских зависимостей Сразу хочу вас предупредить: написание пользовательского класса зависимости — это не пикник, и нужно очень хорошо подумать, прежде чем браться за такое дело, а также тщательно спроектировать необходимую функциональность. Я упоминал, что класс CacheDependency является наследуемым, — вы можете сделать производным от него собственный класс и реализовать в нем дополнительные события, по наступлении которых кэшируемые элементы будут становиться недействительными. Базовый класс CacheDependency обеспечит связывание нового объекта зависимости с кэшем ASP.NET и выполнение служебных задач, относящихся к синхронизации и удалению объекта, а также к управлению временем начала кэширования. Давайте начнем изучение вопроса об определении пользовательских объектов зависимостей с рассмотрения существовавших в ASPNET 1.x ограничений класса CacheDependency, которые привели к удалению в ASPNET 2.0 атрибута sealed, запре- щавшего наследование этого класса. Ограничения зависимостей кэша в ASP.NET 1.x В ASP.NET 1.x с кэшируемым элементом может быть связано четыре типа зависимо- стей: от времени, файлов, других элементов и других зависимостей. Объект Cache из ASP.NET 1.x решает многие проблемы разработчиков, позволяя без особых усилий поддерживать в памяти коллекции часто используемых данных. Однако этот механизм не совершенен и не является расширяемым. Например, какие данные, по-вашему, должно держать в кэше ASP.NET распре- деленное приложение, управляемое данными? Во многих случаях это результаты запроса к базе данных. Однако если вы сами не реализовали соответствующую программную логику, что сделать очень непросто, объект Cache не поддерживает зависимостей от базы данных. Зависимость кэшируемого элемента от базы данных предполагает, что если в таблицы, данные которых он содержит, будут внесены изменения, этот элемент автоматически станет недействительным. В ASP.NET 1.x класс CacheDependency помечен атрибутом sealed, то есть он не является наследуе- мым, и в нем не предусмотрена никакая иная форма настройки, которая давала бы программистам возможность делать кэшируемые элементы недействительными на основе пользовательских условий. Когда речь идет об объекте Cache, важное отличие ASP.NET 1.x от ASP.NET 2.0 заключается в том, что последняя поддерживает пользовательские зависимости. Про- блема эта решена за счет открытости класса CacheDependency для наследования, а так- же введения готового класса SqlCacheDependency, который обеспечивает поддержку зависимостей от базы данных для SQL Server версии 7.0 и выше. Расширения базового класса CacheDependency Для обеспечения поддержки производных классов и их интеграции в инфраструкту- ру кэширования ASP.NET 2.0 была введена группа новых открытых и защищенных членов класса CacheDependency — они описаны в табл. 14-6.
Кэширование в ASP.NET Глава 14 569 Табл. 14-6. Новые члены класса CacheDependency Член Описание DependencyDispose Защищенный метод. Освобождает используемые классом ре- 1 сурсы GetUniquelD Открытый метод. Извлекает уникальный строковый иденти- фикатор объекта NotifyDependencyChanged Защищенный метод. Уведомляет базовый класс об изменении зависимости, представленной данным объектом SetUtcLastModified Защищенный метод. Отмечает время последнего изменения зависимости UtcLastModified Открытое свойство, доступное только для чтения. Принимает время последнего изменения зависимости. Это свойство суще- ствует в ASP.NET 1.x, но недоступно извне Как уже упоминалось, пользовательский класс зависимости полагается на роди- тельский класс только в отношении взаимодействия с объектом Cache. Метод Notify- Dependency Changed вызывается классами, наследующими CacheDependency, для уве- домления базового класса об изменении зависимости. В ответ базовый класс обновляет значения свойств HasChanged и UtcLastModified. Весь пользовательский код, который должен в этой связи выполняться, помещают в метод DependencyDispose. Получение уведомления о разрушении зависимости Как вы могли заметить, в открытом интерфейсе базового класса CacheDependency нет ни одного элемента, который позволил бы узнать, выполняется ли представленное зависимостью условие. Почему это так? Потому, что данный класс был разработан для поддержки ограниченного числа видов зависимостей. Для обнаружения изменений в файле объект CacheDependency создает внутренний объект его мониторинга и получает от него вызов, когда в файл вносится изменение. Более конкретно, класс CacheDependency создает объект FileSystemWatcher vi передает ему обработчик события. Подобным образом устанавливается программная связь между объектом CacheDependency и кэшируемым элементом, являющимся предме- том зависимости: когда подлежащий мониторингу элемент кэша изменяется, объект Cache вызывает внутренний метод объекта CacheDependency. Что все это означает для разработчика? Пользовательский объект зависимости должен быть способен получать уведом- ления от внешнего объекта, являющегося предметом мониторинга. В большинстве случаев организовать это непросто, если вы не можете установить связь с внешним механизмом уведомлений (таким как монитор файловой системы или подсистема уведомлений SQL Server 2005). Когда от подобного внешнего источника поступает уведомление, объект зависимости, используя инфраструктуру своего родительского класса, уведомляет объект Cache. Все это мы немного погодя рассмотрим на конкрет- ном примере. Класс AggregateCacheDependency ASP.NET 2.0 позволяет создавать зависимости не только от отдельных объектов, но и от их групп. Например, кэшируемый элемент может зависеть и от дискового фай- ла, и от таблицы SQL Server. Следующий код показывает, как создать элемент кэша с именем MyData, зависящий от двух разных файлов: // Создаем массив объектов CacheDependency CacheDependency depl = new CacheDependency(fileNamel);
570 Часть III Инфраструктура ASP.NET CacheDependency dep2 = new CacheDependency(fileName2); CacheDependency deps[] = {dep1, dep2}; // Создаем агрегатный объект AggregateCacheDependency aggDep = new AggregateCacheDependencyO; aggDep.Add(deps); Cache.Insert("MyData", data, aggDep) Любой пользовательский класс зависимостей кэша, включая SqlCacheDependency, наследует класс CacheDependency, так что массив может содержать зависимости лю- бого типа. В ASP.NET 2.0 класс AggregateCacheDependency создан как еще один пользова- тельский класс зависимости, наследующий CacheDependency. Зависимость кэшируемого элемента от XML-данных Предположим, что ваше приложение считывает некоторые данные, играющие клю- чевую роль в его функционировании, из пользовательского XML-файла и вы не хо- тите, чтобы доступ к этому файлу осуществлялся при выполнении каждого запроса. Поэтому вы решили кэшировать содержимое XML-файла, но при этом вам нужно отслеживать вносимые в него изменения, чтобы своевременно обновлять кэширован- ную копию. Если вы полагаете, что это как раз тот случай, когда нужно определять файловую зависимость, то вы правы. Однако в таком случае изменение штампа времени файла будет рассматривать- ся как сигнал к удалению элемента из кэша и повторному чтению файла с диска. В результате, даже если в файле будет изменен всего лишь комментарий либо узел, который не имеет отношения к вашему приложению, файл будет загружаться в кэш заново. Предположим далее, что такое положение дел вас не устраивает и вы хотели бы, чтобы элемент кэша становился недействительным только в случае изменения определенных узлов XML-файла. Тогда вам нужно создать класс пользовательской зависимости, который будет отслеживать результат выполнения определенного за- проса XPath. [ Примечание Если целевой источник данных предоставляет встроенный и полностью асинхронный механизм уведомления, подобный механизму уведомления об изменении данных, возвращаемых SQL-запросом, то вам достаточно просто им воспользоваться. В противном случае для обнаружения изменений в источнике данных можно опрашивать ресурс с приемлемой частотой. Проектирование класса XmlDataCacheDependency Для того чтобы вы лучше поняли суть концепции пользовательских зависимостей, давайте рассмотрим такой пример. Вам нужно кэшировать внутренний текст опреде- ленного узла XML-файла. Для этого вы решили определить пользовательский класс зависимости, объект которого, будучи созданным, кэширует текущее содержимое узла, а затем периодически считывает данный узел из памяти и сравнивает его с кэшированным значением. Обнаружив изменение, этот объект сигнализирует о раз- рушении зависимости. [ , Примечание Очевидно, что опрос не является оптимальным решением данной про- блемы; позднее я вкратце расскажу о более эффективном ее решении. Тем не менее, технология опроса очень часто применяется при реализации пользовательских зависи- мостей кэша.
Кэширование в ASP.NET Глава 14 571 Хороший способ опроса локального или удаленного ресурса основан на исполь- зовании функции обратного вызова, срабатывающей по таймеру. Вот краткая схема такого решения. 1. Пользовательский класс XmlDataCacheDependency подготавливается к работе. Он инициализирует некоторые свои внутренние свойства и запоминает частоту опроса, имя файла и выражение XPath. 2. После инициализации объект зависимости задает функцию обратного вызова, которая будет использоваться для доступа к файлу и проверки его содержимого. 3. В функции обратного вызова сравнивается значение, возвращенное выражением XPath, и ранее сохраненное значение. Если они различны, кэшируемый элемент делается недействительным. Разработчику не нужно заботиться о деталях процесса, в результате которого элемент кэша становится недействительным. Это за него делает стандартный класс ASP.NET 2.0 CacheDependency. Примечание Вам интересно, как объект Cache определяет, что зависимость разрушена? При добавлении в кэш элемента MyData с пользовательской зависимостью вместе с ним добавляется служебный элемент, от которого и ставится на самом деле в зависимость элемент MyData. Метод NotifyDependencyChanged изменяет этот служебный элемент, в результате чего элемент MyData автоматически становится недействительным. Этот процесс проиллюстрирован на рис. 14-3. Рис. 14-3. Пользовательские классы зависимостей используют вспомогательные кэшируемые элементы Реализация зависимости Далее приведена базовая реализация класса XmlDataCacheDependency. public class XmlDataCacheDependency : CacheDependency { // Внутренние члены static Timer -timer; int _pollSecs = 10; string _fileName; string _xpathExpression; string _currentValue; public XmlDataCacheDependency(string fileName, string xpath, int pollTime) {
572 Часть III Инфраструктура ASP.NET // Инициализируем внутренние члены .fileName = fileName; .xpathExpression = xpath; .poll Secs = pollTime; // Считываем текущее значение .currentValue = CheckFileO; I/ Инициализируем таймер if (_timer == null) { int ms = _pollSecs * 1000; TimerCallback cb = new TimerCallback(XmlDataCallback); _timer = new Timer(cb, this, ms, ms); } } 11 Текущее значение public string Currentvalue { get { return _currentValue } } // Функция вызывается каждые N секунд public void XmlDataCallback(object sender) { // Получаем ссылку на текущий объект зависимости XmlDataCacheDependency dep = (XmlDataCacheDependency)sender; // Проверяем, есть ли изменения, и если есть, уведомляем базовый класс string value = CheckFileO; if (!String.Equals(_currentValue, value)) dep.NotifyDependencyChanged(dep, EventArgs.Empty); } // Извлекает из файла текущее значение и возвращает его string CheckFileO { // Выполняем XPath-запрос к файлу XmlDocument doc = new XmlDocumentO; doc.Load(_fileName); XmlNode node = doc.SelectSingleNode(_xpathExpression); return node.InnerText; } // Освобождает внутренние ресурсы protected override void DependencyDisposeO { // Уничтожаем таймер, а дальше все как обычно _timer = null; base,DependencyDispose(); } }
Кэширование в ASP.NET Глава 14 573 При создании объекта зависимости выполняется XPath-запрос к файлу и его результат сохраняется во внутреннем члене объекта. Одновременно создается и ини- циализируется таймер, предназначенный для повторения запроса с определенной периодичностью. Каждый раз возвращаемое значение сравнивается с исходным зна- чением, хранящемся во внутреннем члене объекта, и при обнаружении различия вы- зывается метод Notify Dependency Changed базового класса CacheDependency, который делает недействительным элемент, хранящийся в объекте Cache. Тестирование пользовательской зависимости Объект написанного вами класса зависимости может использоваться в Web-приложе- нии в любой ситуации, в которой допустимо использование объекта CacheDependency. Например, вы создаете экземпляр своего класса в обработчике события PageLoad и передаете его методу CacheJnsert protected const string CacheKeyName = MyData"; protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) // Создаем новый объект зависимости XmlDataCacheDependency dep = new XmlDataCacheDependency( Se rve r.MapPath("employees.xml"), "MyDataSet/NorthwindEmployees/Employee[employeeid=3]/lastname", D; Cache.Insert(CacheKeyName, dep.Currentvalue, dep); } // Обновляем пользовательский интерфейс Msg Text = DisplayO; } Остальная часть страницы пишется как обычно, но нужно уделить особое внимание доступу к кэшируемому элементу, предусмотрев возможность того, что вследствие разрушения зависимости при попытке прочитать этот элемент из кэша может быть возвращено значение null. Вот пример правильно построенного кода: protected string DisplayO { object о = CachefCacheKeyName]; if (о == null) return "[No data available—dependency broken]"; else return (string) o; } Объект XmlDataCacheDependency позволяет отслеживать внесение изменений в XML-файл и определять, какие из них требуют обновления кэшируемых данных. Для идентификации подмножества важных для приложения узлов используется вы- ражение XPath. В нашем примере отслеживается содержимое узла lastname в записи сотрудника с идентификатором 3‘. <MyDataSet> <NorthwindEmployees> <Employee> <employeeid>3</employeeid>
574 Часть III Инфраструктура ASP.NET <lastname>Leverling</lastname> <fi r stname>Janet</firstname> <title>Sales Representative</title> </Employee> </NorthwindEmployees> </MyDataSet> Демонстрационная страница, в которой используется объект XmlDataCacheDepen- dency, представлена на рис. 14-4. Рис. 14-4. Демонстрационная страница до и после внесения изменений в XML-файл На верхнем снимке экрана показано, что видит пользователь, только что запро- сивший страницу. Вверху этой страницы выводится значение кэшируемого элемента данных. Если изменить содержимое указанного выше узла XML-файла и щелкнуть кнопку, инициирующую повторное чтение элемента из кэша, результат получится таким, как показано на нижнем снимке. Изменение других узлов файла никак не влияет на кэшируемое значение. Примечание В демонстрационном классе пользовательской зависимости я использовал метод пулинга, поскольку он является широко распространенным, а иногда и единственно возможным решением для реализации пользовательских зависимостей. Однако в данном конкретном случае это не лучшее решение. Для достижения оптимального результата следовало бы воспользоваться классом FileSystemWatcher, специально предназначенным для отслеживания изменений в дисковых файлах, и сконфигурировать его объект для работы с файлом XML. Лишь при обнаружении изменения следовало бы выполнять за- прос XPath, чтобы выяснить, относится ли это изменение к важному для приложения узлу файла. Класс FileSystemWatcher действует асинхронно по отношению к приложению, обеспечивая тем самым более высокую производительность.
Кэширование в ASP.NET Глава 14 575 Зависимость кэшируемого элемента от базы данных SQL Server Многие приложения ASP.NET извлекают данные из базы данных, кэшируют их и за- тем на их основе предоставляют информацию пользователю, например, в виде отчета. Связывание такого отчета с кэшируемыми данными позволяет сократить как время формирования отчета, так и трафик между Web-сервером и базой данных. Но следует помнить, что если со времени помещения данных в кэш они будут изменены в базе данных, пользователь получит отчет с устаревшей информацией. Когда изменения происходят с заранее известной частотой, можно просто установить определенную длительность кэширования данных и обновлять их с такой же периодичностью. Од- нако такая мера не пригодна для случаев, когда предоставление актуальных данных критично для приложения, а также когда изменения происходят редко или частота их непредсказуема. В подобных ситуациях невозможно подобрать оптимальную частоту обновления кэшируемых данных: задаете большой интервал — рискуете предоставить пользователю устаревшие данные, малый — увеличиваете нагрузку на приложение, выполняя ненужные запросы. Решением проблемы является определение пользовательской зависимости от базы данных: как только исходные данные запроса в таблицах базы данных изменяются, ваш объект зависимости делает их кэшированную копию недействительной и запрос выполняется снова. В ASP.NET 1.x такие зависимости не поддерживались. Однако в ASP.NET 2.0 для них был разработан специальный класс SqlCacheDependency, на- следующий класс CacheDependency и поддерживающий зависимости от таблиц SQL Server. Этот класс совместим с MSDE, SQL Server 7.0 и последующими версиями SQL Server, включая SQL Server 2005. Реализация зависимости от базы данных Сегодня ни одна из СУБД, за исключением SQL Server 2005, не предлагает функцию отслеживания изменений в определенном наборе данных. Это означает, что в СУБД SQL Server 7.0, SQL Server 2000, а также СУБД, отличных от SQL Server, для реали- зации указанной функции вам пришлось бы самостоятельно создавать необходимую инфраструктуру уровня базы данных — такую, которая бы отслеживала изменения в определенных таблицах и уведомляла о них кэш ASP.NET. В течение последних лет командой создателей ASP.NET и сообществом разра- ботчиков исследовано несколько таких технологий. Ни одна из них не идеальна, но с ними стоит ознакомиться, если возникнет необходимость реализовать зависимость кэша от базы данных в приложениях ASPNET 1.x. Реализация такой зависимости базируется на двух ключевых элементах: механиз- ме обнаружения изменений в базе данных и механизме уведомления о них ASP.NET. Для обнаружения изменений обычно используются триггеры. С каждой таблицей базы данных, подлежащей мониторингу на предмет изменений, должен быть связан активный триггер. Этот триггер будет перехватывать операции вставки, обновления и удаления данных таблицы и выполнять определенные действия. Какие именно это будут действия, зависит от реализуемого вами механизма. В частности, предлагаются следующие два подхода. Расширенная хранимая процедура вызывает специфический для приложения HTTP-обработчик (он проще страницы). Этот обработчик получает ключ кэши- руемого элемента и удаляет элемент из кэша. Расширенная хранимая процедура модифицирует штамп времени дискового файла, от которого зависит кэшируемый элемент.
576 Часть III Инфраструктура ASP.NET Хотя некоторые программисты недолюбливают триггеры, лично я нахожу, что гораздо более серьезные проблемы при реализации описанного решения связаны с применением расширенных хранимых процедур. Они должны писаться на C++ и вручную развертываться в SQL Server. Более того, для их выполнения требуются права администратора, поскольку они представляют собой внешние программы и при их применении встает вопрос о блокировках. Расширенная хранимая процедура не может завершить свою работу, пока не завершится вызов HTTP или модификация файла. Но что если время ответа Web-сервера окажется слишком большим? А что если файл будет заблокирован? И наконец, поскольку в процессе участвует база данных, нагрузка на нее увеличивается и поток информации к ней и от нее может замедлиться. Указанные решения могут годиться для небольших приложений, в от- ношении которых не стоит вопрос масштабируемости, но для крупномасштабных сайтов они едва ли пригодны. Совет Если вы не любите триггеры, можете попробовать воспользоваться функциями V проверки контрольных сумм, определенными в языке Т-SQL. Например, следующий за- прос возвращает значение, которое изменяется при изменении любой записи таблиц Customers: SELECT CHECKSUM_AGG(BINARY_CHECKSUM(*)) FROM Customers Функции контрольных сумм выполняются быстро, но они не работают со ссылочными столбцами, содержащими текст и изображения Преимущество этих функций в том, что развертыванию в базе данных подлежит только хранимая процедура, содержащая запрос, подобный приведенному выше. Решение на основе триггеров и расширенных хранимых процедур реализует модель с активной базой данных, когда последняя сама сообщает об изменениях приложению ASP.NET. Возможен и обратный подход, основанный на опросе, и именно он является основой реализации зависимостей кэша от базы данных в ASP.NET 2.0. В базу данных помещаются триггеры и вспомогательная таблица, содержащая по одной записи на каждую таблицу, подлежащую мониторингу. Триггеры обновляют эту вспомогательную таблицу, когда в основные таблицы базы данных вносятся из- менения. Пользовательский компонент ASP.NET опрашивает эту вспомогательную таблицу на предмет наличия изменений, а поскольку она очень мала, результаты помещаются в память SQL Server для повышения производительности. Когда ком- понент, производящий опрос, обнаруживает изменения, он делает связанный с ним кэшируемый элемент данных недействительным. Примечание Реализация зависимостей от базы данных в ASP.NET 1.x осуществляется следующим образом. Вы создаете класс CacheDependency, воспроизводящий поведение одноименного класса из ASP.NET 2.0. В своем конструкторе этот абстрактный класс должен запускать таймер и вызывать переопределяемый метод, скажем, HasChanged, служащий для проверки наличия изменений. Далее вы создаете класс DatabaseCacheDependency, производный от CacheDependency, и переопределяете в нем метод HasChanged, чтобы он выполнял запрос к вспомогательной таблице или проверял контрольные суммы основных таблиц. Для помещения в кэш данных, связанных с объектом класса DatabaseCacheDe- pendency, вы используете вспомогательный метод, расширяющий функции метода Insert объекта Cache. Он должен создавать пару элементов кэша: один для реальных данных, а второй — вспомогательный, используемый объектом зависимости. Подобная схема была продемонстрирована на рис. 14-3. Обнаружив изменения, объект DatabaseCacheDependency обновляет вспомогательный элемент кэша, в результате чего основной элемент становит- ся недействительным. Подробное описание такого решения и демонстрационный код вы найдете в статье http://msdn.microsoft.com/msdnmag/issues/04/07/CuttingEdge.
Кэширование в ASP.NET Глава 14 577 Зависимость от базы данных в ASP.NET 2.0 В ASP.NET 2.0 зависимость от базы данных реализована в виде класса SqlCacheDe- pendency, который работает с SQL Server 7.0, SQL Server 2000 и SQL Server 2005. Для его использования с SQL Server 2005 вам придется выполнить минимум работы, а в случае с SQL Server 7.0 и SQL Server 2000 нужно будет произвести некоторые до- полнительные действия. С них и начнем. Для того чтобы класс SqlCacheDependency мог правильно выполнять свою за- дачу, для таблиц, подлежащих мониторингу, должна быть включена поддержка уведомлений. Для этого необходимо внести в базу данных изменения, требующие административных привилегий, и сделать это нужно до того, как приложение будет запущено. Более конкретно, необходимо создать специальные триггеры и храни- мые процедуры, которые будут обрабатывать входящие запросы UPDATE, INSERT и DELETE. выполнения всех необходимых операций можно воспользоваться утилитой с интерфейсом командной строки aspnetregsql. Сначала вы включаете поддержку уведомлений для базы данных, а затем для одной или более ее таблиц. Например, для включения поддержки уведомлений в базе данных Northwind нужно выполнить следующую команду на компьютере, где установлена SQL Server: aspnet_regsql.exe -S (local) -U YourUserName -P YourPassword -d Northwind -ed Включение уведомлений для таблицы Customers выполняется такой командой: aspnet_regsql.exe -S (local) -U YourUserName -Р YourPassword -d Northwind -et -t Customers Первая команда добавляет в базу данных новую таблицу с именем AspNetJSql- CacheTablesForChangeNotification плюс несколько хранимых процедур и триггеров. Заметьте, что для выполнения указанных действий нужно задавать имя пользователя, имеющего необходимые разрешения. Вторая команда добавляет триггер, связанный с заданной таблицей, и вставляет относящуюся к ней строку в таблицу AspNetSqlCacheTablesForChangeNotification. Вот этот триггер: CREATE TRIGGER dbo [Customers_AspNet_SqlCacheNotification_Trigger] ON [Customers] FOR INSERT, UPDATE, DELETE AS BEGIN SET NOCOUNT ON EXEC dbo.AspNetJJqlCacheUpdateChangeldStoredProcedure N'Customers’ END На рис. 14-5 показана структура служебной таблицы AspNetJSqlCacheTablesFor- ChangeNotification (напомню, что она содержит по одной записи на каждую таблицу базы данных, подлежащую мониторингу). Когда в таблицу, за которой ведется наблюдение, вносится изменение, триггер выполняет следующую хранимую процедуру: BEGIN UPDATE dbo.AspNet_SqlCacheTablesForChangeNotification WITH (ROWLOCK) SET changeld = changeld + 1 WHERE tableName = @tableName END
578 Часть III Инфраструктура ASP.NET Рис. 14-5. Структура таблицы AspNet_SqlCacheTablesForChangeNotification Заключительным шагом подготовки к использованию класса SqlCacheDependency является создание в файле web.config следующего сценария: <system web> <caching> <sqlCacheDependency enabled="true" pollTime="1000" > <databases> Odd name="Northwind" connectionStringName="LocalNWind" /> </databases> </sqlCacheDependency> </caching> </system.web> В атрибуте pollTime задается период опроса в миллисекундах. В приведенном примере проверка наличия изменений в таблицах производится ежесекундно. В узле <databases> размещаются ссылки на подлежащие мониторингу базы данных. Атрибут пате содержит имя зависимости, а атрибут connectionStringName указывает на эле- мент раздела <connectionStrings> файла web.config, содержащий строку подключения к базе данных. Примечание Для создания среды выполнения, необходимой для работы зависимостей от баз данных SQL Server 7 и SQL Server 2000, помимо утилиты aspnet_regsql с интер- фейсом командной строки можно использовать программный интерфейс. Так, следующий код включает поддержку уведомлений для базы данных Northwind: SqlCacheDependencyAdmin.EnableNotifications("Northwind"); Добавление таблицы в список таблиц, подлежащих мониторингу, осуществляется так: SqlCacheDependencyAdmin.EnableTableForNotifications( "Northwind”, "Employees"); Кроме того, класс SqlCacheDependencyAdmin содержит методы для отключения включен- ных ранее зависимостей. Давайте посмотрим, как создается и используется объект SqlCacheDependency.
Кэширование в ASP.NET Глава 14 579 Использование зависимостей от базы данных SQL Server Класс SqlCacheDependency имеет два конструктора. Первый принимает объект Sql- Command, а второй — две строки: имя базы данных и имя таблицы. Конструктор, при- нимающий объект SqlCommand, предназначен только для работы с SQL Server 2005, а второй конструктор может работать с предыдущими версиями этой СУБД. Следующий код создает зависимость от базы данных SQL Server и связывает ее с ключом кэшируемого элемента: protected void AddToCache(object data) { string database = "Northwind ; string table = "Customers"; SqlCacheDependency dep = new SqlCacheDependency(database, table); Cache.Insert("MyData", data, dep); } protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) // Извлекаем данные из базы данных Customercollection data = Customers.LoadByCountry("USA"); // Кэшируем их с зависимостью от таблицы Customers AddToCache(data); } } Находящиеся в кэше данные можно связать с любым элементом управления, свя- занным с данными: Customercollection data = null; object о = Cachet"MyData"]; if (o != null) data = (CustomerCollection)o; else Trace.Warn( Null data") CustomerList.DataTextField = "CompanyName"; CustomerList.DataSource = data; CustomerList.DataBind(); При обновлении базы данных элемент MyData становится недействительным, и список оказывается пустым. Разумеется, в реальном приложении при получении из кэша значения null необходимо повторно загрузить данные из базы данных. • Внимание! Вы получаете уведомления об изменении таблицы как целого. Однако в при- веденном выше коде на экран выводится лишь следующее подмножество ее данных: SELECT * FROM customers WHERE country=‘USA' Если в таблицу будет, например, добавлена новая запись, вы получите уведомление независимо от значения поля country этой записи. Так же будет обстоять дело и при модификации или удалении одной из существующих записей. SQL Server 2005 поддерживает более высокий уровень гранулярности зависимостей, при котором уведомление об изменениях направляется приложению только при условии, что они затрагивают данные, возвращаемые заданным запросом.
580 Часть III Инфраструктура ASP.NET Настроив механизм уведомлений о вносимых в таблицу изменениях, можно ре- ализовать более интеллектуальную форму кэширования — с повторной загрузкой данных в случае их изменения: <asp:SqlDataSource ID="SqlDataSource1" runat="server" ConnectionString-”<%$ ConnectionStrings:LocalNWind %> ' SelectCommand="SELECT * FROM Customers" EnableCaching="true" SqlCacheDependency="Northwind Customers"> </asp:SqlDataSource> Вы присваиваете свойству SqlCacheDependency строку в формате база:таблица. Первый токен — это имя зависимости от базы данных, определенной в разделе <databases>, а второй — имя таблицы. Можно задать несколько зависимостей, раз- делив пары токенов символами точки с запятой: <asp SqlDataSource ID="SqlDataSource1" runat="server" EnableCaching="true" SqlCacheDependency="Northwind:Customers; Pubs:Authors" /> Конечно, чтобы все это сработало, функция кэширования должна быть включена. Примечание Хотя я упоминал только компонент SqlDataSource, свойство SqlCacheDependency имеется и у компонента ObjectDataSource. Но пользоваться этим свойством компонента Object- DataSource можно лишь при условии, что он получает данные в виде объектов ADO.NET. Зависимости от базы данных SQL Server 2005 Как уже упоминалось, класс SqlCacheDependency имеет два конструктора, один из которых в качестве единственного аргумента принимает объект SqlCommand, Этот конструктор используется для создания объектов SqlCacheDependency при работе с базами данных SQL Server 2005: protected void AddToCacheO { SqlConnection conn = new SqlConnection( Configu rationManage r.ConnectionSt rings["NWindO5"]. ConnectionSt ring); SqlCommand cmd = new SqlCommand( "SELECT * FROM Customers WHERE country='USA'", conn); SqlDataAdapter adapter = new SqlDataAdapter(cmd); DataTable data = new DataTableO; adapter.Fill(data); SqlCacheDependency dep = new SqlCacheDependency(cmd); Cache.Insert("MyData", data, dep); } Заметьте, что когда данные хранятся в базе данных SQL Server 2005 никакие под- готовительные работы не требуются и в базу данных не приходится добавлять ника- кие внешние объекты. Не нужны ни триггеры, ни хранимые процедуры, ни таблицы уведомлений. В состав SQL Server 2005 входит специальный компонент, который отслеживает изменения на наиболее подходящем уровне, что было невозможно в предыдущих версиях этой СУБД. Данный компонент принимает командный объект и отслежива- ет изменения набора данных, возвращаемого заданной SQL-командой. Обнаружив такое изменение, компонент сообщает об этом объекту-слушателю. Клиентской со-
Кэширование ASP.NET Глава 14 581 ставляющей этого механизма является класс ADO NET SqlDependency, о котором рассказывалось в главе 7. Когда вы создаете экземпляр класса SqlCacheDependency и задаете команду, создается объект SqlDependency, служащий оболочкой этой команды и генерирующий событие при обнаружении изменений в извлекаемых с ее помощью данных. В свою очередь, объект SqlCacheDependency перехватывает это событие и делает кэшируемый элемент недействительным. Схема соответствующего потока данных показана на рис. 14-6. Делает соответствующий кэшируемый элемент недействительным Рис. 14-6. Внутренняя реализация объекта SqlCacheDependency при работе с SQL Server 2005 Примечание Реализация зависимостей кэша от базы данных в SQL Server 2005 выпол- нена наиболее оптимально, поскольку необходимая инфраструктура встроена в СУБД. Приложения ASP.NET получают асинхронные уведомления об изменениях, что хорошо с точки зрения производительности и снимает проблему блокировок. Кроме того, для использования данной функциональности не требуется подготовительной работы. Тогда как ее реализация в SQL Server 7.0 и SQL Server 2000 основана на методе опроса и для ее использования необходимо выполнить несколько подготовительных операций. Команда разработчиков ASP.NET постаралась максимально упростить эти операции, но вам все же требуются административные привилегии для входа в базу данных и соз- дания в ней триггеров, таблиц и хранимых процедур. Это может быть проблематично, если вы не имеете полного доступа к базе данных или, хуже того, ваше приложение функционирует на площадке интернет-провайдера. Является ли метод опроса лучшим из возможных решений? Он имеет серьезные недостатки, но при отсутствии встроенного в СУБД механизма уведомлений лучшего решения не существует. Без сомнения, опрос маленькой таблицы много эффективнее периодического выполнения основного запроса Кроме того, метод опроса снимает проблему блокирования, возникающую при исполь- зовании расширенных хранимых процедур, и пригоден для использования в архитектуре Web-фермы или Web-сада. Поэтому в команде разработчиков ASP.NET было решено, что для широчайшего спектра приложений ASP.NET метод опроса является наилучшим решением задачи удаления из кэша потерявших актуальность данных.
582 Часть III Инфраструктура ASP.NET Кэширование вывода страниц ASP.NET Согласно технологии ASP.NET страница HTML, отправляемая браузеру в ответ йа запрос ресурса .aspx, каждый раз генерируется заново. Однако существует множе- ство ситуаций, когда эту страницу имеет смысл сохранить для отправки в качестве ответа на следующие запросы того же ресурса .aspx. Как пример можно рассмотреть приложение для электронной коммерции, точнее, его страницы, на которых выводит- ся каталог товаров. Создание этих страниц — операция дорогостоящая, требующая одного, а чаще и более, обращения к базе данных и обработки полученных оттуда данных. Подготовка страницы может обходиться в несколько миллионов циклов центрального процессора. Какой же смысл генерировать одну и ту же страницу сотни раз в секунду? Ассортимент товаров может не меняться неделями, и уж во всяком случае он редко обновляется чаще одного раза в день. Но какой бы ни была частота обновления данных страниц, все равно едва ли имеет смысл обновлять сами страницы при выполнении каждого запроса. Гораздо лучше создать страницу один раз и кэшировать ее где-нибудь на достаточно длительный срок. Когда кэшированная копия станет неактуальной, для следующего запроса она будет сгенерирована заново и опять помещена в кэш. Применяемое в ASP.NET кэширование вывода позволяет существенно ускорить выполнение запросов за счет предоставления клиенту готовых страниц. Оно может осуществляться на двух уровнях: страниц и их частей. Подсистема кэширования до- статочно гибкая, чтобы позволить вам сохранять вывод, соответствующий определен- ному URL, строке запроса или параметрам формы и даже пользовательской строке. Ее легко настраивать, причем это можно делать как декларативно, используя директиву ©OutputCache, так и программно — посредством API, реализованного в виде класса HttpCachePolicy. 4 Л Внимание! По сути кэширование вывода — это просто способ ускорения выдачи страниц. Оно не имеет ничего общего со сложными стратегиями кэширования или элегантной архитектурой кода, призванными сделать приложение более эффективным и масштаби- руемым, хотя и уменьшает нагрузку на сервер. Также имейте виду, что кэширование вывода применимо только к анонимному контенту. Запросы кэшированных страниц об- рабатываются непосредственно IIS 6.0 (или рабочим процессом ASP.NET в IIS 5.0). Они никогда не достигают тех стадий конвейера ASP.NET, на которых производится аутен- тификация. Директива ^OutputCache Для включения кэширования страницы достаточно разместить вверху ее файла ди- рективу ©OutputCache. У этой директивы есть несколько атрибутов, два из которых, Duration и VaryByParam, являются обязательными. В атрибуте Duration задается срок кэширования вывода страницы в секундах, а атрибут VaryByParam позволяет указать, должно ли кэширование зависеть от строки запроса GET или значений, переданных в запросе POST. Например, страница, содержащая следующую директиву, будет кэши- роваться в течение минуты независимо от параметров команд GET и POST. <%@ OutputCache Duration="60" VaryByParam=”None" %> Для часто запрашиваемых и относительно статических страниц директива ©Out- putCache обеспечивает невероятное повышение производительности. Но даже при более коротком сроке кэширования, скажем, ограниченном одной-двумя секундами, работа приложения существенно ускоряется.
Кэширование в ASP.NET Глава 14 583 Список атрибутов, поддерживаемых директивой ©OutputCache, приведен в табл. 14-7. Заметьте, что эту директиву можно задавать не только для страниц (.aspx), но и для пользовательских элементов управления (.ascx). Некоторые ее атрибуты применимы только к страницам либо только к пользовательским элементам управления. Табл. 14-7. Атрибуты директивы @ OutputCache Атрибут К чему применим Описание CacheProfile Страница, пользо- вательский элемент управления Связывает страницу с группой параметров кэши- рования вывода, заданных в файле web.config (подробнее об этом далее в настоящей главе). В ASPNET 1jc данный атрибут не поддерживается Duration Страница Позволяет задать срок кэширования страницы или пользовательского элемента управления в секундах Location Страница Позволяет указать, где может храниться вывод страницы; атрибут принимает значения из пере- числения OutputCacheLocation NoStore Страница Позволяет указать, следует ли отправлять заго- ловок Cache-Controlmo-store для предотвращения дополнительного сохранения вывода страницы. В ASPNET 1.x данный атрибут не поддерживается Shared Пользовательский элемент управления Указывает, может ли вывод пользовательского элемента управления совместно использоваться несколькими страницами; по умолчанию имеет значение false SqlDependency Страница, пользо- вательский элемент управления Позволяет задать зависимость от заданной табли- цы базы данных SQL Server. Когда содержимое таблицы изменится, вывод страницы будет удален из кэша. В ASPNET 1х данный атрибут не под- держивается VaryByControl Пользовательский элемент управления Разделенный символами точки с запятой список строк, представляющих свойства пользовательско- го элемента управления. Для каждого сочетания значений этих свойств будет создаваться отдель- ная копия страницы в кэше VaryByCustom Страница, пользо- вательский элемент управления Разделенный символами точки с запятой список пользовательских строк или слово "browser” VaryByHeader Страница Разделенный символами точки с запятой список заголовков HTTP. Для каждого заголовка будет создаваться отдельная копия страницы в кэше VaryByParam Страница, пользо- вательский элемент управления Разделенный символами точки с запятой список строк, содержащих имена атрибутов строки запро- са GET или значений полей, возвращенных в теле запроса POST. Для каждого сочетания значений этих атрибутов (полей) будет создаваться отдель- ная копия страницы в кэше Заметьте, что атрибут VaryByParam является обязательным, и если его опустить, во время выполнения будет выброшено исключение. Однако это не значит, что обя- зательно должно выполняться варьирование вывода по значениям параметров: если оно не требуется, атрибуту VaryByParam нужно просто присвоить значение None.
584 Часть III Инфраструктура ASP.NET Выбор длительности кэширования вывода страницы Когда для страницы активен сервис кэширования вывода, атрибут Duration определя- ет, как долго копия страницы будет сохраняться в кэше. После помещения страницы в кэш последующие запросы на ее получение будут обрабатываться в обход конвейера HTTP. Как уже упоминалось, такой подход исключает возможность аутентификации. Кроме того, поскольку ее код не выполняется, не генерируются ее события и не обнов- ляется состояние. Способ реализации кэширования вывода зависит от используемой модели процесса ASP.NET (см. главу 3). Когда действует модель процесса IIS 5.0, запросы страниц ASP.NET всегда пере- даются рабочему процессу, связанному с объектом HttpApplication, и обрабатываются конвейером. В состав конвейера ASP.NET входит модуль HTTP OutputCacheModule, который перехватывает два события уровня приложения, связанные с кэшированием вывода. ResolveRequestCache и UpdateRequestCache. В частности, обработчик события ResolveRequestCache используется этим модулем для сокращения цикла выполнения запросов страниц, находящихся в кэше. В конце запрос передается модулю HTTP и обслуживается путем возврата копии страницы из кэша. Когда вывод страницы, по- меченной директивой ©OutputCache, генерируется в первый раз или заново, модуль OutputCacheModule перехватывает его и помещает в кэш. Вывод страницы хранится в приватном слоте объекта Cache ASP.NET. Срок кэширования определяется атри- бутом Duration. Если же действует модель процесса IIS 6.0, используется механизм кэширования вывода страниц, интегрированный в Web-сервер, благодаря чему повышается про- изводительность и снижается время отклика системы. Данный механизм называется кэшированием на уровне ядра (kernel caching). Когда он активен, IIS перехватывает вывод страниц, сгенерированных ASP.NET, и этот вывод кэшируется ядром I IS. Вхо- дящие запросы фильтруются драйвером уровня ядра (http.sys) и анализируются на предмет соответствия кэшированным страницам. Если соответствие найдено, код уровня ядра отправляет клиенту ответ, вообще не обращаясь к рабочему процессу и конвейеру ASP.NET. Если у вас имеются приложения ASP.NET и вы раздумываете над тем, не обновить ли IIS до версии 6.0, сделайте это как можно скорее. Способность IIS 6.0 кэшировать вывод страниц используется ASP.NET, начиная с версии 1.1. Оптимальное значение атрибута Duration зависит от конкретного приложения, но обычно оно не должно превышать 60 секунд. Это значение особенно хорошо подходит для страниц, которые обновляются редко. Меньший интервал обновления (скажем, 1 секунда) используется для приложений, пользователям которых необходимы «жи- вые» данные. Подробнее о кэшировании на уровне ядра, применяемом в IIS 6.0 В состав IIS 6.0 входит специальный компонент, обеспечивающий кэширование динамически генерируемых ответов на уровне ядра. Эта функция обладает огром- ным потенциалом и может очень сильно повысить производительность Web-сер- вера, если значительная часть его контента подлежит кэшированию. Согласно данным, приведенным в технической документации IIS 6.0, прирост производительности, достигаемый за счет кэширования на уровне ядра IIS, может быть более чем десятикратным. Кроме того, кардинально сокращается время от- вета сервера. Ниже приведена сравнительная таблица результатов кэширования в режиме ядра IIS 6.0 и в пользовательском режиме в IIS 5.0.
Кэширование в ASP.NET Глава 14 585 Пользовательский режим Режим ядра Запросов/с 1394 15 416 TTFB/ITLB’ (мс) 70,82/70,97 3,39/4,02 Тактов хфоцессора в пользователь- ском рёжиме (%) 76,57 % 0,78% Тактов процессора в режиме ядра (%) 22,69 % 99,22 % Системных вызовов/с 20110 2 101 Сетевой трафик (Кбайт/с) 6153 68 326 Переключений контекста/с 2 621 6 261 TTFB (Total Time the First Byte is received) — время, спустя которое получен первый байт, TTLB (Total Time the Last Byte is received) — время, спустя которое получен последний байт. По приведенным в таблице данным (источник: http://www.microsoft.com/tech- net/prodtechnol/windowsserver2003/technologies/webapp/iis/iis6perf.mspx} вы може- те составить некоторое представление о результатах применения двух режимов кэширования, но это лишь приблизительные оценки, реальные показатели будут отличаться от них в той или иной степени в зависимости от объема работы, ко- торую необходимо выполнить для подготовки ответа, и от его размера. В целом можно сказать, что кэширование на уровне ядра может оказать огромное влияние на производительность приложения. И, что особенно приятно, для его применения не нужно вносить в приложение никаких изменений, за исключением директивы @ OutputCache. Ддя. больших сайтов кэширование вывода даже длительностью в одну секунду может положительно сказаться на производительности всего Web-сервера. Однако вам нужно еще кое-что знать о кэшировании на уровне ядра. Прежде всего, оно возможно только для страниц, которые запрашиваются с использованием глагола GET. При возврате формы оно не осуществляется. Далее, страницы с атрибутами VaryByParam и VaryByHeader также не могут сохраняться в кэше ядра. И наконец, счётчики производительности ASPNET Request/Cache не будут обновляться для страниц, обслуживаемых с использованием кэша ядра. Выбор места кэширования вывода страницы Кэш вывода страниц может размещаться на разных уровнях, в частности на клиенте, сервере и промежуточном прокси-сервере. Перечень поддерживаемых возможностей (задаваемых в атрибуте Location директивы ©OutputCache) приведен в табл. 14-8. Они представлены значениями из перечисления OutputCacheLocation. Страница, помеченная директивой ©OutputCache, генерирует несколько заголов- ков HTTP, в том числе Cache-Control и Expires. Прокси-серверы, такие как Microsoft ISA Server, понимают эти заголовки и кэшируют страницу, благодаря чему после- дующие запросы на ее получение могут удовлетворяться вообще без обращения к Web-серверу. HTTP-заголовок Expires используется для задания времени обновления страницы на сервере. Пока это время не наступит, запросы удовлетворяются из локального кэша браузера или кэша прокси сервера. Заголовок Cache-Control может принимать значения No-Cache, public или private. Установка No-Cache означает, что кэширование страницы запрещено, public — что ее может кэшировать как браузер, так и прок- си-сервер, a private разрешает кэширование страницы только браузером. Заголовок Cache-Control определяется спецификацией HTTP 1.1 и поддерживается браузером Microsoft Internet Explorer версии 5.5 и выше.
586 Часть ill Инфраструктура ASP.NET Табл. 14-8. Местоположения кэша Значение атрибута Location Значение заголовка Cache-Control Описание Any Public Заголовок HTTP Expires устанавливается в соответ- ствии с длительностью кэширования, заданной в дирек- тиве ©OutputCache. В объект ASP.NET Cache помещает- ся новый элемент, представляющий вывод страницы Client Private Кэш вывода расположен на браузере, от которого посту- пил запрос. Заголовок HTTP Expires устанавливается в соответствии с длительностью кэширования, заданной в директиве ©OutputCache. Добавление элемента в объ ект ASP.NET Cache не производится DownStream Public Кэш вывода может размещаться на любом устройстве, отличном от Web-сервера. Это может быть как прокси- сервер, так и клиент, от которого поступил запрос. За- головок HTTP Expires устанавливается в соответствии с длительностью кэширования, заданной в директиве ©OutputCache. Добавление элемента в объект ASP.NET Cache не производится None No-Cache Заголовок HTTP Expires не задается. Заголовку Pragma присваивается значение No-Cache. Добавление элемента в объект ASP.NET Cache не производится Server No-Cache Заголовок HTTP Expires не задается. Заголовку Pragma присваивается значение No-Cache. В объект ASPNET Cache помещается новый элемент, представляющий вы- вод страницы Если вы просмотрите заголовки HTTP, генерируемые ASP.NET при включенном кэшировании, то обнаружите, что иногда, в частности когда атрибуту location присвое- но значение Server, используется также заголовок Pragma. Ему присваивается значение No-Cache, указывающее, что клиентское кэширование запрещено и на прокси-сервере, и на клиенте. В результате каждый запрос на получение страницы разрешается путем подключения к Web-серверу. Примечание Если говорить точнее, когда заголовок Pragma имеет значение No-Cache, кэширование отключено только для каналов HTTPS. При использовании обычного неза- щищенного канала HTTP страница кэшируется, но помечается как устаревшая. Давайте проанализируем конфигурацию клиента и Web-сервера при использова- нии каждого из значений атрибута location. Any Значение по умолчанию. Данная установка означает, что страница может кэшироваться где угодно, включая браузер, Web-север и прокси-север. Заголовку Expires присваивается абсолютное время устаревания страницы согласно значению, заданному в атрибуте Duration. Заголовок Cache-Control получает значение public, указывающее на тот факт, что прокси-сервер также может кэшировать страни- цу, если пожелает. На Web-сервере в объект Cache добавляется новый элемент с HTML-выводом страницы. В результате вывод страницы кэшируется везде, где это возможно. И если обращение к ней происходит до того, как она устареет, сервер вообще не получает запроса. Если же пользователь подает команду обновления страницы, то есть принудительного запроса ее с сервера, издержки оказываются минимальными, поскольку запрос удовлетворяется из кэша Web-сервера и полная его обработка все равно не производится.
Кэширование в ASP.NET Глава 14 587 Client Страница кэшируется только браузером, поскольку заголовок Cache- Control имеет значение private. Ни прокси-сервер, ни ASP.NET не хранят копию страницы. Заголовок Expires устанавливается в соответствии со значением атрибута Duration. Downstream Страница может кэшироваться и на клиенте, и в памяти любого прокси-сервера. Заголовок Expires устанавливается в соответствии со значением атрибута Duration, и копия вывода страницы в кэше ASP.NET не сохранится. None Кэширование вывода страницы отключено везде — на Web-сервере, прокси- сервере и клиенте. Заголовок Expires не генерируется, а заголовкам Cache-Control и Pragma присваивается одно и то же значение No-Cache. Server Вывод страницы кэшируется только на Web-сервере, и его ответ сохра- няется в объекте Cache. Клиентское кэширование отключено. Заголовок Expires не создается, заголовкам Control и Pragma присваивается значение No-Cache. ServerAndClient Вывод страницы кэшируется на Web-сервере и клиенте, но прокси-серверам сохранять его копии запрещено. Добавление в вывод страницы зависимости от базы данных Атрибут SqlDependency является интерфейсом директивы ©OutputCache к описанному ранее в этой главе классу SqlCacheDependency. Когда этому атрибуту присвоена строка в формате база.таблица, создается объект зависимости кэша от базы данных SQL Server. При разрушении этой зависимости кэшированный вывод страницы становится недействительным, и следующий запрос проходит полный конвейер обработки, после чего его вывод кэшируется. < % ©OutputCache Duration="15" VaryByParam="none” SqlDeperidency="Northwind: Employees" %> Вывод страницы, содержащей эту директиву, будет кэшироваться в течение 15 секунд или до тех пор, пока в одну из записей таблицы Employees базы данных Northwind не будет внесено изменение. Заметьте, что Northwind здесь — не имя базы данных, а имя элемента в разделе <databases> конфигурационного файла. Этот элемент содержит ссылку на строку подключения к базе данных. Можно определить несколько зависи- мостей, разделив соответствующие пары база:таблица символами точки с запятой. Г~— I Примечание Пользовательский элемент управления можно сделать кэшируемым дву- мя способами: с помощью директивы ^OutputCache или путем определения атрибута PartialCaching в объявлении класса пользовательского элемента управления в файле отделенного кода: [PartialCaching(60)] public partial class CustomersGrid : UserControl { } Атрибут PartialCaching позволяет задать длительность кэширования и значения параметров VaryByParam, VaryByControl и VaryByCustom. Класс HttpCachePolicy Класс HttpCachePolicy является программной альтернативой директиве ©OutputCache. Он содержит методы, с помощью которых можно задать необходимые заголовки HTTP (напомню, что это также делается с помощью объекта HttpResponse).
588 Часть III Инфраструктура ASP NET Свойства класса HttpCachePollcy Свойства класса HttpCachePolicy описаны в табл. 14-9. Табл. 14-9. Свойства класса HttpCachePolicy Свойство Описание VaryByHeaders Возвращает объект типа HttpCacheVaryByHeaders, представляющий список всех заголовков HTTP, используемых для варьирования кэшируемых копий страницы VaryByParams Возвращает объект типа HttpCacheVaryByParams, представляющий список параметров запроса GET или POST, которые влияют на кэширование Когда для страницы заданы заголовки и параметры, по значениям которых будут варьироваться ее кэшируемые копии, отдельная копия вывода страницы сохраняется в кэше для каждого сочетания значений этих заголовков или параметров. Методы класса HttpCachePolicy С перечнем методов класса HttpCachePolicy вы можете ознакомиться в табл 14-10. Табл. 14-10. Методы класса HttpCachePolicy Метод Описание AddValidationCallback Регистрирует функцию обратного вызова, которая будет использоваться для проверки на сервере выво- да страницы перед его возвратом AppendCacheExtension Добавляет в заголовок HTTP Cache-Control заданный текст. Существующий текст при этом не заменяется SetAUowResponselnBrowserHistory Если передать этдму методу значение true, ответ будет доступен в кэше журнала браузера независимо от установки HttpCacheability на сервере SetCacheability Присваивает заголовку HTTP Cache-Control задан- ное значение из перечисления HttpCacheability SetETag Присваивает заголовку HTTP ETag заданную строку. Этот заголовок содержит уникальный идентифика- тор версии документа SetETagFromFileDependencies Присваивав! заголовку HTTP ETag строку, получен- ную путем объединения и последующего хэширова- ния дат последней модификации всех файлов, от которых зависит страница SetExpires Присваивает заголовку HTTP Expires заданное абсо- лютное значение даты и времени SetLastModified Присваивает заголовку HTTP Last-Modified заданное значение даты и времени SetLastModibedFrorn FileDependencies Присваивает заголовку HTTP Last -Modified значение последнего из штампов времени файлов, от которых зависит страница SetMaxAge Присваивает заданное значение атрибуту max-age за- головка Cache-Control Скользящий период не может превышать одного года SetNoServerCaching Отключает для текущего ответа кэширование вывода на сервере
Кэширование в ASP.NET Глава 14 589 Табл. 14-10. (окончание) Метод Описание SetNoStore Присваивает заголовку Cache-Control директиву no-store SetNoTransforms Присваивает заголовку Cache-Control директиву no-transforms SetOmitVaryStar Если передать этому методу значение true, класс HttpCachePolicy будет игнорировать значение * свойства VaryByHeaders. В ASP.NET 1 х данный метод не поддерживается SetProxyMaxAge Присваивает заголовку Cache-Control директиву s-maxage SetRevalidation Присваивает заголовку Cache-Control директиву must-revalidate или proxy-revalidate SetSlidingExpiration Устанавливает скользящий режим устаревания страницы. В этом режиме значение установки CacheControl обновляется после каждого ответа SetValidUntilExpires Позволяет указать, должен ли кэш ASPNET игнори- ровать заголовки Cache-Control, присланные браузе- рами для удаления страницы из кэша. Если передать методу значение true, страница будет оставаться в кэше, пока не устареет SetVai yByCustom Присваивает заголовку HTTP Vary заданную тексто- вую строку Большинство методов класса HttpCachePolicy позволяют контролировать значения некоторых заголовков HTTP, связанных с кэшем браузера. Метод AddValidationCall- back обеспечивает возможность программного контроля за действительностью вывода страницы в кэше сервера. Функция обратного вызова, используемая для проверки кэшированной страницы Перед отправкой клиенту ответа, извлеченного из кэша ASP.NET, всем зарегистри- рованным обработчикам предоставляется возможность проверить, является ли этот ответ действительным. Если хоть один обработчик пометит кэшированную копию страницы как недействительную, она будет удалена из кэша, и запрос пройдет полный цикл обработки. Сигнатура функции обратного вызова, выполняющей указанную проверку, такова: public delegate void HttpCacheValidateHandler( HttpContext context, object data, ref HttpValidationStatus validationstatus ): Первый ее аргумент представляет контекст текущего запроса, второй содержит пользовательские данные, которые приложение хочет передать обработчику, а в тре- тьем аргументе задается ссылка на переменную, принимающую значения из перечис- ления HttpValidationStatus. Функция обратного вызова обновляет значение этой пере- менной, сообщая таким образом о результате валидации. Допустимыми значениями являются IgnoreThisRequest, Invalid и Valid. Когда получено значение IgnoreThisRequest, кэшированный ресурс не помечается как недействительный, но запрос обрабатывается так, словно кэшированного ответа не существует. Если же функция вернула значение Invalid, кэшированная страница не используется и помечается как недействительная.
590 Часть III Инфраструктура ASP.NET Ну а значение Valid сигнализирует о том, что все в порядке и кэшированную страницу можно вернуть в качестве ответа клиенту. Кэширование нескольких версий страницы В зависимости от контекста приложения, которому принадлежит вызванная страница, она может генерировать различные результаты. В частности, так бывает, когда одна и та же страница вызывается с различными параметрами, конфигурируется с исполь- зованием разных заголовков HTTP, ее вывод зависит от типа браузера и т. д. С учетом этого ASP.NET позволяет кэшировать много версий вывода страницы; они могут варьироваться на основе сочетаний параметров метода GET или POST, со- четаний значений заголовков HTTP, типов браузеров, свойств элемента управления и, наконец, просто пользовательских строк. Варьирование вывода по значениям параметров Для варьирования кэшируемого вывода страницы на основе значений параметров ис- пользуется атрибут VaryByParam директивы ©OutputCache или одноименное свойство класса HttpCachePolicy. Декларативный синтаксис таков: <% ©OutputCache Duration="60" VaryByParam="employeeID" %> Напомню, что атрибут VaryByParam является обязательным; если варьирование по значениям параметров не производится, ему нужно присвоить значение None. Если же вы хотите, чтобы кэшируемый вывод варьировался по значениям всех параметров, задайте в атрибуте VaryByParam символ *. Когда в этом атрибуте задано несколько параметров (разделенных символами точки с запятой), в кэше может быть столько версий вывода страницы, сколько существует сочетаний значений этих параметров. Допустимыми именами параметров являются имена атрибутов строки запроса GET или имена полей, задаваемых в теле запроса POST. Если вы хотите определить параметры с использованием класса HttpCachePolicy, задайте вначале срок службы и разрешение кэширования страницы с помощью ме- тодов SetExpires и SetCacheability, а уж после этого устанавливайте значение свойства VaryByParams: Response.Cache.SetExpires(DateTime.Now.AddSeconds(60)); Response.Cache.SetCacheability(HttpCacheability.Public); Response Cache.VaryByParams[employeeid;lastname ] = true. Этот код показывает, как обеспечивается варьирование кэшируемого вывода страницы на основе заданных в запросе идентификатора сотрудника (employeeid) и его фамилии (lastname). Заметьте, что свойство Cache класса HttpResponse содержит просто экземпляр типа HttpCachePolicy. Обработка возврата формы Большинство страниц ASP.NET осуществляет возврат формы. Вот и страница, по- казанная на рис. 14-7 (sqldepoutputcache.aspx), не является исключением. У нее есть две важные для нас особенности: она зависит от изменений в таблице Customers базы данных Northwind, а длительность ее кэширования задана равной 30 секундам. Рас- крывающийся список Countries при выборе элемента автоматически осуществляет возврат формы, отправляя серверу запрос POST. Если установить свойство VaryByParam в None, пользователю придется полминуты дожидаться, пока сделанный им выбор будет обработан, а до этого он может сколько угодно выбирать элемент в списке, получая одну и ту же страницу. Хуже того, если
Кэширование в ASP.NET Глава 14 591 протестировать страницу на встроенном Web-сервере Visual Studio .NET. спустя не- сколько попыток вы получите сообщение «page not found». А при тестировании этой страницы под управлением IIS будет просто предоставляться один и тот же запрос, пока страница не обновится, наконец, в кэше. Рис. 14-7. Для правильного кэширования такой страницы нужно варьировать ее копии по значениям одного или более параметров Из сказанного можно сделать два вывода. Во-первых, страницы со статическим контентом подходят для кэширования гораздо лучше, чем динамические страницы. Во-вторых, механизм возврата формы передает серверу значения группы полей, и некоторые из них должны использоваться для варьирования кэшируемых копий страницы. В примере, показанном на рис. 14-7, есть несколько скрытых полей, в част- ности __VIEWSTATE и LASTFOCUS (в чем нетрудно убедиться, отобразив в браузере HTML-представление страницы), а также раскрывающийся список. Варьировать вывод по состоянию представления, конечно, бессмысленно, а вот варьирование по выбранной пользователем стране — это именно то, что нам нужно: <%@ OutputCache VaryByParam="Countries" Duration="30" SqlDependency=”Northwind:Customers" %> При наличии такой директивы в кэше будет сохраняться отдельная копия страницы для каждой из стран, уже выбиравшихся тем или иным пользователем, — они будут хра- ниться в течение 30 секунд, если только в таблицу Customers не будут внесены измене- ния, поскольку тогда все кэшированные копии страницы станут недействительными. Как видите, включение кэширования вывода для интерактивной страницы — не столь уж простая операция. Она обходится наиболее дешево и выполняется наиболее просто для относительно статичных страниц, например таких, на которых выводится список товаров или услуг, или, скажем, новости.
592 Часть III Инфраструктура ASP.NET ©Внимание! Кэшированная страница ASP.NET предоставляется быстрее, чем страница, проходящая полный цикл обработки, но не так быстро, как статическая страница HTML. Однако время ответа при выдаче кэшированной страницы и страницы HTML при ис- пользовании технологии кэширования на уровне ядра IIS 6.0 практически одинаково. К сожалению, IIS 6.0 не хранит в кэше своего ядра страницы ASP.NET, запрошенные с использованием глагола POST, и, что еще важнее, страницы, для которых задан атрибут VaryByParam или VaryByHeader. Страницы, предоставляемые в ответ на возврат формы, могут кэшироваться только в кэше ASP.NET, а также на прокси-серверах. Варьирование вывода по заголовкам HTTP Учтите, что атрибут VaryByHeader директивы ©OutputCache и одноименное свойство класса HttpCachePolicy позволяют кэшировать несколько версий страницы с учетом значений одного или более заголовков HTTP. Для обеспечения такого кэширования нужно задать список заголовков, разделив их символами точки с запятой, либо с по- мощью символа * указать, что кэшированные копии должны варьироваться по всем заголовкам. Например, для страницы с приведенной ниже директивой вывод будет кэшироваться в течение минуты с учетом поддерживаемого браузером языка: <%@ OutputCache Duration="60" VaryByParam="None" VaryByHeader="Accept-Language" %> Если вы предпочитаете программный подход, задайте такую установку: Response Cache.VaryByHeaders["Accept-Language"] = true; А для варьирования кэшированного вывода по всем заголовкам HTTP используйте метод VaryByUnspecifiedParameters класса HttpCacheVaryByHeaders\ HttpCacheVaryByHeaders.VaryByUnspecifiedParametersO; Приведенный выше вызов эквивалентен заданию символа звездочки в атрибуте VaryByHeader. Варьирование вывода по пользовательским строкам Атрибут VaryByCustom директивы ©OutputCache позволяет варьировать кэширован- ные версии страницы с применением пользовательской строки. В строке, которую вы присваиваете этому атрибуту, содержится описание алгоритма варьирования вы- вода страницы. Эта строка передается методу GetVaryBy CustomString, если таковой определен в файле globaLasax. Данный метод возвращает другую строку, специфиче- скую для запроса. Давайте рассмотрим все это на примере варьирования страниц по типам запросившего их устройства. Для этого в атрибуте VaryByCustom мы зададим, скажем, строку "device ь <%@ OutputCache Duration="60" VaryByParam="None" VaryByCustom="device" %> Затем нужно будет добавить в файл globaLasax метод GetVaryByCustomString, реали- зация которого зависит от конкретного приложения. Вот какой она будет в нашем случае: string GetVaryByCustomString(HttpContext context, string custom) { if (custom == "device") retu rn context.Request.В rowse r.Type; return null; } Теперь вывод страницы будет варьироваться по заголовкам пользовательского агента. В общем случае можно использовать любую информацию, доступную посред- ством класса HttpContext. Однако нельзя использовать в методе GetVaryByCustomString
Кэширование в ASP.NET Глава 14 593 сведения, которые страница получает только после загрузки (например, данные темы). Пользовательская информация, собранная модулем HTTP, может использоваться при условии, что этот модуль упакует ее в коллекцию объекта HttpContext и что он будет вызван до того, как запрос будет разрешен путем возврата страницы из кэша. Описанная функциональность (варьирование вывода с учетом строки пользова- тельского агента) поддерживается еще со времен ASP.NET 1.0. Для ее использования вместо ключевого слова device, которое, как видно из кода метода GetVaryByCustom- String, является произвольным, нужно использовать стандартное ключевое слово browser. Иными словами, приведенный ниже код позволяет активизировать для выбора кэшируемой копии страницы встроенную реализацию метода GetVaryByCustomString базового класса HttpApplication'. <%@ OutputCache Duration="60” VaryByParam="None" VaryByCustom="browser" %> Можно сделать то же самое и программным способом: Response.Cache.SetVa ryByCustom("browser"); Кэширование частей страницы ASP.NET Способность кэшировать вывод Web-страниц делает ваш арсенал программиста ис- ключительно мощным, но иногда кэширование всего содержимого страницы невоз- можно или не является эффективным решением. Некоторые страницы состоят из частей, очень различающихся в отношении требований к кэшированию. В таких слу чаях целесообразно кэшировать страницу частями, задавая для них разные установки. Кэширование частями предполагает применение пользовательских элементов управления — небольших вложенных страниц, которые наследуют некоторые ха- рактеристики главной страницы. Такие элементы управления можно кэшировать по отдельности с учетом типа браузера, параметров метода GET или POST, а также значений определенных свойств. О пользовательских элементах управления подроб- но рассказывается в моей книге «Programming Microsoft ASP.NET 2.0 Applications: Advanced Topics» (Microsoft Press, 2005), но краткий обзор этой темы будет полезен и здесь. Что такое пользовательский элемент управления Пользовательским элементом управления называется Web-форма, сохраненная в от- дельном файле с расширением .asex. Сходство между таким элементом управления и страницей не случайно. Пользовательский элемент управления создается практиче- ски так же, как обыкновенная страница Web Forms, и состоит из сочетания клиентских и серверных элементов управления, объединяемых с помощью серверного программ- ного кода и клиентских сценариев. После создания пользовательский элемент управ- ления включается в состав страницы подобно любому другому серверному элементу управления, страница видит его как атомарный компонент и работает с ним, как со встроенным элементом управления Web. Внутреннее содержимое пользовательского элемента управления скрыто от хост- страницы, однако он может иметь открытый программный интерфейс и фильтровать доступ к своим внутренним составляющим, используя свойства, методы и события. Пользовательские элементы управления и страницы имеют так много общего, что превращение страницы или ее части в пользовательский элемент управления является минутным делом. Вы просто копируете нужную часть страницы в новый файл .asex и убеждаетесь, что в него не попал ни один из следующих тэгов: <html>, <body> или <form>. В завершение остается связать с элементом управления файл отделенного кода (или блок <script runat= "server">), а затем добавить в начало файла элемента
594 Часть III Инфраструктура ASP.NET управления директиву ©Control, которая играет здесь ту же роль, что директива ©Page в файле страницы. Вот пример содержимого файла .ascx пользовательского элемента управления: <%@ Control Language="C#" CodeFile="Message.ascx.cs" Inherits="Message" %> <span style="color:<%= ForeColor%>"><%= Text%x/span> А вот класс отделенного кода этого элемента: public partial class Message : System.Web.UI.UserControl { public string ForeColor; public string Text; } Для вставки данного элемента управления в страницу .aspx в режиме дизайнера достаточно перетащить его из окна проекта в Web-форму. Visual Studio .NET заре- гистрирует пользовательский элемент управления в файле страницы и подготовит базовую конструкцию, которую вы сможете дополнить собственным кодом: <%@ Page Language="C#" CodeFile="Test.aspx.cs" Inherits="TestUserCtl" %> <%@ Register Src="Message.ascx" TagName="Message" TagPrefix="uc1" %> <html><body> <form id="form1" runat="server"> <uc1:Message ID="Message1" runat="server" /> </form> </body></html> В классе отделенного кода страницы вы можете работать с переменной Message 1 так, как работали бы с любой переменной, представляющей стандартный серверный элемент управления: protected void Page_Load(object sender, EventArgs e) { Message1.ForeColor = "blue"; Messaged Text = "Hello world"; } Кэширование вывода пользовательских элементов управления Пользовательские элементы управления хороши не только для модуляризации поль- зовательского интерфейса, но и как средство кэширования частей Web-страниц. Они полностью поддерживают директиву ©OutputCache, но с некоторыми отличиями, которые были указаны в табл. 14-7. Страница, содержащая динамические разделы, не может кэшироваться целиком. Но что если такая страница содержит в то же время редко обновляемые разделы, формирование которых является довольно затратным? Тогда можно выделить ее статический контент в один или более пользовательских элементов управления и использовать директиву ©OutputCache для их кэширования. Например, пользователь- ский элемент управления со следующей директивой будет кэшироваться в течение одной минуты независимо от страницы, на которой расположен: <% ©OutputCache Duration="60" VaryByParam="None" %> Атрибут Location для пользовательских элементов управления не поддерживается, поскольку все элементы управления страницы кэшируются в одном месте. Поэтому, если вы захотите указать местоположение кэша элемента управления, это нужно сделать на уровне страницы. То же касается и атрибута VaryByHeader.
Кэширование в ASP.NET Глава 14 595 Кэшируемый вывод пользовательского элемента управления может варьироваться с учетом пользовательских строк и параметров формы, однако чаще его варьируют по значениям свойств. Для этого применяется новый атрибут VaryByControl. Варьирование вывода по значениям свойств элемента управления Атрибут VaryByControl позволяет варьировать кэшируемые копии элемента управ- ления с учетом значения его свойства. Указывать это свойство обязательно, если не задан атрибут VaryByParam. При желании можно задать оба атрибута, VaryByParam и VaryByControl. Следующий пользовательский элемент управления выводит таблицу клиентов из страны, заданной в его свойстве Country'. <%@ Control Language="C#" CodeFile="CustomersGrid.ascx.cs" Inherits="CustomersGridByCtl" %> < %@ OutputCache Duration="30" VaryByControl="Country” %> < h3><%= DateTime.Now.ToStringO %></h3> <asp:ObjectDataSource ID="ObjectDataSoureel” runat=”server" TypeName="ProAspNet20.DAL.Customers" SelectMethod="LoadByCount ry"> < /asp:Obj ectDataSou rce> <asp:GridView ID="GridView1" runat=”server" AutoGenerateColumns="false"> <Columns> <asp:BoundField DataField="ID" HeaderText=”ID" /> <asp:BoundField DataField="CompanyName" HeaderText="Company" /> <asp:BoundField DataField="ContactName" HeaderText="Contact" /> <asp:BoundField DataField="Country" HeaderText="Country" /> </Columns> </asp:GridView> Вот содержимое файла кода этого элемента управления: public partial class CustomersGridByCtl : System.Web.UI.UserControl public string Country: protected void Page_Load(object sender, EventArgs e) { if (!String.IsNullOrEmpty(Country)) ObjectDataSoureel.SelectParameters.Add("country”, Country); GridViewl.DataSourcelD = "ObjectDataSourcel"; } } } Директива ©OutputCache обеспечивает кэширование копий элемента управления для каждого из значений свойства Country. На рис. 14-8 показана страница с нашим элементом управления в действии. Строки, которые вы присваиваете атрибуту VaryByControl, могут быть свойствами пользовательского элемента управления или идентификаторами элементов управ- ления, которые входят в его состав. Если этих свойств несколько, отдельная копия элемента управления будет кэшироваться для каждого сочетания их значений.
596 Часть III Инфраструктура ASP NET 1 Partial Caching Microsoft Internet Explorer _ ” X| 0e E* , Fawtes look Ж 9 a ^Fevtrtes ffi rgL* Ajjdrea ]ProA$pNet20/Samples/Chl4/OUtputCache/Partia<achlng.aspx *' Я Go U.« M The output you see has been generated at: 5:46:30 PM Country Selected: USA 3/2/2005 5:46:30 PM ID Company DXXXX My Company GREAL Great Lakes Food Market HUNGC Hungry Coyote Import Store LAZYk Lazy К Kountry Store LETSS Let's Stop N Shop LONER Lonesome Pine Restaurant OLDWO Old World Delicatessen RATTC SAVEA SPUR THEBI THECR TRAIH WHITC Rattlesnake Canyon Grocery Seve-a-lot Markets Split Ran Beer & Ale The Biq Cheese The Cracker Box Trail's Head Gourmet Provisioners White Clover Markets .j'XXW Рис. 14-8. Две копии страницы, сгенерированные в разные моменты времени, но содержащие один и тот же вывод элемента управления Атрибут Shared На рис. 14-8 показаны два экземпляра одной и той же страницы, совместно исполь- зующие кэшированный вывод пользовательского элемента управления Попробуйте провести следующий простой эксперимент. Создайте копию страницы, которая на- зывается, скажем, pagel.aspx, и присвойте ей имя page2.aspx. у вас получатся две разные страницы, генерирующие одинаковый вывод. В частности, обе страницы будут содержать один и тот же кэшируемый пользовательский элемент управления. Пред- положим, что длительность его кэширования составляет 30 секунд. Далее откройте обе страницы в разные моменты времени, но в пределах действи- тельности копии элемента управления в кэше (скажем, одну страницу на десять секунд позже другой). Интересно, что эти две страницы не будут использовать одну и ту же кэшируемую копию элемента управления, что подтверждает рис. 14-9. По умолчанию разные страницы не используют одну и ту же кэшированную копию пользовательского элемента управления — для каждой из них поддерживается своя такая копия. Так сделано во избежание конфликтов, но в результате появляется риск
Кэширование в ASP.NET Глава 14 597 заполнения памяти совершенно ненужными копиями элемента управления: мало того, что отдельная его копия будет создаваться для каждого сочетания параметров, эти копии будут еще и дублироваться в большом количестве, если элемент управления используется многими страницами. Для того чтобы этого избежать, достаточно до- бавить в директиву ©OutputCache пользовательского элемента управления атрибут Shared со значением true'. <%@ OutputCache Duration= 30" VaryByParam="None” Shared="true" %> 3 Partial Caching Vary By Control Microsoft Internet Exptoier linfx. I* C* JJsw Frortee Jods h* л • ©Back - (g Ц WFawdtm О’ G'bfi rt ^W»jjBhttp:/flocahost/ProAspNet2Q/Sampies/Chl4/OutputCatfie/PartlalVaryByControl.aspx *^6(160 The output you see has been generated at: 6:59:13 PM Country Selected: USA 6:59:13 PM IO Company DXXXk MyCompeny GREAL Grsat Uk»5 Food M»rk«t HUNGC Hungry Coyote Import Store LAZYK Lazy К Country fetors LETSS Lots Stop. N Shop lONEp Lonesome Pine Restaurant OLD WO Ota World Dau ito< en RATTC SAVEA SPUR THEBI THECR TRAIH WHITC Wfrlt* Clover Markets R Rattlesnake Canyon Grocery Save-a-lot Markets Split Rail Beer & Ale The Big Cheese The Cracker Box Trail's Head Gourmet, Provisioned Рис. 14-9. Автоматически совместное использование кэшированной копии элемента управления двумя страницами не обеспечивается Совет Во избежание проблем с памятью необходимо ограничить объем памяти, доступ- ной программному обеспечению IIS. Он может составлять порядка 60 % общего объема физической памяти компьютера, но Microsoft рекомендует не выделять более 800 Мбайт. Установка параметра Maximum Used Memory IIS 6.0 приобретает особо важное значение при интенсивном использовании кэша. Данный параметр задается для каждого пула приложений. Кэширование фрагментов кэшируемых страниц Если вы планируете кэшировать пользовательские элементы управления, то, по всей вероятности, не хотите или не можете кэшировать всю страницу. А возможно ли кэ- шировать элемент управления, входящий в состав кэшируемой страницы?
598 Часть III Инфраструктура ASP.NET Это возможно, и таком случае в кэше будут находиться копия вывода элемента управления и полная копия вывода страницы. Если длительность их кэширования различна, приоритет имеет установка длительности кэширования страницы, и копия пользовательского элемента управления обновляется только при обновлении страницы. Кэшируемый пользовательский элемент управления может входить в состав не только кэшируемой страницы, но даже другого кэшируемого пользовательского эле- мента управления. ©Внимание! С кэшируемыми пользовательскими элементами управления в коде страницы нужно работать очень аккуратно. В отличие от обычного элемента управления присутствие в памяти пользовательского элемента управления, помеченного директивой @OutputCache, не гарантируется. Если пользовательский элемент управления извлекается из кэша, свой- ство, которое содержит на него ссылку, возвращает null. Во избежание сюрпризов перед выполнением любого кода, относящегося к пользовательскому элементу управления, нужно всегда проверять, не пуста ли ссылка: if (CustomerGridl != null) CostowerGrid1.Country = "USA"; Продвинутые функции кэширования в ASP.NET 2.0 Подсистема кэширования вывода в ASP.NET 2.0 была обогащена новыми функциями. К ним относятся поддержка профилей кэширования и подстановка после кэширова- ния. Профилем кэширования называется сохраненный в конфигурационном файле блок связанных с кэшированием установок. Что касается подстановки после кэши- рования (post-cache substitution), то эта технология является недостающим звеном в цепочке возможностей, обеспечиваемых кэшированием страницы и пользователь- ских элементов управления, и позволяет кэшировать всю страницу, за исключением определенных областей. Профили кэширования Директива ©OutputCache страниц поддерживает атрибут CacheProfile, в котором за- дается ссылка на элемент, который содержится в разделе <outputCacheProfiles> файла web.config: <caching> <outputCacheSettings> <outputCacheProfiles> Odd name=”..." enabled="true|false" duration="..." location="...” sqlDependency="...” varyByCustom=”..." varyByControl="..." varyByHeader="...“ varyByParam="..." noStore=true|false" /> </outputCacheProfiles> </outputCacheSettings> </caching> Определив именованный элемент с помощью специального узла <add>, вы мо- жете сохранить в нем все связанные с кэшированием установки, предназначенные для применения к страницам. Это позволит вам не задавать в каждой странице один и тот же набор параметров, а просто ссылаться на него по имени. <%@ OutputCache CacheProfile="MySettings" %> Более того, тогда вы сможете модифицировать установки кэширования группы страниц, внеся единственное изменение в конфигурационный файл. В приведен-
Кэширование в ASP.NET Глава 14 599 ной директиве Му Settings является именем элемента в разделе <outputCacheProfiles> конфигурационного файла, из которого автоматически извлекаются дополнитель- ные атрибуты директивы ©OutputCache. Нетрудно догадаться, что атрибуты этого элемента (за исключением атрибута пате, который служит для его идентификации) совпадают с атрибутами директивы ©OutputCache. Подстановка после кэширования Если пользовательские элементы управления позволяют кэшировать только опреде- ленные части страницы ASPNET, то подстановка после кэширования позволяет кэши- ровать всю страницу, за исключением определенных частей. При использовании этой технологии можно, например, создать кэшируемую страницу с элементом управления AdRotator, который при каждом запросе будет выводить новую рекламу. Для того чтобы применить подстановку после кэширования, нужно включить в состав страницы специальный элемент управления <asp:substitution> — в то место, куда должен быть подставлен некэшируемый контент, — и в его свойстве MethodName задать функцию обратного вызова. Вот пример: <form id=”form1" runat="server"> <h3>The output you see has been generated at: <%=DateTime. Now. ToStringO %> and is valid for 30 seconds</h3> <hr /> This content is updated regularly <h2><asp:Substitution ID="Substitution1" runat="server" MethodName="WriteTimeStamp" /></h2> <hr /> This is more static and cached content <asp:Button runat="server" Text="Refresh" /> </form> Как выглядит эта страница, показано на рис. 14-10. Рис. 14-10. Фрагмент страницы, расположенный между двумя горизонтальными линиями, обновляется при каждом возврате формы, а остальная часть страницы извлекается из кэша Атрибут MethodName должен содержать имя статического метода, который может быть инкапсулирован делегатом HttpResponseSubstitutionCallback: public static string WriteTimeStamp(HttpContext context) { return DateTime.Now.ToStringO; }
600 Часть III Инфраструктура ASP.NET Возвращаемая этим методом строка становится выводом элемента управления Substitution. Заметьте, что метод должен быть статическим и поддерживать выполнение в многопоточной среде. Его аргумент HttpContext может использоваться для извлече- ния параметров запроса, таких как переменные строки запроса, аутентификационная информация или персонализированные данные. Функцию обратного вызова можно задать и программно с помощью метода Write - Substitution объекта HttpResponse'. Response.WriteSubstitution( new HttpResponseSubstitutionCallback(WriteTimeStanip)); При выполнении данного вызова в объект ответа вставляется заменитель, на место которого будет затем подставлен вывод метода WriteTimeStamp. При использовании технологии подстановки после кэширования автоматически включается серверное кэширование вывода страницы. Если она сконфигурирована для клиентского кэширования, эта установка игнорируется (ведь очевидно, что редак- тирование разметки может осуществляться только на сервере). Кроме того, страница, в которой выполняется подстановка после кэширования, не может кэшироваться ядром IIS 6.0, поскольку подстановку выполняет ASP.NET. [__j Примечание Элемент управления Substitution может использоваться в составе страниц, вывод которых не кэшируется. В таком случае функция обратного вызова, возвращающая подставляемый текст, будет вызываться на этапе рендеринга. Поэтому элемент управления Substitution можно представлять себе как серверный элемент, способный генерировать выводимый на странице текст. Из соображений производительности следует избегать обращения к элементу управления Substitution из функции обратного вызова. Иначе эта функция будет под- держивать ссылку на элемент управления и содержащую его страницу. В результате экземпляр страницы не будет удален при сборке мусора до тех пор, пока не истечет срок хранения кэшированного контента. Примечание Следует заметить, что на протяжении всего цикла разработки вплоть до выпуска Beta 2 в ASP NET 2.0 поддерживалась функция, называвшаяся кэшированием дискового вывода. Суть ее заключалась в сохранении ответа страницы на диске, а не в памяти, благодаря чему кэшированный контент сохранялся даже между перезапусками приложения, в результате которых кэш, естественно, очищался. Однако в окончательном выпуске ASP.NET 2.0 эта функция была удалена. Заключение Возможность сохранения в памяти блоков часто используемых данных является одним из определяющих факторов, влияющих на эффективность Web-приложения, которое работает с большими объемами данных. Вместо того чтобы постоянно под- ключаться к серверу базы данных, блокировать записи и задействовать каналы связи, можно просто считывать из памяти ранее сохраненные в ней данные. В той или иной форме кэширование может использоваться практически в любом приложении или, во всяком случае, в подавляющем их большинстве. Правда, кэширование является, так сказать, обоюдоострым мечем, и пользовать- ся им нужно умело. Иначе существует риск неконтролируемого увеличения объема хранящихся в памяти данных, что приводит не к повышению, а к резкому снижению производительности Web-сервера. Если система кэширования плохо спроектирована, сервер становится очень уязвимым к атакам на отказ, при которых с помощью боль-
Кэширование в ASP.NET Глава 14 601 шого количества ложных запросов его память перегружается до такой степени, что он останавливается или перестает отвечать на запросы реальных пользователей. Но в то же время выгоды от эффективно построенной системы кэширования на- лицо, они ощутимы для конечного пользователя. Я не хочу сказать, что кэшированием можно исправить недостатки проектирования приложения, но иногда их удается по крайней мере замаскировать на то время, пока производится рефакторинг приложения для оптимизации его производительности. Базовый принцип разработки Web-приложений гласит: кэшируйте все, что можно. Однако еще раз повторю: делать это нужно разумно, чтобы не получить обратного эффекта вследствие перегрузки памяти. ASP.NET поддерживает кэширование на двух основных уровнях: данных и страниц. Кэширование вывода страниц является простым и эффективным способом обойтись без прохождения каждого запроса через конвейер HTTP, а кэширование данных, осуществляемое объектом Cache, которым вы управляете с помощью специального API, позволяет создать слой кэширования внутри бизнес-слоя или слоя данных при- ложения. Только факты Кэширование — это выполняемое системой или приложением сохранение наиболее часто используемых данных в промежуточном контейнере. ASP.NET поддерживает кэширование на двух основных уровнях: данных и страниц. Для кэширования данных приложения используется объект Cache — глобальный контейнер данных с API словарного типа, способный автоматически очищать память от неиспользуемых элементов. Элементы, добавляемые в кэш, могут зависеть от группы файлов и каталогов, других элементов кэша, таблиц базы данных или внешних событий. При модифи- кации предмета такой зависимости элемент кэша, связанный с этим предметом, становится недействительным и автоматически удаляется из памяти. Кэшированием вывода страниц называется сохранение сгенерированного ими HTML-кода на клиенте, на Web-сервере или на прокси-сервере. У вас есть воз- можность задать срок хранения кэшированной копии страницы, а также указать, что копии должны варьироваться в зависимости от значений определенных пара- метров, заголовков HTTP или пользовательских строк. Когда действует модель процесса IIS 6.0, может использоваться интегрированный механизм кэширования Web-сервера. В таком случае запросы страниц ASP.NET фильтруются драйвером уровня ядра и при наличии в кэше действительного от- вета обрабатываются без участия ASP.NET. Создавая пользовательские элементы управления, можно кэшировать отдельные части страницы ASP.NET. В ASP.NET 2.0 введены две новые функции кэширования: профили кэширования и подстановка после кэширования.
Глава 15 Безопасность в ASP.NET Многие разработчики на собственном печальном опыте убедились, что защита — это не та функция, которою можно добавить в готовое приложение или ввести на поздних этапах разработки. Она тесно связана со всеми основными функциями приложения и должна планироваться еще на этапе его проектирования, причем в первую очередь и параллельно с остальными функциями. По своей природе приложения Web являются объектами многих видов атак раз- ной степени опасности. Поэтому хорошо защищенное Web-приложение должно не потенциально, а реально отражать все атаки. Задача обеспечения надежной защиты является очень сложной, многоплановой, и ее оптимальное решение индивидуально для каждого приложения. Важно помнить, что надежная защита обычно реализуется на двух уровнях: системы и приложения. ASP.NET значительно упрощает программную защиту приложений, предостав- ляя встроенную инфраструктуру для защиты на уровне приложения от неавтори- зированного доступа к Web-страницам. Однако имейте в виду, что защита такого типа — лишь половина дела: надежно защищенный сайт должен быть защищен и от серверных атак. В этой главе мы поговорим о контексте защиты ASP.NET и его связи с аутенти- фикационными механизмами IIS, обсудим лучшие программные приемы защиты от Web-атак. Откуда исходят угрозы Концепция защиты предполагает наличие атакующего. В табл. 15-1 перечислены наиболее распространенные типы Web-атак. Табл. 15-1. Типичные Web-атаки Тип атаки Описание Межсайтовый скриптинг (Cross-Site Scripting, XSS) Атакующий подставляет в поле формы или URL сценарий, используемый приложением при формировании новой страницы Атака на отказ (Denial of Service, DoS) Сервер атакуется ложными запросами, в результате чего система перегружается и настоящий трафик блокируется Подслушивание (eavesdropping) Атакующий использует специальное программное обеспе- чение для перехвата и чтения незашифрованных пакетов, передаваемых по сети Подмена значения скрытого поля (hidden-field tampering) Атакующий подменяет значения скрытых полей, содержа- щих критичные для приложения данные Атака одним щелчком (one-click) Серверу направляются опасные РО5Т-запросы HTTP, выда- ваемые за запросы от его собственных страниц
Безопасность в ASP.NET Глава 15 603 Табл. 15-1. (окончание) Тип атаки Описание Похищение сеанса (session hijacking) Внедрение SQL-кода (SQL injection) Атакующий вычисляет или похищает идентификатор сеанса и подключается к серверу, используя сеанс другого пользо- вателя В поле формы, которое предназначено для ввода значений, включаемых в формируемые SQL-команды путем конкатена- ции, атакующий вводит злонамеренный SQL-код, в резуль- тате чего команда изменяется и выполняет нужные атакую- щему действия Идея защиты от подобных атак проста: все, что сервер получает от браузера, перед использованием должно тщательно проверяться — иначе ваше приложение становится уязвимым для атак внедрением, то есть для любых разновидностей атак XSS и вне- дрения SQL-кода. Кроме того, критичные данные никогда не должны пересылаться по сети в открытом виде, без шифрования, и, разумеется, при хранении на сервере также должны быть защищены. Если существует способ написания «пуленепробиваемого» приложения, защищен- ного и от подмены, и от похищения информации и кода, то он заключается в при- менении той или иной комбинации перечисленных ниже мер. Программный код Проверка данных, контроль типов и размера данных, меры, предотвращающие подмену. Доступ к данным Применение ролей, наделение пользователей минимальными полномочиями, использование хранимых процедур или, по крайней мере, пара- метризированных команд. Хранение и администрирование Запрет отправки клиенту критичных данных, использование хэш-кодов для обнаружения манипуляций, аутентификация пользо- вателей и защита идентификационных данных, предъявление строгих требований к паролям. Как видите, надежная защита приложения может быть обеспечена лишь объеди- ненными усилиями разработчиков, архитекторов и администраторов. Контекст защиты ASP.NET Защита приложения заключается прежде всего в аутентификации пользователей и авторизации выполняемых ими операций над системными ресурсами. В состав ASP.NET входит необходимый комплекс механизмов аутентификации и авторизации, реализованных с использованием средств IIS и .NET Framework, а также сервисов защиты операционной системы. Поэтому в контексте защиты приложения ASP.NET можно выделить три уровня. На уровне IIS с отправителем запроса связывается токен защиты, сгенерированный механизмом аутентификации IIS. На уровне рабочего процесса ASP.NET идентифицируется учетная запись того потока рабочего процесса ASP.NET, который будет обслуживать запрос. Если при- меняется имперсонализация, токен защиты потока может быть изменен. Иденти- фикатор потока определяется на основе установок, заданных в конфигурационном файле или метабазе IIS в соответствии с используемой моделью процесса.
604 Часть III Инфраструктура ASP.NET На уровне конвейера ASP.NET производится получение учетных данных пользо- вателя приложения. Способ выполнения этой задачи зависит от заданных в кон- фигурационном файле установок, управляющих аутентификацией и авторизацией. Чаще всего в приложениях ASP.NET применяется аутентификация Forms. Учетная запись рабочего процесса помимо прочего определяет, к каким файлам, папкам и базам данных он имеет доступ. Идентификация пользователя приложения Когда запрос страницы ASP.NET поступает на Web-сервер, I IS назначает его одному из имеющихся в пуле потоков. (Сам же IIS выполняется от имени учетной записи SYSTEM — наиболее мощной учетной записи в Microsoft Windows.) С этого момента в процессе обработки запроса один за другим используются три перечисленных мною выше контекста защиты. Рассмотрим их подробнее. Контекст защиты потока IIS Поток, физически обрабатывающий запрос, выполняется от имени учетной записи, выбор которой зависит от текущей аутентификационной установки IIS: Basic, Digest, Integrated Windows или Anonymous. Если сайт допускает анонимный доступ, во вре- мя имперсонализации поток использует заданную вами учетную запись (рис. 15-1). По умолчанию это IUSR_XXX, где XXX — имя компьютера. (Диалоговое окно, пока- занное на рис. 15-1, открывается из консоли управления IIS, точнее, из окна свойств сайта, открываемого из этой консоли.) Рис. 15-1. Включение анонимного доступа и редактирование аутентификационных установок сайта Установка Basic является стандартом HTTP и поддерживается всеми браузерами. При таком способе аутентификации запрос возвращается браузеру с определенным кодом состояния ATL, который говорит ему о необходимости отобразить стандартное диалоговое окно для ввода имени и пароля пользователя. Эти данные отправляются IIS, который ищет соответствующую им учетную запись Web-сервера. Поскольку имя и пароль пользователя передаются просто в виде текста в кодировке Base64 (по сути,
Безопасность в ASP.NET Глава 15 605 в виде чистого текста), аутентификацию типа Basic рекомендуется производить только через защищенные каналы HTTPS. Аутентификация типа Digest отличается от аутентификации типа Basic тем, что перед отправкой имя и пароль пользователя хэшируются Данная функция введена в HTTP 1.1 и поддерживается не всеми браузерами. Кроме того, в Windows 2000 для ее осуществления необходимо, чтобы пароль хранился на сервере с обратимым шифрованием. В Windows Server 2003 такого требования больше не существует. Аутентификация типа Digest и Basic успешно осуществляется через брандмауэры и прокси-серверы. Аутентификация Integrated Windows заключается в обмене информацией между браузером и Web-сервером: браузер передает Web-серверу учетные данные текущего пользователя, которому ничего вводить не нужно. Для этого пользователь должен иметь учетную запись на Web-сервере или в доверяемом домене. Аутентификация производится методом NTLM «запрос ответ» или с использованием системы Kerberos. Препятствием для нее является наличие брандмауэра, а кроме того, она поддержива- ется немногими браузерами. га Примечание Существует еще один тип аутентификации, основанный на использова- нии сертификатов пользователей. Суть его заключается в том, что входящее в состав IIS программное обеспечение SSL проверяет полученный от браузера сертификат поль- зователя. В Интернете пользователи получают сертификаты от доверенных организаций, а в интранет-сетях — от администрации, своей компании. После аутентификации поток диспетчеризирует запрос внешнему модулю. Когда речь идет о запросе к ASP.NET, дальнейшее зависит от модели процесса, используе- мой приложением. Контекст защиты рабочего процесса В модели процесса IIS 5.0 поток IIS вручает запрос модулю aspnet_isapi.dll, который, в свою очередь, запускает рабочий процесс aspnet wp.exe. В модели процесса IIS 6.0 запрос ставится в очередь пула приложений и выбирается оттуда рабочим процессом w3wp.exe, который обслуживает пул приложений. От имени какой учетной записи выполняется рабочий процесс? При использовании модели процесса IIS 5.0 это локальная учетная запись ASPNET, создаваемая при инсталляции .NET Framework. Она обладает минимальными при- вилегиями, необходимыми для выполнения ее роли, и значительно ограниченными по сравнению с привилегиями учетной записи SYSTEM. Изменить имя и пароль учет- ной записи рабочего процесса можно с помощью следующей установки, задаваемой в файле machine.config: <processModel userName="..." password=".. " /> Если используется модель процесса IIS 6.0, то учетная запись рабочего процесса имеет имя NETWORK SERVICE. Изменение ее имени и пароля выполняется в диало- говом окне, показанном на рис. 15-2. Для того чтобы вывести его на экран, в управ- ляющей консоли IIS следует выбрать нужный пул приложений и открыть окно его свойств. Учетная запись NETWORK SERVICE имеет столь же ограниченные приви- легии, как учетная запись ASPNET, и является новой встроенной учетной записью Windows Server 2003 и Windows ХР. Внутри рабочего процесса выбранный из пула поток принимает запрос и приступает к его выполнению. Какая учетная запись должна быть у этого потока? Если для при- ложения ASP.NET имперсонализация отключена, данный поток наследует учетную
606 Часть III Инфраструктура ASP.NET запись рабочего процесса. Так происходит по умолчанию. Если же вы включите им- персонализацию, то рабочий поток будет наследовать токен защиты полученный от IIS. Для включения имперсонализации нужно поместить в файл web.config такую установку: impersonation enabled="true" /> Рис. 15-2. Ввод имени и пароля рабочего процесса, обслуживающего пул приложений при использовании модели процесса IIS 6.0 Тема имперсонализации в ASP.NET, и особенно ее расширенной формы, в которой используется фиксированная учетная запись, требует более детального обсуждения. Я вернусь к ней немного погодя. Когда применяется имперсонализация, учетная запись рабочего процесса не изменя- ется. Он по-прежнему компилирует страницы и считывает установки из конфигураци- онных файлов, используя исходную учетную запись. Имперсонализация же действует только в отношении кода самой страницы, а не тех предварительных операций, кото- рые осуществляются до того, как запрос будет передан непосредственно обработчику страницы. Любые обращения к локальным файлам и базам данных происходят от имени имперсонализированной учетной записи, а не учетной записи рабочего процесса. Контекст защиты конвейера ASP.NET Контекст защиты конвейера ASP.NET представлен учетной записью пользователя, от которого поступил запрос. Для создания этого контекста необходимо аутентифициро- вать пользователя и авторизировать доступ к странице и используемым ею ресурсам. Очевидно, что если страница доступна всем пользователям, этот шаг не выполняется, страница обрабатывается и ее вывод предоставляется пользователю. Чтобы защитить страницы от неавторизированного доступа, необходимо определить для приложения ASP.NET политику аутентификации — Windows, Passport или Forms. Аутентификационные модули перехватывают запросы защищенных страниц и выполняют операции, необходимые для получения имени и пароля пользователя, возвращая ему за- прошенную страницу только при условии, что доступ к ней этому пользователю разрешен.
Безопасность в ASP.NET Глава 15 607 Изменение учетной записи процесса Как следует действовать в том случае, если требуется изменить используемую по умолчанию учетную запись ASP.NET, для того чтобы получить дополнительные при- вилегии? Что лучше: создать для рабочего процесса пользовательскую учетную запись или осуществлять имперсонализацию рабочим процессом определенной учетной записи? Примечание Едва ли возможно создать новую функциональную учетную запись, обла- дающую меньшими привилегиями, чем учетная запись ASPNET или NETWORK SERVICE. Если вы все же решите попытаться это сделать, обязательно очень тщательно протести- руйте работу приложения. Определение учетной записи процесса Использование раздела <processModel> (в модели процесса IIS 5.0) или диалогового окна, показанного на рис. 15-2 (в модели процесса IIS 6.0), — единственный способ изменения реальной учетной записи процесса ASP.NET. Если вы измените учетную запись процесса, то все его потоки будут выполняться от имени новой учетной за- писи и никакой дополнительной работы делать не придется. Не забудьте только убедиться, что новая учетная запись обладает как минимум полными правами на доступ к папке Temporary ASP.NET Files. (Внимательно изучите список разреше- ний, предоставленных стандартной учетной записи ASP.NET, который вы найдете в разделе «Привилегии используемой по умолчанию учетной записи ASP.NET» далее в этой главе.) В качестве альтернативы можно дать рабочему процессу указание выполнять имперсонализацию определенной учетной записи, для чего используется раздел <identity> файла web.config. Заметьте, что в таком случае каждый поток рабочего процесса должен будет имперсонализировать заданную учетную запись. Имперсо- нализацию необходимо выполнять при каждом переключении потока, потому что потоку, на который производится переключение, заново назначается учетная запись процесса. Имперсонализация фиксированной учетной записи Для имперсонализации фиксированной учетной записи необходимо создать эту учет- ную запись и затем добавить в файл web.config соответствующую установку: <identity impersonate="true" userName="MyAspNetAccnt” password="ILoveA$p*SinceVer3.О" /> Как уже упоминалось, имперсонализация — это не изменение физической учетной записи процесса ASPNET, а лишь выполнение этого процесса от имени другой учетной записи. При имперсонализации потоки, выполняющиеся в контексте рабочего про- цесса ASP.NET, в течение всего времени работы приложения выполняются от имени заданного пользователя. Имперсонализация фиксированной учетной записи отличается от классической, осуществляемой в рамках одного запроса, имперсонализации пользователя Windows, от которого поступил запрос. Имперсонализация на уровне запроса не требует задания фиксированной учетной записи, поскольку рабочим процессом наследуется токен защиты, созданный IIS. Когда же осуществляется имперсонализация фиксированной учетной записи, токен защиты генерируется рабочим процессом ASPNET. Учтите, что если у рабочего процесса не окажется требуемых разрешений, он не сможет вы- полнить имперсонализацию.
608 Часть III Инфраструктура ASP.NET Подробнее об имперсонализации фиксированной учетной записи В Windows 2000 процесс, который выполняется от имени учетной записи, не имеющей привилегий администратора, не может имперсонализировать фикси- рованную учетную запись, если не предоставить ему необходимого для этого разрешения, а именно разрешения Act Xs Part Of The Operating System (работа в режиме операционной системы). Это довольно мощная привилегия, и из сооб- ражений безопасности учетным записям ASPNET и NETWORK SERVICE она по умолчанию не предоставлена. В Windows ХР и Windows Server 2003 такого требования более не существует, и процессы, не имеющие привилегии Act As Part Of The Operating System, могут имперсонализировать фиксированную учетную запись. Однако в ASP.NET 1.1 имперсонализация фиксированной учетной записи воз- можна и на компьютерах, работающих под управлением Windows 2000 и IIS 5.0. Исполняющая среда прибегает к специальному приему, позволяющему ей пере- направить вызов обратно модулю aspnet_isapi.dll, который выполняется в со- ставе IIS 5.0 от имени учетной записи SYSTEM. ISAPI-расширение ASP.NET 1.1 создает токен защиты и копирует его в пространство памяти рабочего процесса. Таким образом обеспечивается поддержка имперсонализации фиксированной учетной записи для рабочего процесса, не имеющего привилегии Act As Part Of The Operating System. Итак, беспокоиться об обеспечении разрешений для имперсонализации фик- сированной учетной записи вам нужно будет только при наличии приложений ASP.NET 1.0, функционирующих в Windows 2000. Тогда придется запускать эти приложения от имени учетной записи SYSTEM со всеми вытекающими отсюда угрозами безопасности. Имперсонализация анонимной учетной записи Третьей возможностью изменения учетной записи, от имени которой выполняется рабочий процесс ASP.NET, является имперсонализация анонимной учетной записи. В этом случае доступ к приложению ASPNET разрешается анонимным пользователям и в качестве учетной записи приложения конфигурируется анонимная учетная запись. Имперсонализация осуществляется на уровне запроса, и код ASP.NET выполняется от имени имперсонализированной учетной записи. Учетной’записью рабочего процесса при этом остается ASPNET или NETWORK SERVICE, благодаря чему вам не нужно бес- покоиться о создании для него новой учетной записи и назначения ей минимального набора разрешений на доступ к папкам, необходимых для работы ASP.NET. Привилегии используемой по умолчанию учетной записи ASP.NET Из всего набора возможных разрешений учетной записи ASPNET или NETWORK SERVICE назначены лишь пять: Access this computer from the network (доступ к компьютеру из сети); Deny logon locally (отклонить в локальный вход); Deny logon through Terminal Services (запретить вход в систему через службу тер- миналов); Log on as a batch job (вход в качестве пакетного задания); Log on as a service (вход в качестве службы).
Безопасность в ASP.NET Глава 15 609 Кроме того, учетной записи ASP.NET предоставлены отдельные разрешения NTFS на доступ к определенным папкам, а также на создание временных файлов и сборок. Вот какие это папки: Корневая папка .NET Framework Содержит системные сборки .NET Framework, доступ к которым необходим ASP.NET. Обычно это папка Microsoft.NET\Frame- work\[ версия], расположенная в папке Windows. ASP.NET разрешены только чте- ние и обзор каталогов. Временные файлы ASP.NET Эта папка представляет поддерево файловой систе- мы, в котором находятся все временные файлы. ASP.NET гарантирован полный контроль над этим поддеревом. Глобальный кэш сборок (Global Assembly Cache, GAC) ASP.NET необходи- мы полные разрешения на доступ к сборкам, находящимся в GAC, то есть в папке %WINDOWS%\Assembly\GAC. В Проводнике Windows папка GAC не отобра- жается, но вы можете просмотреть список установленных сборок, открыв папку %WINDOWS%\Assembly. Системная папка Windows Процессу ASP.NET необходим доступ для чтения к папке System32, откуда он загружает необходимые DLL Win32. Корневая папка приложения Процесс ASP.NET нуждается в доступе для чтения к файлам, составляющим Web-приложение. Обычно папка приложения находится в папке %SYSTEMDRIVE%\Inetpub\Wwwroot. Корневая папка Web-сайта ASP.NET может понадобиться просканировать эту папку в поиске конфигурационных файлов, которые она должна иметь право читать. Обычно корневой папкой сайта является %SYSTEMDRIVE%\Inetpub\Wwwroot. Если приложение ASP.NET выполняется от имени учетной записи, которая не имеет каких-либо из церечисленных разрешений, в его работе может произойти сбой. Поэтому настоятельно рекомендуется присваивать все эти разрешения фиксирован- ным учетным записям, которые будут имперсонализироваться приложением. Уровни доверия приложений ASP.NET Как известно, приложения ASP.NET состоят из управляемого кода и выполняются в общеязыковой среде (Common Language Runtime, CLR). В CLR выполняющемуся коду назначается определенная зона безопасности в зависимости от сведений, которые он предоставляет о своем источнике, например об URL. Каждой зоне безопасности со- ответствует определенный набор разрешений, а каждому набору разрешений, в свою Очередь, соответствует определенный уровень доверия. По умолчанию приложения ASP.NET выполняются в зоне Му Computer с полным доверием. Хорошо это или плохо? Приложение ASP.NET работает на Web-сервере и не может повредить систему пользователя, который подключается к нему через браузер. Более того, без браузера такое приложение использоваться вообще не может. И тем не менее, некоторые раз- работчики возражают против полного доверия к приложению ASPNET. Проблема здесь не в самом приложении, а в том, что оно открыто для общего доступа через Интернет, то есть функционирует в самом опасном окружении, которое только можно себе представить. Если полностью доверенное приложение ASPNET подвергнет- ся успешной атаке, тогда, используя рабочий поток, хакер сможет выполнить любые действия, разрешенные данному потоку. Поэтому полностью доверенное приложение, открытое для общего доступа, является потенциальным плацдармом для хакерских атак. И чем менее доверенным является приложение, тем лучше оно защищено.
610 Часть III Инфраструктура ASP.NET Раздел <trust> По умолчанию приложения ASP.NET работают без ограничений и могут делать все, что позволено их учетным записям. Реальные ограничения безопасности, иногда применяемые к приложениям ASP.NET (например, запрет записи файлов) являются не признаком ограниченного доверия, а просто результатом использования учетной записи с ограниченными разрешениями, как та, от имени которой обычно выполня- ются приложения ASP.NET. Настроив раздел <trust> корневого файла web.config, для приложения можно сконфигурировать разрешения на доступ к программному коду и указать, с каким доверием, полным или частичным, оно должно выполняться: <trust level="Medium" originUrl=”" /> Поддерживаемые уровни доверия к приложению ASP.NET описаны в табл. 15-2. Табл. 15-2. Уровни доверия, задаваемые в разделе <trust> Уровень Описание Full Приложения работают с полным доверием и могут выполнять любой код в кон- тексте того процесса, в котором они работают High Приложение наделяется большинством поддерживаемых разрешений. Такой режим подходит для приложений, которым необходимы достаточно широкие полномочия, в условиях, когда все же требуется снизить возможные риски Medium Приложение может выполнять чтение и запись элементов своего каталога и взаимодействовать с базами данных Low Приложению позволено считывать его собственные ресурсы, но запрещено об- ращаться к ресурсам, расположенным вне его пространства Minimal Приложение не может взаимодействовать с защищенными ресурсами. Данная установка подходит для непрофессиональных сайтов, которым достаточно под- держки обычного HTML и очень изолированной бизнес-логики Е Примечание Web-приложения и Web-сервисы, созданные с использованием NET Frame- work 1.0, всегда выполняются с неограниченными разрешениями на доступ. Конфигуриру- емые уровни, перечисленные в табл. 15-2, к приложениям ASP.NET 1.0 неприменимы. Ограничение разрешений приложения поначалу может стеснять вашу свободу, но позволит создавать более эффективный и защищенный код. Примечание Для раздела <trust> поддерживается атрибут originUrl, название которого не отвечает его назначению. В этом атрибуте задается URL ресурса, доступ к которому разре- шен приложению. Программный доступ к ресурсам через HTTP осуществляется с помощью класса Socket или WebRequest. При этом используется класс разрешения WebPermission. Разумеется, доступ к внешним ресурсам возможен лишь при условии, что он допускается уровнем доверия, заданным в разделе <trust>. Его допускают уровень доверия Medium, а также более высокие уровни. Разрешения ASP.NET Давайте подробнее рассмотрим разрешения, предоставляемые приложениям ASP.NET на разных уровнях доверия (табл. 15-3). Более детальная информация о разрешениях, предоставляемых на разных уровнях доверия, содержится в конфигурационных файлах защиты каждого уровня. Имена этих файлов задаются в разделе <trustLevel>. Действия приложений, наделенных полным доверием, ничем не ограничиваются. Приложения с высоким уровнем доверия могут считывать и записывать любые файлы
Безопасность в ASP.NET Глава 15 611 своего пространства, но доступ к каждому из этих файлов регулируется его списком контроля доступа, поддерживаемым NTFS. Приложения с высоким уровнем доверия имеют неограниченный доступ к Microsoft SQL Server, но не имеют, например, досту- па к классам OLE DB. (Разрешение OleDbPermission и другие разрешения на доступ к управляемым провайдерам предоставляются всем приложениям, за исключением полностью доверенных. Вызовы .NET Reflection запрещены, за исключением обра- щенных к классам из пространства имен System.Reflection.Emit.') Табл. 15-3. Основные разрешения у ровней доверия ASP.NET High Medium Low Minimal FilelO He ограничены Read/Write в простран- стве приложения Read Отсутствуют IsolatedStorage He ограничены ByUser ByUser (максимум 1 Мбайт) Отсутствуют Printing DefaultPrinting To же, что для High Отсутствуют Отсутствуют Security Assertion, Execution, ControlThread, Controlprincipal То же, что для High Execution Execution SqlClient He ограничены Не ограничены (пустой пароль недопустим) Отсутствуют Отсутствуют Registry He ограничены Отсутствуют Отсутствуют Отсутствуют Environment He ограничены Отсутствуют Отсутствуют Отсутствуют Reflection ReflectionEmit Отсутствуют Отсутствуют Отсутствуют Socket He ограничены Отсутствуют Отсутствуют Отсутствуют Web He ограничены Подключение к ис- ходному хосту, если сконфигурировано То же, что для High Отсутствуют Приложения с уровнем доверия Medium имеют неограниченный доступ к SQL Server, однако при условии, что пароли их учетных записей не пустые. Разреше- ние WebPermission предоставляется приложениям с уровнями доверия Medium и Low, при этом необходимо, чтобы URL ресурса, доступ к которому осуществляет прило- жение, был задан в атрибуте origmUrl раздела <trust>. Приложения с уровнем дове- рия Low имеют право читать файлы в своем каталоге, но не записывать. Изолирован- ное хранение разрешено, однако для него установлена квота в 1 Мбайт. Как правило, уровня Medium достаточно любому Web-приложению, и с ним не бывает никаких проблем, если только вам не требуется доступ к унаследованным COM-объектам и базам данных, взаимодействие с которыми осуществляется по- средством провайдеров OLE DB. Предоставление привилегий, выходящих за рамки уровня доверия Предположим, что для выполнения одной из задач приложения необходимы при- вилегии, не обеспечиваемые выбранным уровнем доверия. У этой проблемы есть два решения. Простейшее заключается в том, чтобы внести изменения в файл политики данного уровня доверия, добавив требуемое разрешение. Такое решение легко реа- лизовать, и оно не требует внесения изменений в программный код приложения. (Ра- зумеется, для редактирования файла политики безопасности нужны соответствующие
612 Часть III Инфраструктура ASP.NET права.) Однако с позиций безопасности это не слишком удачное решение, поскольку вы предоставляете всему приложению разрешения, необходимые лишь одному методу определенной страницы или сборки. Второй подход требует некоторой переработки приложения, но позволяет сделать код более безопасным. Его идея заключается в том, чтобы поместить серверный код в «песочницу» и сделать так, чтобы он делегировал внешнему компоненту (например, сервису или программе с интерфейсом командной строки) те задачи, для выполнения которых он не имеет достаточных разрешений. Очевидно, что этот внешний компонент должен иметь такие разрешения. Примечание Помещение кода в «песочницу» является единственным решением для приложений с частичным доверием, если им необходимо вызывать сборку, для которой не установлен атрибут AllowPartiallyTrustedCallers. За дополнительной информацией о программировании на среднем уровне доверия вы можете обратиться по следующему адресу: http://msdn.microsoft.eom/library/en-us/dnpag2/html/PAGHT000020.asp. Методы аутентификации ASP.NET В зависимости от типа запрошенного ресурса IIS обрабатывает запрос самостоятельно или передает его зарегистрированному расширению ISAPI. Если требуется участие ASP.NET (как при запросе файла .aspx), IIS передает запрос этой системе вместе с токеном аутентифицированного или анонимного пользователя. Что происходит далее, зависит от конфигурации ASP.NET. ASP.NET поддерживает три метода аутентификации — Windows, Passport и Forms, которым соответствуют одноименные установки, задаваемые в разделе <authentication> файла web.config. По умолчанию используется режим аутентификации Windows. В раз- деле <authentication> может задаваться и четвертая установка — None. Она означает, что ASP.NET не пытается выполнять аутентификацию самостоятельно и полностью полагается на сведения, предоставленные ей Web-сервером. В таком случае возмож- но удовлетворение запросов, поступивших от анонимных пользователей. Страницы приложения, расположенные в подразделах его основного раздела, наследуют метод аутентификации, заданный на уровне приложения. Давайте вкратце рассмотрим методы Windows и Passport, а затем подробнее по- говорим о наиболее распространенном методе аутентификации — Forms. Аутентификация Windows При использовании аутентификации Windows ASP.NET действует совместно с IIS. Собственно аутентификацию выполняет IIS, применяющий для этого один из трех рассмотренных нами методов: Basic, Digest или Integrated Windows. После аутен- тификации пользователя Web-сервер передает ASP.NET токен защиты. В режиме аутентификации Windows ASP.NET не выполняет никаких дальнейших действий. Для доступа к ресурсам она использует токен защиты, полученный от IIS. Обычно метод аутентификации Windows применяется в интранет-сценариях, ког- да пользователи приложения имеют учетные записи Windows, которые могут быть аутентифицированы только Web-сервером. Предположим, что вы сконфигурировали IIS для работы в режиме Integrated Windows. Что происходит при получении запро- са к приложению ASP.NET? Вначале IIS аутентифицирует пользователя (для чего, если учетная запись локального пользователя не соответствует ни одной из учетных записей Web-сервера или доверенного домена, отобразит диалоговое окно для ввода имени и пароля), а затем передаст ASP.NET токен защиты.
Безопасность в ASP.NET Глава 15 613 Авторизация доступа с использованием ACL Как правило, при аутентификации Windows производится авторизация доступа к фай- лам, осуществляемая йосредством HTTP-модуля FileAuthorizationModule. Страницы, предназначенные для конкретных пользователей, могут быть защищены от неавто- ризированного доступа с использованием своих списков контроля доступа (Access Control List, ACL). Когда ASP.NET собирается обратиться к какому либо ресурсу, вызывается модуль FileAuthorizationModule, который проверяет наличие пользователя в списке контроля доступа. Поэтому, например, пользователь Joe никогда не получит страницу .aspx, ACL которой не содержит записи с указанием его прав. Заметьте, что авторизация доступа к файлам не требует имперсонализации на уровне ASPNET и, что более важно, работает независимо от того, включена ли им- персонализация вообще. Это и хорошо и плохо одновременно. Хорошо то, что для обеспечения или запрета доступа определенных пользователей к ресурсу ASP.NET достаточно сконфигурировать его список контроля доступа. Но как быть с теми ло- кальными файлами, которые не являются ресурсами ASP.NET? Как, например, за- щитить от неавторизированного доступа страницу HTML? На самом деле это не так уж трудно сделать, поскольку страницы HTML редко нуждаются в защите от несанкционированного доступа. Но если возникнет такая необходимость, страницу всегда можно переименовать в .aspx, чтобы она обрабатывалась исполняющим кон- вейером ASP.NET. Г Примечание HTML-файлы и другие ресурсы все же можно защищать с использованием разрешений NTFS. В частности, когда для файла заданы определенные ограничения NTFS, доступ к нему без соответствующих разрешений невозможен. В качестве альтернативы можете присвоить файлу пользовательское расширение и написать легковесное расширение ISAPI или управляемый обработчик HTTP, ре- ализовав в нем пользовательский механизм авторизации. Об обработчиках HTTP подробно рассказывается в моей книге «Programming Microsoft ASP.NET 2.0 Applica- tions? Advanced Topics» (Microsoft Press, 2005). г Примечание Аутентификация Windows работает и с авторизацией на основе URL, ре- ализованной HTTP-модулем URLAuthorizationModule. Этот модуль разрешает или запре- щает определенным пользователям и членам определенных ролей доступ к ресурсам, идентифицируемым с помощью URL. (Я расскажу об этом подробнее при обсуждении аутентификации Forms.) Аутентификация Passport Аутентификация Passport поддерживается централизованным аутентификационным сервисом Microsoft. Этот метод обеспечивает автоматическую аутентификацию поль- зователей на любом из сайтов, участвующих в данной инициативе. Иными словами, пользователь может один раз зарегистрироваться в службе Passport, получить серти- фикат и входить с ним на любые сайты. В дополнение к централизованному сервису входа аутентификация Passport обеспечивает также базовые сервисы поддержки про- филей пользователей для сайтов-членов инициативы Passport. Данный метод аутентификации реализован в виде HTTP-модуля PassportAuthen- ticationModule, который должен быть установлен на каждом сайте, поддерживающем аутентификацию Passport. Когда такой сайт получает запрос HTTP, он проверяет, со- держится ли в запросе действительный билет Passport. Если не содержится, Web-сер- вер возвращает код состояния 302 и перенаправляет клиента к сервису входа Passport. Сгенерированная при этом строка запроса содержит зашифрованную информацию
614 Часть III Инфраструктура ASP.NET об исходном запросе. Клиент направляет запрос GET серверу входа и передает ему указанную строку запроса. Сервер входа Passport пересылает клиенту HTML-форму входа, пользователь заполняет ее и введенные им данные возвращаются серверу входа через защищенный канал SSL. Далее сервер входа аутентифицирует пользователя, и если эта процедура вы- полняется успешно, создает билет Passport, после чего направляет пользователя по исходному URL, отправив в запросе зашифрованный билет. Браузер получает ин- струкцию о перенаправлении и снова запрашивает защищенный ресурс Web-прило- жения, но на этот раз в запросе содержится действительный билет, поэтому модуль PassportAuthenticationModule разрешает выполнение запроса. Аутентификация Forms Методы аутентификации Windows и Passport редко применяются в реальных Web- приложениях. Аутентификация Windows основана на использовании учетных записей Windows и списков контроля доступа NTFS, поэтому для ее применения необходимо, чтобы пользователи подключались к приложению с компьютеров, на которых установ- лена Windows. Для приложений интранет и некоторых интернет-сценариев этот метод удобен и эффективен. Однако он не подходит для типичного Web-приложения, поль- зователи которого не имеют учетных записей Windows из его домена. Неприменим в таких приложениях и метод Passport, хотя по другой причине. Сертификаты не бес- платны, кроме того, требуются основательные меры защиты сервера, которые дорого обходятся и нужны далеко не каждому сайту. Поэтому метод Passport применяется главным образом в системах электронной коммерции в группах сайтов, имеющих общих пользователей. Каков же оптимальный механизм аутентификации пользователей Web-прило- жений? Опыт показывает, что лучше всего поместить поверх каждой страницы, не предназначенной для общего доступа, некоторый более или менее стандартный код и при попытке анонимного подключения к сайту перенаправлять с его помощью поль- зователя на страницу входа. Эта страница предложит пользователю ввести свое имя и пароль, выполнит аутентификацию и, в случае успеха, перенаправит пользователя на ту страницу, которую он запросил. Такой метод называется аутентификацией Forms (то есть аутентификацией с помощью формы). Код его реализации отнюдь не сложен, но, конечно, писать его снова и снова — не вдохновляющая перспектива. К счастью, этого и не требуется, поскольку в ASP.NET существует встроенная инфраструктура для реализации такой схемы входа. Аутентификация Forms является прекрасным (и, пожалуй, единственным) решением, позволяющим получить имя и пароль пользователя и обработать их внутри приложения, например, проверить по базе данных учетных записей пользователей. Главное отличие этого метода заключается в том, что все происходит под строгим контролем приложения. Для того чтобы настроить приложение для применения аутентификации Forms, нужно внести изменения в его корневой файл web.config: <system.web> Outhentication mode="Forms"> <forms loginllrl="login.aspx" /> </authentication> <authorization> <deny users="?" /> </authorization> </system.web>
Безопасность в ASP.NET Глава 15 615 В разделе <authenticatiori> задается URL созданной вами формы ввода. ASP.NET выводит эту форму только для тех пользователей, которым явно запрещен доступ в разделе <authorization>. Символ ? представляет любого анонимного, неаутенти- фицированного пользователя. Заметьте, что под анонимными пользователями здесь понимаются не анонимные пользователи IIS, а просто те, которые не были аутенти- фицированы с использованием вашей формы ввода. Все заданные в элементах <deny> пользователи перенаправляются на страницу ввода, где им предлагается ввести свои учетные данные. И Примечание Механизм аутентификации Forms защищает ресурсы ASP.NET, расположен- ные в папке, для которой включены аутентификация и авторизация Forms. Заметьте, что защищены этим механизмом только те ресурсы, запросы которых обрабатывает ASP.NET К ним относятся файлы aspx, .asmx и ashx, но не страницы HTML и не классические страницы ASP. Поток выполнения при аутентификации Forms Аутентификацией Forms управляет HTTP-модуль, реализующий класс FormsAuthen- ticationModule. Его поведение определяется установками, которые задаются в файле web.config. Когда браузер пытается обратиться к защищенному ресурсу, модуль Forms- AuthenticationModule включается в игру и пытается найти аутентификационный билет клиента. В ASP.NET 1.x этим билетом является просто cookie с определенным конфи- гурируемым именем. В ASP.NET 2.0 билетом может служить значение, включенное в состав URL (аутентификация Forms без применения cookie). Если допустимый cookie не найден, модуль перенаправляет запрос странице входа, поместив в строку запроса информацию об исходной запрошенной стра- нице. После этого выводится созданная разработчиком страница входа, которая содержит, как минимум, текстовые поля для ввода имени и пароля пользователя и кнопку для их отправки серверу. Обработчик щелчка этой кнопки проверяет введенные учетные данные, применяя пользовательский алгоритм, зависящий от конкретного приложения. Если пользователь аутентифицирован, код страницы входа перенаправляет браузер по исходному URL, который содержится в строке запроса указанной страницы: http //YourApp/login.aspx?Retuгnilrl=original.aspx Но теперь к запросу ресурса присоединяется аутентификационный билет. Когда браузер отправляет серверу повторный запрос этого ресурса, модуль HTTP извлекает билет и разрешает выполнение запроса. Давайте рассмотрим процесс аутентификации Forms на конкретном примере. Пе- ред его выполнением откройте файл web config демонстрационного приложения и удалите символы комментария, которые блокируют фрагмент разметки, включающей аутентификацию Forms — тем самым вы запретите анонимный доступ к страницам приложения. Далее в качестве пользователя введите в адресной строке браузера URL страницы LoginProcess\welcome.aspx, входящей в состав демонстрационного при- ложения, и щелкните кнопку перехода. Поскольку страницы этого приложения не разрешается открывать анонимно, модуль FormsAuthenticationModule перенаправит вас на страницу входа, показанную на рис. 15-3. • Внимание! Аутентификация Forms требует защиты трафика, передаваемого между клиентом и сервером во время аутентификации, и в настоящее время такая защита обеспечивается только протоколом HTTPS. Я еще вернусь к этой теме в подразделе «Общие вопросы защиты».
616 Часть 111 Инфраструктура ASP NET Рис. 15-3. Страница входа демонстрационного приложения Ввод учетных данных пользователя Структура страницы входа обычно всегда одна и та же: два текстовых поля, кнопка и, возможно, некоторый поясняющий текст. Однако ничто не мешает вам сделать эту страницу сколь угодно сложной. Пользователь вводит свои учетные данные (для которых обычно учитывается регистр) и щелкает кнопку входа. При возврате формы на сервере выполняется следующий код: void LogonUseг(object sender, EventArgs e) { string user = userName.Text: string pswd = password.Text; // Пользовательская аутентификация bool bAuthenticated = AuthenticateUser(user, pswd); if (bAuthenticated) FormsAuthentication.RedirectFromLoginPage(user, false); else errorMsg.Text = "Sorry, yours seems not to be a valid account."; } Обработчик события извлекает строки, введенные в поля имени пользователя и пароля, и вызывает локальную функцию с именем AuthenticateUser. Эта функция проверяет учетные данные пользователя и возвращает значение булева типа, ука- зывающее, успешно ли аутентифицирован пользователь. Если успешно, вызывается статический метод RedirectFromLoginPage класса FormsAuthentication, информирующий браузер о том, что он может направить повторный запрос на получение страницы. Таким образом, метод RedirectFromLoginPage перенаправляет аутентифицирован- ного пользователя по исходному запрошенному им URL. Метод имеет две перегру- женные версии: public static void RedirectFromLoginPage(string, bool); public static void RedirectFromLoginPage(String, bool, string); Первым аргументом обеих версий является имя пользователя, которое должно быть записано в аутентификационный билет, а вторым — значение булева типа, ко-
Безопасность в ASP NET Глава 15 617 торое определяет срок действия cookie, созданного для аутентификационного билета. Если этот аргумент равен false, задается обычный срок действия, определенный в атрибуте timeout (по умолчанию 30 минут). Если же этот аргумент равен true, cookie назначается срок действия 50 лет. В третьем, не обязательном, аргументе метода Re- directFromLoginPage задается путь к cookie. Аутентификация пользователя Алгоритм аутентификации, то есть код упоминавшегося выше метода AuthenticateUser, является произвольным. Например, вы можете проверять введенные пользователем имя и пароль по базе данных или использовать какой-нибудь другой источник дан ных. Приведенная ниже функция сравнивает имя и пароль пользователя со значе- ниями столбцов firstname и lastname таблицы Employees базы данных Northwind SQL Server 2000: private bool AuthenticateUser(string username, string pswd) { // Здесь выполняется аутентификация string connString = "..."; string cmdText = "SELECT COUNT(*) FROM employees " + "WHERE firstname=@user AND lastname=@pswd"; int found = 0; using(SqlConnection conn = new SqlConnection(connString)) { SqlCommand cmd = new .SqlCommand(cmdText, conn); cmd Parameters.Add("©user", SqlDbType.VarChar, 10).Value = username; cmd.Pa ramete rs.Add("@pswd", SqlDbType.VarChar, 20).Value = pswd; conn.0pen(); found = (int)cmd.ExecuteScalar(); conn.Close(); } return (found > 0); } Запрос возвращает количество строк таблицы, удовлетворяющих заданному кри- терию. Обратите внимание на использование в SQL-команде типизированных пара- метров фиксированного размера — это линия защиты против возможного внедрения злоумышленником вредоносного SQL-кода. Также заметьте, что приведенный код не гарантирует использования сильных паролей, поскольку в нем выполняется самое обычное сравнение с использованием оператора =, а не сравнение с учетом регистра, которое может быть, например, таким: SELECT COUNT(*) FROM employees WHERE л. CA3T(RTRIM(firstname) AS VarBinary)=CAST(RTRIM(@user) AS VarBinary) AND CAST(RTRIM(lastname) AS VarBinary)=CAST(RTRIM(@pswd) AS VarBinary) В этой команде оператор CAST служит для получения двоичного представления значения, а оператор RTRIM удаляет завершающие пробелы. На рис. 15-4 показана страница демонстрационного приложения, открываемая после успешной аутентификации пользователя. Вот ее исходный код: <%@ Page Language="C#" CodeFile="Welcome.aspx.cs" Inherits="Welcome" %> <btml><body>
618 Часть III Инфраструктура ASP.NET <form id="formT' runat="server”> <h1>Welcome, <%=llser. Identity. Name %></h1> </form> </body></html> Рис. 15-4. Пользователь аутентифицирован и его имя выведено на странице Явный выход Если явный вход обеспечивается любым Web-сайтом, пользователи которого долж- ны быть аутентифицированы, то явный выход является менее типичной операцией. Однако аутентификационный модуль ASP.NET поддерживает и ее. Соответствующий метод, SignOut класса FormsAuthentication, не имеет параметров и просто очищает аутентификационный билет приложения. Когда используются cookie, он удаляет текущий билет из коллекции Cookies текущего объекта HttpResponse и заменяет его пустым и устаревшим cookie. После вызова метода SignOut вы, скорее всего, пожелаете перенаправить пользова- теля на другую страницу, например, на домашнюю страницу приложения. Для этого не нужно переадресовывать браузер — воспользуйтесь более эффективным методом Server.Transfer, описанным в главе 12: void Signout(object sender, EventArgs e) { Fo rmsAuthentication.SignOut(); Server.Transfer("home.aspx ); } В ASP.NET 2.0 у класса FormsAuthentication имеется новый метод RedirectToLogin- Page, реализующий описанную функциональность, только вместо метода Server.Trans- fer он вызывает Response.Redirect. Мы рассмотрели основы аутентификации Forms, и теперь вы готовы к подробному изучению аутентификационного API ASP.NET 2.0 — методов класса FormsAuthentica- tion, настраиваемых параметров, задаваемых в файле web.config, а также API управ- ления членством и ролями. Класс FormsAuthentication Класс FormsAuthentication имеет ряд статических членов, предназначенных для ма- нипулирования аутентификационными билетами и выполнения базовых аутенти- фикационных операций. В частности, для перенаправления аутентифицированного
Безопасность в ASP.NET Глава 15 619 пользователя обратно на запрошенную им страницу используется метод RedirectFrom- LoginPage, а для удаления аутентификационного билета пользователя — метод SignOut. Остальные методы и свойства служат для манипулирования билетом и связанным с ним cookie. Свойства класса FormsAuthentication С перечнем свойств класса FormsAuthentication вы можете ознакомиться в табл. 15-4. Многие из них связаны с именованием и использованием cookie или возвращают значения конфигурационных атрибутов из раздела <forms>. Подробнее о нем рас- сказывается в следующем разделе. В ASP.NET 2.0 в состав класса FormsAuthentication было добавлено несколько новых свойств. Все перечисленные в табл. 15-4 свойства являются статическими. Табл. 15-4. Свойства класса FormsAuthentication Свойство Описание CookieDomain Возвращает домен, заданный для аутентификационного билета. Это свойство эквивалентно атрибуту domain раздела <forms> конфигурационного файла. В ASP.NET 1л данное свойство не под- держивается CookieMode Указывает, реализована ли аутентификация Forms с поддержкой cookie. В ASP.NET 1л данное свойство не поддерживается CookiesSupported Возвращает значение true, если текущий запрос поддерживает cookie. В ASPNET 1л данное свойство не поддерживается DefaultUrl Возвращает URL страницы, к которой нужно вернуться после успешной аутентификации запроса. Соответствует атрибуту de- faultUrl раздела <forms>. В ASPNET 1л данное свойство не под- держивается EnableCrossAppRedirects Указывает, разрешено ли перенаправлять пользователя к другому Web-приложению. В ASP.NET 1л данное свойство не поддержива- ется FormsCookieName Возвращает заданное в конфигурационном файле имя cookie, используемое в текущем приложении; по умолчанию имеет зна- чение ASPXAUTH FormsCookiePath Возвращает заданный в конфигурационном файле путь к файлу cookie, используемый в текущем приложении; по умолчанию имеет значение представляющее корневую папку LoginUrl Возвращает заданный в конфигурационном файле или использу- емый по умолчанию URL страницы входа. Соответствует атри- буту loginUrl раздела <forms>. В ASP.NET 1л данное свойство не поддерживается RequireSSL Указывает, обязательно ли передавать cookie через HTTPS-со- единение SlidingExpiration Указывает, включен ли режим скользящего устаревания Большинство перечисленных статических свойств инициализируется значения- ми, прочитанными из раздела <forms> конфигурационного файла, во время запуска приложения. Методы класса FormsAuthentication В табл. 15-5 описаны методы, поддерживаемые классом FormsAuthentication. Как и свойства, все они являются статическими.
620 Часть III Инфраструктура ASP.NET Табл. 15-5. Методы класса FormsAuthentication а Метод Описание Authenticate Сравнивает заданные учетные данные с содержащи- > мися в разделе <credentials> (об этом разделе я рас- скажу далее) Decrypt Получает действительный аутентификационный билет и возвращает экземпляр класса FormsAuthentica- - tionTicket Encrypt Генерирует строку печа гных URL-совмесгимых сим- волов. содержащую представление аутентификацион- ного билета. Это представление по вашему желанию может быть хэшировано и зашифровано GetAuthCookie Создает для заданного имени пользователя аутенти- фикационный билет GetRedirectUrl Возвращает URL для перенаправления пользователя к исходной запрошенной им странице HashPasswordForStoringlnConfigFile Получив пароль и строку, определяющую способ хэ- ширования, метод хэширует пароль для сохранения в файле web.config Initialize Инициализирует класс FormsAuthentication RedirectFromLoginPage Перенаправляет аутентифицированного пользователя по исходному запрошенному им URL RedirectToLoginPage Перенаправляет пользователя к заданной или исполь- зуемой по умолчанию странице входа. В ASPNET 1jc данный метод не поддерживается Renew TicketlfOld Обновляет скользящее время устаревания аутентифи- кационного билета SetAuthCookie Создает аутентификационный билет и присоединяет его к текущему запросу. Не перенаправляет Пользова- теля по исходному запрошенному им URL SignOut Удаляет аутентификационный билет Метод Initialize, инициализирующий перечисленные в табл. 15-4 свойства, вы- зывается в жизни приложения лишь однажды. Он также считывает значения cookie и ключи шифрования, которые будут использоваться приложением. Метод RedirectToLoginPage, отсутствующий в ASP.NET 1.x, заполняет пробел в про- граммном интерфейсе класса FormsAuthentication. Он применяется после того, как пользователь осуществил выход из приложения, если вы снова хотите направить его на страницу входа. В таком случае метод RedirectToLoginPage определяет, что это за страница, и вызывает метод Response.Redirect. 1 Примечание Пусть имена методов GetAuthCookie и SetAuthCookie не вводят вас в за- блуждение: в ASP.NET 2.0 они оба считывают и устанавливают аутентификационный билет, чтобы это ни значило для приложения. Если оно сконфигурировано для выполнения аутентификации Forms без применения cookie, методы считывают информацию билета из URL запроса и туда же ее записывают. А когда применение cookie разрешено, методы работают с cookie. Конфигурирование аутентификации Forms Хотя суть аутентификации Forms исключительно проста, для настройки аутентифи- кационного механизма предлагается множество параметров. Эти параметры задаются
Безопасность в ASP.NET Глава 15 621 в подразделе ><forms> раздела <authentication>. Большая их часть связана с хране- нием аутентификационных билетов в cookie-файлах. Раздел <forms> Аутентификация Forms производится согласно установкам, заданным в подразделе <forms> раздела <authentication> конфигурационного файла. Синтаксис этого под- раздела таков: <forms name="cookie" loginUrl="url" protection="All|None|Encryption|Validation" timeout="30" requireSSL="true|false" slidingExpiration="true|false" path=’7" enableCrossAppsRedirects="true|false" cookieless="UseCookies|Usellri|AutoDetect|UseDeviceProfile" defaultUrl="url" domain="string"> </forms> В табл. 15-6 описаны атрибуты раздела <forms>. Табл. 15-6. Атрибуты раздела <forms> Атрибут Описание cookieless Указывает, должны ли для хранения аутентификационных билетов использоваться cookie и в каких именно случаях. Допустимыми значениями являются UseCookies, UseUri, AutoDetect и UseDeviceProfНе. В ASP.NET 1.x данный атрибут не поддерживается defaultUrl Определяет используемый по умолчанию URL, по которому следует перенаправить пользователя после аутентификации; по умолчанию имеет значение default.aspx. В ASP.NET 1.x данный атрибут не поддерживается domain Определяет имя домена, который будет задаваться для аутенти- фикационных cookie (я расскажу об этом подробнее). В ASP.NET 1.x данный атрибут не поддерживается enable CrossAppRedirects Указывает, может ли пользователь быть аутентифицирован внешним приложением, когда cookie не используются. Если под- держка cookie включена, данная установка игнорируется и аутен- тификация внешним приложением всегда возможна. В ASP.NET 1 х данный атрибут не поддерживается loginUrl Определяет URL страницы входа, на которую пользователь бу- дет перенаправлен при отсутствии действительного аутентифи- кационного билета name Определяет имя аутентификационного cookie path Определяет путь, связываемый с аутентификационным cookie; по умолчанию имеет значение представляющее путь к корне- вой папке приложения (такой cookie действителен для всех стра- ниц приложения). Заметьте, что некоторые браузеры чувстви- тельны к регистру и не будут отправлять обратно cookie, если при указании пути вы используете символы не того регистра см. след. стр.
622 Часть III Инфраструктура ASP.NET Табл. 15-6. (окончание) Атрибут Описание protection Определяет, как приложение должно защищать аутентификаци- онные cookie. Допустимые значения: АН (по умолчанию), Encryp- tion, Validation, None requireSSL Указывает, обязательно ли передавать cookie только через SSL-со- единение; по умолчанию имеет значение false. Когда этот атрибут установлен в true, ASP-NET задает свойство Secure аутентифи- кационного cookie так, чтобы браузер (поддерживающий такую функцию) не возвращал cookie, если подключение не является защищенным. В ASPNET 1.0 данный атрибут не поддерживается SlidingExpiration Указывает, включен ли режим скользящего устаревания. По умолчанию данный атрибут имеет значение false, то есть cookie устаревает спустя заданное время после создания. Интер- вал определяется атрибутом timeout. В ASPNET 1.0 данный атри- бут не поддерживается timeout Задает время в минутах, спустя которое cookie станет устарев- шим; по умолчанию имеет значение 30 Атрибут defaultUrl позволяет задать имя страницы, открываемой по умолчанию после успешной аутентификации запроса. В ASP.NET 1.x этот URL, default.aspx, жестко закодирован и его нельзя изменить, но в ASP.NET 2.0 данный параметр стал конфигурируемым. Но для чего он нужен, если URL указанной страницы включается в строку запроса (в параметре Return Url)? Если пользователь перенаправляется к странице входа аутентификационным мо- дулем, переменная Return Url всегда правильно установлена и значение атрибута по- просту игнорируется. Однако если ваша страница содержит ссылку на страницу входа или переход к странице входа осуществляется программно (скажем, после выхода пользователя из приложения), за установку значения переменной RetumUrl отвечаете вы сами. Если она оказывается неустановленной, используется атрибут defaultUrl. Аутентификация Forms с применением cookie В ASP.NET 1.x аутентификация Forms основана исключительно на использова- нии cookie. Содержимое аутентификационного билета сохраняется в cookie, имя которого задано в атрибуте пате раздела <forms>. Этот cookie содержит всю инфор- мацию, необходимую для идентификации направившего запрос пользователя. По умолчанию cookie действителен в течение 30 минут и защищен путем валида- ции данных и шифрования. Валидация данных гарантирует, что содержимое cookie не было подменено. Шифрование выполняется по алгоритму Triple-DES (3DES). Когда валидация включена, cookie создается путем конкатенации валидационного ключа и подлежащих сохранению данных. ASP.NET вычисляет аутентификацион- ный код сообщения (Message Authentication Code, МАС) и добавляет его в созда- ваемый cookie. Валидационный ключ и алгоритм хэширования задаются в разделе <machineKey> файла web.config. Там же содержатся криптографические ключи, ис- пользуемые в случае применения шифрования. ©Внимание! При создании в Microsoft Visual Studio .NET нового приложения в его состав включается стандартный файл web.config, который может не содержать упоминаемых в этой главе и ранее в настоящей книге установок. Значения всех поддерживаемых атрибутов по умолчанию заданы в файле machine.config. Если значение определенного параметра не задано (или не переопределено) в файле web.config, он получает значение, указанное в файле machine.config.
Безопасность в ASP.NET Глава 15 623 Аутентификация Forms без применения cookie в ASP.NET 2.0 Применение cookie требует поддержки со стороны клиентского браузера. В систе- ме ASP.NET 1.x при использовании встроенного механизма аутентификации cookie обязательны, но в ASP.NET 2.0 семантика аутентификационного API не требует их применения Для этого указанный API был переработан, и теперь он поддерживает обе семантики. Когда cookie не используются, аутентификационный билет включается в состав URL, как показано на рис. 15-5. Рис. 15-5. Аутентификация Forms в действии URL страницы, предоставляемой аутентифицированному пользователю, имеет такой вид: http://YourApp/(F(XYZ .1234))/samples/default.aspx Билет в кодировке на базе URL-совместимого алфавита вставляется в URL сразу после имени пользователя. Примечание Независимо от того, какие установки вы задали в отношении валидации и шифрования, и применяются ли для аутентификации cookie, информация аутентифи- кационного билета кодируется печатными символами. Механизм аутентификации Forms применяет URI-совместимую разновидность кодировки Base64 с шестью битами на символ. При выполнении аутентификации Forms определенный фильтр ISAPI перехва- тывает запрос, извлекает из него билет и обновляет путь к запрошенной странице Фильтр делает этот билет доступным приложению в виде дополнительного заголовка запроса. Тот же компонент aspnet_filter.dll, с которым вы познакомились в главе 13, используется для разбора URL во время аутентификации. Во избежание путаницы дополнительная входящая в URL информация заключается внутрь конструкции S(...), когда это идентификатор сеанса, и конструкции когда это аутентификацион- ный билет. Фильтр ISAPI извлекает информацию билета, помещает ее в заголовок AspAuthenticationTicket и удаляет из URL. Настройка аутентификации Forms без применения cookie Атрибут cookieless раздела <forms> определяет, следует ли использовать cookie для хранения аутентификационных билетов и в каких случаях это будет делаться. До- пустимые значения данного атрибута описаны в табл 15-7. Между установками UseDeviceProfile и AutoDetect имеется тонкое различие. Рас- смотрим его на примере. Предположим, что пользователь направляет запрос из In- ternet Explorer 6.0. Этот браузер поддерживает cookie, как указано в установленной в ASP.NET базе данных возможностей браузеров. Однако пользователь мог отключить их поддержку. В таком случае при использовании установки AutoDetect ситуация будет оценена правильно и аутентификация выполнится без применения cookie. Если же задана установка UseDeviceProfile, ASP.NET не станет выяснять, включена ли под- держка cookie у текущего пользователя, и ограничится сведениями о возможностях
624 Часть Ul Инфраструктура ASP.NET браузера из собственной базы данных. В результате, поскольку на самом деле cookie не поддерживаются, будет выброшено исключение. t Табл. 15-7. Значения атрибута cookieless .. « Значение Описание AutoDetect Использовать cookie, если их поддержка включена в браузере, в против- ном случае применять механизм, действующий без использования cookie UseCookie UseDeviceProfile Всегда использовать cookie независимо от возможностей браузера Использовать cookie, если браузер их поддерживает, в противном случае применять механизм, действующий без использования cookie. Когда задана эта установка, ASP.NET не пытается проверить, включена ли поддержка cookie на устройстве, от которого поступил запрос. Таково значение атрибута cookieless по умолчанию UseUri Никогда не использовать cookie независимо от возможностей браузера Для совместимости с ASP.NET 1.x используемым по умолчанию значением атри- бута cookieless в ASP.NET 2.0 оставлено UseDeviceProfНе, но желательно заменить его значением AutoDetect. Продвинутые функции аутентификации Forms Рассмотрим несколько менее очевидных вопросов, которые могут возникнуть при использовании аутентификации Forms. Совместное использование cookie приложениями Согласно протоколу HTTP cookie поддерживают атрибут path, в котором задается путь к страницам приложения, для которых этот cookie предназначен. Страницы, находящиеся за пределами указанной папки, не могут прочитать или использовать данный cookie. Если же путь не задан явно, по умолчанию им становится URL соз- давшей cookie страницы. Для аутентификационных cookie по умолчанию задается путь к корневой папке приложения, чтобы они были действительными для всех его страниц. Следует отметить, что в ASPNET 1.x два приложения из одного домена Интернета могут совместно использовать cookie, реализуя таким образом модель единого входа. Обычно оба приложения имеют страницы входа; пользователь может выполнить вход в любом из них, а затем свободно переходить между страницами обоих. Для Toto что- бы это было возможно, определенные установки в корневых файлах web.config двух приложений должны быть идентичными. В частности, сказанное касается атрибутов name, protection и path раздела <forms>. Кроме того, в оба файла web.config необходимо добавить раздел <machineKey> с явно заданными ключами валидации и дешифрации. <machineKey validationKey="C50B3C89CB21F4F1422FF158A5B42D0...Е" decryptionKey="8A9BE8FD67AF6979E7D20198C.. D” validation="SHA1“ /> О том, как создаются машинные ключи, рассказывается в статье Knowledge Base 312906. Заметьте, что по умолчанию атрибуты validationKey и decryptionKey имеют значение Auto Generate, то есть ключи валидации и дешифрования задаются автома- тически при установке приложения и сохраняются Windows-сервисом Local Security Authority (LSA), который управляет всей системой безопасности компьютера. Если оставить значение AutoGenerate, каждый компьютер будет иметь собственный ключ и совместное использование cookie станет невозможным.
Безопасность в ASP.NET Глава 15 625 В ASP.NET 2.0 приложения из одного домена также могут совместно использо- вать аутентификационные cookie. Более того, такое возможно даже в том случае, когда одно приложение разработано для ASP.NET 1.x, а другое — для ASP.NET 2.0. А вот приложения ASP.NET 1.0 в этом отношении не совместимы с приложениями ASP.NET 1.1 и ASP.NFT 2.0. Предположим, что у вас имеются два Web-сайта, с адресами www.contoso.com и blogs.contoso.com. Каждый из них генерирует аутентификационные cookie, которые не могут использоваться другим сайтом. Дело в том, что аутентификационные cookie по умолчанию связываются с исходным доменом. Однако cookie HTTP поддержи- вают атрибут domain, дополняющий атрибут path и делающий систему связывания cookie с их источниками более гибкой. В атрибуте domain задается домен, для кото- рого действителен данный cookie. Это может быть домен Интернета первого, второго и даже более глубоких уровней. В ASPNET 2.0 атрибут domain раздела <forms> определяет значение одноименного атрибута аутентификационного cookie: <forms domain="contoso.com" /> Стоит добавить эту строку в файл web.config двух Web-сайтов из нашего приме- ра, и они смогут совместно использовать аутентификационные cookie. Необходимо также, чтобы клиентский браузер распознавал атрибут domain, но его распознают практически все современные браузеры. В результате добавления приведенной установки заданный вами домен и все его цоддомены смогут использовать аутентификационные cookie друг друга при усло- вии, что их файлы web.config синхронизированы в отношении значений машинных ключей. JjJ Примечание Установка атрибута domain — это просто указание для всех аутентифика- ционных методов Forms задавать свойство domain аутентификационных билетов. В сами билеты, при этом никакая дополнительная информация не включается. Когда аутенти- фикация производится без применения cookie, данная установка игнорируется. Атрибут domain раздела <forms> имеет преимущество перед одноименным атрибутом из раздела <httpCookies> и действует для всех cookie, создаваемых в приложении ASP.NET. Аутентификация пользователя внешними приложениями Аутентификация Forms позволяет задавать страницу входа, которая принадлежит другому приложению, функционирующему на том же Web-сервере: <forms loginUrl="/anotherAppsamples/login1.aspx" /> Два приложения должны иметь идентичные машинные ключи. Если аутентифика- ционные билеты сохраняются в cookie, больше ничего вам делать не придется. Аутен- тификационный билет будет сохранен в cookie и отправлен исходному приложению. Такое возможно в ASP.NET 1.x и ASP.NET 2.0. Если же в ASP.NET 2.0 аутентификация производится без применения cookie, вам нужно будет выполнить некоторую дополнительную работу, в частности уста- новить атрибут enableCrossAppRedirects в разделе <forms> файлов web.config обоих приложений: <forms enableCrossAppRedirects="true” /> При успешной аутентификации генерируется билет, который присоединяется к строке запроса в виде параметра для передачи исходному приложению. На рис. 15-6 показан пример аутентификации внешним приложением.
626 Часть III / / Инфраструктура ASP.NET Рис. 15-6. Внешнее приложение выполняет аутентификацию и возвращает сериализованный билет Если атрибут enableCrossAppRedirects не задан и аутентификация осуществляется без применения cookie, внешнее приложение выбрасывает исключение. Аутентификация Forms и SSL Если злоумышленнику удастся похитить аутентификационный билет, он сможет ата- ковать приложение, пока этот билет будет оставаться действительным. И если вы соз- даете неустаревающие cookie, атаки будут возможны в течение 50 лет! Во избежание риска подобных атак можно производить аутентификацию через SSL-соединение. Для этого страницу входа нужно разместить на сервере, поддерживающем протокол HTTPS, и в разделе <forms> конфигурационного файла установить атрибут requireSSL в true. В таком случае ASP.NET станет задавать для создаваемых ею cookie атрибут Secure и поддерживающие его браузеры будут отправлять cookie с аутентификаци- онным билетом только ресурсам, защищенным по методу SSL. При этом областью действия cookie может оставаться все приложение. Если вы не хотите, чтобы защита билетов осуществлялась с использованием про- токола HTTPS, задайте для аутентификационных билетов, по крайней мере, мини- мальный срок действия. Тогда, даже если билет будет перехвачен, у атакующего оста- нется мало времени на вредоносные действия. ф Совет А как поступить, если вы не хотите привязываться к фиксированному времени • устаревания билетов, но и делать их бессрочными тоже не согласны? В таком случае можно создавать билеты, действующие в течение 50 лет, но каждый раз, когда вы по- считаете, что пора их заменить, изменять в конфигурационном файле ключи валидации и шифрования. Как вы знаете, внесение изменений в файл web.config приводит к переза- пуску приложения, и смена криптографических параметров делает аутентификационные билеты недействительными. Напоследок отмечу еще одно обстоятельство, связанное с SSL-защитой: если задан атрибут requireSSL и пользователь пытается подключиться к приложению не через
Безопасность в ASP.NET Глава 15 627 SSL-соединение, выбрасывается исключение, как показано на рис. 15-7. Если же в такой ситуации пользователь предоставляет аутентификационный cookie (возможно, похищенный), исключение не выбрасывается, но cookie удаляется и для пользователя выводится обычная страница входа. Рис. 15-7. Приложение, сконфигурированное для работы с защищенными cookie, может принимать запросы только через SSL-соединение Заметьте, что когда то же самое происходит при аутентификации без применения cookie, проверка протокола не производится и запрошенная страница предоставляется пользователю, который может оказаться злоумышленником. Общие вопросы защиты С точки зрения функциональности наиболее подходящим методом аутентифика- ции для приложений Web и ASP.NET является Forms. Однако при его применении необходимо учитывать приведенные ниже соображения, которые связаны с без- опасностью. Прежде всего, при выполнении аутентификации Forms учетные данные пересыла- ются клиентом серверу в виде чистого текста (рис. 15-8). Для защиты взаимодействия может применяться протокол SSL, но все равно аутентификация Forms столь же не- надежна, как IIS-аутентификация типа Basic. Утилита lEWatch 2.0, окно которой вы видите на рисунке, — это надстройка Micro- soft Internet Explorer, позволяющая анализировать запросы HTTP/HTTPS и исходный код HTML. Ее можно загрузить по адресу http://www.iewatch.com. Как уже было сказано, похищенные cookie могут использоваться для атак на сервер до тех пор, пока они действительны. Поэтому риск таких атак существенно снижается при уменьшении срока службы cookie. Требование установки SSL-соединения для передачи cookie является лучшим решением, но только если для аутентификации применяются cookie.
628 Часть III Инфраструктура ASP.NET Рис. 15*8. Учетные данные передаются в виде чистого текста Наконец, аутентификация Forms основана на коде приложения, что имеет как положительные, так и отрицательные стороны. Вы, конечно же, полностью контро- лируете процесс, однако любые допущенные вами ошибки откроют бреши в системе защиты сайта. Во избежание подобных нежелательных ситуаций можно воспользо- ваться новым API членства ASP.NET 2.0. API управления членством и ролями В ASPNET 2.0 аутентификация Forms в целом выполняется так же, как в ASP.NET 1.x, поэтому большинством освоенных приемов и технологий вы сможете пользоваться как прежде. Наиболее заметным изменением, внесенным в механизм аутентификации Forms в ASP.NET 2.0, является введение дополнительного API, а именно API управ- ления членством и ролями. Он представляют собой набор классов, предназначенных для управления учетными записями пользователей и ролями. Используемые совместно с классом FormsAuthentication новые классы Membership и Roles составляют полный арсенал необходимых разработчикам ASP.NET средств защиты. Класс Membership предоставляет методы для управления учетными записями пользователей, в частности для добавления учетных записей новых пользователей, а также для удаления и редактирования существующих записей. Класс Roles служит связующим звеном между пользователями и их ролями. Что означает выражение «управление членством пользователей»? Да просто то, что класс Membership знает, как создать новую учетную запись пользователя и как изменить его пароль. Как создается учетная запись пользователя? Обычно она добавляется в опре- деленное хранилище данных. Кто в таком случае решает, какое хранилище использовать
Безопасность в ASP.NET Глава 15 629 и как выполнять запись информации нового пользователя? Это решает разработчик приложения для чего в его распоряжение предоставлен новый API членства. API управления членством и ролями не привязывает вас к фиксированному храни- лищу данных и фиксированной их схеме. Как раз наоборот: он дает вам в этом отноше- нии полную свободу. Данный API основан на модели провайдеров (см. главу 1) и де- легирует выполнение всех основных своих функций выбранным вами провайдерам. Управление членством Центральным компонентом API членства ASP.NET является класс Membership, скры- вающий от вас детали извлечения и сравнения информации о пользователях. В его состав входит несколько методов, в том числе позволяющих получить уникальный идентификатор каждого подключенного пользователя. Он может вызываться и други- ми сервисами ASPNET, в частности сервисами персонализации, а также использовать- ся для включения и отключения тех или иных функций с учетом роли пользователя. Среди членов класса Membership имеются также методы для создания, обновления и удаления учетных записей пользователей, но нет методов для управления ролями и программного определения возможностей и ограничений пользователя. Такие методы включены в состав класса Roles. По умолчанию класс Membership работает с провайдером, который сохраняет ин- формацию о пользователях в базе данных SQL Express в стандартном формате. Если вы пожелаете работать с пользовательским хранилищем данных, вам достаточно будет создать собственный провайдер и подключить его к приложению. Программный интерфейс класса Membership Перечень свойств класса Membership, сопровождаемый кратким описанием, приведен в табл. 15-8. Табл. 15-8. Свойства класса Membership Свойство Описание < ^pplicatior Name Возвращает строку, идентифицирующую прило- жение. Но умолчанию содержит путь к его корне- вой папке Enable Ра ssz. ordReset Возвращает значение, true, если провайдер под- держивает сброс паролей EnablePassb ordRelr.eial 1 1 Возвращает значение true, если провайдер под- держивает восстановление паролей MaxInvalidPasswordAttempts Возвращает максимальное количество попыток i "r: ввода пароля перед блокированием пользователя MinRequiredNonAlphanumericCharacters Возвращает минимальное число знаков препина- ния в паро те MinRequiredPasswordLength Возвращает минимальную длину паро 1Я Password irtemptWindow Возвращает время в минутах, в Течение которо- го пользователь может пытаться ввести Пароль, прежде чем будет заблокирован PasswordStrengthRegularExpression Возвращает регулярное выражение, которому должен отвечать пароль Prouder Возвращает экземпляр используемого провайдера ^r^nde-^s Возвращает коллекцию зарегистрированных про- вайдеров см. след. стр.
630 Часть III Инфраструктура ASP.NET Табл. 15-8. {окончание) t Свойство Описание RequiresQuestionAndAnswer Возвращает значение true, если при восстановле- нии или сбросе пароля провайдер требует ответа на определенный вопрос UserlsOnlineTimeWindow Возвращает время в минутах, истекшее после последнего действия пользователя, в течение которого пользователь все еще будет считаться подключенным Свойство Provider возвращает ссылку на используемый провайдер членства (он за- дается в конфигурационном файле). В состав ASP.NET 2.0 входит пара предопределен- ных провайдеров, работающих с MDF-файлами SQL Server Express и с Active Directory. Но существует множество других провайдеров от Microsoft и сторонних производите- лей. Список доступных провайдеров можно получить с помощью свойства Providers. Все свойства класса Membership являются статическими и доступными только для чтения, а реализация их очень проста. Каждому из них соответствует член текущего провайдера: public static int PasswordAttemptWindow { get { Membersh ip.Initialize(), return Membership.Provider.PasswordAttemptWindow; } Метод Initialize обеспечивает правильную инициализацию внутренней структуры класса Membership и наличие ссылки на провайдер. Класс Membership обладает богатой функциональностью, например, он может определить, сколько пользователей в данный момент подключено к приложению (свойство UserlsOnlineTime Window). Пользователь считается подключенным, если он взаимодействует с приложением и паузы в его работе не превышают интервала, заданного в свойстве OnlineTimeWindow интервала (по умолчанию 15 минут). В табл. 15-9 перечислены методы, поддерживаемые классом Membership. Из этого перечня вам станет ясно, каковы его задачи. Табл. 15-9. Методы класса Membership Метод Описание CreateUser Создает новую учетную запись пользователя (или ничего не делает, если учетная запись заданного пользователя уже суще- ствует). Этот метод возвращает объект MembershipUser, пред- ставляющий всю доступную информацию о пользователе DeleteUser Удаляет учетную запись заданного пользователя FindUsersByEma.il Возвращает коллекцию объектов MembershipUser, для которых задан указанный вами адрес электронной почты FindUsersByName Возвращает коллекцию объектов MembershipUser, для которых задано указанное вами имя пользователя GeneratePassword Генерирует случайный пароль заданной длины GetAllUsers Возвращает коллекцию всех пользователей
Безопасность в ASP.NET Глава 15 631 Табл. 15-9. (окончание) Метод Описание GetNumberOfUsersOnline Возвращает количество подключенных в данный момент поль- зователей GetUser Извлекает объект MembershipUser, связанный с текущим или за- данным пользователем GetUserNameByEma.il Возвращает имя пользователя, имеющего заданный адрес элек- тронной почты. Если несколько пользователей имеют один и тот же адрес, возвращается первый из них UpdateUser Принимает объект MembershipUser и обновляет хранящуюся в приложении информацию о пользователе ValidateUser Аутентифицирует пользователя по заданным учетным данным Конфигурирование системы управления членством Для того чтобы создать аутентификационный слой, основанный на API членства, прежде всего следует выбрать провайдер, а также создать и сконфигурировать источ- ник данных. В простейшем случае можно воспользоваться стандартным провайдером, сохраняющим информацию в локальном mdf-файле SQL Server 2005 Express. Создание и администрирование учетных записей зарегистрированных пользова- телей можно выполнять с помощью утилиты Web Site Administration Tool (WSAT), входящей в состав Visual Studio .NET 2005. Ее пользовательский интерфейс пред- ставлен на рис. 15-9. Рис. 15-9. Утилита WSAT позволяет конфигурировать модель данных членства и управлять учетными записями пользователей
632 ^сгьШ ‘ i ’Чл Инфраструктура ASP.NET Для добавления новой учетной записи пользователя и редактирования существу- ющей используются ссылки. Редактирование свойств новой учетной записи произ- водится на страниц*1, представленной на рис. 15-10. > v Рис. 15-10. Выбор пользователя для редактирования или удаления посредством утилиты WSAT Валидация пользователей Теперь вы знаете достаточно^ чтобы я мог представить вам демонстрационный код, написанный с использованием API членства Начнем с наиболее простой операции — аутентификации. Применяя аутентификационные функции API членства, можно переписать код страницы входа из предыдущего примера: void LogonUseг(object sender, EventArgs e) , { string user = userName.Text; string pswd = password.Text; if (Membership.ValidateUser(user, psvyd)) ,, FormsAuthentication.RedirectFromLoginPage(user, false); else errorMsg.Text = "Sorry, yours seems, not to be a valid account."; } Данный код незначительно отличается от того, который можно было бы написать bASP.NET 1.x, но одно существенное отличие все же имеется: вызов встроенного метода ValidateUser, реализованного в сборке system.web. Вот псевдокод этого метода: public static bool ValidateUser(string username, string password) { return Membership.Provider.ValidateUser(usernamg, password); }
Безопасность в ASP.NET' Глава 15 633 Как видите, вся базовая функциональность по выполнению аутентификации реа- лизована в провайдере. И замечательно, что имя этого провайдера содержится в файле web.config, так что его можно изменить, не затрагивая код приложения. Общая схема подсистемы аутентификации приведена на рис. 15-11. Проверка членства, основанная на использовании провайдера Метод ValidateUser класса Membership Провайдер членства Аутентификация Доступ к источнику данных Фиксированное поведение Пользовательская схема Пользовательское хранилище Рис. 15-11. Использование модели провайдеров ASP.NET в API членства Управление учетными записями пользователей и паролями Класс Membership предоставляет удобные методы для работы с учетными записями пользователей. В частности, для программного создания новой учетной записи до- статочно вызвать метод CreateUser. Membership.CreateUser(userName, pswd); Удаление учетной записи пользователя выполняется с помощью метода DeleteUser. Membership.DeleteUser(userName); Для получения информации об определенном пользователе используется метод GetUser. Он принимает имя пользователя и возвращает объект MembershipUser. MembershipUser user = Membership.GetUser("DinoE"); Получив объект MembershipUser, вы обладаете всей необходимой информацией о пользователе и можете программно изменять его пароль и прочие сведения. При- ложение обычно выполняет несколько операций над паролями: изменяет их, отправ- ляет пользователям, сбрасывает (обычно с использованием протокола запрос-ответ). Например, следующий код изменяет пароль пользователя: *> MembershipUser user = Membership.GetUser("DinoE’’); user.ChangePassword(user.GetPassword(), newPswd); Методу Changepassword передается старый пароль. В некоторых случаях можно предоставить пользователю возможность просто сбросить пароль (то есть удалить
634 Часть III Инфраструктура ASP.NET старый и автоматически сгенерировать новый), вместо того чтобы его изменять. Для этого используется метод Re set Password: MembershipUser user = Membership.GetUser("DinoE"); string newPswd = user.ResetPassword() Вызвавшая его страница отвечает за отправку пользователю нового пароля, скажем, по электронной почте. Методы GetPassword и ResetPassword имеют по одной пере- груженной версии, принимающей строковый параметр, в котором задается ответ на вопрос, предусмотренный на случай потери пароля. Провайдер членства проверяет правильность этого ответа, и если пользователь идентифицирован, генерируется новый пароль, который сообщается пользователю. Примечание Поддержка возможности сброса паролей, а также схемы удостоверения вопрос-ответ зависит от конкретного провайдера вовсе не обязательно, чтобы каж- дый из них реализовывал все функции API членства Если провайдер не поддерживает определенную функцию, при попытке обратиться к ней выбрасывается исключение. Провайдер членства Модель членства ASP.NET примечательна исключительно компактным кодом ис- пользующего ее приложения, а также тем, что она абстрактна и расширяема. Если, к примеру у вас имеется созданное ранее хранилище данных с информацией о пользо- вателях, ее легко можно интегрировать с API членства Для этого достаточно написать пользовательский провайдер членства — класс, наследующий MembershipProvider, который, в свою очередь, наследует класс ProviderBase: public class MyAppMembershipProvider : MembershipProvider // Реализует все абстрактные члены класса // и при необходимости определяет пользовательскую функциональность } Такой подход может быть успешно применен для переноса существующего аутен- тификационного кода ASP.NET 1.x на платформу ASP.NET 2.0 и, что еще важнее, для связывания существующего пользовательского хранилища данных с API членства. Мы еще вернемся к этой теме, а пока рассмотрим сам класс ProviderBase. Класс ProviderBase Все используемые в ASP.NET 2.0 провайдеры, а не только провайдеры членства, ре- ализуют общий набор членов, определенный в классе ProviderBase. Этот класс имеет один метод, Initialize, и одно свойство, Name. Свойство возвращает официальное имя класса провайдера. Что касается метода, то он принимает имя провайдера и коллекцию пар имя-значение, содержащую установки из конфигурационного раздела провайдера, а затем инициализирует внутреннее состояние класса указанными значениями. Класс MembershipProvider Большинство методов и свойств класса Membership просто вызывают соответствую- щие методы и свойства провайдера. Поэтому не удивительно, что многие из методов базового класса MembershipProvider, описанных в табл. 15-10, выполняют функции, указанные в табл. 15-9. Обратите внимание, что эти таблицы не идентичны, хотя и очень похожи. Все эти методы помечены как abstract virtual (в Visual Basic .NET — must-inherit, overridable).
Безопасность в ASP NET Глава 15 635 Табл. 15-10. Методы класса Membershipprovider Метод Описание ChangePassword Получает имя пользователя, старый и новый паро- ли и изменяет пароль пользователя ChangePasswordQuestionAndAnswer Получает имя и пароль пользователя и изменяет пару вопрос-ответ, используемую при восстанов- лении и сбросе паролей CreateUser Создает новую учетную запись пользователя и возвращает объект класса, производного от MembershipUser. Метод принимает имя пользова- теля, пароль и адрес электронной почты DeleteUser Удаляет учетную запись пользователя с заданным именем FindUsersByEmail Возвращает коллекцию объектов MembershipUser, в которых адреса электронной почты соответству- ют заданному адресу FindUsersByName Возвращает коллекцию объектов MembershipUser, в которых имена пользователей соответствуют заданному имени GetAUUsers Возвращает коллекцию всех учетных записей, которыми управляет этот провайдер GetNumberOfUsersOnline Возвращает количество подключенных в данный момент пользователей GetUser Извлекает объект MembershipUser, связанный с текущим или заданным пользователем GetUserNameByEmail Возвращает имя пользователя, имеющего задан- ный адрес электронной почты UpdateUser Обновляет хранящуюся в приложении информа- цию о заданном пользователе ValidateUser Аутентифицирует пользователя по заданным учет- ным данным Кроме того, класс MembershipProvider имеет ряд свойств, которые описаны в табл. 15-11. Табл. 15-11. Свойства класса MembershipProvider Свойство Описание ApplicationName Возвращает имя провайдера EnablePasswordReset Указывает, поддерживает ли провайдер сброс пароля EnablePasswordRetrieval Указывает, поддерживает ли провайдер восстанов- ление пароля MaxInvalidPasswordAttempts Возвращает максимальное количество попыток ввода пароля перед блокированием пользователя MinRequiredNonAlphanumericCharacters Возвращает минимальное число знаков препина- ния в пароле MinRequiredPasswordLength Возвращает минимальную длину пароля PasswordAttemptWindow Возвращает время в минутах, в течение которо- го пользователь может пытаться ввести пароль, прежде чем будет заблокирован см. след. стр.
636 Часть til Инфраструктура ASP.NET Табл. 15*11. (окончание) Свойство Описание PasswordStrengthRegularExpression Возвращает регулярное выражение, которому дол- жен отвечать пароль RequiresQuestionAndAnswer Возвращает значение true, если при восстановле- нии или сбросе пароля провайдер требует ответа на определенный вопрос RequiresUniqueEmail Указывает, требует ли провайдер, чтобы у каждого пользователя имелся уникальный адрес электрон- ной почты Расширение интерфейса провайдера Провайдер членства способен хранить дополнительную информацию о пользователях. Например, создав класс, производный от MembershipUser, вы можете добавите в него дополнительные члены и возвращать экземпляр этого класса через стандартный ме- тод GetUser API членства. Для использования этого объекта нужно будет приводить возвращенное методом GetUser значение к нужному типу: MyCompanyllser user = (MyCompanyUser) Membership.GetUser(name); В дополнение к членам, перечисленным в табл. 15-10 и 15-11, пользовательский провайдер может содержать собственные методы и свойства. Они определяются вне официальной схемы базового класса провайдера и потому доступны только тем при- ложениям, которые знают о возможностях вашего провайдера: MyCompanyProvider prov = (MyCompanyProvider) Membership.Provider; [~^ Примечание Свойство-коллекция Providers позволяет использовать динамически вы- бранный провайдер, что дает приложению возможность одновременно взаимодействовать с несколькими разными провайдерами: Membershipprovider prov - Membership.Providers["ProviderName”]; Например, вы можете создать такое приложение, которое бы работало с унаследованной базой данных пользователей посредством пользовательского провайдера, а сведения о новых пользователях хранило бы в стандартной таблице SQL Server 2005. Пользовательский провайдер членства для приложения ASP.NET 1.x Ранее в этой главе обсуждались демонстрационные страницы, которые в качестве источника данных учетных записей использовали таблицу Employees базы данных Northwind SQL Server 2000. Давайте превратим их код в провайдер членства и за- регистрируем его с помощью утилиты WSAT: ,д . public class MyMembershipProvider : Membershipprovider { “ -- • фЖ public MyMembershipProvider() { } public override bool ChangePassword(string username, string oldPassword, string newPassword) { // Если вы не намерены поддерживать определенный // метод, просто выбросьте- исключение throw new NotSupportedException(); }
Безопасность в ASP.NET Глава 15 637 public override bool ValidateUser(string username, string password) { return AuthenticateUser(username, password); } private bool AuthenticateUser(string username, string pswd) { 11 Сюда поместите аналогичный код, возможно, И уже имеющийся в вашем приложении ASP.NET 1.x } } Вы определяете новый класс, производный от MembershipProvider, и переопределяете в нем все члены из табл. 15-10 и 15-11. (Если определенный метод вы поддерживать не намерены, просто выбросьте из него исключение NotSupportedException.) При этом сво- бодно можно использовать существующий код приложения, разве что несколько его пе- реработав. Таким образом обеспечивается повторное использование большей части кода наряду с возможностью пользоваться преимуществами модели членства ASP.NET 2.0. При написании собственного провайдера необходимо отслеживать три важных аспекта его функционирования: время жизни, многопоточную работу и атомарность. Экземпляр провайдера создается, как только в этом возникает необходимость, но лишь один раз за все время работы приложения. Этот факт сообщает провайдеру статус компонента, сохраняющего свое состояние, но ценой защиты его от многопоточного доступа. Провайдер не является многопоточным, и вы сами должны определять его критические секции и блокировать данные. Не следует забывать, что некоторые опе- рации провайдера могут состоять из нескольких шагов, и вам нужно обеспечить их атомарность За дополнительной информацией вы можете обратиться к документации ASP.NET 2.0 Provider Toolkit по адресу http://msdn.microsoft.com/asp.net/provider. Конфигурирование провайдера членства Вы регистрируете новый провайдер в разделе <membership> файла web.config. Этот раздел содержит дочерний элемент <providers>, в котором конфигурируются допол- нительные провайдеры: <membership> <providers> <add name="MyMembe rsh i pP rovide r” type="ProAspNet20.MyMembershipProvider" /> <providers> </membership> Используемый по умолчанию провайдер задается в атрибуте defaultProvider раз- дела <membership>. На рис. 15-12 показана вкладка утилиты WSAT, которая исполь- зуется для выбора провайдеров, выполняющих различные функции. При использовании нового провайдера код проверки введенных пользователем учетных данных остается в точности таким, как раньше: void LogonUseг(object sender, EventArgs e) { string user = userName.Text; string pswd = password Text; if (Membership.ValidateUser(user, pswd)) FormsAuthentication.RedirectFromLoginPage(user, false); else errorMsg.Text = "Sorry, yours seems not to be a valid account.”; }
638 Часть III Инфраструктура ASP.NET Рис. 15-12. Выбор нового провайдера членства в окне утилиты WSAT Но это еще не все возможности API членства ASP.NET 2.0. Теперь страница входа имеет относительно стандартные структуру и код и, по крайней мере в простейших случаях, ее содержимое может сводиться к составному элементу управления, вообще не требующему написания кода. Причем такой элемент управления уже имеется — вам не придется создавать его самостоятельно. Но прежде чем приступать к изучению этого нового семейства элементов управления, давайте поговорим о ролях и управ- лении ими с помощью провайдеров. Управление ролями Роли в ASP.NET используются для упрощения процесса создания приложений, в ко- торых требуется авторизировать пользователей на выполнение тех или иных действий. Сама по себе роль — это просто присвоенный пользователю логический атрибут: строка, представляющая логическую роль, которую играет пользователь в контексте приложения. Одному пользователю может быть назначено несколько ролей. Эта ин- формация присоединяется к идентификационному объекту, и код приложения может проверить ее, прежде чем выполнять критичные операции. Например, в приложении могут быть определены две роли: Admin и Guest, из которых первая наделена более широкими полномочиями. Примечание Назначение ролей учетным записям пользователей не налагает никаких ограничений защиты. Приложение само должно позаботиться о том, чтобы неавторизи- рованные пользователи не могли выполнять в нем критичные операции. В ASP.NET функция управления ролями заключается в поддержании связей между ними и пользователями. Встроенной подсистемы поддержки ролей в ASP.NET 1.x не существует, однако к учетной записи пользователя можно присоединить информацию о его ролях, что требует написания программного кода. Реализовать это несложно, но в ASP.NET 2.0 данная задача еще более упростилась.
Безопасность в ASP.NET Глава 15 639 Примечание API управления ролями состоит из набора методов и свойств и в целом действует так же, как API управления членством. Многое из того, что сказано выше о членстве и учетных записях пользователей, можно применить и к ролям. Для включения поддержки ролей необходимо добавить в файл web.config при- ложения следующую строку: <roleManager enabled="true" /> С помощью ролей устанавливаются правила доступа к страницам и папкам. Напри- мер, следующий блок <authorization> определяет, что только члены роли Admin имеют доступ к страницам, на которые распространяется действие файла web.config: <configuration> <system,web> <authorization> <allow roles="Admin" /> <deny users=”*" /> </authorization> </system.web> <configuration> API управления ролями Для программного определения ролей, а также для связывания с ними пользователей в ASP.NET применяется API управления ролями, представленный членами класса Roles. Когда управление ролями включено, ASP.NET создает экземпляр этого класса и добавляет его в контекст каждого запроса, то есть в объект HttpContext. Следующий код показывает, как программным способом создать роли Admin и Guest и заполнить их именами пользователей: Roles.CreateRole("Admin"); Roles.AddUsersToRole("DinoE", "Admin"); Roles.CreateRole("Guest"); string[] guests = new string[2]; guests[0] = "JoeUsers"; guests[1] = "Godzilla"; Roles.AddllsersToRoleCguests, "Guest") Во время выполнения страницы информация о запросившем ее пользователе и его ролях доступна через объект User контекста HTTP. Следующий код показывает, как установить, принадлежит ли пользователь к определенной роли, и включить соот- ветствующие функции: if (User.IsInRole("Admin")) // Включаем функции, специфические для данной роли } Примечание Если ASP.NET 2.0 самостоятельно получает информацию о ролях теку- щего пользователя и связывает ее с объектом User, то в ASP.NET 1 .х данную функцию приходится программировать вручную. Обычно информацию о ролях кэшируют либо для каждого пользователя в отдельности с использованием cookie, либо сразу для всех пользователей в объекте Cache. В обоих случаях это делается при запуске приложения в обработчике его события Application_Start, который определен в файле globaLasax. По- сле этого пишут функцию get, которая считывает информацию о ролях из хранилища, и вызывают ее, когда такая информация требуется.
640 Часть III Инфраструктура ASP.NET Добавлю, что создание учетных записей пользователей, включение поддержки ролей и определение связей между пользователями и ролями можно также выполнить с помощью утилиты WSAT (см. рис. 15-2). Члены класса Roles Класс Roles обладает удобным интерфейсом, позволяющим работать как с отдель- ными пользователями и ролями, так и с их группами. Методы этого класса описаны в табл. 15-12. Табл. 15-12. Методы класса Roles Метод Описание AddUsersToRole Включает группу пользователей в состав роли AddUsersToRoles Включает группу пользователей в группу ролей AddUserToRole Включает пользователя в состав роли AddUserToRoles Включает пользователя в состав группы ролей CreateRole Создает новую роль DeleteCookie Удаляет cookie, которые менеджер ролей использовал для кэши- рования всех данных указанной роли DeleteRole Удаляет роль FindUsersInRole Извлекает имена пользователей, принадлежащих к заданной роли GetAllRoles Возвращает список всех доступных ролей GetRolesForUser Возвращает строковый массив ролей, к которым принадлежит заданный пользователь GetUsersInRole Возвращает строковый массив с именами пользователей, принад- лежащих к заданной роли IsUserlnRole Определяет, принадлежит ли указанный пользователь заданной роли Remove UserFromRole Удаляет пользователя из заданной роли RemoveUserFromRoles Удаляет пользователя из группы заданных ролей RemoveUsersFromRole Удаляет пользователей из заданной ро и RemoveUsersFromRoles Удаляет пользователей из i руппы заданных ролей RoleExists Возвращает значение true, если заданная роль существ) ет Свойства класса Roles перечислены в табл. 15-13. Все они являются статическими и доступны только для чтения. Значения этих свойств соответствуют установкам из конфигурационного раздела <roleManager>. Табл. 15-13. Свойства класса Roles Свойство Описание ApplicationName Возвращает имя провайдера CacheRolesInCoolne Возвращает значение true, если хранение данных ролей в cookie разрешено CookieName Определяет имя cookie, используемого для хранения информации о ролях
Безопасность в ASP.NET Глава 15 641 Табл. 15-13. (окончание) Свойство Описание CookiePath Определяет путь, ассоциированный с ролевым cookie CookieProtection Value Содержит установку шифрования ролевого cookie: All. Clear, Hashed или Encrypted CookieRequireSSL Указывает, должны ли ролевые cookie передаваться через SSL-со- единение CookieSlidingExpiration Указывает, является срок действия ролевого cookie фиксирован- ным или скользящим CookieTimeout Возвращает срок действия ролевого cookie в минутах CreatePersistentCookie Создает ролевой cookie, сохраняющийся в течение сеанса Domain Определяет домен ролевого cookie Enabled Указывает, включена ли функция управления ролями MaxCachedResults Определяет максимальное количество ролей, которое может быть сохранено для одного пользователя в cookie-файле Provider Возвращает текущий провайдер ролей Providers Возвращает полный список поддерживаемых провайдеров ролей Некоторым методам класса Roles необходима информация о ролях пользователя, поэтому ее обычно кэшируют в зашифрованных cookie. Получив очередной НТТР- запрос, ASP.NET проверяет, имеется ли в его составе ролевой cookie, и если имеется, дешифрует билет роли и присоединяет ее информацию к объекту User. По умолча- нию ролевой cookie является сеансовым и становится недействительным, как только пользователь закрывает браузер. Заметьте, что cookie содержит сведения о ролях лишь того пользователя, от кото- рого поступил запрос. Когда вы запрашиваете информацию о ролях других пользо- вателей, она считывается провайдером ролей из источника данных. га Примечание За управление членством и ролями отвечает специальный модуль HTTP. Он добавляет сведения о ролях в текущий объект User, для чего перехватывает событие AuthenticateRequest. Отметим, что это единственный код, который вам необходимо на- писать в ASP.NET 1 .х для поддержки управления ролями. Провайдер ролей Для выполнения ввода-вывода данных о ролях пользователей менеджер ролей ис- пользует провайдерную модель. Провайдер ролей — это класс, наследующий класс RoleProvider. Его структура очень похожа на структуру класса провайдера членства. Методы класса RoleProvider перечислены в табл. 15-14. Табл. 15-14. Методы класса RoleProvider Метод Описание AddUsersToRoles Добавляет группу пользователей в группу ролей CreateRole Создает новую роль DeleteRole Удаляет заданную роль FindUsersInRole Возвращает имена принадлежащих к данной роли пользователей, соответствующие заданному шаблону см. след. стр.
642 Часть III Инфраструктура ASP NET Табл. 15-14. (окончание) Метод Описание GetAllRoles Возвращает список доступных ролей GetRolesForUser Возвращает массив с именами всех ролей, к которым принадле- жит текущий пользователь GetUsersInRole Возвращает массив с именами всех пользователей, которые при- надлежат к заданной роли IsUserlnRole Указывает, принадлежит ли текущий пользователь к заданной роли RemoveUsersFromRoles Удаляет из заданной роли группу заданных пользователей RoleExists Указывает, существует ли заданная роль Некоторые из этих методов подобны методам класса Roles и, как и в случае с член- ством, это не совпадение. В состав ASP.NET 2.0 входят два встроенных провайдера ролей — AspNetSqlRole- Provider (используется по умолчанию) и AspNetWindowsTokenRoleProvider. Первый сохраняет информацию о ролях в том же mdf-файле SQL Server 2005 Express, что и ис- пользуемый по умолчанию провайдер членства, а второй предоставляет информацию о ролях, хранящуюся в Active Directory или том домене Windows, где аутентифицирован пользователь. Этот провайдер не позволяет добавлять и удалять роли. Пользовательские провайдеры ролей создаются как производные от класса Role- Provider и регистрируются в подразделе <providers> раздела <roleManager>. Заметьте, что этот процесс практически идентичен процессу создания пользовательских про- вайдеров членства. Элементы управления, связанные с защитой В дополнение к API членства и ролей ASPNET 2 0 предоставляет в распоряжение раз- работчиков несколько серверных элементов управления, которые значительно упрощают разработку составляющих Web-приложения, обеспечивающих его защиту. Это элементы Login, LoginName, LoginStatus, LoginView, PasswordRecovery, ChangePassword и CreateUser- Wizard. Все они являются составными, имеют богатый и настраиваемый пользовательский интерфейс. Эти элементы инкапсулируют типичные код и разметку, которые до сих пор разработчику приходилось повторять в каждом приложении. На рис. 15-13 представ- лена схема платформы членства, отражающая роль указанных элементов управления. Элемент управления Login Приложению, в котором используется модель аутентификации Forms, всегда требу- ется страница входа. Если не считать графического оформления, все такие страницы выглядят одинаково. Они содержат пару текстовых полей (для имени пользователя и пароля), кнопку для отправки введенных учетных данных серверу, а кроме того, могут содержать флажок «Запомнить меня» и ссылки для пользователей, забывших свой пароль или желающих создать новую учетную запись. Элемент управления Login включает все эти элементы и способен проверить учетные данные пользователя с по- мощью вызываемого по умолчанию провайдера членства. Использование элемента управления Login Составной элемент управления Login содержит полный комплекс элементов, типич- ных для формы входа. На рис. 15-14 показан его стандартный пользовательский ин-
Безопасность в ASP.NET Глава 15 643 терфейс. Вам нужно просто перетащить этот элемент управления в форму или же вручную ввести следующую разметку: <asp:login runat="server" id="MyLoginForm" /> Элементы управления Login LoginView ChangePassword CreateUserWizard Loginstatus LoginName PasswordRecovery API членства Membership | | MembershipUser Провайдеры SqlMembershipProvider ActiveDirectoryMembershipProvider Другие Хранилища данных F •• - SQL Sernsr 200П и другие Рис. 15-13. Элементы управления, обеспечивающие защиту приложения, в общей структуре платформы членства Рис. 15-14. Элемент управления Login в действии Помимо элементов интерфейса, которые вы видите на рисунке, элемент управле- ния Login поддерживает несколько дополнительных элементов, служащих для вос- становления забытого пароля, регистрации нового пользователя, вывода сообщений об ошибках и выполнения пользовательского действия при успешном входе. Пере- тащив элемент управления в форму в Visual Studio .NET 2005, можно выбрать для него один из нескольких предопределенных стилей, воспользовавшись командой автоформатирования (рис. 15-15).
644 Часть III Инфраструктура ASP.NET Mir f- J hstSe £fe £d* Ив». wetwTe i'u d Qebug Djto Ретпа1 Layout loois Window Community ВД ^iflr *'WCT1J *1 Verdana 4^ '*rrc* ~ :ж —w— lamptei/QilS/.up/LogirLaspx, sampes/ChlS.^wwGome.aspx в O.tan 8. S’ -• 3 AutoForm»... Convert to Trwptate Administer Wfcjta } Edt Temptetes User Name Q Password " I; □ Remember me next time. 1 Log In I Рис. 15-15. Предопределенные стили элемента управления Login Внешний вид элемента управления Login настраивается посредством шаблонов и стилевых установок, а текст, который в нем выводится, — в соответствующих свойствах. Программный интерфейс элемента управления Login Элемент управления Login имеет модульную структуру, и все его части — поля для ввода имени и пароля пользователя, кнопка подтверждения ввода, кнопка создания новой учетной записи, флажок «Запомнить меня» и текстовые инструкции — на- страиваются по отдельности. Если стандартный пользовательский интерфейс элемента управления вам не под- ходит, можете определить для него собственный шаблон: <asp:login runat="server" id="MyLoginForm"> <layouttemplate> </laycuttemplate> </asp:login> Наряду с новыми он может содержать стандартные элементы формы входа, для чего соответствующим элементам пользовательского шаблона необходимо присвоить идентификаторы элементов стандартного шаблона компонента Login. Чтобы упро- стить создание шаблона, щелкните на элементе управления Login правой кнопкой мыши в дизайнере Visual Studio, выберите команду Convert То Template и пере- ключитесь в режим отображения исходного кода. В результате вы увидите разметку стандартного шаблона элемента управления Login, которую можно использовать как отправную точку.
Безопасность в ASP.NET Глава 15 645 События элемента управления Login Элемент управления Login генерирует серверные события, которые перечислены в табл. 15-15. Табл. 15-15. События элемента управления Login Событие Когда генерируется Authenticate Пользователь аутентифицирован Loggedln Пользователь вошел на сайт после успешной аутентификации Loggingin После ввода пользователем учетных данных, но до аутентификации Loggingin Обнаружена ошибка входа Как правило, обрабатывать эти события нет необходимости, как не требуется и программным способом обращаться к свойствам элемента управления. Типичное применение элемента управления Login — это создание страницы вхо- да, единственным элементом которой он является. При выполнении стандартных операций, таких как валидация введенных пользователем учетных данных, вывод сообщения об ошибке и перенаправление пользователя к исходной запрошенной им странице, элемент управления всецело полагается на API членства (и выбранный провайдер). Если у вас имеется пользовательский провайдер, возможности которого должны быть отражены в элементе управления Login, модифицируйте его раскладку, добавив новые визуальные элементы, и свяжите эти элементы с методами класса отделенного кода, в которых выполняются вызовы пользовательского провайдера. Элемент управления LoginName Маленький, но весьма полезный элемент управления LoginName — это обыкновенная надпись, в которой выводится имя пользователя: <asp:loginname runat="server" /> Он считывает имя пользователя из внутреннего объекта ASP.NET User. string name - HttpContext.Current.User.Identity.Name; и выводит с применением заданного стиля, для чего создает динамический экземпляр элемента управления Label с соответствующим текстом и задает его цвета и шрифт. Программный интерфейс элемента управления LoginName состоит из единствен- ного свойства, FormatString, определяющего формат выводимого текста. Оно может содержать единственный параметр: myLogin.Formatstring = Welcome, {0}"; Если в данный момент со страницей работает пользователь Dino, приведенный код генерирует сообщение Welcome, Dino. Элемент управления Loginstatus В элементе управления LoginStatus отображается состояние аутентификации текущего пользователя. Его пользовательский интерфейс состоит из кнопки ссылочного типа, предназначенной для входа в приложение либо выхода из него, смотря в каком со- стоянии находится пользователь. Если это анонимный пользователь, элемент управ- ления выводит ссылку, приглашающую выполнить вход; в противном случае, если пользователь успешно прошел аутентификационный контроль, элемент управления вводит ссылку для выхода.
646 Часть III Инфраструктура ASP.NET Использование элемента управления LoginStatus Элемент управления LoginStatus часто используется совместно с элементом управле- ния LoginName. Например, следующий код определяет таблицу с именем пользователя и ссылкой для выполнения входа или выхода: <table width="100%" border="0"> <tr> <td> <asp:loginname runat="server" FormatString="Welcome, {0}” /> </td> <td align="right"> <asp:loginstatus runat="server" LogoutText=”Log off" /> </td> </tr> </table> На рис. 15-16 показаны результаты обработки этой разметки. На первом снимке экрана представлена страница, приглашающая пользователя войти, а на втором — стра- ница, содержащая элемент управления для выхода из приложения. Рис. 15-16. Элемент управления LoginStatus приглашает пользователя войти в приложение и позволяет из него выйти
Безопасность в ASP.NET Глава 15 647 Для определения текущего состояния пользователя и адаптации к нему пользо- вательского интерфейса элемента управления LoginStatus можно воспользоваться свойством IsAuthenticated объекта User. void Page_Load(object sender, EventArgs e) { if (User.Identity.IsAuthenticated) // Корректировка пользовательского интерфейса - // вывод подходящего текста надписи Msg.Text = "Enjoy more features"; else Msg.Text = "Login to enjoy more features."; } Программный интерфейс элемента управления LoginStatus Хотя элемент управления LoginStatus может быть полезным и в своем стандарт- ном виде, у него имеется группа свойств и событий, с помощью которых выпол- няется его конфигурирование. Свойства этого элемента управления перечислены в табл. 15-16. Табл. 15-16. Свойства элемента управления LoginStatus Свойство Описание LoginlmageUri Возвращает и позволяет задать изображение для кнопки входа LoginText Возвращает и позволяет задать текст для кнопки входа LogoutAction Определяет действие, выполняемое при выходе пользователя из при- ложения. Допустимыми значениями являются Refresh, Redirect и RedirectlbLoginPage. Установка Refresh указывает, что нужно снова загрузить текущую страницу, а две другие установки позволяют пере- направить пользователя на страницу входа (RedirectlbLoginPage) или на другую страницу (Redirect) Logoutimage Url Возвращает и позволяет задать изображение для кнопки выхода LogoutPageUrl Возвращает и позволяет задать URL страницы, куда пользователь бу- дет перенаправлен после выхода LogoutText Возвращает и позволяет задать текст для кнопки выхода Кроме того, у элемента управления имеется пара событий: LoggingOut и Logged- Out. Первое генерируется после щелчка кнопки выхода, а второе — после его об работки. Элемент управления LoginView Элемент управления LoginView соединяет в себе функциональность элементов управ- ления LoginStatus и LoginName, позволяя вам отобразить пользовательский интерфейс, который зависит от состояния аутентификации текущего пользователя и от его ролей. Этот элемент управления основан на шаблонах: вы можете задать для него по одному шаблону для каждого состояния и каждой роли пользователя. Программный интерфейс элемента управления LoginView Со свойствами элемента управления LoginView, определяющими его пользовательский интерфейс, вы можете ознакомиться в табл. 15-17.
648 Часть III Инфраструктура ASP.NET Табл. 15-17. Свойства класса LoginView Свойство Описание AnonymousTemplate Возвращает и позволяет задать шаблон отображаемый для пользова- телей, не выполнивших вход в приложение LoggedlnTemplate Возвращает и позволяет задать шаблон, отображаемый для пользова- телей, вошедших в приложение RoleGroups Возвращает коллекцию шаблонов, определенных для поддержива- емых ролей. Эти шаблоны можно декларативно задать с помощью дочернего тэга <roleGroups> Заметьте, что шаблон LoggedlnTemplate выводится лишь для тех пользователей, которые вошли в приложение, но не являются членами ни одной из ролевых групп, заданных в свойстве RoleGroups. Шаблон, заданный в тэге <roleGroups> (если таковой существует), имеет преимущество перед шаблоном LoggedlnTemplate. Элемент управления Login View генерирует события ViewChanging и ViewChanged. Первое достигает приложения, когда элемент управления собирается изменить свое представление (например, после выполнения пользователем входа), а второе — когда представление элемента управления изменено. Создание шаблона входа Для элемента управления LoginView можно определить два разных шаблона, отобра- жаемых для анонимных и зарегистрированных пользователей. Для единообразного оформления страниц приложения, отражающего состояние аутентификации пользо- вателя, можно использорать такую разметку: <asp:loginview runat-"server"> <anonyi.)oustemplate> <table width="100%" border="0"><trxtd> To enjoy more features, <asp:loginstatus runat="server"> </tdx/t rx/table> </anonymoustemplate> <loggedintemplate> <table width="1OO%" border="0”xtr> <tdxasp: loginname runat=”server" /x/td> <td align="righf><asp: loginstatus runat="server" /x/td> </trx/table> </loggedintemplate> </asp:loginview> Как видите, элемент управления LoginView делает в сущности то же самое, что совместно используемые элементы управления LoginStatus и LoginName, но обладает большей гибкостью. Создание шаблонов для разных ролей Второй функцией элемента управления LoginView является определение блоков поль зовательского интерфейса, выводимых для всех пользователей, которые принадлежат к определенной роли. Как упоминалось, эти шаблоны имеют преимущество перед шаблоном <loggedintemplate>, если он задан наряду с ними: <asp:loginview runat="server"> <rolegroups> <asp:rolegroup roles="Admin">
Безопасность в ASP.NET Глава 15 649 <contenttemplate> </contenttemplate> </asp:rolegroup> <asp:rolegroup roles="Guest"> <contentterr.plate> </contenttemplate> . </asp:rolegroup> </rolegroups> </asp:loginview> Содержимое каждого блока <contenttemplate> выводится только для тех пользо- вателей, чьи роли заданы в соответствующем атрибуте roles. Эту функцию элемента управления LoginView можно использовать для определения фрагментов страницы, содержимое которых зависит от роли пользователя. Разумеется, чтобы элемент управ- ления работа7! правильно, функция управления ролями должна быть включена. Эле- мент LoginView использует провайдер, вызываемый по умолчанию. Элемент управления PasswordRecovery PasswordRecovery — это еще один серверный элемент управления, инкапсулирующий готовый фрагмент пользовательского интерфейса, который вы, в принципе, могли бы составить и сами. Он представляет форму, служащую для восстановления или сброса пароля. Пользователь получает новый или восстановленный пароль по электронной почте, в сообщении, направленном на указанный им при регистрации адрес. Элемент управления PasswordRecovery поддерживает три представления, выво- димых на разных стадиях процесса восстановления пароля. Первое представление отображается, когда пользователь должен ввести свое имя, после чего элемент управ- ления запрашивает у провайдера членства соответствующий объект MembershipUser. Второе представление предлагает пользователю ответить на контрольный вопрос, без чего не может быть получен пароль или осуществлен его сброс. Наконец, третье представление информирует пользователя о результатах операции. Условия, при которых возможно восстановление паролей Для того чтобы элемент управления PasswordRecovery мог правильно выполнить свою задачу, вы должны убедиться, что выбранный провайдер членства поддерживает вос- становление паролей. Кроме того, необходимо, чтобы в провайдере был определен объект MembershipUser и реализован метод GetUser. Напомню, что вы можете указать провайдеру членства, как должны храниться пароли: в виде чистого текста, хэширо- ванными или зашифрованными. Если пароли хранятся в виде хэшированных значений, элемент управления Pass- wordRecovery может выполнить свою задачу, поскольку алгоритмы хэширования не являются двунаправленными. Иными словами, они годятся для шифрования и срав- нения паролей, но не позволяют узнать исходный пароль. Так что если вы намерены пользоваться элементом управления PasswordRecovery, позаботьтесь о том, чтобы провайдер хранил пароли либо в текстовом виде, либо зашифрованными. восстановление пароля Элемент управления PasswordRecovery имеет дочерний элемент MailDefinition: <asp:passwo rd recove ry runat="se rve r"> <maildefinition from="admin@contoso.com" /> </asp:passwo rd recove ry>
650 Часть III Инфраструктура ASP.NET В нем конфигурируется сообщение электронной почты, в частности указываются отправитель, формат тела сообщения (текст или HTML), приоритет, тема и получа- тель копии. Эти установки можно задать и программно с помощью соответствующих свойств класса Framework. Если потерявший пароль пользователь определил пару вопрос-ответ при реги- страции, элемент управления отображает пользовательский интерфейс для получения ответа на контрольный вопрос. Процесс работы с элементом управления PasswordRe- covery проиллюстрирован на рис. 15-17. Рис. 15-17. Элемент управления PasswordRecovery в действии Элемент управления запрашивает у пользователя его имя, затем извлекает со- ответствующую информацию, задает контрольный вопрос, если таковой определен пользователем, и, наконец, при наличии адреса электронной почты отправляет поль- зователю сообщение (рис. 15-18). Элемент управления ChangePassword Если приложение выполняет аутентификацию Forms, то почти наверняка дает поль- зователям возможность изменять свои пароли (если только они не назначаются ав- томатически). Готовое решение этой задачи реализовано в ASP.NET в виде элемента управления ChangePassword'. <asp:ChangePassword ID="ChangePasswordT' runat="server" />
Безопасность в ASP.NET Глава 15 651 Он отображает пользовательский интерфейс для ввода желаемого пароля и ав- томатически заменяет старый пароль новым, пользуясь описанным в этой главе API членства. Рис. 15-18. Сообщение электронной почты, содержащее пароль пользователя Аутентификация пользователя Элемент управления ChangePassword работает как с аутентифицированными, так и еще не аутентифицированными пользователями. Его текстовое поле для ввода имени поль- зователя можно не отображать, но если при этом вы предоставите возможность поль- зователям изменять пароли, элемент управления будет блокировать такие попытки. Когда поле ввода имени пользователя отображается, неаутентифицированный пользователь может ввести свое имя, текущий пароль и новый пароль, чтобы операции аутентификации и смены пароля были выполнены одновременно. Изменение пароля Изменение пароля пользователя производится с помощью метода ChangePassword представляющего этого пользователя объекта MembershipUser. Заметьте, что провайдер может ограничивать количество неудачных попыток смены или сброса пароля и дан ное ограничение распространяется на элемент управления ChangePassword, который будет блокировать дальнейшие попытки. После успешной смены пароля элемент управления ChangePassword может от- править пользователю подтверждение электронной почтой (рис. 15-19) Это сообщение
652 Часть III Инфраструктура ASP.NET конфигурируется в разделе <MailDefinition>, таком же, как у описанного ранее эле- мента управления PasswordRecovery. Кнопка Continue элемента управления ChangePassword служит для перехода к дру- гой странице, с которой пользователь продолжит работу с приложением после смены пароля. Если не задать значение атрибута ContinuePageDestinationUrl, по щелчку этой кнопки будет просто обновляться текущая страница. Рис. 15-19. Элемент управления ChangePassword в действии Элемент управления CreateUserWizard Элемент управления Create UserWizard предоставляет пользователю возможность соз- дать и сконфигурировать новую учетную запись, для чего этот элемент пользуется API членства ASP.NET. Вы можете дать элементу управления указание отправлять новому пользователю уведомление по электронной почте. Адаптация элемента управления к нуждам конкретного приложения осуществляется в двух направлениях: настройки стандартных шагов мастера и добавления новых, служащих для ввода дополнитель- ных сведений, таких как адрес, номер телефона, роли и т. п. На рис. 15-20 вы видите элемент управления Create UserWizard в действии — он используется на странице Add User утилиты WSAT. В отличие от метода Create User провайдера членства, элемент управления Greater UserWizard не просто регистрирует новое имя и пароль пользователя, а предостав- ляет мастероподобный интерфейс для одновременного добавления дополнительной информации.
Безопасность в ASP.NET Глава 15 653 ® titbox • Microsoft Outlook Рис. 15-20. Элемент управления CreateUserWizard на странице Add User утилиты WSAT Ресурсы для написания кода, устойчивого к атакам Как обеспечить надежную защиту приложений ASP.NET? Прежде всего, она тесно связана с назначением приложения, количеством его пользователей и с тем, что это за пользователи. Парадоксально, но плохо защищенное приложение, по той или иной причине не привлекательное для хакеров, может считаться более безопас- ным, чем приложение с мощной линией защиты на уровне системы и приложения, имеющей одну-две бреши. Через эти бреши могут осуществиться успешные атаки, и чем более популярным является приложение, тем выше вероятность этих атак. К сожалению, в отношении защиты не существует стандартного набора средств и методов, на который вы могли бы полностью положиться. Защита — это прежде все- го состояние ума разработчиков, и пробелы в ней часто являются результатом неудач- ного кодирования или просто лени. Разрабатывая систему защиты приложений Web и ASP.NET, никогда и ничему не следует доверять слепо. Ваша задача — установить как можно больше заграждений, чтобы злоумышленнику труднее было их преодолеть. Следующие статьи из раздела шаблонов и практик Microsoft содержат ин- формацию об отражении наиболее типичных атак и реализации эффективной проверки ввода в приложениях ASP.NET 1.x и ASP.NET 2.0: «How То — Protect from Injection Attacks in ASPNET» http://msdn.micro- soft.com/library/dejault.asp?url~/lihrary/en-us/dnpagj2/html/paght000003. asp «HowTo-UseRegularExpressionstoConstrainInputinASP.NET» http://msdn.mi- crosoft.com/library/dejault.asp?url=/library/en-us/dnpag//html/paghtOOOOO1.asp «How To — Protect from SQL Injection in ASP.NET» http://msdn.microsoft.com/ library/default.asp?url~/library/en-us/dnpag2/html/paght000002.asp «How To — Prevent Cross-Site Scripting in ASP.NET» http://msdn.microsoft.com/ library/default.asp?url=/library/en-us/dnpag)2/html/paght000004.asp
654 Часть 111 Инфраструктура ASP.NET Заключение Защита приложений ASP.NET осуществляется на трех уровнях: IIS, рабочего процесса ASP.NET и приложения. Как разработчик вы можете конфигурировать отдельные параметры защиты первых двух уровней и полностью отвечаете за планирование и реализацию третьего уровня защиты. Оптимальным способом защиты страниц от неавторизированного доступа в приложениях, доступных через Интернет, является аутентификация Forms, а для интранет-приложений — аутентификация Windows Хотя метод Forms не идеален, он получил широкое распространение благодаря своей простоте. В приложениях ASP.NET 2.0 аутентификация Forms реализуется на основе API членства. API членства не меняет сути аутентификации Forms, он просто предоставляет в распоряжение разработчика набор эффективных средств ее осуществления. При на- писании нового приложения ASPNET 2.0 вы обязательно будете им пользоваться. Что касается переноса на эту платформу старых приложений, то имеет смысл провести их частичный рефакторинг, заключив существующий код в оболочки, интегрированные в новую инфраструктуру. Достаточно ли всего этого для того, чтобы приложения ASP.NET можйо было счи- тать хорошо защищенными? К сожалению, недостаточно. Разработка по-настоящему защищенных приложений должна выполняться опытными и знающими архитекто- рами и программистами, но все же программное обеспечение не может считаться защищенным, если столь же надежно не защищена сеть. Верно и обратное: сколь эффективным ни было бы администрирование, оно бесполезно, если код написан плохо и открывает бреши для атак. Для надежной защиты всех элементов программного обеспечения, доступного через Web, необходимо обеспечить защиту сети и приложения. Типичными сетевыми атаками являются атаки на отказ (DoS), получение доступа обманным путем (на- пример, с использованием ложного IP-адреса) и прослушивание сети. Результатами таких атак могут стать кражи паролей и других критичных для приложения данных, перегрузка приложения ложными запросами и получение злоумышленниками ин- формации о внутренней топологии сети. Примерами атак уровня приложения явля- ются внедрение SQL-кода и межсайтовые сценарные атаки (XSS). Как разработчик приложений вы должны усвоить одно непреложное правило: никогда не доверяйте пользовательскому вводу. Только факты Рабочий процесс ASP.NET выполняется от имени слабой учетной записи ASPNET или NETWORK SERVICE в зависимости от действующей модели процесса. Эти учетные записи не имеют административных привилегий. Вы можете сменить учетную запись рабочего процесса, но его новой учетной за- писи необходимо предоставить полные разрешения на доступ к папкам, в которых ASP.NET держит свои временные файлы. Наиболее распространенным способом защиты страниц и ресурсов ASP.NET от неавторизированного доступа является аутентификация Forms. Для ее выполнения к запросу присоединяется аутентификационный билет, удостоверяющий личность пользователя. Специальный модуль HTTP перехватывает каждый входящий за- прос и проверяет билет пользователя. Для реализации аутентификации Forms в ASP.NET имеется API членства, по- зволяющий отделить код страниц от кода, работающего с информацией о пользо-
Безопасность в ASP NET Глава 15 655 вателе. Этот второй тип кода изолирован в провайдерном компоненте, который подключается к приложению с учетом установок из конфигурационного файла. Также в ASP.NET имеется богатый API для управления ролями. Используя соот- ветствующий провайдер, аутентификационный модуль HTTP получает информа- цию о ролях пользователя и присоединяет ее к запросу. API членства интегрирован в Visual Studio .NET через утилиту WSAT, с помощью которой администратор может управлять учетными записями, а также ролями пользователей. API членства используется новым семейством элементов управления, автома- тизирующих такие типичные операции, как вход в приложение и выход из него, восстановление и изменение пароля, создание новой учетной записи пользователя. Для защиты приложения необходимо знать о типичных атаках. Ключевое правило защиты на этом уровне — никогда не доверять пользовательскому вводу. Проверяй- те и фильтруйте любые вводимые пользователем данные, используя стандартные средства и индивидуальные решения, зависящие от дальнейшего использования этих данных.
Алфавитный указатель #-выражение, 347,384 $-выражение, 354 А авторизация доступа, 613 адаптер данных, 272,297,298 исключения, 306 заполнение и обновление, 298,301 командные свойства, 309 сопоставление схемы, 304,305 рендеринга, 27 атака, типы, 96,602,603 атрибут [Serializable], 316 [ValidationProperty], 152 action, 12,164,167 allow Anonymous, 194 allowCustomSqlDatabase, 525 allowPartiallyTrustedCallers, 612 aspCompat, 488 attachDBFileName, 525 className, 218 clientTarget, 160 codeFile, 87 connectionStringName, 199 cookieless, 515,623 debug, 183 defaultProvider, 199, 637 defaultRedirect, 180 defaultUrl, 622 domain, 625 duration, 582,584 enabled раздела <profile>, 192 раздела <trace>, 184 enableTheming, 225 enableViewState, 540 enableViewStateMac, 537,538 headerText, 444 id, 208,122 location, 585,586 masterPageFile, 211,212,219 maxRequestLength, 134 атрибут (продолжение) mode раздела <customErrors>, 179 раздела <sessionState>, 525 name, 122 onclick, 127 onserverclick, 127 originUrl, 610,611 pageOutput, 184,188 redirect, 181 requestLimit, 188 requireSSL, 626 runat, 9,11,13,14,112 scope, 476 shared, 596 skinlD, 225,227 sqlCommandTimeout, 525 sqlConnectionString, 525 sqlDependency, 587 stateConnectionString, 524 statusCode, 181 styleSheetTheme, 224 theme, 224 themeable, 226 title, 213 trace, 185 traceMode, 185 typeName, 171 useHostingldentity, 528 validation, 537 VaryByCustom, 592 VaryByHeader, 585,592 VaryByParam, 582,585,590,592 virtualPath, 171 visibility, 156 writeToDiagnosticsTrace, 187 аутентификация, 604,612,632 Basic, 604 Digest, 605 Forms, 604,612,614,624 без применения cookie, 623 внешним приложением, 625 и SSL, 626 поток выполнения, 615 Integrated Windows, 605 Passport, 605,612,613,614
Алфавитный указатель 657 аутентификация (продолжение) Windows, 612,614 методом NTLM, 605 с использованием Kerberos, 605 Б база данных aspnetdb.mdf, 69 просмотр и редактирование, 200 структура. 199 ASPState, 525 билет аутентификационный, 621 Forms, 615 Passport, 613 блокировка для записи, чтения 510 оптимистическая, 308 браузер, возможности, 496 В валидация, 149 валидатор, 149 сравнивающий, 151 валидационная группа, 160 вывод сообщений об ошибках, 156,159 клиентская. 154,156,159 отключение, 161 пользователя, 632 при межстраничном постирге, 172 пустого поля, 152 серверная, 154,159 версионность, 29,31 визуализатор, 60 возврат формы, 8,107 вход в приложение, 616 выражение AppSettings, 354 Connectionstrings, 355 Resources, 354 динамическое (пользовательское), 353 регулярное, 154 связывания с данными, 347 выход из приложения, 618 Г генератор идентификаторов сеанса. 533 глобальный кэш сборок (GAC), 9, 609 д директива #Include, 477 ©Application, 476 ©Assembly, 90 ©Control, 87 ©Implements, 93 ©Import, 92 директива (продолжение) ©Master, 87,206,207,215 атрибуты, 207 ©MasterType, 218 ©OutputCache, 582,598 ©Page, 87,183,185,207,537 атрибуты, 87 89,90 ©PreviousPageType, 171,174 ©Reference, 93 ©Register, 15,16 синтаксис, 87 список директив страницы, 86 доверие, уровни, 609-611 домен приложения (AppDomain), 22 драйвер ODBC, 244,251 Ж жизненный цикл страницы, 75,104 выгрузка страницы, 110 загрузка страницы, 106 инициализация страницы, 105 обработка возврата формы, 107 подготовка страницы, 104 предрендеринг, 109 рендеринг, 109 3 зависимость кэша, 557 от базы данных SQL Server, 575,577,580 от XML-данных, 570 пользовательская, 568 уведомление о разрушении, 569 заголовок HTTP AspFilterSessionld, 516,533 Cache-Control, 491,585 Connection, 7 Expires, 585 If-Modified-Since, 7 Pragma. 586 User-Agent, 7 загрузка файлов на сервер, 132,144 заменитель, 206,208 запрос HTTP замена URL, 482 информация от клиента, 497 обработка, 82 сохранение на диске, 499 стартовая строка, 7 запутывание, метод, 65 защита на уровне доступа к коду, 13 уровни, 603 И идентификатор браузера, 212
658 Алфавитный указатель идентификатор (продолжение) потока, 603 сеанса, 513,533 имперсонализация, 605. 606 анонимной учетной записи, 608 включение, 606 на уровне запроса 607 фиксированной учетной записи, 607,608 интерфейс, 29,93 lAsyncResult, 282,285 IButtonControl, 140,170 ICallbackContainer, 398,438 ICallbackEventHandler, 398,438 ICollection, 331 IComponent, 113 ICustomTypeDescnptor, 332 IDbConnection, 251,255 IDbDataAdapter. 298 IDictionary, 331 IDisposable, 257 lEnumerable, 128,330,333 IFormatter, 544 IHttpHandler, 85,94 IHttpModule, 469,507 IList, 331 IListSource, 311,330 INamingContamer, 94 IPageHeader, 125 IPostBackDataHandler, 106,129,132 IPostBackEventHandler, 108 IRepeatlnfoUser, 345 ISerializable, 311,316 ISessionlDManager, 532,533 ISessionStateltemCollection, 532 IStateManager, 535 ITemplate, 343 ITextControl, 143 IValidator, 150 IXmlSerializable, 311,319 IXPathNavigable, 352 и базовый класс, 29 именование, 29 источник данных декларативное связывание, 264 подключение, 254 итератор, 343 К карта сайта, 376,377 класс AggregateCacheDependency, 569,570 AltSerialization, 521 Array, 331 ArrayList, 331 ASP.global_asax, 84 BaseDataBoundControl, 332 класс (продолжение) BaseDataList, 332 BaseValidator, 150,151 Binary Formatter, 521,522,544 BoundColumn, 392 BoundField, 406 ButtonColumn, 393 Cache, 505,551,553 создание оболочки, 563 CacheDependency, 555,557 наследование, 568 CacheMultiple, 554, 567 CacheSingle, 554,567 CheckBoxField, 409 CollectionBase, 331 CompositeDataBoundControJ, 398 Content, 208 Control, 113,114,116,117 Control Adapter, 118 Controlcollection, 117 ControlParameter, 370 CssStyleCollection, 137 DataBinder, 350 DataBoundControl, 332 DataBoundLiteralControl, 347 DataColumn, 310,319,320, 322 DataControlField, 404 DataControlFieldCell, 452 DataControlLinkButton, 452 DataGridColumn, 391 DataGridltem, 389,393 DataKey, 432 DataRelation, 310,322, 323, 297 DataRow, 302,310,319,320 DataRowView, 327 DataSet, 297,309,311 заполнение, 301 имена таблиц, 301 передача между слоями приложения, 310 сериализация в XML формат, 311,315 сериализация в двоичный формат, 316 слияние, 313 создание копии, 312 фиксация изменений, 314 DataSourceView, 358 DataTable, 297,309,317,322 вычисления, 320 имя объекта, 301 интерфейс, 317,318 ограничение, 321 отношение, 322 первичный ключ, 322 сериализация, 319 текущие и исходные значения, 302 фиксация изменений, 302 DataTableMapping, 305
Алфавитный указатель 659 класс (продолжение') DataTableMappingCollection, 304 DataTableReader, 313 DataView, 297,310,325 поиск строк, 327 создание объекта, 326 сортировка, 327 DataViewManager, 330 DbConnection, 255 DbDataAdapter, 298 DbProviderFactories, 252 DbProviderFactory, 251 Debug, 184 EditCommandColumn, 397 FileSystemWatcher, 569 ForeignKeyConstraint, 321 FormsAuthentication, 616,618,628 интерфейс, 619 Hashtable, 331 HierarchicalDataSourceView, 359 HtmlContainerControl, 124,164 HtmlControl, 122 HtmIForm, 164,165 HtmlGenericControl, 15,17 HtmllnputControl, 129 HtmlTableRow, 128 HttpApplication, 84,468 глобальные члены, 505 интерфейс, 469-471 HttpApplicationFactory, 83 HttpApplicationState, 501,502 интерфейс, 503 перебор элементов, 504 синхронизация доступа, 504 HttpBrowserCapabilities, 496 HttpCachePolicy, 491,582,587 интерфейс, 588 HttpContext, 468,479 интерфейс, 480-482 создание экземпляра, 479 HttpCookie, 514 HttpPostedFile, 133 HttpRequest, 495-498 HttpResponse, 489-493 HttpRuntime, 82,468 HttpServerUtility, 483,484 HttpSessionState, 501,506 интерфейс, 511-513 HttpSessionStateContainer, 532 HyperLinkField, 408 ImageField, 409 LinkButton, 452 ListControl, 332 ListDictionary, 331 Listitem, 127 Literal Control, 135 класс (продолжение) LosFormatter, 544,547 MasterPage, 207,208,213 Membership, 628- -630 Membershipprovider, 31,634 интерфейс, 634,635 MembershipUser, 633,649 MemoiyStream, 492 MobileControl, 19 MobilePage, 19 NameObjectCollectionBase, 503,504 ObjectStateFormatter, 544,547 OleDbSchemaGuid, 260 OracleDataReader, 276,277 Page, 13,15,19,85,94 внутренние объекты, 94 методы, 98 свойства, 94 события, 102 PageHandlerFactory, 85 PagerSettings, 400 Pair, 544 ParameterCollection, 362 ProfileBase, 198 ProfileCommon, 190,198 ProfileEventAigs, 198 ProfileProvider, 31,201 ProviderBase, 634 Recordset, 298 Repeateritem, 344 RepeaterltemCollection, 343 RoleProvider, 31,641,642 Roles, 628,638-640 ServerViewStatePage, 549 SessionlDManager, 532 SessionStateModule, 507,512 SessionStateStoreData, 532 SessionStateStoreProviderBase, 31,508,530 SiteMapProvider, 31 Socket, 610 SortedList, 331 SqlCacheDependency, 568,575,577,579 SqlCommand, 272,273,294 SqlConnection, 255,256 SqlDataAdapter, 298-300 SqlDataReader, 276 SqlDataSource, 361 SqlDependency, 292,293 SqlNotificationRequest, 292 SqlTransaction, 286,287 StateBag, 536 Stateitem, 536 Stream, 492 StringCollection, 331 Style, 137 TemplateControl, 94
660 Алфавитный указатель класс {продолжение) TemplateField, 411,454 TimeSpan, 561 Trace, 184 TraceContext, 186 TransactionScope, 288,289 Triplet, 544 UniqueConstraint, 321,322 UserControl, 207,213 ViewState, 501 WaitHandle, 285 WebControl, 136-138 WebPermission, 610 WebRequest, 610 WizardStep, 234 WizardStepBase, 231,234 XmlDocument, 379 XmlNode,379 XmlReader, 275 XmlTextReader, 292 XPathBinder, 352,353 XslTransform, 147 контейнерный, 310 отделенного кода, 44 пользовательского профиля, 190 совместно используемый, 51 создание вспомогательного, 50 страницы, 19 имя, 80 фабрика, 85 частичный, 44,80,81 клиент целевой, выбор, 46 ключ валидационный для cookie, 622 первичный, 322 пользователя, 96 кнопка типа submit, 9,129 коллекция, 331 Attributes, 123 ColumnMappings, 305 Columns, 390 Connectionstrings, 264 Constraints, 322 DataKeys, 389 DeleteParameters, 373 ExtendedProperties, 318 FilterParameters, 361 Form, 167 Headers, 177 InsertParameters, 373 Parametercollection, 362 QueryString, 167 Request.Form, 9 Rows, 451 SelectParameters, 369,370 TableMappings, 304 коллекция {продолжение) Tables, 321 UpdateParameters, 373,374 UserDefinedTypes, 258 Validators, 150 ViewState, 105 команда Visual Studio .NET Check page for accessibility, 45 Project\Cupy 38 Refactor\Encapsulate, 48 Tools Accessibility, 144 SQL асинхронное выполнение, 275,280 283 выполнение, 272 параллельное выполнение, 283 результирующие наборы, 280 уведомление об изменении данных, 292 компиляция по требованию, 75 компонент AccessDataSource, 365 ObjectDataSource, 366,415 кэширование данных, 371 обновление данных, 373 разбиение данных на страницы, 372,415 свойства, 366 сортировка, 422 удаление записи, 429 PartitionResolver, 529 SiteMapDataSource, 376-378 SqlDataSource, 359,415 и ObjectDataSource, 424 кэширование данных, 364 обработка конфликтов, 363 свойства, 360 XmlDataSource, 379-382 представляющий источник данных, 355 357 контейнер данных, 309 именования, 115 ключей, 266 связывания, 116 контекст защиты, 603 конвейера ASPNET, 606 потока IIS, 604 рабочего процесса, 605 контент, тип, 8 конфигурация приложения, 22 конфликт доступа к данным, 363 изменений, 308 кэш данных приложения, 551 время устаревания элементов 561 абсолютное, 555 скользящее, 556 добавление элементов, 555
Алфавитный указатель 661 кэш данных приложения (продолжение) зависимости, 557 кэширование на уровне запроса, 567 перебор элементов, 565 предпосылки кэширования, 562 приоритет элемента, 556,560 сборка мусора, 562 синхронизация доступа. 566 удаление элементов, 556 функция обратного вызова, 556 кэширование вывода, 551 страницы, 22,582 валидационная функция обратного вызова, 589 варьирование по заголовкам HTTP, 592 варьирование по значениям параметров, 590 варьирование по значениям свойств элемен- та управления, 595 варьирование по пользовательским строкам, 592 дискового, 600 длительность, 584 место, 585-587 на уровне ядра, 584 подстановка после кэширования, 599 при возврате формы, 590 профили, 598 элемента управления, 593,594.597 совместное использование вывода, 596 м мастер, создание, 168,203,230 боковая панель, 231,237 валидация ввода, 236 кнопки, 233 навигация, 237 , .. пользовательский интерфейс, 230 событие перехода, 234,239 суффиксы свойств кнопок, 233 типы экранов, 235 межсайтовый скриптинг, 602 межъязыковая интеграция. 13 менеджер состояния, клиентский, 507 метод Abandon, 513 AcceptChanges, 302,314,322 Add класса Cache, 554.555 класса TableMappings., 304 коллекции,331 AddCacheltemDependencies, 494 AddCacheltemDependency, 494 AddFileDependencies, 494 AddFileDependency, 494 AddNew, 326 AddRequiredFieldValidator, 456 метод (продолжение) Apply AppPathModifier, 516 ApplyPagePersonalization, 195 AuthenticateUser, 617 BeginExecuteReader, 282 Beginlnit, 326 BeginTransaction, 286 BmaryWrite, 495 Bind, 353 ChangeMode, 441 ChangePassword, 257,633 Clear, 331 ClearAllPools, 271 Clear Error, 177 Clear Pool, 271 Clone, 312 Close, 257 Commit, 287 Complete, 289,290 Compute, 320 Contains, 331 Copy, 312 CopyTo, 331 Create, 198 CreateConnection, 252 CreateDataReader, 313 CreateObject, 487 CreateObjectFromClsid, 487 CreateSessionlD, 534 CreateUser, 633 Delete класса DataRow, 321 класса DataView, 326 Deleteltem, 459 DeleteUser, 633 Dependency Dispose, 569 Deserialize, 547 Dispose, 110 класса HttpApplication, 471 класса SqlConnection, 257 класса TransactionScope, 289 EndExecuteReader, 282 Endlnit, 326 EnlistDistributedTransaction, 286 EnlistTransaction, 290 Eval, 350 класса DataBinder, 350,394,435 класса Page, 350 класса TemplateControl, 351 класса XPathBinder, 352 Execute, 485 ExecuteNonQuery, 275 ExecuteReader, 275,278,313 ExecuteScalar, 275 - ExecuteXmlReader, 275. 292
662 Алфавитный указатель метод (продолжение) Fill адаптера данных, 301,304 исключения, 306 FillSchema, 307 Find, 327 FindControl, 166,171 FindRows, 327 GetAppConfig, 482 GetAuthCookie, 620 GetBytes, 278 GetChildRows, 323 GetConfig, 482 GetDataltem, 351 GetDeleteCommand, 309 GetEnumerator, 326,331,504 GetFactory, 252 GetFactoryClasses, 253 GetGlobalResourceObject, 483 GetHistory, 234 GetlnsertCommand, 309 Getltem, 532 GetltemExclusive, 532 GetLastError, 177 GetLocalResourceObject, 483 GetOleDbSchemaTable, 259 GetPassword, 634 GetSchema, 258,259 GetSection, 482 GetSessionlD, 534 GetSqlXml, 292 GetUpdateCommand, 309 GetUser, 633,649 GetWebApplicationSection, 482 GetWebResourceUrl, 98 GetXml, 315 HasRows, 280 ImportRow, 321 IndexOf, 331 Init, 471 Initialize класса FormsAuthentication, 620 класса Membership, 630 класса ProviderBase, 634 Insert класса Cache, 553,554,555 коллекции,331 Insertitem, 459 IsItemDirty, 536 IsValid, 427 LoadControlStat, 545 LoadPageStateFromPersistenceMedium, 109, 547,548 LoadPostData, 106,108 LoadViewState, 106 Lock, 504,505 Merge, 313 метод (продолжение) MoveTo, 234 NewRow, 321 NotifyDependencyChanged, 569,573 Onlnit, 105 ProcessRequest, 82,85,94 RaisePostBackEvent, 108 RaisePostDataChangedEvent, 129 Read, 278 Redirect, 174,487 RedirectFromLoginPage, 616 RedirectToLoginPage, 620 Rejectchanges, 314 Remove, 321,331 RemoveAt, 331 ResetPassword, 634 Response.Redirect, 620 Response. Write, 14 RewritePath, 482 Rollback, 287 SaveControlState, 546 SavePageStateToPersistenceMedium, 109,547 SaveViewState, 109 Select, 319,322 Serialize, 547 Server.MapPath, 476 SetAuthCookie, 620 SetCacheability, 491 SetExpires, 491 SetltemExpireCallback, 531 SignOut, 618 Sort, 396 ToString, 350 Transfer, 173,177,486 TransmitFile, 495 UnloadAppDomain, 82,86 UnLock, 504,505 Update, 308 Updateitem, 459 UrlTokenDecode, 485 UrlTokenEncode, 485 Validate, 150,161,534 ValidateUser, 632 VerifyRenderinglnServerForm, 163 WaitAll, 285 WaitAny, 285 Warn, 186 Write, 186 WriteFile, 495 WriteSubstitution, 600 WriteXml, 315 XPath, 352 XPathSelect, 353 модель Windows Forms, 5 многопоточная апартаментная (МТА), 488
Алфавитный указатель 663 модель (продолжение) однопоточная апартаментная (STA), 488 провайдеров, 23-27 процесса IIS, 21,603 версии 5.0,77 версии 6.0,78 событий страницы, 103 страницы с одной формой, 103 «стратегия», 24 модуль HTTP, 22,469 FileAuthorizationModule, 613 Forms AuthenticationModule, 615 OutputCacheModule, 584 PassportAuthenticationModule, 613 ProfileModule, 197 URLAuthorizationModule, 613 H наследование визуальное, 205 О обложка элемента управления, 21,120,221 именованная, 225 используемая по умолчанию, 225 создание, 224 файл, 222 обновление пакетное, 300,308 обработка исключений, 13,174 глобальная, 178 на уровне страницы, 177 основы, 175 принципы, 178 обратный вызов сценария, 20,424 общеязыковая среда, загрузка, 79 объект Application, 502,551 BinaryReader, 521 Binary Writer, 521 COM, создание, 487 Request, 9,167,468,483,495 Response, 9,468,483,489 Server, 9,177,483 Session, 502,506 Trace, 186 User, 639,641 исключения, 182 командный, 272,278 чтения данных, 275,312 закрытие, 279 результирующие наборы, 280 ограничение, 321 оператор tiy...catch...finally, 269 using, 269,289 ответ HTTP, структура, 8 отладка приложения, 59 папка, 53 AppBrowsers, 54 App_Code, 54,55 App_Data, 54 AppGlobalResources, 54,55 AppLocalResources, 54,55 App_Themes, 54,58 App_WebReferences, 54,58 Bin, 54 временных файлов ASP-NET, 609 глобального кэша сборок (GAC), 609 корневая .NET Framework, 609 приложения, 609 сайта, 609 системная Windows, 609 параметр Maximum Used Memory, 597 определяемый декларативно, 362 пароль восстановление, 649 доступа к базе данных, 257 изменение, 650,651 перезапуск приложения, 85 перечисление AcceptRejectRule, 322 CacheltemPriority, 560 CacheltemRemovedReason, 557 Conflictoptions, 363 DataRowState, 314 DataViewRowState, 325 DetailsViewMode, 440 HttpCookieMode, 515 HttpValidationStatus, 589 ListltemType, 388 LoadOption, 302 MissingMappingAction, 306 MissingSchemaAction, 306 Rule, 322 SchemaType, 308 SerializationFormat, 317 SessionStateMode, 508 WriteXmlMode, 315 персонализация, 188,189 повторное использование процессов, 22 подключение время жизни, 270 к источнику данных, 254 освобождение объекта, 268 получение объекта, 268 утечка подключений, 269 подстановка после кэширования, 598,599 поле _PREVIOUSPAGE, 170,174 __VIEWSTATE, 109,537,548
664 Алфавитный указатель поле (продолжение) NoAbsoluteExpiration, 553,556,561 NoSlidingExpiration, 553 скрытое, 144 пользователь анонимный, персонализация, 190,191,198 идентификация, 604 постинг межстраничный, 20,170,172 построитель выражений, 354 команд, 309 строк подключения, 262 похищение сеанса, 602 предкомпиляция сайта, 65 без поддержки обновлений 67 для развертывания, 65,66 на месте, 65 предрендеринг, этап, 109 представление данных, 358 иерархическое, 359 табличных данных, 310 привилегии ASPNET, 608 приложение ASPNET, 3 администрирование, 67 перезапуск, 85 разрешения, 610 свойство статическое, 477 уровни доверия, 610 провайдер, 23 базовые классы, 31 данных, 244 EXOLEDB,251 MSDAIPP, 251 MSDAORA, 250 ODBC, 251 ODP.NET, 250 OLE DB, 247 SQLOLEDB, 249 получение списка установленных провайдеров, 252 создание экземпляра, 251 управляемый (.NET), 245-247 фабрика, 251 профилей, 189,198 AspNetSqlProfileProvider, 199 конфигурирование, 199 пользовательский, 201 ролей, 629,641 AspNetSqlRoleProvider, 642 AspNetWindowsTokenRoleProvider,642 пользовательский, 642 состояния сеанса InProcSessionStateStore, 529 OutOfProcSessionStateStore, 529 SqlSessionStateStore, 529 провайдер (продолжение) пользовательский, 529 регистрация, 532 типы, 30 шифрования DPAPIProtectedConfigurationProvider, 265 RSAProtectedConfigurationProvider, 265 членства, 629,634 конфигурирование, 637 пользовательский, 636 проект инсталляционный, 62 копирование, 38 создание, 40 файл, 37 пространство имен <asp:>, 15,16 System.Data, 309 System.Data.OleDb, 251 System.Data.SqlClient, 255 System.Net.Mail, 178 System.Transactions, 286,2У0 System. Web.Mail. 178 System. Web.SessionState, 511 System. Web.UI.HtmlControls, 17,122 System. Web.UI.WebControls, 17,135 Systems-Diagnostics, 184 профиль кэширования, 598 пользователя, 189 анонимного, 191,198 доступ к свойствам, 194 класс, 190 определение свойств, 190-191 процесс рабочий aspnet_wp.exe, 605 w3wp.exe, 59,78,605 учетная запись, 605,607 процессор страницы, 105 пулинг подключений к источникам данных, 266,267 очистка пула, 271 приложений, 78 потоков, 22 Р развертывание приложения, 54,62 раздел <advertisements>, 145 <appSettings>, 69 <authentication>, 612, G15,621 <authorization>, 615,639 <browserCaps>, 212 <compilation>, 183 <connectionStrings>, 199,263 <customErrors>, 179
раздел (продолжение) <еггог>, 181 <forms>, 619,621 <httpHandlers>, 85 <identity>, 607 <imageUrl>, 146 <1осайоп>, 65 <machineKey>, 622 <mailDefinition>, 649 <membership>, 637 <outputCacheProfiles>. 598 <processModel>, 77,78,607 <profile>, 190,192,199 <properties>, 190,192 <protectedData>, 265 <providers>, 199,518,637,642 <roleManager>, 640,642 <sessionState>, 507,508,515» 517 <trace>, 184,187,188 <trust>, 610 разрешение OleDbPermission, 611 WebPermission, 611 за рамками уровне доверия, 611 редактирование на месте, 397 рендеринг адаптивный, 19,118 для конкретного браузера, 118 этап, 109,110 ресурс загрузка программная, 483 локальный, 55,56 рефакторинг кода, 47 С сборка мусора, 13 в кэше приложения, 562 ресурсов, 55 связываемая со страницей по умолчанию, 91 свойство AcceptChangesDuringFill, 303,314 AcceptRejectRule, 322 ActiveStepIndex, 234 Adapter, 118 AllowDBNull, 322 AllowPaging, 413,417,442,445 AllowReturn, 235 AllowSorting, 396,399,419 AppDomainAppId, 528 AppendDataBoundltems, 336 AssociatedControlID, 143,144 AsyncState, 283 Attributes, 123 AutoGenerateColumns, 390,404 AutoGenerateDeleteButtpn, 428 Алфавитный указатель 665 свойство (продолжение) AutoGenerateEditButton, 426 AutoGenerateRows, 443,444 AutoGenerateSelectButton, 432 AutoGenerateXxxButton, 440 Cache, 552 CacheControl, 491 CacheKeyDependency, 364 Cancel, 445 CanUpdate, 426 CausesValidation, 131,161,226 ClientID, 115 ClientValidationFunction, 153 Columns, 404 Command Argument, 431 CommandName, 393,407,452 CommandType, 272 ConflictDetection, 363 ConnectionString, 260,359 ContentPlaceHolderlD, 208 ContinueUpdateOnError, 308 Controlstyle, 137 ControlToCompare, 152 ControlToValidate, 151 ConvertEmptyStringToNull, 406 Count, 331 Current, 481 CurrentExecutionFilePath, 496 CurrentStepIndex, 238 DataField, 392,406,427 DataFormatString, 392,406 DatalmageUrlField, 409 DatalmageUrlFormatString, 409 DataKey, 445 DataKeyField, 336,389,431 DataKeyNames, 375,427,431,432,445,450 DataKeys, 336,432 DataMember, 128,333,334 DataNavigateUrlField, 392 DataNavigateUrlFields, 408 DataNavigateUrlFormatString, 392,408 DataObjectTypeName, 374,428 DataSetlndex, 389 DataSource, 128,332,333 DataSourcelD, 332,333,356 элемента DataGrid, 395 элемента GridView, 402,403 DataSourceMode, 361,364,421 DataTextField, 128,334,392,407 DataTextFormatString, 408 DataValueField, 128,335 DefaultMode, 439 DeleteCommand, 300,309 DeleteRule, 322 Depth, 276 DisplayMode, 158
666 Алфавитный указатель свойство (продолжение) Editindex, 426 EditltemTemplate, 394,458 Empty DataTemplate, 403,430,443,448,459 EmptyDataText, 443 EnableCaching, 395,421 EnableClientScript, 159 EnablePaging, 372,415 EnablePagingCallbacks, 446 EnableSortingAndPagingCallbacks, 400,424 EnableTheming, 225,226,424 EnableViewStat, 542 Enctype, 132 ErrorMessage, 150,156 Expires, 491,515 ExpiresAbsolute, 491 FieldHeaderStyle, 444 FilePath, 496 FillLoadOption, 302,303 Filter, 491 FilterExpression, 361 FooterRow, 452 FooterTempIate, 394,459 FooterText, 406 Handler, 174 HasChanged, 569 Header, 125 HeaderlmageUrl, 406 HeaderTemplate, 394,459 HeaderText, 406 мастера, 230 ID, 115,165,235 ImageUrl, 141 InnerHtml, 124 InnerTex, 124 InsertCommand, 300,309 InsertltemTemplate, 458 IsCallback, 105 IsCompleted, 283 IsCrossPagePostBack, 105,172,174 IsDirty, 536 IsEnabled, 186 IsPostBack, 105,106,172 IsSynchronized, 512 IsValid, 150,173 Item, 331 Itemindex, 393 Items, 532,481 ItemTemplate, 394,458 Keys, 331 MachineName, 483 Master, 217,219 MasterPageFile, 219 MaximumRowsParameterName, 372,415 Method, 164,165 MissingMappingAction, 306,307 свойство (продолжение) MissingSchemaAction, 306,307 Mode, 417 Modules, 469 Name, 165,634 NavigateUrl, 141 NextStepIndex, 238 NullDisplayText, 406 NullImageUrl, 411 OldValuesParameterFormatString, 363,427 OnClientClick, 140 OnlineTimeWindow, 630 Operator, 152 PageCount, 445 Pagelndex, 445 PagenTemplate, 459 PagerSettings, 400,445,460 PagerStyle, 445,460 PagerTemplate, 430,460 PostBackUrl, 140,170 PreviousPage, 171-174 Profile, 191,194,198 Provider, 630 ProviderName, 359 Providers, 630,636 Readonly, 392,406 RemotingFormat, 316,317 Request.Browser, 159 RowFilter, 325 RowState, 302,314 RowStateFilter, 325 ScriptTimeout, 483 SelectCommand, 300,301,309 SelectedDataKey, 432 Selectedlndex, 393,432 SelectedltemStyle, 393 SelectedValue, 447 Session, 507 SkinlD, 225 Sort, 327 SortDirection, 399 SortExpression, 396,399 SortParameterName, 421,422 SqlCacheDependency, 580 StartRowIndexParameterName, 372,415 StaticObjects, 532 StepType, 235 SyncRoot, 512 TableMappings, 300,301 Target, 164 Text, 143 Timeout, 532 Trace, 186 TraceEnabled, 185 TraceMode, 186 TraceModeValue, 185
Алфавитный указатель 667 свойство [продолжение) Transaction, 287 Transform, 382 TransformArgumentList, 382 TransformFile, 382 Unique, 322 UniquelD, 115 UpdateCommand, 300,309,427 UpdateRule, 322 UserlsOnlineTimeWindow, 630 UseSubmitBehavior, 140 UtcLastModified, 569 ValidationGroup, 140,161 Values, 331 ValueToCompare, 152 VaryByParam, 590 ViewStateUserKey, 538 Visible, 116 WizardSteps, 235 коллекция, 17 статическое приложения, 477 связывание позднее, 487 раннее, 487 с данными, 332 связь данных, 322 сервис aspnet_state, 524 SQLServerAgent, 527 стандартные сервисы, 23 сериализация, 324 словарь, 331 слой хранения, 30 слушатель трассировки, 184 событие ActiveViewChanged, 239 AdCreated, 146 Application_End, 471,472 Application_Error, 478 Application_Start, 471,472 AcquireRequestState, 509 AuthenticateRequest, 641 CancelCommand, 389,397 DataBinding, 349,390 DeleteCommand, 389 Disposed, 110 EditCommand, 389,397 EndRequest, 519 Error, 177,182 FinishButtonClick, 237 Init, 19,105 InitComplete, 105 ItemCommand, 393,442 ItemCreated, 451,455 ItemDeleted, 451 ItemDeleting, 451 событие (продолжение) Load, 19,106,107 LoadComplete, 108 MigrateAnonymous, 198 NextButtonClick, 237 ObjectCreating, 371 ObjectDisposing, 371 Page_Init, 197 PageLoad, 147 Page_PreInit, 197 PagelndexChanged, 395,414,445 PagelndexChanging, 445 Personalize, 197 Prelnit, 105,219,226,229 PreLoad, 106 PreRender, 19,109 PreRenderComplete, 109 ReleaseRequestState, 509 ResolveRequestCache, 584 RowChanged, 320 RowCommand, 402,406,407,418,430,432 RowCreated, 402,407,430,434 RowDataBound, 402,434 RowDeleting, 429 RowUpdated, 427 RowUpdating, 402,426 SaveStateComplete, 109 SelectedlndexChanged, 393,432 ServerChange, 129,132 ServerClick, 127 ServerValidate, 154 Session_End, 513,519 Session_Start, 519 SideBarButtonClick, 238 SortCommand, 389,396 TextChanged, 143 Unload, 19,110 UpdateCommand, 389,397 UpdateRequestCache, 584 Updating, 376 создание обработчика, 49 страницы, 19 состояние клиента, 4 представления страницы, 103,535 отключение, 541 сериализация, 542,543 сохранение на сервере, 546 приложения, 501 сеанса, 506 конфигурирование, 517,522,525 провайдер, 507 режим InProc, 508,513,519-521,528 режим SQLServer, 528 режим StateServer, 522,524 режимы поддержки, 507
668 Алфавитный указатель состояние (продолжение) сериализация, 521 синхронизация доступа, 510 создание хранилища SQL Server, 525 сохранение, как концепция, 4 сравнение объектов, 501 страницы, 4 сериализация, десериализация, 6 элемента управления, 109,120,131,542, 544,545 список контроля доступа, 613 стандарты удобства доступа Section и WCAG, 144,508 статистика использования памяти, 562 столбец вычисляемый, 324 страница ASP.NET, 6,9 входа, 616 демонстрационная, 10 директивы, 9 жизненный цикл, 19,75 исполняющая среда, 75 клиентский сценарий, 19 контента, 21,42,43,206,210 обработка ошибок, 174 персонализация, 20,188 пользовательский интерфейс, 10 программный код, 9 прототип, 21 с богатым интерфейсом, 203 с несколькими формами, 166,168 с сообщением об ошибке, 175 по умолчанию, 175 связывание с кодом ошибки, 180 стилевое оформление, 20 управляемая данными, 285 эталонная, 21,42,206 вложенная, 214 для определенного устройства, 211 доступ к свойствам, 217 компиляция, 213 контент по умолчанию, 208 программирование, 217 программный выбор, 219 строка подключения, 256 атрибут, 260 Connection Lifetime, 270 Connection Timeout, 262 Integrated Security, 257 Load Balance Timeout, 271 MultipleActiveResultSets, 295 Network Library, 261 OLE DB Services 268 Pooling, 268 защита, 264 построитель, 262 структура, 260 хранение, 263 структура GridViewCommandEventArgs 431 WizardNavigationEventArgs, 238,239 таблица, 144 AspNetSqlCacheTablesForChangeNotification, 577 ASPStateTempApplications, 526 ASPStateTempSessions, 526 стилей, 221 обычная (CSS), 222 расширяемая (XSLT), 227 тема, 58,120,219,220 в ASP.NET1.X, 220 динамический выбор, 228 настроечная, 221,223 создание, 226,227 структура, 221 таблицы стилей, 221,223 управление применением, 225 тестирование приложения, 61 токен защиты, 603 транзакция выполнение, 286 локальная, 286 откат, 287 распределенная, 286,289 уровень изоляции, 287 фиксация, 287 трассировка, 184 просмотрщик сообщений, 187 слушатель, 184 •у. •. триггер, обработка зависимости, 575 тэг <а>, 127 <asp:content>, 208,212,213 , <asp:wizardstep>, 230 <asp:xml>, 147 <body>, 15,163 <button>, 130 <caption>, 128 <col>, 128 <colgroup>, 128 <columns>, 391 <div>, 142,165 <fields>, 444 <font>, 15 <form>, 5,8,163 атрибут action, 12 клиентский, 166 <head>, 15,125,226 <iframe>, 15 <img>, 134 <input>, 129,142
Алфавитный указатель 669 тэг (продолжение) <itemstyle>, 392 <label>, 142 <link>, 126 <meta>, 126 <object>, 476 <option>, 128.335 <pagersettings>, 417 <pagerstyle>, 417 <pagertemplate>, 418 <script>, 9 <select>, 128 <span>, 15,153 <table>, 128,165 <tbody>,128 <templatecolumn>, 394 <textarea>, 129 <tfoot>, 128 <thead>, 128 <title>, 213 <wizardsteps>, 231,235 без серверного представления, 17 неизвестный, 15 уровня страницы, 15 У управление ролями, 638-641 членством, 629,631 утечка подключений, 269 утилита aspnet_regiis.exe, 265,266 учетная запись пользователя, 633 процесса ASP.NET, 605,607 Ф фабрика, класс, 85 обработчиков HTTP, 84 файл .asax, 77 .asex, 77 .ashx, 77,85 .asmx, 77,85 .aspx, 77,85 .axd, 77 .browser, 212 .css, 222 .master, 208 .rem, 85 .resx, 57 .skin, 222 .soap, 85 .vssettings, 48 globaLasax, 84,197,472,474,505 директивы, 475 компиляция, 474 файл (продолжение) программный код, 476 синтаксис, 475 создание, 178 статические свойства, 477 machine.config, 23 web.config, 23,53 визуальный редактор, 70 XSD.55 класса страницы, 183 определения рекламы, 145 отделенного кода страницы. 9 проекта, 37 ресурсов, 55 фильтр ответа, 491 фокус ввода, установка, 121 форма HTML, 166 логическая, 166 методы, 165 реентерабельная, 5,163 свойства, 164 функция __doPostBack, 170,131 Application_Error, 178 Bind, 455,462,464 Eval, 455,462 WebFormDoPostBackWithOptions, 170 X хранимая процедура расширенная, 575 ш шаблон, 222 э элемент управления, 144 AdRotator, 145 BulletedList, 341 Button, 140 Calendar, 146 ChangePassword, 642,650,651 CheckBoxList, 338 CompareValidator, 152 Content, 208,209,214 ContentPlaceHolder, 206,208,209,214 CreateUserWizard, 642,652 CustomValidator, 153,173 DataGrid, 346,356,385 индексные свойства, 389 коллекция Columns, 387 коллекция Items, 387 переход между страницами, 394 редактирование, 397 свойства столбцов, 391 события, 389
670 Алфавитный указатель элемент управления (продолжение) сортировка, 396 столбец гиперссылок, 392 столбец кнопок, 393 столбец с шаблоном, 393 типы столбцов, 390 DataList, 345 DetailsView, 437 валидация, 455 вставка записи, 453 кэширование данных, 449 объектная модель, 437 редактирование данных, 450 свойства, 438 события, 441 шаблоны, 441 DropDownList, 337 FileUpload, 144 Form View, 458 методы, 458 подсистемы защиты, 642 разметка, 460 раскладка, 460 свойства, 458 связывание с данными, 460 GridView, 356,398 блок листания, 417 выделение строки, 432 листание, 413 редактирование, 425 свойства, 399 свойства столбцов, 405 события, 402 сортировка, 419 столбец гиперссылок, 408 столбец данных, 406 столбец изображений, 409 столбец кнопок, 406 столбец флажков, 409 типы столбцов, 404 HiddenField, 144 HTML, 17,121,124,131 Html Anchor, 127 HtmlButton, 125,130 HtmlHead, 125 Htmllmage, 134 HtmllnputButton, 125,130 HtmllnputCheckBox, 142 HtmllnputFile, 132 Htmllnputlmage, 131 HtmllnputRadioButton, 142 HtmllnputReset, 131 HtmllnputSubmit, 131 HtmlLink, 126 HtmlMeta, 126 HtmlSelect, 127 элемент управления (продолжение) HtmlTable, 128,144 HtmITextArea, 129 Hyper Link, 141 Image, 141 ImageButton, 140 input-типа, 129 Label, 143 LinkButton, 140 ListBox, 340 Literal, 143 LiteralControl, 135 Login, 642 программный интерфейс, 644 события, 645 LoginName, 642,645 LoginStatus, 642,645-647 LoginView, 642,647,648 MultiView, 148,169,196,203,234 Panel, 142 PasswordRecovery, 642,649 PlaceHolder, 148 RadioButtonList, 340 RangeValidator, 155 RegularExpressionValidator, 154 Repeater, 343 RequiredFieldValidator, 155,172 SiteMapPath, 377 Substitution, 599,600 Table, 144,128 TextBox, 143 TreeView, 377 Validationsummary, 151,158 View, 148 Web, 6,17,135,137,138,140,143 Wizard, 149,170,230-234 WizardStep, 230 Xml, 147 валидационный, 149,150 видимость, 116 итеративный, 342 литеральный, 135 мобильный, 19 пользовательский, 18,203,213,593 серверный, 6,113 состояние, 120 специализированный, 18 списочный, 337
Алфавитный указатель 671 А ADO, технология, 244,245 ADO.NET, технология, 244, 245 доступ к SQL Server, 249 источники данных, 248 API персонализации, 189 управления ролями, 638, 639 управления членством, 629 управления членством и ролями, 628 AppDomain, 22 ASPNET, технология, 2 компонентная модель, 12 стек разработки, 18 типы обрабатываемых файлов, 76 aspnet_filter.exe, фильтр ISAPI, 516 aspnet_isapi.dll, модуль, 21,605 aspnet wp ехе, процесс, 59,77,605 ASPState_Job_DeleteExpiredSessions, задание, 527 С СОМ, технология, 245 cookie, 502 алгоритм хэширования, 622 аутентификационный, 622 совместное использование приложениями, 624 сеансовый, 514 CSS, каскадная таблица стилей, 20,219,227 D DiffGram, формат, 316 Distributed Transaction Coordinator (DTC), 286 DNS, 7 DoS, атака на отказ, 602,654 DPAPI, технология шифрования, 266 F FrontPage Server Extensions (FPSE), 34 FTP, доступ к сайту, 36 G GAC, глобальный кэш сборок, 9,609 н HTTP, протокол, 7,8 исполняющая среда, 21 http.sys, драйвер, 79 I IDE, импорт и экспорт установок, 48 IntelliSense, 39 Internet Information Server (IIS) обработка запросов, 75 режим аутентификации, 604 IP-адрес, 7 Internet Server Application Programming Interface (ISAPI), 2,8 расширение, 8,76 фильтр, 22 J Java Server Pages (JSP), 2 L LAMP, платформа, 2 M MARS, технология, 262,293 Microsoft Access, доступ к данным, 365 MIME-тип контента, 8 N NotSupportedException, исключение, 637 О ODBC, технология, 244 OLE DB, технология, 244,259,611 Oracle, доступ к базам данных, 250 R RSA, технология шифрования, 266 S SFI, модель, 163 SQL Server Express, СУБД, 200 SQL Server, доступ к данным, 249 SSL, аутентификация, 626 т TCP, протокол, 7 Transaction Processing Monitor, 286 V Visual Studio .NET, 33,34 w Web-сад, 77 Web-сервис, связанный, 58 Web Administration Service (WAS), 80 Web Forms, модель, 4 Web Site Administration Tool (WSAT), 67,68 Windows Forms, модель, 5 Windows SharePoint Services (WSS), 34 X XHTML, стандарт, 119 XML как тип данных, 291 XSLT, расширяемая таблица стилей, 227 XSS, межсайтовая сценарная атака, 602,654
Об авторе Дино Эспозито является экспертом по техно- логиям ASP.NET и ADO.NET в компании Solid Quality Learning, занимающей одну из лидиру- ющих позиций в сфере тренинга и консалтинга. Автор этой книги ведет раздел «Cutting Edge* в журнале «MSDN Magazine» и регу- лярно пишет статьи о .NET Framework для Mi- crosoft ASP.NET Developer Center и Microsoft Visual Studio Developer Center, а также для раз- личных журналов, в число которых входят «asp. netPRO Magazine», «CoDe Magazine» и инфор- мационный бюллетень «ASP.NET-2-The-Max». Перу Дино Эспозито принадлежат изданные в Microsoft Press книги «Programming Micro- soft ASP.NET» (2003), «Building Web Solutions with ASPNET and ADO.NET» (2002), «Applied XML Programming for Microsoft .NET» (2002) и «Programming Microsoft ASP.NET 2.0: Advanced Topics» (2006). Свежую информацию о пла- нах автора можно найти в его блоге по адресу http://weblogs.asp.net/despos. Будучи членом команды докладчиков .NET Association (INETA), Дино Эспозито часто выступает на различных мероприятиях, организуемых сообществами разработ- чиков, главным образом в Европе и США. Прежде чем стать исключительно автором, консультантом и инструктором, Дино Эспозито работал в нескольких ведущих консалтинговых компаниях. Он положил начало внедрению систем DNA в Европе и разработал в 1994 году одно из первых серьезных Web-приложений — банк изображений. В наши дни Дино часто можно встретить на главных конференциях отрасли, таких как DevConnections, DevWeek, WinDev и Microsoft TechEd. Проживает он в Италии, в Риме.
Дино Эспозито Microsoft* ASP.NET 2.0 Базовый курс МАСТЕР-КЛАСС 1И.РУССШ РЕДАКЦИЯ ^пптер( Москва * Санкт-Петербург * Нижний Новгород * Воронеж Новосибирск * Ростов-на-Дону * Екатеринбург * Самара Киев * Харьков * Минск 2007
УДК 004.45 ББК 32.973.26-018.2 Э85 Эспозито Д. Э85 Microsoft ASP.NET 2.0. Базовый курс. Мастер-класс / Пер. с англ. — М. : Изда- тельство «Русская Редакция» ; СПб. : Питер, 2007. — 688 стр. : ил. ISBN 978-5-7502-0304-8 («Русская Редакция») ISBN 978-5-91180-423-7 («Питер») Эта книга — подробное руководство для профессионалов-разработчиков при- ложений ASP.NET. В ней описаны технологии создания эффективных, масштабируемых и надеж- ных сайтов на платформе ASP.NET 2.0, обладающих разнообразным и согла- сованным пользовательским интерфейсом. Вы узнаете, как создавать эталон- ные страницы, персонализировать вывод сайта и адаптировать его к возмож- ностям браузера, ознакомитесь с широким ассортиментом средств ASP.NET для работы с данными, научитесь эффективно кэшировать информацию, аутен- тифицировать пользователя и авторизировать его доступ к серверным ресур- сам. Книга адресована тем, кто не ограничивается прикладными сведениями, по- черпнутыми из обычных учебных пособий, а намерен разобраться во всех де- талях внутреннего функционирования исполняющей среды ASP.NET 2.0; состоит из 15 глав и предметного указателя. УДК 004.45 ББК 32.973.26-018.2 Подготовлено к печати по лицензионному договору с Microsoft Corporation, Редмонд, Вашингтон, США. Microsoft, ActiveX, IntelliSense, JScript, Microsoft Press, MSDN, MS-DOS, Visual Basic, Visual C#, Visual Studio, Win32, Windows и Windows Media являются товарными знаками или охраняемыми товарными знаками кор- порации Microsoft в США и/нли других странах. Все другие товарные знаки являются собственностью со- ответствующих фирм. Все названия компаний, организаций и продуктов, а также имена лиц, используемые в примерах, вы- мышлены и не имеют никакого отношения к реальным компаниям, организациям, продуктам и лицам. Эспозито Дино Microsoft ASP.NET 2.0 Базовый курс Совместный проект издательства «Русская Редакция» и издательства «Питер» «.РУССКИ РЕДАКЦИЯ [^пгтр' © ISBN 0-7356-2176-4 (англ.) ISBN 978-5-7502-0304-8 © («Русская Редакция») ISBN 978-5-91180-423-7 («Питер») © Оригинальное издание на английском языке, Dino Esposito, 2006 Перевод на русский язык, Microsoft Corporation, 2006 Оформление и подготовка к изданию, издательство «Русская Редакция», 2007 ООО «Питер Пресс», 198206, Санкт-Петербург, Петергофское шоссе, д. 73, лит. А29. Налоговая льгота — общероссийский классификатор продукции ОК 005-93, том 2; 95 3005 — литература учебная. Подписано в печать 12.12.06. Формат 70x100/16. Усл. п. л. 55,47. Тираж 2500. Заказ 3478 Отпечатано по технологии CtP в ОАО «Печатный двор» им. А. М. Горького. 197110, Санкт-Петербург, Чкаловский пр., 15.
Оглавление Благодарности.........................................................XII Введение.............................................................XIII Кому адресована книга.................................................XIV Системные требования..................................................XIV Конфигурирование SQL Server 2005 Express Edition.......................XV Обновления технологий.................................................XVI Примеры программного кода.............................................XVI Поддержка.............................................................XVI Вопросы и комментарии.................................................XVI Часть I Разработка страниц ASP.NET Глава 1 Модель программирования ASP.NET.................................2 Что такое ASPNET........................................................2 Программирование в эру Web Forms.....................................4 Событийно-управляемое программирование в НТТР-среде..................4 Протокол HTTP........................................................7 Структура файла страницы ASP.NET.....................................9 Компонентная модель ASPNET .............................................12 Модель взаимодействия компонентов...................................13 Атрибут runat.......................................................13 Серверные элементы управления ASPNET................................17 Стек разработки ASP.NET................................................18 Уровень представления...............................................18 Внутренняя организация страницы и процесс ее выполнения.............19 Исполняющая среда HTTP..............................................21 Модель провайдеров ASP.NET.............................................24 Для чего введена модель провайдеров.................................25 Реализация модели провайдеров в ASP.NET.............................28 Заключение.............................................................31 Глава 2 Разработка Web-приложений в Microsoft Visual Studio .NET 2005...33 Введение в Visual Studio .NET 2005.....................................33 Ограничения Visual Studio .NET 2003.................................34 Достоинства Visual Studio .NET 2005.................................35 Создание проекта ASPNET................................................40 Средства разработки страницы........................................41 Добавление кода в проект............................................48
VI Оглавление Зарезервированные папки ASP.NET......................................53 Компиляция проекта...................................................59 Развертывание приложения................................................62 Развертывание методом ХСору..........................................62 Предкомпиляция сайта.................................................65 Администрирование приложения ASP.NET....................................67 Web Site Administration Tool.........................................68 Редактирование конфигурационных файлов...............................70 Заключение..............................................................73 Глава 3 Анатомия страницы ASP.NET.......................................75 Активизация страницы....................................................75 Особенности функционирования исполняющей среды.......................75 Обработка запроса....................................................82 Директивы обработки страницы.........................................86 Класс Page..............................................................94 Свойства класса Page.................................................94 Методы класса Page...................................................98 События класса Page.................................................102 Модель событий ASPNET...............................................103 Жизненный цикл страницы................................................104 Подготовка страницы к выполнению....................................104 Обработка возврата формы............................................107 Завершающий этап выполнения страницы................................108 Заключение.............................................................110 Глава 4 Базовые серверные элементы управления ASP.NET..................112 Серверные элементы управления ASPNET...................................113 Свойства класса Control.............................................114 Методы класса Control...............................................116 События класса Control..............................................117 Нововведения ASPNET 2.0.............................................118 Элементы управления HTML...............................................121 Общая информация об элементах управления HTML.......................122 Контейнерные элементы управления HTML...............................124 Элементы управления inpaf-типа......................................129 Элемент управления Htmllinage.......................................134 Элементы управления Web................................................135 Общая информация об элементах управления Web........................136 Базовые элементы управления Web.....................................138 Разные элементы управления Web......................................145 Валидационные элементы управления......................................149 Общая информация о валидационных элементах управления...............150 Типы валидационных элементов управления.............................152 Дополнительные возможности..........................................156 Заключение.............................................................161
Оглавление VII Глава 5 Работа со страницей............................................163 Программирование форм................................................ 163 Класс HtmIForm......................................................164 Страница с несколькими формами......................................166 Межстраничный постинг...............................................170 Обработка ошибок страниц............................................. 174 Основы обработки ошибок.............................................175 Определение соответствия между ошибками и страницами с сообщениями об ошибках..........................................................179 Трассировка приложений ASP.NET.........................................184 Трассировка потока выполнения.......................................184 Генерирование трассировочных сообщений..............................186 Средство просмотра трассировочных сообщений.........................187 Персонализация страниц.................................................188 Создание пользовательского профиля................................ 189 Работа с пользовательским профилем..................................192 Провайдеры профилей.................................................198 Заключение.............................................................201 Глава 6 Страницы с богатым интерфейсом.................................203 Эталонные страницы.....................................................204 Разработка страниц с богатым интерфейсом в ASPNET 1.x...............204 Создание эталонной страницы.........................................206 Создание страницы контента..........................................209 Обработка эталонной страницы и страниц контента.....................213 Программирование эталонной страницы.................................217 Темы...................................................................219 Понятие темы в ASP.NET..............................................220 Применение тем к страницам и элементам управления...................223 Разработка тем......................................................226 Мастера................................................................230 Обзор возможностей элемента управления Wizard.......................230 Добавление экранов мастера..........................................234 Навигация по экранам мастера........................................237 Заключение.............................................................241 Часть II Размещение данных на сайте Глава 7 Провайдеры данных ADO.NET......................................244 Инфраструктура доступа к данным .NET...................................244 Управляемые провайдеры данных .NET..................................245 Источники данных, доступные посредством ADO.NET.....................248 Модель фабрики провайдеров..........................................251 Подключение к источникам данных........................................254 Класс SqlConnection............................................... 255
VIII Оглавление Строки подключения...................................................260 Пулинг подключений...................................................266 Выполнение команд.......................................................272 Класс SqlCommand.....................................................272 Объекты чтения данных ADO.NET........................................275 Асинхронное выполнение команд........................................280 Выполнение транзакций................................................286 Нововведения, ориентированные на SQL Server 2005.....................291 Заключение..............................................................295 Глава 8 Контейнеры данных ADO.NET.......................................297 Адаптеры данных.........................................................297 Класс SqlDataAdapter.................................................298 Механизм сопоставления таблиц........................................304 Пакетное обновление данных...........................................308 Объекты-контейнеры данных...............................................309 Объект DataSet.......................................................311 Объект DataTable.....................................................317 Связи данных.........................................................322 Объект Data View.....................................................325 Заключение..............................................................327 Глава 9 Модель связывания с данными.....................................329 Связывание элементов управления с данными...............................329 Поддерживаемые источники данных......................................330 Свойства для связывания с данными....................................332 Списочные элементы управления........................................337 Итеративные элементы управления......................................342 Выражения связывания с данными..........................................347 Простейшие случаи связывания с данными...............................347 Класс DataBinder.....................................................350 Другие средства связывания с данными.................................352 Компоненты, представляющие источники данных.............................355 Обзор компонентов, представляющих источники данных...................355 Внутренняя структура компонентов, представляющих источники данных....357 Компонент SqlDataSource..............................................359 Компонент AccessDataSource...........................................365 Компонент ObjectDataSource...........................................3G6 Компонент SiteMapDataSource........................................ "375 Компонент XmlDataSource..............................................379 Заключение..............................................................383 Глава 10 Таблицы, связанные с данными...................................385 Элемент управления DataGrid.............................................385 Объектная модель элемента управления DataGrid........................386 Связывание элемента управления DataGrid с данными....................390 Работа с элементом управления DataGrid...............................394
Оглавление IX Элемент управления GridView.................................................398 Объектная модель элемента управления GridView............................398 Связывание элемента управления GndView с данными.........................403 Переход между страницами данных..........................................412 Сортировка данных........................................................419 Редактирование данных....................................................425 Дополнительные возможности...............................................430 Заключение..................................................................435 Глава 11 Отображение отдельных записей......................................437 Элемент управления Details View.............................................437 Объектная модель элемента управления DetailsView.........................437 Связывание элемента управления Details View с данными....................443 Создание главного и подчиненного представлений ..........................446 Редактирование данных....................................................450 Элемент управления Form View----............................................458 Объектная модель элемента управления Form View...........................458 Связывание элемента управления Form View с данными.......................460 Редактирование данных....................................................463 Заключение..................................................................466 Часть III Инфраструктура ASP.NET Глава 12 Контекст запроса HTTP..............................................468 Инициализация приложения....................................................468 Свойства класса HttpApplication------------------------------------------469 Модули HTTP приложения...................................................469 Методы класса HttpApplication------------------------------------------- 470 События класса HttpApplication-------------------------------------------471 Файл global.asax............................................................474 Компиляция файла global.asax.............................................474 Синтаксис файла global.asax..............................................475 Выявление ошибок и аномалий..............................................478 Класс HttpContext...........................................................479 Свойства класса HttpContext..............................................480 Методы класса HttpContext------------------------------------------------482 Объект Server...............................................................483 Свойства класса HttpServerUtility........................................483 Методы класса HttpServerUtility........................................... 484 Объект Response.............................................................489 Свойства класса HttpResponse.............................................489 Методы класса HttpResponse...............................................493 Объект Request--------------------------------------------------------------495 Свойства класса HttpRequest----------------------------------------------495 Методы класса HttpRequest................................................498 Заключение..................................................................500
X Оглавление Глава 13 Управление состоянием.............................................501 Состояние приложения.......................................................501 Свойства класса HttpApplicationState....................................503 Методы класса HttpApplicationState......................................503 Синхронизация доступа...................................................504 Затраты на управление состоянием приложения.............................505 Состояние сеанса...........................................................506 Модуль HTTP, отвечающий за управление состоянием сеанса.................507 Свойства класса HttpSessionState........................................511 Методы класса HttpSessionState..........................................513 Работа с состоянием сеанса.................................................513 Идентификация сеанса....................................................513 Время жизни сеанса......................................................518 Сохранение состояния сеанса на удаленном сервере........................520 Сохранение данных состояния в таблице SQL Server........................524 Настройка управления состоянием сеанса.....................................529 Создание пользовательского провайдера состояния сеанса..................529 Пользовательский генератор идентификаторов сеанса.......................532 Состояние представления страницы...........................................535 Класс StateBag..........................................................535 Общие вопросы управления состоянием представления.......................536 Разработка Web-форм без использования состояния представ, гения.........539 Изменения, внесенные в управление состоянием представления в ASPNET 2.0.542 Состояние элемента управления...........................................544 Сохранение состояния представления страницы на сервере..................546 Заключение.................................................................550 Глава 14 Кэширование в ASP.NET.............................................551 Кэширование данных приложения..............................................551 Класс Cache.............................................................551 Работа с кэшем ASP.NET..................................................555 Практические вопросы кэширования........................................562 Определение пользовательских зависимостей...............................568 Зависимость кэшируемого элемента от XML-данных..........................570 Зависимость кэшируемого элемента от базы данных SQL Server..............575 Кэширование вывода страниц ASP.NET.........................................582 Директива ©OutputCache..................................................582 Класс HttpCachePolicy...................................................587 Кэширование нескольких версий страницы..................................590 Кэширование частей страницы ASP.NET.....................................593 Продвинутые функции кэширования в ASPNET 2 0............................598 Заключение.................................................................600
Оглавление XI Глава 15 Безопасность в ASP.NET..........................................602 Откуда исходят угрозы....................................................602 Контекст защиты ASP.NET..................................................603 Идентификация пользователя приложения.................................604 Изменение учетной записи процесса.....................................607 Уровни доверия приложений ASP.NET.....................................609 Методы аутентификации ASP.NET.........................................612 Аутентификация Forms.....................................................614 Поток выполнения при аутентификации Forms.............................615 Класс FormsAuthentication.............................................618 Конфигурирование аутентификации Forms.................................620 Продвинутые функции аутентификации Forms..............................624 API управления членством и ролями........................................628 Управление членством..................................................629 Провайдер членства................................................... 634 Управление ролями.....................................................638 Провайдер ролей.......................................................641 Элементы управления, связанные с защитой.................................642 Элемент управления Login..............................................642 Элемент управления LoginName..........................................645 Элемент управления LoginStatus........................................645 Элемент управления Login View.........................................647 Элемент управления PasswordRecovery...................................649 Элемент управления ChangePassword.....................................650 Элемент управления Create UserWizard..................................652 Заключение...............................................................654 Алфавитный указатель.....................................................656
Благодарности Своим появлением эта книга обязана усилиям небольшой, но слаженной команды, членами которой являются Бен Райан (Ben Ryan), Линн Финнел (Lynn Finnel), Кен Скрибнер (Кепп Scribner), Роджер Леблан (Roger LeBlanc) и Роберт Лайон (Robert Lyon). Я от всей души благодарю их за доброту, терпение, точность и аккуратность в работе. Ведь они вычитали, отрецензировали, отредактировали и протестировали весь текст этой книги и весь ее программный код. Весомый вклад в создание книги внесли и многие другие люди, которые помогали сделать ее более качественной, и среди них Джеф Просайз (Jeff Prosise), Фернандо Гуэрро (Fernando Guerrero), Мариус Константинеску (Marius Constantinescu), Марко Беллинасо (Marco Bellinaso), Стив Тауб (Steve Toub). Отдельно хочу поблагодарить Андреа Салтарелло (Andrea Saltarello), который провел много вечеров, переписываясь со мной с помощью программы обмена мгновенными сообщениями, — он помогал мне с особенно сложными примерами и шаблонами кода, приемами программирования, а также с незнакомыми мне функциями Internet Information Server (IIS). Получал я помощь и от множества других людей, но имена большинства из них мне неизвестны — их сообщения я находил в блогах и форумах через Google. Среди тех, кого я не могу не упомянуть здесь с благодарностью, Фриц Аниен (Fritz Onion), Джулия Лерман (Julia Lerman), Шон Фаркас (Shawn Farkas), Скотт Хансельман (Scott Hanselman), Анджел Сейенз-Бадиллос (Angel Saenz-Badillos), Бертран Ле- рой (Bertrand LeRoy), Майк Поуп (Mike Pope), Фредрик Нормен (Fredrik Normen). Существенную поддержку оказали мне Мэтью Гиббс (Matthew Gibbs), Нихиль Котари (Nikhil Kothari) и Стефан Щаков (Stefan Schackow) из команды разработ- чиков ASP.NET — они помогли превратить мои догадки и гипотезы в достоверные утверждения. Благодарю также Скотта Газраи (Scott Guthrie), который удивительно быстро находил ответы на все мои вопросы, несмотря на масштабы работы, которую ему пришлось проделать для воплощения идеи ASP.NET в замечательную реальность. Отдельные фрагменты этой книги ежемесячно публиковались в колонке «Cutting Edge» в MSDN Magazine. Написание подобной книги — долгий процесс, состоящий из множества небольших шагов. Иногда хорошая глава начинается с хорошей статьи, а хорошая статья — с хорошей идеи. Ну а хорошие идеи быстрее приходят в голову, когда в их основе лежат замечательные технологии. Я благодарю Стива Тауба (Steve Toub), Джоша Трапина (Josh Trupm) и всех остальных замечательных ребят из «MSDN Magazine». Было так здорово с вами работать! — Дино Р. S. Поскольку я пишу уже много лет, для моей жены и детей работа над очередной книгой на 1000 страниц — это всего лишь мой повседневный труд. Мои домаш- ние знают, как нужно себя вести, чтобы я мог всецело отдаваться творческому процессу, и за это я им очень признателен.
Введение История Web-разработки насчитывает более десяти лет. Много технологий появи- лось и исчезло за этот период подобно технологии ActiveX Documents, которая как метеор пронеслась по небосклону Web. Однако были и такие технологии, которые, появившись однажды, остались навсегда, и Active Server Pages, увидевшая свет в 1997 году, — из их числа. После ее освоения всем стало ясно, что настоящая разработка приложений для Web возможна только при наличии богатой и мощной серверной программной модели. ASP сделала для Web-разработки примерно то же, что Microsoft Visual Basic сде- лала для разработки Windows-приложений: предоставила средства быстрого и эффек- тивного построения динамических приложений и указала дальнейшее направление развития. После нее одна за другой стали появляться технологии, каждая из которых была основана на предшествующей и устраняла имеющиеся в ней пробелы Наиболее продвинутой и мощной Web-платформой на сегодня является ASP.NET, обладающая богатой функциональностью и предназначенная для разработки распределенных при- ложений, в основе которых лежит транспортный протокол HTTP. Чем больше вы работаете с ASP.NET, тем лучше понимаете, что ей многого еще недостает. Данная платформа упрощает решение множества задач и является насто- ящим раем для тех программистов, которые перешли на нее от классической ASP. Однако ее первая версия, ASP.NET 1.1, лишь подогрела аппетиты сообщества раз- работчиков И после нескольких месяцев работы с ней они уже хотели большего, много большего. Важной вехой на столбовом пути Web-разработки, прелагаемом компанией Microsoft, стала ASP.NET 2.0. В ней не предлагается ни радикально нового подхода к проектированию и написанию кода, ни новой синтаксической модели, тем не менее в нее как в платформу разработки приложений был внесен ряд существенных из- менений и усовершенствований. При создании ASP.NET был принят на вооружение передовой опыт создания программного обеспечения для Web, в ней реализованы готовые решения многих типичных задач, которые до сих пор еще не были авто- матизированы, а предлагаемые данной технологией методы и приемы разработки приложений заслуживают самого пристального внимания со стороны архитекторов и ведущих программистов. Очевидно, что профессиональный разработчик приложений ASP.NET должен быть хорошо знаком с существующими практиками и технологиями создания Web-при- ложений, и в первую очередь с технологиями Microsoft .NET. Настоящая книга по- может вам получить необходимые знания. Она будет вам полезна независимо от того, какую версию ASPNET вы эксплуатируете, поскольку каждая тема рассматривается в ней в нисходящей последовательности: от общих перспектив до деталей реализации в конкретных версиях ASP.NET. Я задумал подробно описать текущее состояние технологий разработки приложе- ний ASP.NET, но обнаружил, что столь глобальные планы требуют создания книги слишком большого объема — порядка 2000 страниц. Поэтому было решено разбить имеющийся материал на два тома: «Programming Microsoft ASP.NET 2.0: Core Refe- rence» и «Programming Microsoft ASPNET 2.0 Applications: Advanced Topics». Те чита-
XIV Введение тели, которые купят обе эти книги, получат исчерпывающее руководство по платформе ASP.NET и связанных с ней технологиях, таких как ADO.NET, технологии разработки приложений для мобильных устройств и технологии создания Web-сервисов. Те же, кто купит только одну из двух книг, смогут изучить либо основы ASP.NET, либо ее продвинутые возможности. В настоящей книге довольно полно и подробно описываются основы разработки приложений ASP.NET. Ее основными темами являются исполняющая среда HTTP, безопасность, кэширование, управление состоянием, создание страниц, использова- ние элементов управления, связывание элементов управления с данными и доступ к данным. Кому адресована книга Сразу хочу отметить: эта книга не для начинающих разработчиков. Если вы имеете лишь слабое представление о том, что собой представляет ASP.NET, или вам требуется введение в эту технологию, то данная книга не для вас. Тем, кто хотел бы получить пошаговые инструкции, я рекомендую обратиться к книге «Microsoft ASP.NET 2.0 Step By Step» Джорджа Шеперда (George Shepherd), выпущенной издательством в Microsoft Press в 2005 году. Но если вам уже знакомы основные принципы програм- мирования в ASP.NET и ее функциональные возможности, но хотелось бы расширить свои знания или, скажем, научиться оптимальным образом применять возможности этой системы, тогда настоящая книга — именно то, что вам нужно. Также хочу сразу предупредить, что вы не найдете здесь таких элементарных вещей, как снимки экранов мастеров Microsoft Visual Studio 2005 или указания относительно того, какой переключатель следует установить, для того чтобы ак- тивизировать либо деактивизировать ту или иную функцию IDE. Конечно, это не означает, что я не люблю Visual Studio 2005 или не рекомендую использовать ее для разработки приложений ASP.NET. Visual Studio 2005 — прекрасное средство, но по отношению к технологии ASP.NET это всего лишь рабочий инструмент. Книга же посвящена именно ASP.NET. Поэтому я предполагаю, что мои читатели уже знакомы с Visual Studio 2005 и ее замечательными возможностями и готовы при- ступить к изучению технологий создания с ее помощью конкретной разновидности приложений. Итак, я рекомендую эту книгу, во-первых, начинающим разработчикам прило- жений ASP.NET, прочитавшим упомянутое выше пошаговое руководство по этой системе или другое аналогичное пособие и освоившим приведенный там материал, а во-вторых, тем разработчикам, которые уже имеют практический опыт создания Web-приложений для ASP.NET и хотят расширить и углубить свои знания. Системные требования Для выполнения приведенных в книге примеров программного кода вам потребуются следующие ресурсы: Microsoft Windows ХР с Service Pack 2, Microsoft Windows Server 2003 c Service Pack 1 либо Microsoft Windows 2000 c Service Pack 4; Microsoft Visual Studio 2005 Standard Edition либо Microsoft Visual Studio 2005 Professional Edition; Internet Information Services (IIS); этот сервер не обязательно использовать для написания и отладки приложений ASP.NET в Visual Studio, но при его наличии вы сможете тестировать демонстрационные приложения в среде, в большей степени приближенной к реальной;
Введение XV Microsoft SQL Server 2005 Express (входит в состав Visual Studio 2005) либо Mi- crosoft SQL Server 2005; база данных Northwind из Microsoft SQL Server 2000, используемая в большинстве примеров этой книги для демонстрации технологий доступа к данным (если у вас установлен SQL Server 2005, можно загрузить инсталляционные сценарии базы данных Northwind с сайта Microsoft по адресу http://www.microsoft.com/downloads/ details.aspx?FamilyId4)6616212-0356-46A0-8DA2-EEBC53A68034&:displaylang=eriy, процессор Pentium 766 МГц или совместимый с ним (рекомендуется Pentium с частотой 1,5 ГГц); RAM объемом 256 Мбайт (рекомендуется 512 Мбайт или более); монитор с разрешением 800 600 или выше, поддерживающий минимум 256 цветов (рекомендуемые параметры: разрешение 1024 768, глубина цвета — 16 бит); привод CD-ROM или DVD-ROM; мышь типа Microsoft Mouse либо совместимая с ней. Конфигурирование SQL Server 2005 Express Edition В главах 5-11 рассматриваются вопросы доступа к данным, и для практического изучения содержащегося в них материала вам потребуется СУБД SQL Server 2005 Express Edition (или SQL Server 2005) с базой данных Northwind Traders. Если у вас установлен SQL Server 2005 Express Edition, войдите в операционную систему своего компьютера с правами администратора и выполните перечисленные ниже действия, чтобы предоставить необходимые разрешения той учетной записи пользователя, от имени которой вы будете запускать демонстрационные приложения. 1. В Windows выберите команду Пуск\Все программы\Стандартные\Командная строка, чтобы открыть окно консоли. 2. В указанном окне введите следующую команду, заменив в ней ваш_сервер именем своего компьютера: sqlcmd -S eaw_cepsep\SQLExpress -Е Чтобы узнать имя компьютера, достаточно выполнить на нем в окне консоли команду hostname (введя ее перед командой sqlcmd). 3. В ответ на приглашение 1> введите следующую команду, включая квадратные скобки: sp_grantlogin [ваш.сервер\имя_пользователя] Замените здесь ваш_сервер именем своего компьютера, а имя_пользователя — име- нем учетной записи Windows, которую вы будете использовать при работе с SQL Server. 4. В ответ на приглашение 2> введите такую команду: до Если увидите сообщение об ошибке, проверьте, правильно ли вы ввели команду sp_grantlogin. 5. На приглашение 1> введите указанную ниже команду, включая квадратные скобки: sp_addsrvrolemember [вай/_сераер\имя_лользоаателя], dbcreator 6. В ответ на приглашение 2> введите команду: до
XVI Введение Если получите сообщение об ошибке, проверьте, правильно ли вы ввели команду spaddsrvrolemember. 7. И наконец, на приглашение 1> введите приведенную ниже команду: exit 8. Закройте окно консоли. Обновления технологий Связанные с данной книгой технологии постоянно обновляются, а ссылки на инфор- мацию об этих обновлениях размещаются на Web-странице Microsoft Press Technology Updates по адресу: http://www.microsoft.com/mspress/updates/ Примеры программного кода Все обсуждаемые в книге примеры кода можно загрузить с ее страницы: http://www.microsoft.com/rnspress/companion/0-7356-2176-4/ Поддержка Было приложено максимум усилий для обеспечения точности приведенной в книге информации и сопутствующих материалов. Кроме того, мы собираем изменения и исправления и добавляем их в статью Microsoft Knowledge Base, которую вы найдете по адресу: http://support.microsoft.com/kb/905045 ] Примечание Когда эта книга еще только писалась, Visual Studio 2005 была доступна лишь в виде выпуска Community Technical Preview (СТР) за август 2005 года — это по- следний из предварительных выпусков данного продукта перед выходом его финальной версии. Предполагается, что материал книги будет полностью соответствовать этой окон- чательной версии Visual Studio 2005, но в случае обнаружения каких-либо расхождений я планирую сообщать о них в указанной статье Microsoft Knowledge Base. Страница поддержки книги на сайте издательства Microsoft Press расположена по адресу: http://www.microsoft.com/learning/support/books/ Вопросы и комментарии Ваши комментарии вопросы и предложения относительно этой книги, а также во- просы, ответов на которые вы не нашли на перечисленных выше сайтах, направляйте, пожалуйста, в Microsoft Press по адресу: mspinput@microsoft.com или же обычной почтой: Microsoft Press Attn: «Programming Microsoft ASP.NET 2.0: Core Reference» Editor One Microsoft Way Redmond, WA 98052-6399 Обращаем ваше внимание: поддержка программных продуктов Microsoft по ука- занным адресам не осуществляется.
Microsoft® ASP.NET 2.0 Базовый курс Овладейте мастерством программирования для ASP.NET Эта книга призвана помочь профессионалам в решении любых задач, связанных с разработкой сложных приложений. Она позволит вам изучить тонкости разработки масштабируемых динамичных веб-приложений, создаваемых с использованием Microsoft Visual С#"’для выполнения в среде ASP.NET 2.0. Вы узнаете, как: • разрабатывать страницы с разнообразным согласованным интерфейсом, централизованно определять их структуру и оформление, используя темы и эталонные страницы; • создавать персонализированные страницы, запоминающие предпочтения пользователей; • извлекать, обрабатывать и модифицировать данные посредством ADO.NET; • конфигурировать конвейер обработки запросов на предоставление страниц ASP.NET 2.0; • контролировать поток выполнения приложения; • оптимизировать производительность приложения; • управлять доступом к приложению и его ресурсам; • формировать слой доступа к данным; • связывать элементы управления, представляющие источники данных, с пользовательскими коллекциями данных; • эффективно работать с табличными элементами управления. Об авторе Дино Эспозито — известный специалист по ASP.NET и ADO.NET, работает в компании Solid Quality Learning, занимающей одну из лидирующих позиций в области тренинга и консалтинга по вопросам, связанным с технологиями Microsoft Он — автор многочисленных статей, опубликованных в журнале MSDN Magazine и других периодических изданиях. Его перу принадлежат также несколько книг, выпущенных издательством Microsoft Press, в том числе Microsoft ASP.NET2.0: углубленное изучение- (вышедшая на русском языке в начале 2006 г.). Издательский дом Питер Санкт-Петербург Б Сампсониевский пр., 29а E-mai sites pneroom Internet www.piter.com Тел факс. 1812) 703-7383 Издательство Русская Редакция Москва Шелепихинская наб., 32 E-mai< info ° rusedrt com Internet wwwnjsedit.com тел факс .495) 256-7145 Microsoft