This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
, ,
, ячеек строки заголовка – | . Исходя из этого, создадим стиль для обычных ячеек таблицы. Для этого необходимо выполнить следующие операции. 1. Щелкнуть правой кнопкой внутри окна редактора стилей, открывшемся контекстном меню выбрать пункт Add Style Rule. 2. В окне Add Style Rule выбрать переключатель Class name и ввести имя уже существующего класса mytable, после чего добавить его к иерархии стилей. 3. Выбрать переключатель Element и в активизировавшемся выпадающем списке выбрать тэг, отвечающий за описание обычных ячеек – | , после чего добавить его к иерархии стилей, как показано на рис. 5.19. Нажать OK. Рис. 5.19. Добавление тэга к существующему элементу. В результате этой операции в файл стиля будет добавлен стиль .mytable TD { }. 4. Щелкнуть правой кнопкой мыши внутри только что созданного стиля в выбрать пункт Build Style. 5. В окне Style Builder установить параметры Borders в разделе Edges аналогично параметрам, установленным ранее для класса mytable. В результате проделанных действий будет получена таблица, внешний вид которой аналогичен предыдущей. Более того, теперь для каждой вновь создаваемой таблицы можно настроить такое же оформление просто указав в ее свойстве CssClass значение mytable. При необходимости что-то поменять в 110 оформлении таблицы, необходимо изменить соответствующие настройки стилей. Аналогичным образом, добавив соответствующий стиль для строки заголовка таблицы и установив свойство TableSection=TableHeader первой строки таблицы, получим результат, изображенный на рис. 5.20. Рис. 5.20. Внешний вид таблицы после применения стиля для заголовка. Добавленный стиль выглядит следующим образом. .mytable THEAD { font-weight: bold; color: white; font-family: Arial; background-color: black; } Иногда возникает необходимость изменять содержимое таблицы на основе производимых пользователем действий. В этом случае обойтись статическими элементами, вводимыми в таблицу на этапе разработки приложения невозможно. Решить данную задачу можно воспользовавшись механизмом добавления элементов в таблицу во время выполнения. Здесь следует отметить, что элемент управления Table не сохраняет те элементы, которые добавляются в него во время выполнения приложения. Это приводит к тому, что таблицу необходимо заново создавать на основе данных, сохраненных в переменной состояния. В следующем примере на странице расположена таблица, у которой установлена строка заголовка и первая строка содержимого. Пользователь имеет возможность добавлять новые строки. Для этого он должен внести содержимое новой строки в поле ввода, разделяя значения ячеек запятыми. После нажатия на кнопку Добавить новая строка добавляется к уже 111 существующим строкам таблицы. Исходный код страницы с комментариями представлен ниже. public partial class _Default : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { } protected void btn_Add_Click(object sender, EventArgs e) { //Записать текст в состояние отображения страниы ViewState.Add(ViewState.Count.ToString(), tb_TableRow.Text); CreateTable(); } private void CreateTable() { //массив слов string[] arrWords; //строка, состоящая из значений ячеек строки таблицы string strWords; TableRow newRow; TableCell newCell; //цикл для каждого элемента ViewState for (int i = 0; i < ViewState.Count;i++) { //символ-разделитель значений полей таблицы char[] c ={','}; //создать новую строку newRow = new TableRow(); //извлечь из ViewState очередной элемент strWords = ViewState[i.ToString()].ToString(); //преобразовать строку в массив используя разделитель arrWords = strWords.Split(c); //цикл для каждого элемента массива слов for (int j = 0; j порошок стиральный отбеливатель Здесь было использовано выражение привязки данных, которое располагается между ограничителями . В качестве выражения привязки данных не допускается использование программного кода, это может быть значение свойства, переменная-член или функция, либо любое другое выражение, которое может быть вычислено во время выполнения. В данном случае, в качестве выражения привязки данных указана переменная listData, являющаяся именем созданного ранее массива строк. Теперь, в момент запуска приложения, элемент DropDownList будет заполнен значениями элементов массива. ASP.NET предоставляет много других полезных элементов управления, реализующих самые различные задачи. Рассмотрим самые основные из них, использование которых необходимо практически в каждом Web приложении. Остальные будут рассматриваться в следующих частях данного курса. Использование элементов CheckBox, CheckBoxList, RadioButton, RadioButtonList и BulletedList В ряде случаев необходимо принимать от пользователя булевые значения, для этих целей применяются элементы CheckBox, CheckBoxList, RadioButton и RadioButtonList. Элементы CheckBox и RadioButton отличаются тем, что CheckBox позволяет устанавливать значения одного или нескольких флажков одновременно, в то время как RadioButton – только одно значение. Окончание List говорит о том, что данный элемент управления представляет собой целый набор флажков, объединенных в группу. Группы флажков легче привязывать к источникам данных, поэтому их целесообразнее использовать в случае, если флажки должны отображать данные, находящиеся в БД, либо с помощью них данные должны вводиться в базу данных, либо если флажки логически связаны между собой. Для получения значений элементов управления CheckBox и RadioButton необходимо использовать свойство Checked. Например, в следующем коде осуществляется проверка значения флажка, которое выводится на экран. protected void Button1_Click(object sender, EventArgs e) { Response.Write("Флажок установлен в значение "+CheckBox1.Checked.ToString()); } 118 При необходимости использования элементов RadioButton, необходимо установить одинаковые значения свойства GroupName элементов, которым необходимо взаимодействовать друг с другом. В противном случае, все элементы будут работать независимо друг от друга. Для получения и установки значений элементов управления RadioButtonList и CheckBoxList необходимо последовательно опрашивать все элементы управления списка проверяя их значения. Для этого очень удобно использовать цикл For Each. В следующем примере последовательно проверяются значения всех флажков элемента CheckBoxList1. Если флажок выбран, на экран выводится соответствующее сообщение. protected void Button1_Click(object sender, EventArgs e) { foreach (ListItem l in CheckBoxList1.Items) { if (l.Selected) Response.Write("выбран " + l.Text + " "); } } Следует обратить внимание на тот факт, что элемент управления CheckBoxList, как и RadioButtonList содержит элементы управления ListControl, а не CheckBox или RadioButton, как можно было бы предположить. Аналогичным образом производятся проверки и для элемента RadioButtonList. BulletedList является аналогом HTML элементов, предназначенных для организации упорядоченных и неупорядоченных списков с помощью тэгов
BulletedStyleImageUrl Позволяет задавать тип списка numbered – нумерованный, loweralpha – маленькие латинские буквы, upperalpha – большие латинские буквы, lowerroman – маленькие римские цифры, upperroman – большие римские цифры, disc, circle, square – символы маркеров Устанавливает изображение, расположенное слева от каждого элемента списка. Для установки изображения необходимо, чтобы значение BulletedStyle= CustomImage FirstBulletNumber Устанавливает значение для первого элемента списка, 119 DisplayMode относительно которого в дальнейшем будет продолжена нумерация Устанавливает тип элемента списка (Text, HyperLink, LinkButton) При использовании LinkButton в качестве элемента списка становится возможным программное определение того элемента, по которому был произведен щелчок пользователем, как показано в следующем примере. protected void BulletedList1_Click(object sender, BulletedListEventArgs e) { Response.Write("Вы щелкнули по элементу списка "+BulletedList1.Items[e.Index].ToString()); } Использование Image, ImageMap, ImageButton Отображение графики на Web странице является абсолютно необходимым условием. ASP.NET предоставляет несколько возможностей отображения графики. 1. Как фон, заполняющий всю страницу, для этого можно воспользоваться свойством Background Web формы. Можно также использовать свойство BackImageUrl элемента Panel. 2. На переднем плане. Для этого используется элемент управления Image. 3. На кнопке, способной реагировать на действия пользователя. Для этого используется элемент управления ImageButton. 4. С помощью карты изображений, реализуемой посредством элемента управления ImageMap. Элемент управления Image позволяет отображать графические изображения. При этом он неспособен реагировать на действия пользователя, однако способен отображать графические элементы в зависимости от действий, выполненных над другими элементами управления. В следующем примере при нажатии на соответствующую кнопку отображается изображение обезьяны, лося либо кошки (Рис. 5.28). protected void btn_Monkey_Click(object sender, EventArgs e) { Image1.ImageUrl = "~//Images//monkey.jpg"; } protected void btn_Elk_Click(object sender, EventArgs e) { Image1.ImageUrl = "~//Images//elk.jpg"; } protected void btn_Cat_Click(object sender, EventArgs e) { Image1.ImageUrl = "~//Images//cat.jpg"; } 120 Рис. 5.28. Смена выводимого на экран изображения при нажатии на соответствующую кнопку. Элемент управления ImageButton имеет встроенную возможность реагирования на события, совершаемые пользователем. При этом, в обработчик события щелчка левой кнопкой мыши по нему отправляется специальный объект ImageClickEventArgs, предоставляющий свойства X и Y, которые определяют место изображения, на котором был совершен щелчок. Используя эти свойства можно создать карту изображения. Ниже показан код, который отображает координаты точки, в которой был произведен щелчок мыши, а также определяет в пределах какой фигуры он был произведен. Результат показан на рис. 5.29. protected void ImageButton1_Click(object sender, ImageClickEventArgs e) { string strClickFigure=""; if (!(e.X = 20 && e.Y = 20)) strClickFigure = "рамка"; if (e.X>=60 && e.X=60 && e.Y=495-e.X) && (e.Y>=e.X-45) && e.Y, Panel – внутри и т.д. Размещение элементов внутри тэгов создает возможность для поддержки ими стилей оформления, Literal же, не имеет такой возможности. Рекомендуется использовать элемент Literal в том случае, когда необходимо размещать текст на странице без использования дополнительной разметки, либо в том случае, когда существует HTML текст (например, хранящийся в файле или базе данных), содержащий форматирование с использованием тэгов, и его необходимо вывести на экран. Самым важным свойством элемента управления Literal является Mode. Возможны три значения этого свойства. 125 Transform PassThrough Encode Любая разметка, добавляемая к элементу управления преобразуется таким образом, чтобы максимально удовлетворять особенностям протокола браузера, запрашивающего страницу. Добавляемая к элементу управления разметка передается браузеру как есть, без каких-либо модификаций. Добавляемая к элементу управления разметка декодируется с использованием метода HtmlEncode, преобразующего HTML представление страницы в текст. Например, при использовании следующего примера элемента Literal могут быть получены различные результаты. При использовании значения Mode=PassThrough, как показано в следующем фрагменте исходного кода private string s = "
будет получен результат, изображенный на рис. 5.33. Рис. 5.33. Использование Literal для отображения информации в режиме PassThrough. А при использовании режима Encode – следующий. 126 Рис. 5.34. Использование Literal для отображения информации в режиме Encode. Элемент управления HiddenField целесообразно использовать в том случае, когда на странице необходимо сохранить какие-то данные, но не отображать их. Например, это может быть какая-нибудь информация о пользователе, которая необходима для выполнения сервисных функций. У элемента HiddenField существует лишь одно свойство, заслуживающее внимания. Это Value, посредством которого ему и присваивается значение. При использовании элемента управления HiddenField необходимо помнить о безопасности, т.к., несмотря на то, что информация, хранящаяся в данном элементе не отображается на экране, ее легко можно увидеть просмотрев исходный код страницы. Более того, пользователь может изменить данную информацию перед инициированием postback и отправкой ее на сервер. Поэтому не рекомендуется хранить какие-либо конфиденциальные данные, а также данные, критичные для выполнения приложения на сервере в данном элементе управления. Использование Panel Элемент управления Panel используется для группирования элементов управления. При этом он выступает в качестве контейнера, способного вмещать в себя различные другие элементы управления, манипулирование которыми становится возможным как единым целым. Например, возможно скрытие или показ элемента Panel со всеми входящими в него элементами путем задания значения свойства Visible равным true или false. При размещении внутри Panel элементов для ввода текста и кнопок, становится возможным определение кнопки, используемой по умолчанию. Это означает, что при вводе текста в элементы управления, размещенные внутри Panel пользователь имеет возможность нажать кнопку Enter, что будет равносильно щелчку левой кнопки мыши по кнопке, указанной в свойстве DefaultButton элемента Panel. Это позволяет создавать более эффективные пользовательские формы для ввода данных. Panel облегчает размещение динамически создаваемых элементов управления на форме. И, наконец, с помощью этого элемента управления становится возможным создание областей в рамках страницы, обладающих 127 собственными свойствами поведения и внешнего вида. Это выражается в следующем. 1. С помощью Panel можно создать область, содержащую полосы прокрутки. Для этого достаточно установить соответствующее значение свойства ScrollBars, а также установить необходимые значения свойств Height и Width. 2. С помощью Panel можно создать область группы с заголовком. Для этого необходимо ввести в свойство GroupingText строку заголовка группы. Пример изображения группы, получаемой в этом случае приведен на рис. 5.35. Рис. 5.35. Пример изображения группы элементов. Следует отметить, что при установки свойства GroupingText становится невозможным использование полос прокрутки. 3. С помощью Panel можно создать область страницы, имеющей отдельно заданные свойства, такие как цвет фона, рамка и т.д. В следующем примере, создается простейшая форма авторизации пользователя на сайте. При вводе имени пользователя и пароля, а также щелчка по кнопке Войти (либо нажатии Enter), происходит проверка введенных данных с хранящимися в коде программы. В случае, если данные совпадают, панель, содержащая элементы управления скрывается, а вместо нее выводится приветственное сообщение. Исходный код страницы выглядит следующим образом. 128 Исходный код процедуры нажатия на кнопку, осуществляющий проверку введенных данных, представлен ниже. protected void btn_Enter_Click(object sender, EventArgs e) { if (tb_Name.Text == "user" && tb_Password.Text == "12345") { Panel1.Visible = false; Label1.Text = tb_Name.Text + " успешно вошел в систему."; } else { Label1.Text = "Неверное имя или пароль"; Label1.ForeColor = System.Drawing.Color.Red; tb_Name.Text = ""; } } Использование LinkButton Элемент управления LinkButton представляет собой кнопку, которая выглядит как гиперссылка, но имеет поведение кнопки. Этот элемент управления внедряет в HTML код страницы элементы JavaScript, необходимые для обработки событий кнопки, поэтому необходимо, чтобы клиентский браузер поддерживал JavaScript. LinkButton может быть двух разновидностей: командная кнопка и кнопка перенаправления. Кнопка перенаправления не содержит сопоставленного с ней обработчика события и просто инициирует событие postback. Командная кнопка ведет себя как обычная кнопка и может иметь несколько обработчиков событий, сопоставленных с ней. Как и обычная кнопка, она может реагировать на щелчок левой кнопки мыши с помощью события LinkButton_Click, кроме того, возможна обработка события LinkButton_Command. В качестве аргументов, обработчику события Command передаются значения свойств CommandName и CommandArgument. С помощью этого события становится возможным определение того, какая из нескольких, расположенных на странице кнопок была нажата. В следующем примере на странице размещены два элемента LinkButton. Открыть Перейти Процедура LinkButton_Command выводит на экран сообщение, в котором указаны аргументы, передаваемые обеими кнопками в случае их нажатия. protected void LinkButton_Command(object sender, CommandEventArgs e) { Response.Write("Команда: " + e.CommandName + ", параметр: " + e.CommandArgument); } 129 Проверка вводимых данных Подавляющее большинство страниц Web приложений используются для ввода данных пользователем, которые затем могут сохраняться во вспомогательных файлах, базе данных и т.п. Одним из важнейших этапов получения данных от пользователя является их проверка, необходимая для того, чтобы исключить бесполезную, неинформативную, либо противоречивую информацию. Критерии проверки могут быть самыми разными, начиная с того, вводились ли данные вообще и заканчивая проверкой типа данных. В идеале проверка вводимых данных должна осуществляться на стороне клиента, т.к. в этом случае он может сразу же, еще до отправки данных на сервер, получить уведомление о содержащихся ошибках. Однако, независимо от того, осуществлялась ли проверка вводимых данных на стороне клиента, необходимо осуществлять проверку и на стороне сервера. Клиентская проверка вводимых данных обычно осуществляется за счет использования программного кода, написанного на языке JavaScript, при этом возникает ряд сложностей, которые связаны с различием клиентского программирования и серверного. В ASP.NET реализован целый ряд элементов управления, предназначенных для проверки вводимых данных, так называемых верификаторов. Эти элементы можно привязать к любому элементу управления вводом. После привязки, верификатор выполняет автоматическую клиентскую и серверную проверку вводимых данных. Если данные, вводимые в элемент ввода данных, не удовлетворяют условию верификатора, последний препятствует отправке страницы на сервер. Всего в ASP.NET 2.0 существует шесть элементов управления, предназначенных для осуществления проверки вводимых данных. RequiredFieldValidator RangeValidator RegularExpressionValidator CompareValidator CustomValidator ValidationSummary Контролирует наличие введенных данных в элемент управления Проверяет нахождение значения элемента управления в пределах заданного диапазона Определяет соответствие значения данного элемента управления определенному регулярному выражению Сравнивает значение текущего элемента управления с константой или значением другого элемента управления Выполняет заданную операцию проверки достоверности на стороне клиента, либо на стороне сервера для реализации собственной логики проверки вводимых данных Отображает информацию на странице, либо во всплывающем окне с сообщениями об ошибках 130 для каждого элемента управления, проверка которого завершилась ошибкой Допускается использование нескольких элементов управления проверкой ввода данных, связанных с одним элементом ввода данных. С помощью верификаторов возможно проверить вводимые данные в такие элементы управления как TextBox, ListBox, DropDownList, RadioButtonList, HtmlInputText, HtmlTextArea, HtmlSelect. При использовании валидаторов возможно использование автоматической проверки страницы при ее отправке не сервер, или же вручную в коде. Наиболее распространен первый подход, при котором пользователь видит обычную страницу с элементами ввода данных, заполняет их и инициирует отправку страницы на сервер. Если отправка инициируется нажатием на кнопку (что бывает в большинстве случаев), проверка введенных данных зависит от значения свойства CausesValidation кнопки. 1. Если свойство CausesValidation=false, элементы управления проверкой вводимых данных просто игнорируются, а страница отправляется на сервер. При этом, все обработчики событий будут исполнены. 2. Если свойство CausesValidation=true, осуществляется автоматическая проверка каждого элемента управления, расположенного на странице. Если при проверке элемента управления возникает ошибка, ASP.NET возвращает страницу с сообщениями об ошибке в зависимости от настроек верификаторов. При этом обработчик события нажатия на кнопке, инициировавшей отправку страницы на сервер может выполниться, но может и не выполниться. Для проверки этого необходимо осуществить проверку достоверности страницы. Проверка вводимых данных осуществляется при щелчке пользователя на определенных кнопках, например, при возникновении события изменения значения текущего элемента списка, такого не происходит. Тем не менее, верификаторы добавляют код для проверки вводимых данных на стороне клиента. В этом случае, практически вся проверка может осуществляться на стороне клиента, что для клиента будет выглядеть идентично результатам проверки данных на стороне сервера. Тем не менее, при успешной проверке данных на стороне клиента, на стороне сервера подобная проверка будет осуществлена повторно. Все валидаторы образованы от класса BaseValidator и имеют следующие основные общие для всех свойства. ControlToValidate Display Содержит идентификатор элемента управления ввода данных, подлежащего проверке. Определяет способ отображения сообщения об ошибке. Значение static означает, что при создании страницы 131 Enabled EnableClientScript ErrorMessage Text IsValid SetFocusOnError ValidationGroup будет заранее подсчитано и зарезервировано место на странице, необходимое для отображения сообщения об ошибке. Значение dynamic позволяет изменять страницу в процессе выполнения, добавляя сообщение об ошибке при возникновении необходимости. Определяет состояние валидатора. При значении false элемент является отключенным и не осуществляет проверку вводимых данных. Определяет будет ли выполняться проверка вводимых данных на стороне клиента. Содержит строку ошибки, которая будет отображаться в итоговой информации об ошибках элемента ValidationSummary. Содержит строку текста ошибки, отображаемой валидатором в случае возникновения ошибки. Содержит значение текущего результата проверки ввода данных. Это свойство имеет смысл проверять только в том случае, если проверка на стороне клиента не производилась. Позволяет установить фокус ввода на тот элемент управления, который вызвал ошибку при проверке вводимых данных. Для включения этого режима необходимо установить значение свойства равным true. Определяет группу, в которую возможно объединение нескольких валидаторов для выполнения независимой проверки. Самым простым валидатором является RequiredFieldValidator. Он предназначен для проверки того введены ли данные в связанное с ним поле или нет. Это значит, что если в соответствующем текстовом поле ввода не будет символов отличных от пробелов, возникнет ошибка ввода. В качестве демонстрации работы данного валидатора создадим следующую страницу (Рис. 5.36). 132 RequiredFieldValidator Рис. 5.36. Пример страницы, содержащей RequiredFieldValidator. Исходный код этой страницы выглядит следующим образом. * Результат выполнения данной страницы изображен на рис. 5.37. Рис. 5.37. Отображение страницы, содержащей валидатор в окне браузера. Если при нажатии на кнопку OK поле ввода имени пользователя будет пустым, рядом с полем ввода появится символ красной звездочки. Нетрудно заметить, что при этом, обратная отсылка данных на сервер не производится. 133 При необходимости можно изменить текст, который будет выводиться при возникновении ошибки. Для этого достаточно изменить строку свойства Text валидатора. Элемент управления RangeValidator используется для проверки попадания введенного значения в диапазон значений, валидатора. Основными свойствами данного валидатора являются MinimumValue, MaximumValue и Type. Первые два определяют диапазон допустимых значений, type – тип данных, вводимых в элемент управления. В качестве примера, добавим к предыдущей странице элемент ввода данных о возрасте пользователя, который должен быть в пределах от 18 до 70 лет, а также верификатор RangeValidator, который будет контролировать значение вводимого числа. Исходный код фрагмента страницы, содержащего необходимые элементы управления, а также валидатор представлены ниже. * Валидатор CompareValidator сравнивает значение в одном элементе управления с фиксированным значением, либо со значением в другом элементе управления. Основными свойствами данного валидатора являются ValueToCompare и ControlToCompare, первый из которых задает элемент управления, значение которого должно контролироваться, второе – элемент управления со значением которого будет сравниваться значение элемента управления, указанного в ValueToCompare. Кроме того, свойство Operator определяет тип операции сравнения. В следующем примере приведен фрагмент страницы, добавленный к предыдущей, в котором осуществляется проверка правильности введенного пароля – необходимо, чтобы введенные пароли совпадали. * Стоит отметить, что у валидаторов существует возможность использование графических объектов вместо текста, выводимых на экран в случае возникновения ошибки. Так, в данном примере возможно использование изображения вместо символа звездочка. Для этого 134 необходимо ввести в свойство Text валидатора тэг , с указанием графического файла, который необходимо отображать в случае возникновения ошибки, как показано в примере ниже. Результат показан на рис. 5.38. Рис. 5.38. Использование графических объектов совместно с валидаторами. Валидатор RegularExpressionValidator позволяет сравнить вводимый текст с шаблоном, определенном в регулярном выражении, которое устанавливается в свойстве ValidationExpression. Регулярные выражения позволяют определять правила, задающие количество символов и их положение в строке. Наиболее часто используемые конструкции для регулярных выражений перечислены ниже. Escape последовательности символов, применяемые в регулярных выражениях приведены ниже. Обычные символы \b \t \r \v \f \n \e \ Все символы кроме .$^{[(|)*+?\ соответствуют себе Соответствует символу возврат на одну позицию (backspace) Соответствует табуляции (tab) Соответствует переводу строки (return) Соответствует вертикальной табуляции Соответствует переводу страницы Соответствует новой строке Соответствует символу escape Если следующий символ не распознается как escape последовательность, соответствует самому символу. Например \+ соответствует + 135 Классы символов, определяющие множество символов представлены ниже. . [aeiou] [^aeiou] [0-9a-fA-F] \p{имя} Соответствует любому символу за исключением \n Соответствует каждому отдельному символу, входящему в множество Соответствует каждому отдельному символу, не входящему в множество Соответствует любому символу, приведенному в заданных диапазонах (0–9;a–f;A–F) Соответствует любому символу, входящему в именованный набор символов, заданный с помощью {имя}. Примерами именованных последовательностей символов являются Ll, Nd, Z, IsGreek, IsBoxDrawing \P{имя} \w \W \s \S \d \D Соответствует любому символу, не входящему в именованный набор символов, заданный с помощью {имя} Соответствует любому алфавитно-цифровому символу из слова, то есть любому алфавитно-цифровому символу или символу подчеркивания Соответствует любому символу, не являющимся алфавитноцифровым, либо символом подчеркивания Соответствует любому символу разделителя (пробел, перевод страницы, табуляция, перевод строки, новая строка и т.д.) Соответствует любому непробельному символу Соответствует любому десятичному символу Соответствует любому недесятичному символу Квантификаторы, задающие шаблоны совпадений, перечислены ниже. * + ? {n} {n,} {n,m} Ноль или более совпадений Одно или более совпадений Ноль или одно совпадение n совпадений n или более совпадений Количество совпадений от n до m За более подробной информацией о регулярных выражениях можно обратиться к справочной системе MSDN, произведя поиск по ключевым словам Regular Expression. Ниже приведена таблица, содержащая примеры шаблонов наиболее популярных регулярных выражений. На сайте http://RegexLib.com можно найти гораздо больше примеров регулярных выражений. 136 \S+@\S+\.\S+ \w+ \w{4,10} [a-zA-Z]\w{3,9} [a-zA-Z]\w*\d+\w* \S{4,10} \d{3}-\d{2}-\d{4} ^\d{5}$ ^(\d{5})|(\d{5}-\d{4}$ ^(\d{5}(-\d{4})?$ ^[+-]?\d+(\.\d+)?$ ^[+-]?\d*\.?\d*$ ^(20|21|22|23|[01]\d)[0-5]\d$ /\*.*\*/ Адрес электронной почты (наличие знака @, точки и непробельных символов). Пароль (любая последовательность словесных символов). Пароль, минимальная длина которого должна составлять 4 символа, максимальная – 10. Пароль, длина которого должна быть в пределах от 4 до 10 символов, но при этом первый символ должен быть в диапазоне букв a-z, либо A-Z. Пароль, который начинается в буквы, затем следуют словесные символы, количество которых может быть от 0 до бесконечности, затем должна следовать цифра, после чего опять словесные символы, количество которых может быть от 0 до бесконечности. Строка, содержащая от 4 до 10 непробельных символов (в отличие от \w{4,10} допускаются спецсимволы, такие, например, как звездочки, амперсанды и т.п.) Последовательность групп из трех, двух, а затем четырех цифр, разделенных дефисами. Число, состоящее из пяти знаков. Число, состоящее из пяти знаков, либо число состоящее из пяти и еще четырех знаков, разделенных тире. Более эффективный аналог предыдущего примера. Символ ? используется для указания того факта, что четыре знака, отделенные с помощью тире являются необязательными. Любое действительное число, содержащее знак Matches any real number with optional sign. Аналогично предыдущему, но этому шаблону также может соответствовать пустая строка. Соответствует любому значению времени в формате 24 часов. Соответствует содержимому комментария в стиле языка C /* … */ В качестве примера, добавим в предыдущую страницу поле для ввода адреса электронной почты, а также валидатор, проверяющий корректность его формата. Исходный код фрагмента страницы, содержащего элементы для ввода e-mail, а также валидатор приведены ниже. * CustomValidator позволяет выполнять Элемент управления пользовательские процедуры проверки достоверности на стороне клиента и сервера. Это целесообразно в случае, когда возможности описанных выше валидаторов оказываются недостаточными. CustomValidator можно связать с элементом управления, предназначенным для ввода текста, как и в случае с другими валидаторами. Если проверка вводимых данных завершается неудачей, свойство Page.IsValid устанавливается в значение false (тоже самое происходит и при проверке с помощью другого валидатора). Проверка вводимых данных с помощью CustomValidator происходит в специально создаваемых процедурах, которые могут быть реализованы на стороне клиента и на стороне сервера. При этом, данные процедуры имеют одинаковое определение – они должны принимать два параметра: ссылку на элемент управления проверкой достоверности и целевой объект, содержащий проверяемое значение, доступное через свойство Value, а также свойство IsValid, с помощью которого возможно определить значение результата проверки. Предположим, при вводе данных о пользователе, последнему необходимо ввести свою зарплату на последнем месте работы. Предположим также, что величина зарплаты должны быть кратна 10. Подобную проверку можно реализовать следующим образом. Сначала необходимо добавить те элементы управления, которые будут использоваться для ввода данных, а также элемент CustomValidator. Исходный код этих элементов показан ниже. * В качестве значений свойств ClientValidationFunction и ControlToValidate установлены соответственно значения SalaryCheck и tb_Salary. Первое соответствует имени функции, осуществляющей проверку вводимых данных, второе – ID элемента управления, осуществляющего ввод данных. Функция SalaryCheck может быть написана на языке JavaScript следующим образом. <script language="javascript"> function SalaryCheck(vc,control) { control.IsValid=(control.Value%10==0); } 138 При успешной проверке функцией SalaryCheck вводимых данных, страница отправляется на сервер и там запускается событие ServerValidate элемента CustomValidator. Это событие можно обработать, реализовав исходный код на языке C# или VB.NET. Серверная обработка вводимых данных должна осуществляться всегда, независимо от существования проверок на стороне клиента. Для реализации серверной обработки вводимых данных о зарплате в рассматриваемом примере, необходимо добавить обработчик события ServerValidate элемента CustomValidator. Ниже приведен возможный код такого обработчика. protected void CustomValidator1_ServerValidate(object source, ServerValidateEventArgs args) { try { args.IsValid = int.Parse(args.Value) % 10 == 0; } catch { args.IsValid = false; } } Для отображения итоговой информации о результатах всех проверок вводимой информации, необходимо использовать элемент управления ValidationSummary. Он выводит значение ErrorMessage каждого валидатора, для которого эта проверка завершилась неудачей. Итоговая информация может отображаться либо на странице, либо в отдельном окне. Для указания данного режима необходимо установить свойства ShowMessageBox и ShowSummary. При значении свойства ShowMessageBox=true, сообщение выводится в отдельном окне, если же значение свойства ShowSummary=true – на странице. При отображении итоговой информации на странице возможно установить некоторые дополнительные параметры с помощью свойства DisplayMode, а также задать заголовок для итоговой информации с помощью свойства HeaderText. Пример использования элемента управления ValidationSummary на основе созданной ранее страницы приведен ниже. Исходный код элемента ValidationSummary выглядит следующим образом. В результате использования данного элемента, ошибки на странице выводятся как показано на рис. 5.39. 139 Рис. 5.39. Пример использования элемента управления ValidationSummary. Доступ к верификаторам возможен и из программного кода. Все валидаторы, при их размещении на странице помещаются в коллекцию Validators. Для последовательного обращения ко всем валидаторам можно организовать циклический перебор элементов этой коллекции. Это можно продемонстрировать на следующем примере. Добавим на страницу четыре флажка, с помощью которых будет изменять текущие свойства валидаторов, отвечающие за проверку вводимых данных. Результат показан на рис. 5.40. Рис. 5.40. Пример страницы, содержащей элементы управления, предназначенные для изменения режимов работы валидаторов. 140 Для работы данного примера, необходимо создать обработчик события изменения значения флажка и ввести в него следующий код. protected void cb_CheckedChanged(object sender, EventArgs e) { foreach (BaseValidator v in Page.Validators) { v.Enabled = cb_EnableValidators.Checked; v.EnableClientScript = cb_ClientValidation.Checked; } ValidationSummary.ShowSummary = cb_ShowSummary.Checked; ValidationSummary.ShowMessageBox = cb_ShowMessageBox.Checked; } При реализации механизмов проверки вводимых данных возможно создать пользовательскую проверку. Для этого необходимо установить значение свойства CausesValidation кнопки, инициирующей метод postback равным false, затем создать обработчик события нажатия на эту кнопку, внутри которого и реализовать алгоритм проверки вводимых данных. Для того, чтобы инициировать проверку введенных данных с помощью присутствующих на странице валидаторов, необходимо вызвать метод Validate() страницы. Данный метод устанавливает свойство IsValid страницы на основе данных валидаторов. По значению данного метода можно судить о существовании ошибок на странице. В качестве примера создадим пользовательскую проверку вводимых данных на странице. Для этого установим свойство CausesValidation кнопки OK равным false. В обработчике события нажатия этой кнопки необходимо ввести следующий код. protected void btn_OK_Click(object sender, EventArgs e) { this.Validate(); if (!this.IsValid) { string msg = "В результате заполнения полей возникли следующие ошибки: "; //Перебор валидаторов TextBox tb; foreach (BaseValidator bv in this.Validators) { if (!bv.IsValid) { msg += bv.ErrorMessage + " "; tb = (TextBox)this.FindControl(bv.ControlToValidate); msg += "Ошибка ввода в следующем значении:"; msg += tb.Text+" "; tb.BackColor = System.Drawing.Color.Pink; } } lbl_Error.Text = msg; } } В результате, при вводе данных и нажатии на кнопку OK будет выполнена проверка, в результате которой будет сформирована строка 141 сообщения об ошибках с указанием расшифровки ошибки, а также значения поля ввода, которое привело к возникновению этой ошибки. Кроме того, поле ввода, содержащее ошибку, будет выделено розовым цветом. Пример результатов этой проверки приведен на рис. 5.41. Выделение цветом поля, содержащего ошибку валидации Рис. 5.41. Выделение цветом полей ввода, содержащих ошибку валидации. Группировка элементов проверки ввода данных На сложных страницах ввода данных может существовать много элементов для ввода данных. В зависимости от их назначения, бывает удобно объединить их в логические группы, в которых была бы реализована своя логика проверки данных. Для этого целесообразно сгруппировать валидаторы. Это бывает целесообразно в случае, когда на странице расположено несколько панелей, внутри которых находятся отдельные поля ввода данных и кнопки, инициирующие событие postback. В таких ситуациях необходимо, чтобы внутри каждой панели реализовывалась отдельная логика проверки вводимых данных. В ASP.NET такое возможно благодаря группам проверки. Для создания группы проверки вводимых данных необходимо поместить элементы ввода и кнопку в одну группу проверки данных. Для этого необходимо установить одинаковое значение свойства ValidationGroup всех элементов управления, 142 входящих в эту группу. В качестве значения свойства ValidationGroup используется строка, например Login, Registration и т.п. Например, на следующей странице присутствуют две группы элементов управления для ввода данных, для которых реализована своя логика проверки. Первая группа, объединяющая созданные ранее элементы управления, предназначена для регистрации пользователя и называется registration. Вторая – предназначена для авторизации и называется authorisation. В результате, для каждой группы выполняется своя логика проверки вводимых данных. Группа registration элементов управления и валидаторов Группа authorisation элементов управления и валидаторов Рис. 5.42. Пример страницы, содержащей несколько групп элементов управления и валидаторов. Комбинирование верификаторов Очень часто необходимо осуществлять несколько проверок вводимых данных. Сделать это можно различными способами, в том числе и за счет создания пользовательской логики проверки вводимых данных. Однако, большинство задач могут быть решены путем комбинирования верификаторов. Для интерпретации сообщений об ошибках, генерируемых при использовании нескольких верификаторов с одним элементом управления, 143 необходимо понимать правила проверки вводимых данных, принятые в ASP.NET. Так, все верификаторы за исключением RequiredFieldValidator приравнивают отсутствие введенного значения к вводу правильного значения. Таким образом, при отсутствии введенных данных генерируются одни сообщения (это проверяет RequireFieldValidator) и другие, если введенное значение находится вне допустимого диапазона или имеет неверный формат. Для добавления еще одного верификатора и связывания его с текущим полем ввода, необходимо добавить его на страницу и установить для него свойства Text, ErrorMessage, ControlToValidate, ValidationGroup. 144 Занятие 6.Использование Master Pages и навигация при построении интернет приложений Создаваемые Web страницы сегодня очень редко разрабатываются «с нуля». Обычно, при создании таких страниц используются уже готовые шаблоны, либо таковые создаются в процессе разработки Web страниц приложения. Это связано с тем, что страницы Web приложения составляют некое целое, объединенное едиными требованиями к оформлению, содержанию, расположению элементов управления и т.д. Таким образом, пользователь попадает в некую среду, которая должна быть максимально удобна для удовлетворения всех потребностей пользователя. Одним из средств решения подобных задач являются мастер страницы (master pages). Они реализуют простую модель создания шаблонов форм с возможностью их повторного использования. Для реализации данного механизма, в ASP.NET введены такие типы страниц, как мастер страницы (master pages) и страницы содержимого (content pages). Мастер страница представляет собой шаблон страницы, при этом, она может содержать любые элементы, допустимые для обычной страницы, а также программный код. Страница содержимого содержит допустимые элементы управления, с помощью которых определяет содержимое, которым заполняются специальные области мастер страниц. Каждая страница содержимого ссылается на одну мастер страницу, от которой получает элементы и их расположение. Обычно, мастер страница содержит фиксированные элементы, одинаковые для всех страниц, и заполнитель содержимого для остальной части страницы. Наиболее типичными фиксированными элементами являются верхний и нижний колонтитулы, панель навигации, панель меню и т.д. Страница содержимого получает от мастер страницы фиксированные элементы и предоставляет дополнительное содержимое. Для демонстрации возможностей мастер страниц рассмотрим пример создания простой мастер страницы и страницы содержимого. Схематично структура мастер страницы показана на рис. 6.1. 145 Верхний колонтитул Панель навигации таблица Содержимое страницы Нижний колонтитул Рис. 6.1. Типовая структура мастер страницы. Такая структура характерна для очень большого числа Web сайтов. Изображенные на рис. 6.1 верхний колонтитул, нижний колонтитул и панель навигации являются общими для всех страниц элементами, содержимое которые постоянно и не меняется. Содержимое страницы – та ее часть, которая меняется в зависимости от действий пользователя, именно в ней отображается полезная для пользователя информация и именно здесь будет размещаться область, в которой будет отображаться страница содержимого. Создадим новый сайт с помощью Visual Studio. Как и всегда, по умолчанию он состоит из пустой страницы Default.aspx. Сейчас необходимо добавить к данному Web приложению мастер страницу. Для этого необходимо выполнить команду главного меню Website Ö Add New Item и в открывшемся окне выбрать пункт Master Page как показано на рис. 6.2. 146 Рис. 6.2. Окно добавления мастер страницы к текущему Web приложению. При этом, создается новая мастер страница с именем MainMasterPage.master. Как видно из ее исходного кода, она подобна обычной Web странице ASP.NET. Отличие заключается в том, что Web страницы начинаются с директивы Page, а мастер страница – с директивы Master. Еще одним важным отличием является то, что мастер страница должна содержать элемент управления ContentPlaceHolder, который не имеет какихлибо примечательных свойств и предназначен для определения области, в которую страница содержимого может вставлять содержимое. При создании новой мастер страницы, элемент ContentPlaceHolder создается по умолчанию, хотя его можно удалить или добавить еще один или несколько. На рис. 6.3 показана только что созданная мастер страница во время разработки. 147 Рис. 6.3. Редактирование мастер страницы в режиме дизайна. Для создания страницы в соответствии со структурой, изображенной на рис. 6.1 необходимо выполнить следующие действия. 1. Удалить элемент управления ContentPlaceHolder, присутствующий на мастер странице. Это необходимо, т.к. в данном случае проще будет создать данный элемент управления заново после приведения структуры таблицы в соответствии с заданной. 2. Добавить на страницу HTML таблицу, содержащую три строки. В первой и последней строках таблица будет содержать по одной ячейке, а во второй две. Использование таблиц является одним из наиболее удобных способов размещения элементов внутри мастер страниц. При таком подходе вся страница или ее часть разбивается на столбцы и строки. При этом, содержимое страницы располагается и выравнивается на странице более или менее ровно. Кроме того, возможно использование дескрипторов и применения позиционирования CSS, с помощью которого возможно позиционирование их содержимого либо с помощью абсолютных координат, либо располагая их вдоль одной из сторон страницы. Для добавления таблицы на страницу можно воспользоваться готовыми шаблонами таблиц, предоставляемых Visual Studio. Для выбора шаблона таблицы и добавления ее на страницу необходимо выполнить следующую команду главного меню Layout Ö Insert Table. В открывшемся окне Insert Table можно либо указать необходимое количество строк, столбцов, а также некоторые другие параметры таблицы, либо выбрать один из предлагаемых шаблонов, как показано на рис. 6.4. 148 Рис. 6.4. Окно добавления таблицы на основе шаблона. В данном примере выберем шаблон «Header, footer and side», позволяющий создать описанную ранее таблицу. После нажатия на кнопку OK таблица будет добавлена на текущую страницу. Исходный код страницы в результате выполнения данной операции будет выглядеть следующим образом. Untitled Page
149 Как видно, в результате данной операции была добавлена обычная HTML таблица, для которой были установлены значения ширины и высоты некоторых ячеек. В режиме разработки дизайна страница будет выглядеть следующим образом (Рис. 6.5). Рис. 6.5. Редактирование добавленной таблицы в режиме дизайна. 3. Добавить элемент управления ContentPlaceHolder во вторую ячейку второй строки данной таблицы, а также изменить высоту строк, содержащих верхний и нижний колонтитулы. В результате, мастер страница в режиме разработки будет выглядеть следующим образом (Рис. 6.6). 150 Рис. 6.6. Добавление на страницу элемента ContentPlaceHolder. 4. Установить стиль оформления ячейки, содержащей меню в соответствии со следующими параметрами. Вертикальное выравнивание – по верхнему краю, ширина ячейки – 300 px, цвет фона ячейки – LightGrey. 5. Для размещения логотипа страницы, добавить ячейку в первую строку таблицы (в верхнем колонтитуле). Для этого необходимо щелкнуть правой кнопкой мыши в области верхней строки и в открывшемся контекстном меню выбрать пункт Insert Ö Cell to the Left. В созданную ячейку поместить изображение логотипа страницы, установить выравнивание содержимого ячейки по центру. Ввести вместо строки «верхний колонтитул» строку «Пример использования Мастер Страницы», поместить данную строку внутри тэга , создать и подключить к мастер странице таблицу стилей (файл StyleSheet.css), содержащий следующее определение для тэга . h2 { } font-family:Tahoma; В результате проделанных действий мастер страница в режиме разработки будет выглядеть следующим образом (Рис. 6.7). 151 Рис. 6.7. Результат изменения мастер страницы. Фактически, данная мастер страница готова для использования, т.е. в ней присутствуют все необходимые элементы для того, чтобы отображать содержимое страниц содержимого (фактически, для этого нужен только элемент управления ContentPlaceHolder) и необходимый минимум дизайна. Однако, сама по себе мастер страница не может быть отображена в окне браузера, т.к. она предоставляет необходимые элементы дизайна и расположения элементов управления, входящих в шаблон страницы странице содержимого, которая и содержит те данные, которые должны быть отображены в окне браузера. Поэтому, для проверки результатов работы, необходимо создать хотя бы одну страницу содержимого. В качестве страницы содержимого используем страницу Default.aspx, добавленную к проекту Web приложения по умолчанию. Для того, чтобы превратить обычную страницу в страницу содержимого, необходимо в качестве значения свойства MasterPageFile страницы, указать имя мастер страницы, а также добавить на страницу элемент управления . Данный элемент управления отсутствует на панели элементов управления Toolbox, поэтому его можно создать либо вручную, либо одним из следующих способов. 1. Перейти в режим редактирования дизайна страницы содержимого и выполнить команду контекстного меню Create Custom Content элемента управления ContentPlaceHolder (Рис. 6.8). При этом активизируется содержимое данного элемента управления, в результате чего становится возможным вводить текст, а также 152 добавлять другие элементы управления в него. Фактически, при этом, все вводимые данные размещаются внутри страницы Default.aspx. Рис. 6.8. Создание элемента Content с использованием контекстного меню элемента ContentPlaceHolder. Исходный код страницы Default.aspx в результате проделанных операций, а также ввода текста «это начальная страница Default.aspx», будет выглядеть следующим образом. это начальная страница Default.aspx 2. При добавлении новой страницы к Web приложению, существует возможность указания мастер страницы для нее, как показано на рис. 6.9. В этом случае, элемент управления создается автоматически. 153 Флажок для привязки добавляемой страницы к мастер странице Рис. 6.9. Связка вновь добавляемой страницы содержимого с мастер страницей. После того, как страница содержимого создана, можно запустить Web приложение. Результат представлен на рис. 6.10. Рис. 6.10. Результат работы Web приложения на основе мастер страниц. 154 Для демонстрации возможностей перехода между страницами, произведем следующие изменения. Добавим еще одну страницу содержимого, в которую введем текст как показано на рис. 6.11. В области меню создадим две ссылки на страницы содержимого. Рис. 6.11. Страница содержимого, с размещенным на ней текстом. Фрагмент HTML кода, содержащего ссылки выглядит следующим образом. страница1 страница2 Теперь при щелчке по ссылке, в области ContentPlaceHolder будет загружаться соответствующая страница. Использование мастер страниц является очень мощным инструментом, позволяющим значительно упростить создание Web приложения, страницы которого оформлены в едином стиле, и, что также немаловажно, поддержание данного приложения (обновление, добавление элементов, изменение дизайна и т.д.). Тем не менее, возникают задачи, решение которых при использовании мастер страниц вызывает затруднения. К таким вопросам относятся вопросы, связанные с доступом к мастер странице из кода страницы содержимого, вложением мастер страниц друг в друга и динамическое задание мастер страницы. Создатели Web приложений знают, что достаточно важное значение в этой разработке играют заголовки страниц, а также строки метадискрипторов, от которых зависит индексация страницы поисковыми 155 роботами. Каким же образом возможно задавать заголовки страниц содержимого, ведь они фактически вкладываются в мастер страницу? Сделать это можно двумя способами. Задав заголовок страницы в атрибуте Title директивы Page страницы содержимого, как показано ниже. Обратившись к заголовку страницы из программного кода. Для этого необходимо получить ссылку на объект Page для мастер страницы, к которой возможно обращаться из процедуры обработки загрузки текущей страницы следующим образом. Page masterPage = base.Master.Page; masterPage.Header.Title = "заголовок установлен программно"; Еще одним примером необходимости обращения к мастер странице из программного кода страниц содержимого является необходимость модификации содержимого мастер страницы в зависимости от текущих режимов или обращения к определенной странице содержимого. Самым простым примером в данном случае является необходимость изменения баннера мастер страницы в зависимости от того, к какой странице содержимого обратился пользователь. Модифицируем предыдущий пример таким образом, чтобы при обращении к странице Default.aspx с помощью гиперссылки «страница1» в верхней части мастер страницы отображался текст «Пример обращения к мастер странице», а при обращении к странице Default2.aspx – текст «Прямой доступ к мастер странице». При этом рассмотрим различные способы организации доступа к элементам управления, расположенным на мастер странице. В первую очередь, внесем изменения в саму мастер страницу, добавив элемент Label в верхнюю ее часть. Значение ID этого элемента установим равным lbl_BannerText. Для обращения к ресурсам мастер страницы можно использовать либо прямое обращение к ее элементам управления, что нарушает принципы объектного подхода к разграничению прав доступа, либо создать необходимые свойства в классе мастер страницы. Добавим свойство BannerText в класс мастер страницы. Исходный текст свойства выглядит следующим образом. public string BannerText { get { return lbl_BannerText.Text; } set { lbl_BannerText.Text = value; } } 156 Теперь становится возможным обратиться к свойству BannerText из любой страницы содержимого, использующей в качестве мастер страницу данную. Для этого необходимо выполнить следующие действия. Добавить директиву MasterType в страницу содержимого Default.aspx как показано ниже. В обработчике события Page_Load обратиться к свойству установив значение отображаемой строки. BannerText, Master.BannerText = "Пример обращения к мастер странице"; Возможен и другой способ обращения к элементам управления мастер страницы. Он очень похож на предыдущий, с той разницей, что тип объекта, возвращаемого с помощью свойства Master приводится к типу данной мастер страницы. Приведение типа необходимо, т.к. свойство Master возвращает объект обобщенного типа MasterPage. Естественно, данный объект не содержит тех свойств (в данном случае это BannerText), которые мы ввели в класс конкретной мастер страницы. В предыдущем примере приведение не требовалось, т.к. ссылка на тип была доступна через директиву MasterType. Исходный код обращения к свойству BannerText мастер страницы MainMasterPage будет выглядеть следующим образом. MainMasterPage master=(MainMasterPage) Master; master.BannerText= Пример обращения к мастер странице"; При использовании программного обращения к мастер странице необходимо учесть следующее. При переходе от одной странице к другой, как известно, все объекты Web страницы создаются заново. Это означает, что при переходе от одной страницы содержимого к другой, использующей ту же мастер страницу, последняя создается снова. Поэтому, при необходимости сохранения значения свойств и значений элементов управления мастер страницы, необходимо использовать cookie наборы, текстовые файлы, базы данных, либо любые другие возможности долговременного хранения данных, и каждый раз программно осуществлять инициализацию мастер страницы. Еще одной возможностью для доступа к элементам мастер страницы является прямой доступ к ним. Для демонстрации возможностей данного способа, создадим пример, в котором страница содержимого Default2.aspx будет изменять текст баннера мастер страницы на следующий «Прямой доступ к мастер странице». Для этого создадим в обработчике события Page_Load страницы Default2.aspx следующий код. Label lbl = Master.FindControl("lbl_BannerText") as Label; if (lbl != null) 157 lbl.Text = "Прямой доступ к мастер странице"; Как видно из данного примера, сначала происходит поиск элемента управления lbl_BannerText, и, в случае успеха, устанавливает значение свойства Text. При построении достаточно сложного Web приложения, может потребоваться изменение мастер страницы во время выполнения. Это особенно актуально в случае, когда внешний вид страницы должен меняться в зависимости от действий пользователя, например, в зависимости от того, какой пользователь прошел авторизацию в системе, либо если необходимо предусмотреть возможность настройки внешнего вида приложения в зависимости от предпочтений пользователя. В любом из этих случаев, для реализации данной функциональности, можно создать несколько мастер страниц, которые затем переключать в зависимости от действий пользователя. Для изменения текущей мастер страницы программным способом необходимо задать имя файла новой мастер страницы в свойстве Page.MasterPageFile. Делать это необходимо в событии Page.PreInit страницы содержимого. При этом, необходимо учесть следующее. Мастер страницы, на которые будут переключаться страницы содержимого, должны быть «совместимы» друг с другом. Т.е. они должны содержать одинаковые элементы управления (одинакового типа, с одинаковыми значениями ID), в противном случае, произойдет ошибка при попытке обращения к какомулибо из них. Кроме того, если страница содержимого, для которой предполагается динамическое изменение мастер страницы, должна программно обращаться к элементам мастер страницы, это обращение необходимо осуществлять напрямую, без использования строго типизированного подхода на основе создания свойств мастер страницы, как было показано ранее. При этом, использовать директиву нежелательно. В качестве примера добавим к предыдущему примеру еще одну мастер страницу OtherMasterPage, которая будет содержать те же элементы управления, что и MainMasterPage, однако ее оформление будет выглядеть так, как показано на рис. 6.12. 158 Рис. 6.12. Вторая мастер страница, добавленная к Web приложению. Добавим в обе мастер страницы кнопку «Изменить мастер страницу» ID которой равно btn_ChangeMasterPage. Обработчик события этой кнопки для обеих мастер страниц будет одинаковым. protected void btn_ChangeMasterPage_Click(object sender, EventArgs e) { if (Request.Browser.Cookies) { if (Request.Cookies["masterpage"] != null) { if (Request.Cookies["masterpage"].Value == "1") Response.Cookies["masterpage"].Value = "2"; else Response.Cookies["masterpage"].Value = "1"; } else { HttpCookie mpage = new HttpCookie("masterpage"); mpage.Expires = DateTime.Now.AddDays(1); mpage.Value = "2"; Response.Cookies.Add(mpage); } } Response.Redirect(Request.RawUrl); } Как видно, здесь используется механизм Cookies для запоминания текущей мастер страницы, а также задания той из них, которая будет использоваться в качестве текущей. Сам cookie назван masterpage. При значении masterpage=1 активной будет мастер страница MainMasterPage, при 159 – OtherMasterPage. Команда Response.Redirect(Request.RawUrl); необходима для того, чтобы перегрузить текущую страницу, изменив, таким образом, ее внешний вид за счет подключения другой мастер страницы. Само подключение другой мастер страницы происходит в обработчике события Page.PreInit каждой страницы содержимого. В данном примере, для обеих страниц содержимого, хранящихся в файлах Default.aspx и Default2.aspx создан одинаковый обработчик события Page.PreInit, исходный код которого приведен ниже. masterpage=2 protected void Page_PreInit(object sender, EventArgs e) { if (Request.Browser.Cookies) if (Request.Cookies["masterpage"] != null) if (Request.Cookies["masterpage"].Value == "1") { Page.MasterPageFile = "~//MasterPages// MainMasterPage.master"; } else { Page.MasterPageFile = "~//MasterPages// OtherMasterPage.master"; } } В зависимости от значения cookie masterpage, устанавливается текущая мастер страница. В результате, при нажатии на кнопку Изменить мастер страницу, оформление текущей страницы изменится, а содержимое останется прежним (Рис. 6.13). 160 Рис. 6.13. Пример динамического изменения мастер страницы. 161 Еще одним из важных свойств мастер страниц является возможность вложения одной мастер страницы в другую. Обычно эта возможность используется не часто, однако является хорошим средством стандартизировать Web приложение. Следует помнить, что при использовании вложенных мастер страниц редактирование страниц второго и более низких уровней в визуальном режиме невозможно. В общем случае, возможно использование бесконечного количества вложений мастер страниц, однако, на практике не рекомендуется использовать более одного уровня. Это связано с тем, что при необходимости изменения верхнего, общего для всех страниц колонтитула, придется перестраивать всю иерархию мастер страниц, что является довольно сложной процедурой, способной доставить кучу неприятностей в процессе отладки приложения. Поэтому рекомендуется использовать один уровень мастер страниц, а общие элементы переносить с помощью копирования. В качестве примера создадим иерархию из двух мастер страниц и одной страницы содержимого. Верхний уровень мастер страниц содержит верхний колонтитул, общий для всего приложения. Следующий уровень мастер страницы добавляет область меню в левой части окна. Страница содержимого же содержит пример содержимого страницы и связана с мастер страницей второго уровня. Исходные коды всех трех страниц приведены ниже. Мастер страница верхнего уровня (RootMasterPage.master). Untitled Page
162 Мастер страница второго уровня (Node1MasterPage.master)
Страница содержимого (Default3.aspx) содержимое вложенной мастер страницы В результате выполнения приложения окно браузера будет выглядеть следующим образом. Рис. 6.14. Результат выполнения приложения, содержащего вложенные мастер страницы. 163 Занятие 7.Навигация по Web приложению Любое Web приложение представляет собой достаточно сложную совокупность взаимосвязанных страниц. Хорошо продуманное и спроектированное Web приложение обладает не только красивым и эргономичным дизайном, но и хорошей системой навигации, позволяющей легко переходить от одного раздела к другому, а также решать другие задачи. ASP.NET обладает достаточно большими возможностями, позволяющими реализовывать сложные системы навигации. Для реализации систем навигации используется целый ряд элементов управления, предназначенных для решения отдельных задач. Рассмотрим наиболее важные и распространенные из них. Карты сайта Карты сайта целесообразно использовать в случае, когда Web приложение содержит большое количество страниц. Карты сайта предлагают удобный механизм определения структуры страниц приложения, а также ее отображение с помощью нескольких элементов управления. Эти элементы управления расположены в разделе Navigation панели Toolbox. Основных элементов управления три – SiteMapPath, Menu, TreeView. Все эти элементы предназначены для решения одной и той же задачи – предоставления возможности пользователю Web приложения осуществлять навигацию по страницам. Различие между ними заключается в способах отображения ссылок на соответствующие страницы. Для использования любого их перечисленных компонентов, необходимо определить структуру приложения, которая хранится отдельно. В качестве хранилища структуры обычно выступает XML файл. В Visual Studio 2005 существует уже определенная структура XML файла, предназначенная для хранения структуры приложения, это файлы типа .sitemap. Содержимое данного файла достаточно простое, что позволяет легко вносить данные о структуре приложения. Разделение элементов, отвечающих за отображение данных и элементов, содержащих эти данные позволяет значительно упростить редактирование приложения – для изменения дизайна достаточно изменить настройки визуальных компонентов, для внесения дополнений – добавить данные в XML файл. В качестве примера рассмотрим добавление навигации по страницам Web приложения, созданного ранее на базе мастер страниц. Прежде всего, добавим XML файл, и введем данные о структуре приложения. Для добавления XML файла воспользуемся стандартными средствами Visual Studio 2005 Website Ö Add New Item. В открывшемся окне выберем пункт Site Map и нажмем кнопку OK. 164 Рис. 7.1. Добавление карты сайта. В результате, будет создан файл Web.sitemap, содержащий заготовки для ввода структуры Web приложения. <siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" > <siteMapNode url="" title="" description=""> <siteMapNode url="" title="" description="" /> <siteMapNode url="" title="" description="" /> Как видно из исходного кода, карта сайта должна начинаться с корневого узла <siteMap>. Элементы структуры описываются в тэгах <siteMapNode>. С помощью этих тэгов можно указывать иерархию элементов Web приложения. Для указания вложенных элементов их просто необходимо расположить внутри соответствующего тэга <siteMapNode>. Свойства каждого тэга необходимы для задания соответствующих значений. В примере выше видно, что каждому элементу соответствует три свойства: url, title, description. Их назначение очевидно – url используется для указания интернет адреса страницы, которой соответствует этот элемент, title задает наименование элемента, отображаемое элементом управления, description – описание элемента, которое отображается в виде всплывающей подсказки при наведении указателя мыши на соответствующий элемент. В качестве примера простейшей карты сайта создадим иерархию страниц, состоящую из шести позиций. Самым верхним элементом иерархии будет домашняя страница, ссылающаяся на файл Default.aspx. Все остальные элементы будут вложены в него. Кроме того, в элемент «Страница 4» должны быть вложены еще два элемента. Пример карты сайта, описывающей данную структуру, приведен ниже. 165 <siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" > <siteMapNode url="Default.aspx" title="Домашняя" description=""> <siteMapNode url="Default2.aspx" title="Страница 2" description="Перейти к странице 2" /> <siteMapNode url="Default3.aspx" title="Страница 3" description="" /> <siteMapNode url="Default4.aspx" title="Страница 4"> <siteMapNode url="Default5.aspx" title="Страница 5"/> <siteMapNode url="Default6.aspx" title="Страница 6"/> Использование элементов управления TreeView, Menu и SiteMapPath После того, как карта сайта определена, становится возможным использование элементов управления, связанных с ней для отображения данных о структуре приложения. Для этого возможно использование таких элементов управления как TreeView, Menu, SiteMapPath. Элементы TreeView и Menu имеют одинаковую функциональность и предназначены для вывода элементов структуры приложения на экран. Различие заключается в форме представления данных. Элемент SiteMapPath предназначен для отображения текущего положения пользователя в иерархии Web приложения и позволяет ему переходить вверх по иерархии на более высокий уровень. Рассмотрим примеры использования все трех элементов управления. Использование TreeView и Menu Обратимся к предыдущему примеру, использующему мастер страницы. В соответствии с описанием карты сайта, создадим шесть файлов aspx и добавим их в проект (обратите внимание на то, что часть этих файлов уже создана в предыдущем примере). В результате добавления файлов, Web приложение должно содержать файлы Default.aspx, Default2.aspx, Default3.aspx, … , Default6.aspx. В режиме редактирования дизайна, откроем мастер страницу MainMasterPage.master и добавим элементы TreeView и Menu в область меню, расположенную слева, как показано на рис. 7.2. 166 элемент управления TreeView элемент управления Menu Рис. 7.2. Добавление элементов TreeView и Menu. Теперь необходимо задать источник данных для элементов управления TreeView и Menu. выбор источника данных Рис. 7.3. Определение источника данных элемента TreeView. Для этого необходимо в свойстве Choose Data Source выбрать пункт и в открывшемся окне выбрать Site Map. При этом будет создан и добавлен на страницу источник данных с заданным именем. Исходный код источника данных выглядит следующим образом. 167 карта сайта имя источника данных, добавляемого в приложение Рис. 7.4. Создание источника данных SiteMap. После добавления источника данных, произведем настройки внешнего вида элементов TreeView и Menu. Настройка формата Рис. 7.5. Настройка внешнего вида элемента TreeView. Для элемента TreeView выберем схему оформления Arrows 2, для Menu – Classic. После запуска приложения, его окно будет выглядеть следующим образом (рис.???). Для перехода к соответствующей странице можно воспользоваться любым элементом меню, либо дерева. 168 Рис. 7.6. Окно Web приложения после добавления элементов TreeView и Menu. Элементы управления TreeView и Menu обладают дополнительными свойствами, с помощью которых возможно изменить их внешний вид, в соответствии с дизайном Web приложения. Использование SiteMapPath В отличие от TreeView и Menu, SiteMapPath отображает только текущее положение пользователя в иерархии страниц Web приложения с возможностью перехода к верхнему уровню иерархии. Добавим в пример элемент SiteMapPath. Для этого необходимо добавить еще одну строку между заголовком и содержимым мастер страницы и просто поместить в нее элемент SiteMapPath. Результат представлен на рис. 7.7. 169 результат добавления элемента SiteMapPath Рис. 7.7. Добавление в приложение элемента SiteMapPath. Использование частей карты сайта В некоторых случаях необходимо выводить на экран структуру некоторой части Web приложения, а не всю ее, как это было до сих пор. Например, может потребоваться, чтобы корневой узел не содержал в себе все остальные элементы приложения. Точнее, в структуре эта иерархия должна сохраняться, а на экран выводиться иначе. Для реализации этого эффекта возможно установить свойство ShowStartingNode элемента SiteMapDataSource равным true. В этом случае, корневой элемент на экран выводиться не будет (рис.???). 170 Рис. 7.8. Результат отключения вывода на экран корневого элемента иерархии страниц Web приложения. Переход к нему будет возможен посредством SiteMapPath. В случае же, если необходимо выводить корневой элемент, но так, чтобы остальные элементы приложения не входили в него, необходимо изменить карту сайта добавив фиктивный узел, не привязанный к какому-либо элементу приложения. Навигация с использованием программирования Использование навигации без написания программного кода является очень удобным и быстрым способом создания возможности перехода пользователя от одной страницы приложения к другой. Однако, такой подход не дает целый ряд дополнительных возможностей. Например, может потребоваться изменение некоторых элементов Web страницы при переходе к ним пользователей, или в зависимости от того, какой пользователь перешел к данной странице. В этом случае необходимо использовать программные средства создания навигации. Рассмотрим простейшие принципы создания подобных элементов. Доступ к карте сайта можно организовать посредством API интерфейса, который предоставляется классом SiteMap. Этот класс содержит статические свойства CurrentNode и RootNode. Эти свойства возвращают объект SiteMapNode, с помощью которого можно извлекать информацию из карты сайта, т.е. обращаться к свойствам url, description, title и т.д. Объект SiteMapNode содержит следующий ряд свойств, с помощью которых можно управлять навигацией. 171 ParentNode ChildNides PreviousSibling NextSibling Возвращает узел, расположенный на один уровень выше относительно текущего. В случае, если текущим является корневой узел, возвращает null. Возвращает коллекцию всех дочерних узлов. Проверяет свойство HasChildNodes, чтобы узнать, существуют ли дочерние узлы. Возвращает предыдущий узел, который находится на том же уровне, если такого не существует, возвращает null. Возвращает следующий узел, который находится на том же уровне, если такого не существует, возвращает null. Следующий код устанавливает значение элемента Label, находящегося в заголовке страницы равным значению поля description текущего узла карты сайта. lbl_BannerText.Text = SiteMap.CurrentNode.Description; Еще один пример демонстрирует возможность создания гиперссылок, осуществляющих перемещение к последующей или к предыдущей странице. protected void Page_Load(object sender, EventArgs e) { if (SiteMap.CurrentNode.NextSibling != null) { hl_Next.NavigateUrl = SiteMap.CurrentNode.NextSibling.Url; hl_Next.Visible = true; } else { hl_Next.Visible = false; } if (SiteMap.CurrentNode.PreviousSibling != null) { hl_Previous.NavigateUrl = SiteMap.CurrentNode.PreviousSibling.Url; hl_Previous.Visible = true; } else { hl_Previous.Visible = false; } } Здесь элементы hl_Next и hl_Previos являются элементами hyperlink. Данный пример позволяет перемещаться к предыдущей, либо последующей страницам иерархии приложения в пределах одного уровня. Пример выполнения приложения представлен на рис. 7.9. 172 Рис. 7.9. Окно приложения, реализующего программируемую навигацию. Как видно из рисунка, при активной четвертой странице, переход к следующей странице невозможен, т.к. она является вложенной и относится к следующему уровню иерархии. При создании навигации можно не ограничиваться только стандартными элементами управления, такими как TreeView и Menu. Возможно использование и других элементов управления в связке с картой сайта. Например, возможно связать элемент GridView с картой сайта, настроить шаблоны его отображения и использовать как альтернативный способ организации навигации. Правда этот подход также имеет ограничения. Так, при этом возможно отображение только элементов текущего уровня. При необходимости отобразить вложенные элементы, придется использовать вложенные элементы управления. Более подробно использование элемента GridView описывается далее, в занятии, посвященном работе с базами данных. Кроме того, о дополнительных возможностях организации навигации в рамках Web приложения можно узнать в [1]. Использование MultiView и Wizard Во многих случаях требуется организовать взаимодействие пользователя со многими страницами. Причем достаточно большая часть этих страниц содержит логически сильно связанные страницы. Примерами таких случаев могут быть страницы, предназначенные для организации сбора какой-либо информации, например регистрация пользователя, в процессе которой последний заполняет достаточно много полей данных. В этом случае, размещать все эти поля на одной странице нежелательно, поскольку это не только загромождает саму страницу, но и может привести к осложнению проверки вводимых данных. В другом случае, может 173 потребоваться несколько различных вариантов представления одних и тех же данных при различных обстоятельствах, возникающих в процессе работы пользователя с Web приложением. В обоих примерах наилучшим решением будет использование элементов управления MultiView и Wizard. Использование элемента MultiView Данный элемент управления позволяет реализовывать управление множеством представлений. Фактически он позволяет создавать несколько представлений, только одно из которых отображается при работе пользователя с данной страницей. MultiView не имеет пользовательского интерфейса по умолчанию и отображает те данные, которые в него будут помещены. Содержимое, добавляемое в MultiView должно располагаться в элементе управления View. Рассмотрим пример создания страницы регистрации пользователя с использованием MultiView. Создадим новое Web приложение и расположим на его начальной странице элемент управления MultiView. В данный элемент управления добавим три элемента View. В каждый из элементов View добавим необходимые элементы для ввода данных о пользователе. В результате проделанных операций страница в режиме редактирования дизайна будет выглядеть так, как показано на рис. 7.10. Рис. 7.10. Реализация шагов регистрации пользователя с помощью MultiView. 174 При попытке запуска приложения в окне браузера мы не увидим ничего кроме заголовка. Это объясняется тем, что элемент MultiView отображает тот раздел, индекс которого указан в свойстве ActiveViewIndex. По умолчанию в данном свойстве установлено значение -1. Это означает, что ни один из разделов не будет показан на экране. Изменять значение элемента ActiveViewIndex необходимо программно. Это можно делать различными способами. Одним из таких способов является размещение на странице элемента управления DropDownList, содержащего список, включающий все разделы (представления), входящие в MultiView. Реализовать заполнение списка возможно следующим образом. protected void Page_Load(object sender, EventArgs e) { if (!Page.IsPostBack) { DropDownList1.DataSource = MultiView1.Views; DropDownList1.DataTextField = "ID"; DropDownList1.DataBind(); } } Теперь, при выборе из списка какого-либо элемента необходимо изменять текущее отображение элемента MultiView. Это можно реализовать следующим образом. protected void DropDownList1_SelectedIndexChanged(object sender, EventArgs e) { MultiView1.ActiveViewIndex = DropDownList1.SelectedIndex; } Не забудьте, что для того, чтобы элемент управления DropDownList производил немедленное обновление страницы при изменении текущего элемента списка, необходимо установить значение свойства AutoPostBack равным true. Результат выполнения приложения показан на рис. 7.11. 175 Рис. 7.11. Результат работы приложения, реализующего возможность регистрации пользователя с использованием MultiView. содержит интеллектуальные средства. В частности он способен распознать команды, использованные в качестве значений свойств ID кнопочных элементов управления. Например, можно добавить кнопки навигации, позволяющие активизировать предыдущий, либо следующий раздел. Это можно сделать следующим образом. В разделы «Регистрация» и «Контрольный_вопрос» необходимо добавить кнопку MultiView runat="server" CommandName="NextView" В разделы «Контрольный_вопрос» и «Дополнительная_информация» добавить кнопку Обратите внимание, что помимо стандартных атрибутов данные кнопки содержат атрибут CommandName. Именно благодаря ему элемент MultiView определяет действие, которое необходимо произвести. Кроме двух уже использованных действий NextView и PrevView, допустимы еще два. SwitchViewByID – позволяет перейти к представлению с определенным значением идентификатора ID. При использовании этой команды значение ID необходимо указывать в свойстве CommandArgument кнопочного элемента управления. 176 – позволяет перейти к представлению с определенным числовым индексом, значение которого также задается в свойстве CommandArgument кнопочного элемента управления. Единственным недостатком данного примера является то, что при использовании кнопок для перехода к последующему или предыдущему разделу, значение элемента управления DropDownList остается всегда неизменным. Для исправления этого недостатка необходимо добавить следующий код. SwitcViewByIndex protected void MultiView1_ActiveViewChanged(object sender, EventArgs e) { DropDownList1.SelectedIndex = MultiView1.ActiveViewIndex; } Результат запуска приложения представлен на рис.??? Рис. 7.12. Добавление кнопок для перемещения к предыдущему или последующему элементу MultiView. Элемент управления Wizard Элемент управления Wizard фактически представляет собой аналог элемента MultiView с той лишь разницей, что он в основном ориентирован на создание возможности пошагового выполнения пользователем каких-либо действий. В нем реализован механизм перехода к следующему, либо предыдущему шагу, возможно также переходить к определенному шагу минуя предшествующий (нелинейная навигация) и т.д. При размещении элемента Wizard на странице, он содержит кнопки навигации и боковую панель, содержащую все стадии, которые определены в нем (рис.???). 177 Рис. 7.13. Пример размещения на странице элемента Wizard. Боковую панель можно спрятать. Это необходимо сделать в том случае, когда необходимо, чтобы пользователь не мог пропустить хотя бы один шаг и изменить последовательность выполняемых действий. Для того, чтобы отключить боковую панель необходимо установить свойство DisplaySideBar равным false. После того, как элемент Wizard добавлен на страницу, необходимо определить шаги (добавить или удалить уже существующие) и их содержимое. В качестве содержимого шагов возможно использование любого элемента ASP.NET, либо HTML. Для добавления шагов в Wizard необходимо открыть свойство WizardSteps, а затем в открывшемся окне WizardStep Collection Editor добавить или удалить нужное количество шагов (Рис. 7.14). шаги мастера Рис. 7.14. Окно добавления и настройки шагов элемента Wizard. Каждому шагу соответствует свой набор свойств. Наиболее важными из них являются следующие. Title StepType Имя шага, соответствующее тексту ссылок в боковой панели. Тип шага, может принимать значения Auto, Complete, Finish, 178 AllowReturn Start, Step, определяет тип кнопок навигации, которые будут показаны для данного шага. Например, в случае установки значения Auto тип шага определяется на основании позиции в коллекции, это означает, что для первого шага будет установлена кнопка Start, для последнего – Finish, а для всех промежуточных шагов – Step. В случае установки значения Step будут отображаться кнопки Next и Previous и т.д. Определяет может ли пользователь возвращаться к этому шагу. Если значение свойства установлено равным false, при переходе к следующему шагу, пользователь уже не может вернуться к предыдущему. В качестве примера, создадим аналог примера регистрации пользователя, рассмотренного ранее с использованием элемента MultiView. Для этого разместим внутри страницы элемент Wizard, и определим для него следующие свойства. 1. Добавим необходимое количество шагов и установим для них значения свойства Title, как показано на рис. 7.15. Рис. 7.15. Добавление шагов создаваемого мастера регистрации. 2. Установим значения CancelButtonText=Отмена, свойств FinishPreviousButtonText=Предыдущий, DisplaySideBar=false, FinishCompleteButtonText=Готово, StartNextButtonText=Следующий>, StepNextButtonText=Следующий>, StepPreviousButtonText= При задании темы страницы, ASP.NET в момент выполнения страницы на сервере подменяет значения свойств, существующие в элементах управления, страницы, для которых в теме указаны параметры и применяет их к итоговой странице. Таким образом, тема является более приоритетной по отношению к свойствам элемента управления. На рис. 8.2 показана одна из страниц предыдущего примера, для которой была применена тема, фрагменты которой указаны выше. 184 Цвет текста элементов TextBox определен равным Blue Шрифт элементов Button определен красного цвета и полужирного начертания Цвет фона элементов TextBox определен равным LightSteelBlue Рис. 8.2. Пример использования темы. Как уже было сказано ранее, настройки элемента управления, указанные в теме, имеют более высокий приоритет по сравнению с настройками, указанными непосредственно в самом элементе управления. Это одно из основных отличий использования тем от каскадных таблиц стилей CSS. Как известно, при использовании CSS, параметры, определенные в каскадных таблицах стилей применяются только тогда, когда аналогичные свойства элемента управления не заданы (отсутствует конфликт свойств). Таким образом, CSS имеет меньший приоритет по отношению к параметрам, указанным в самих элементах управления. Иногда желательно, чтобы темы вели себя аналогично CSS, т.е. обладали меньшим приоритетом по отношению к параметрам, определенным в элементах управления. Для реализации такого поведения необходимо вместо атрибута Theme использовать атрибут styleSheetTheme в директиве pages файла Web.config, либо объекта DOCUMENT страницы. Конечно, одним из важнейших свойств тем является возможность их применения к большому количеству элементов управления, создав всего лишь одно описание значений свойств его класса. Тем не менее, может возникнуть необходимость конфигурации отдельных элементов управления, т.е. указание параметров в теме, которые будут применяться не ко всем элементам, а лишь к некоторым, либо настроить некоторые элементы таким образом, чтобы они вообще не принимали участие в формировании темы. В последнем случае достаточно установить значение свойства EnableTheming элемента управления равным false. В этом случае, при обработке данного элемента управления никакие темы применяться не будут. Если же необходимо, чтобы тема применялась лишь к избранным элементам оформления, необходимо создать именованное оформление. Для создания именованного оформления, необходимо: 1. В определении описания темы для элемента управления определить атрибут SkinID, для которого установить значение, однозначно определяющее оформление данного элемента управления. 185 Например, для кнопки можно установить следующий параметр SkinID. 2. Установить значение параметра SkinID элемента управления, равным значению SkinID описания оформления элемента управления темы. На рис. 8.3 показан внешний вид элемента управления Button при установленном свойстве SkinID равным RedButton (справа) и при неустановленном свойстве SkinID (слева). Кроме того, в данном примере, в качестве цвета фона элемента ввода e-mail указан PeachPuff, а в файле Web.config, при определении темы использована директива styleSheetTheme, что привело к тому, что цвет фона элемента ввода e-mail отличается от цвета фона остальных элементов ввода данной страницы. Цвет фона PeachPuff Цвет фона LightSteelBlue Параметр SkinID не установлен Параметр SkinID установлен равным RedButton Рис. 8.3. Пример подключения темы к странице Web приложения. При использовании данного механизма поведение тем становится похожим на поведение CSS при использовании классов. Совместное использование Themes и CSS В ряде случаев неплохих результатов можно добиться используя комбинацию Themes и CSS. Такая комбинация может быть целесообразна в случае, когда необходимо создать оформление для серверных элементов управления и для HTML элементов страниц, либо когда желательно использовать более стандартизованный подход к оформлению страниц, который может быть легко переносим с одной платформы на другую, либо когда уже существует оформление на базе CSS. Для использования таблицы стилей в теме необходимо выполнить следующие операции. 1. Добавить таблицу стилей в папку темы. 2. Определить параметры оформления в таблице стилей. 3. Убедиться в том, что раздел страницы, для которой определены стили, содержит директиву runat=”server”. 186 При добавлении таблицы стилей в тему она становится частью этой темы. При обращении пользователя к странице, ASP.NET динамически свяжет ее с файлом css, определенным в теме, заданной для данной страницы. Таким образом, обязательно необходимо, чтобы страница имела атрибут runat=”server” для включения обработки раздела на стороне сервера при обращении к странице. Раздел здесь используется потому, что при динамическом связывании таблицы стилей со страницей происходит то же, что и при статической связке: добавление ссылки на файл таблицы стилей именно в раздел . Например, если добавить к рассматриваемому примеру таблицу стилей (Рис. 8.4) следующего содержания, Рис. 8.4. Добавление таблицы стилей к теме. h1 { } font-weight: bold; color: teal; font-family: Verdana; то стиль заголовка «Регистрация пользователя» будет переопределен и отображаться как показано на рис. 8.5. 187 Шрифт заголовка определен в файле StyleSheet.css Рис. 8.5. Переопределение стиля заголовка темы с помощью css. Динамическое изменение темы В ряде случаев необходимо иметь возможность динамического изменения темы. Это может быть связано со сложившимися условиями в момент выполнения приложения, либо при необходимости предоставления пользователю возможности изменения внешнего представления приложения. Изменение темы происходит достаточно просто. Для этого необходимо установить соответствующее значение свойства Page.Theme. При этом необходимо помнить, что динамическое изменение темы необходимо производить в рамках события Page.Init, в противном случае будет выдано сообщение об ошибке. В качестве примера, создадим возможность динамического изменения темы предыдущей страницы. Для этого добавим к странице элемент DropDownList, который будет автоматически заполняться списком существующих тем, а также кнопку, при нажатии на которой будет происходить смена текущей темы. Заполнение элемента DropDownList списком существующих тем должно быть организовано в момент загрузки страницы (событие Page_Load). Т.к. каждая тема располагается в отдельной папке проекта, для получения списка тем необходимо прочитать существующие имена папок и установить их в качестве значений выпадающего списка. protected void Page_Load(object sender, EventArgs e) { if (!Page.IsPostBack) { DirectoryInfo tDir = new DirectoryInfo(Server.MapPath( "App_Themes")); DropDownList2.DataTextField = "Name"; DropDownList2.DataSource = tDir.GetDirectories(); DropDownList2.DataBind(); if (Session["CurrentThemeIndex"] != null) 188 } } DropDownList2.SelectedIndex = (int)Session[ "CurrentThemeIndex"]; Класс DirectoryInfo содержит методы, необходимые для выполнения типичных операций над файлами и папками, таких как копирование, перемещение, переименование, создание и удаление. В данном примере, создается новый объект класса DirectoryInfo. С помощью метода GetDirectories() получается список всех подпапок текущей папки (в данном случае App_Themes). Данный список представлен массивом строковых элементов, который затем посредством свойств DataSource и DataBind связывается с выпадающим списком. Так как изменение оформления страницы требует инициации события postback и, как следствие, перезагрузку страницы, при которой должно измениться не только текущая тема, но и текущее значение элемента выпадающего списка, логика работы данного примера будет следующей. При выборе темы и нажатии на кнопку «Применить», устанавливаются две переменные состояния, в одной из которых будет храниться имя темы выбранной пользователем, а во второй – индекс выбранного элемента выпадающего списка. В событии Page_PreInit будет производиться чтение значения переменной состояния, содержащей имя темы и применение ее к текущей странице. Таким образом, условие if (Session["CurrentThemeIndex"] != null) DropDownList2.SelectedIndex = (int)Session["CurrentThemeIndex"]; расположенное в событии Page_Load необходимо для установки текущего значения выпадающего списка после перезагрузки страницы. Исходный код события Page_PreInit выглядит следующим образом. protected void Page_PreInit(object sender, EventArgs e) { if (Session["CurrentTheme"] == null) { Page.Theme = ""; } else { Page.Theme = (string)Session["CurrentTheme"]; } } Исходный код нажатия на кнопку Применить выглядит следующим образом. protected void Button3_Click(object sender, EventArgs e) { Session["CurrentTheme"] = DropDownList2.SelectedValue; Session["CurrentThemeIndex"] = DropDownList2.SelectedIndex; Server.Transfer(Request.FilePath); } 189 В результате работы приложения, при выборе темы и нажатии на кнопку Применить, выбранная тема будет применена к текущей странице (Рис. 8.6). Тема example2 Тема example1 Рис. 8.6. Динамическое изменение активной темы приложения. 190 Занятие 9.Использование кэширования в Web приложениях Web приложения целесообразно создавать в тех случаях, когда необходимы хорошие показатели его масштабируемости и производительности. Количество пользователей таких приложений может достигать нескольких десятков и сотен тысяч. Каждое обращение пользователя может приводить к необходимости чтения и записи каки-либо данных на жесткие диски сервера. В тех случаях, когда некоторая часть информации является более востребованной, чем ее другие части, целесообразно использовать кэширование данных. Как известно, кэширование способно значительно ускорить работу как аппаратных, так и программных частей компьютера. Практически все современные устройства хранения и передачи данных используют кэш память для ускорения работы за счет минимизации задержек, связанных с ожиданием окончания записи, чтения или передачи данных. В ASP.NET кэширование – это способ ускорения работы за счет организации хранения в оперативной памяти сервера копий информации, создание которой связано с большими накладными расходами. Так, если пользователи постоянно запрашивают информацию, для получения которой необходимо выполнить достаточно громоздкую, сложную и длительную вычислительную процедуру, целесообразно будет выполнить ее один раз, записать результат ее выполнения в кэш, и при обращении пользователей сразу выдавать результат. Понятно, что такой подход значительно ускорит процесс получения пользователями результата. Использование кэширования приводит не только к повышению производительности и масштабируемости, но и создает ряд проблем, которые необходимо знать и уметь их обходить. Одна из таких проблем это то, что кэширование задействует оперативную память, которая никогда не бывает лишней. Если попытаться сохранить в оперативной памяти слишком много данных, операционная система сбрасывает лишние данные на диск, что может привести к замедлению работы всей системы в целом. Для управления этим процессом в ASP.NET реализован интеллектуальный механизм определения переполнения кэша, который основан на том, что при попытке записи в кэш данных, объем которых превышает доступный для использования кэша объем оперативной памяти, ASP.NET выборочно удалит из кэша часть данных для обеспечения максимальной общей производительности системы. Кроме того, при использовании механизмов кэширования, необходимо помнить, что должен быть некий принцип, согласно которому будет осуществляться проверка обновления информации, на основе которой строится кэш. Если данные таблицы базы данных или файла изменились, это может означать, что данные кэша устарели и их необходимо обновить. ASP.NET 2.0 предоставляет усовершенствованный механизм управления кэшированием, реализуя все необходимые элементы управления политикой кэширования. В частности, он позволяет управлять замещением 191 информации, находящейся в кэше, управлять профилями кэшей, с помощью которых можно определить настройки кэширования для группы страниц, управлять хранением содержимого кэша в оперативной памяти и на жестких дисках, отслеживание изменений в исходных данных и удалять или объявлять недействительными кэшированные элементы. Основы кэширования в ASP.NET ASP.NET поддерживает два типа кэширования: кэширование данных и кэширование ввода. Рекомендуется использовать оба эти типа кэширования, т.к. это способно значительно повысить производительность приложения. Кэширование данных – управляется непосредственно из кода ASP.NET приложения, в котором разработчик сам определяет какую информацию необходимо поместить в кэш. Страницы, к которым обращаются пользователи, могут проверять существование интересующей их информации в кэше прежде чем выполнять шаги, необходимые для ее получения. В этом смысле кэширование можно сравнить с состоянием приложения с той лишь разницей, что при невозможности сохранения данных в кэше они удаляются, кроме того, возможно настроить автоматическое удаление данных из кэша по истечении определенного времени. Кэширование вывода – позволяет сохранить копию сгенерированной HTML страницы, отправленной клиенту. Таким образом, если еще один или несколько клиентов запросят у сервера ту же страницу, они получат ее копию из кэша. Упомянутые виды кэширования стали основой для создания еще двух разновидностей: • кэширование фрагментов – позволяющего сохранять в кэше лишь часть готового HTML кода страницы. В основном это относится к пользовательским элементам управления; • кэширование источников данных – встроенный в объекты доступа к данным механизм кэширования данных, который используется автоматически при использовании соответствующего элемента. Рассмотрим основные аспекты применения кэширования в ASP.NET Кеширование вывода При использовании данного вида кэширования, сгенерированный в результате выполнения приложения HTML код сохраняется в памяти, и при повторном запросе этой же страницы, клиенту передается уже сгенерированный ранее HTML код. В качестве демонстрации возможностей кэширования вывода воспользуемся хорошо известным в литературе примером вывода значения текущего времени и даты в окно браузера. Создадим новое Web приложение, откроем редактор кода страницы и введем следующую команду в обработчик события Page_Load. 192 Response.Write(DateTime.Now.ToString()); При запуске, на страницу будет выводиться текущая дата и время. Если нажать кнопку обновить в окне браузера, показания времени изменятся в соответствии с текущим значением системных часов. Рис. 9.1. Результат отображения текущей даты и времени в окне браузера без использования кэширования. Добавим данную страницу в кэш. Для этого, добавим к файлу страницы директиву Атрибут Duration задает количество секунд, в течение которых необходимо хранить страницу в кэше. Параметр VaryByParam равный значению None, устанавливает режим кэширования, при котором, не зависимо от дополнительных параметров, в кэше будет сохраняться только одна копия данной страницы. Теперь при запуске приложения, в окне браузера будет выведено текущее значение времени и дата, однако, при попытке обновления страницы, никаких изменений текущих значений происходить не будет в течение 10 секунд. При попытке обновления страницы по истечении 10 секунд, обновится и текущее значение времени, а также дата. Несмотря на явное указание времени, в течение которого страница должна находиться в кэше, она может быть удалена из него ранее. Это может произойти в том случае, если для помещения в кэш нового элемента не хватает места. Благодаря такому механизму, реализованному в ASP.NET можно не заботиться о необходимости очищения памяти кэша. Выше уже отмечались некоторые потенциальные проблемы, связанные с кэшированием страницы вывода. Одним из таких случаев является использование данных, передаваемых странице через строку запроса. При установленном режиме кэширования, рассматриваемом выше, несмотря на ввод в строку адреса значений переменных, страница сохраняется в кэше заданные 10 секунд. Однако, такое поведение можно изменить если указать в качестве значения параметра VaryByParam значение «*», которое устанавливает 193 режим кэширования страниц, содержащих строку запроса. При этом, ASP.NET начинает кэшировать отдельные копии страницы для разных значений аргументов, указанных в строке запроса. Рис. 9.2. Кэширование страницы для разных значений аргументов, указанных в строке запроса. Использование значения «*» в качестве значения параметра VaryByParam в некоторых случаях способно вызвать дополнительные проблемы. Дело в том, что в некоторых типах приложений достаточно часто используется передача множества параметров в строке запроса. Если значение хотя бы одного из передаваемых параметров будет отличаться от значения такого же параметра кэшированной страницы, запрашиваемая страница будет кэширована вновь. Например, если в строке запроса передается информация о клиенте, а также о выбранном им товаре, понятно, что количество комбинаций клиента и товара достаточно велико, а следовательно практически все запрашиваемые страницы будут помещены в кэш, причем их повторное использование будет стремиться к 0. В этом случае целесообразно выявить те параметры, которые наиболее часто используются для передачи параметров, с высокой степенью повторного использования и указать их в качестве значения параметра VaryByParam. Например, следующая строка дает команду ASP.NET для кэширования только тех страниц, которые содержат параметр ClientID, значение которого, в свою очередь, отличается от уже сохраненного в кэше. Фрагментное кэширование В ряде случаев бывает нецелесообразно по каким-то причинам кэшировать всю страницу целиком. Это может быть в том случае, когда на странице находится динамическое содержимое, которое необходимо обновлять постоянно. В этом случае можно воспользоваться возможностью кэширования фрагментов. Реализовать данную разновидность кэширования можно двумя способами. 194 1. Создать пользовательский элемент управления, поместить в него ту часть страницы, которую необходимо кэшировать, настроить для данного элемента кэширование. При этом для страницы настраивать кэширование не требуется. 2. Создать свой метод, который должен возвращать некоторое значение. С помощью объекта Substitution указать на необходимость вызова данного метода при обращении пользователя к Web странице. При этом сама страница может быть кэширована, однако, данный метод будет выполняться в любом случае, возвращая фактически динамическое содержимое, которое будет вставлено в кэшированный фрагмент страницы. Причем такой сценарий обработки будет применяться всегда. Фактически, получается реализация, обратная описанной в первом пункте. Рассмотрим примеры реализации фрагментного кэширования, соответствующего обоим описанным способам. Использование пользовательского элемента управления для реализации фрагментного кэширования Для реализации данного вида кэширования создадим пользовательский элемент управления, отображающий текущее время, а также цветную полосу, величина которой изменяется в соответствии со значением количества секунд текущего времени [4]. Для создания пользовательского элемента управления, необходимо выполнить команду меню WebSite Ö Add New Item и в открывшемся окне выбрать пункт Web User Control. Рис. 9.3. Создание и добавление к проекту пользовательского элемента управления. 195 В результате будет создан и добавлен к приложению файл ShowTime.ascx, представляющий собой фактически Web страницу, у которой отсутствуют обязательные для HTML страниц тэги, такие как , и т.д. Отсутствие упомянутых тэгов объясняется тем, что пользовательский элемент управления представляет собой фактически фрагмент Web страницы, который будет встраиваться в основную страницу. Введем в исходный код созданного пользовательского элемента управления следующий фрагмент. Фактически, создаваемый пользовательский элемент управления создается на основе сервеного элемента управления Label, который помещен внутри двух тэгов . Тэг верхнего уровня необходим для формирования контура элемента управления в виде прямоугольника. Второй тэг необходим для отображения цветной полосы, величина которой зависит от текущего значения секунд. Если с контуром в виде прямоугольника все просто, то о цветной полосе стоит поговорить отдельно. Фактически, она должна являться аналогом элемента progress bar в традиционных win32 приложениях. Реализовать в рамках Web приложения это можно несколькими способами, один из которых подразумевает отображение прямоугольника с необходимым цветом фона внутри элемента управления Label и изменение его размера в зависимости от значения времени. Реализовать это можно с помощью CSS создав несколько описаний стилей классов, обозначенных в искодном коде пользовательского элемента управления, приведенного выше. Пример описания стилей, использованных для данного примера приведен ниже. body { font-family:Lucida Sans; font-size:1em; } .box { position: relative; width: 120px; border: 1px solid gray; padding: 2px; } .box .bar { background: lightblue; height: 1em; line-height: 1em; } .box .bar span 196 { } position: absolute; font-size:small; Теперь необходимо позаботиться о том, чтобы внутри созданного пользовательского элемента управления отображалось текущее значение времени. Для этого необходимо в обработчик события Page_Load пользовательского элемента ввести следующий код. protected void Page_Load(object sender, EventArgs e) { Label1.Text = DateTime.Now.ToString("hh:mm:ss"); timeLabel.Style.Add("width", (DateTime.Now.Second * 2).ToString() + "px"); } Из примера видно, что значению элемента Label1 присваивается значение текущего времени в формате час:минута:секунда. После этого к элементу timeLabel добавляется стиль, определяющий значение ширины элемента и в качестве значения ширины устанавливающий удвоенное значение текущего количества секунд. Здесь следует обратить внимание на то, что элемент timeLabel на самом деле является HTML элементом , в параметрах которого установлено значение runat=”server”, что делает возможным обращение к его параметрам на стороне сервера. Таким образом, после помещения элемента ShowTime в область страницы и запуска приложения, данный элемент управления будет отображать текущее время, значение которого будет обновляться каждый раз при обновлении страницы. Рис. 9.4. Отображение пользовательского элемента на странице без использования кэширования. Создадим копию пользовательского элемента управления ShowTime.ascx и назовем его ShowTime_cache.ascx. Установим для него режим кэширования. Для этого, также, как и в обычной HTML страницы введем директиву 197 Разместим элемент ShowTime_cache.ascx на странице и немго ее модифицируем для того, чтобы различать ранее созданный элемент, не поддерживающий кэширование и только что созданный элемент (Рис. 9.5). Рис. 9.5. Размещение пользовательских элементов управления без поддержки и с поддержкой кэширования. В результате, после запуска приложения получим следующий результат (Рис. 9.6). Элемент, не кэширующий свое содержимое Элемент, кэширующий свое содержимое Рис. 9.6. Результат работы программы с использование кэширующего и не кэширующего элементов управления. Еще одним из вариантов кэширования является кэширование пользовательского элемента управления, параметры которого задаются с помощью свойства класса. Для включения кэширования и установки длительности нахождения страницы в кэше в этом случае достаточно добавить свойство [PartialCaching(20)] public partial class ShowTime : System.Web.UI.UserControl { } 198 Рис. 9.7. Добавление пользовательского элемента управления, параметры кэширования которого задаются с помощью свойств класса. Использование элемента управления Substitution для реализации фрагментного кэширования Рассматривая примеры, приведенные выше нетрудно заметить, что параметры кэширования для страницы, на которую помещаются элементы управления не устанавливались. Это приводило к тому, что кэшировались только те элементы, для которых были установлены соответствующие параметры. Иногда бывает полезнее поступить наоборот, установить параметры кэширования для всей страницы, а для содержимого некоторых элементов сделать возможным отсутствие кэширования. В этом случае необходимо воспользоваться элементом Substitution. Для работы этому элементу управления необходимо название статического метода, возвращающего динамическое содержимое, отображаемое на странице. Элемент Substitution может быть размещен на странице так же как и любые другие элементы управления, что позволяет контролировать расположение его содержимого внутри страницы. В качестве примера создадим новую Web страницу. Установим для нее следующий параметр кэширования Поместим на вновь созданную страницу элемент ShowTime.ascx не осуществляющий кэширование, а также элемент Substitution, в качестве значения параметра MethodName которого установим GetTime. Это означает, что элемент Substitution при обращении к странице будет инициировать вызов процедуры GetTime. Эта процедура должна быть статической процедурой в составе класса страницы, возвращающая результат в виде строки. Именно возвращенное значение и будет подставлено в место расположения элемента Substitution. Код метода GetTime при этом может выглядеть следующим образом. public static string GetTime(HttpContext context) { return "Текущее время:" + DateTime.Now.ToString("hh:mm:ss"); } 199 Результат выполнения данной страницы представлен на рис. 9.8. Как видно, содержимое страницы кэшируется, однако элемент Substitution выводит на экран текущее значение времени, параметры кэширования к которому не применяются. Рис. 9.8. Использование фрагментного кэширования при помощи элемента Substitution. Рассмотренные способы предоставляют возможность использовать встроенные в ASP.NET возможности кэширования. Тем не менее, существует возможность использования собственного алгоритма кэширования данных, при котором в программном коде определяются принципы помещения и извлечения данных из кэша. 200 Занятие 10. Использование баз данных в приложениях ASP.NET Деятельность большинства web-приложений сосредоточена на извлечении, отображении и модификации данных. Данные задачи кажутся достаточно прямолинейными, но за последнее десятилетие способы использования данных постоянно менялись. Разработчики перешли от простых клиентских приложений с локальными базами данных к распределительным системам, основанным на централизованных базах данных и выделенных серверах. В тоже время развивались технологии доступа к данным. Сегодня свыше 95% всех информационных систем так или иначе используют базы данных. Вот почему вопросы организации взаимодействия приложения с базой данных являются актуальными, а внимание ведущих разработчиков программного обеспечения приковано к этой проблеме. Данная лекция посвящена изучению вопросов, связанных с организацией взаимодействия web приложений с базами данных. Основные сведения о модели доступа к данным ADO.NET ADO.NET представляет собой набор библиотек, входящих в Microsoft .NET Framework предназначенных для взаимодействия с различными хранилищами данных из .NET приложений. Библиотеки ADO.NET включают все необходимые классы для подключения к источникам данных практически произвольного формата, выполнения запросов к этим источникам и получения результата. Кроме того, несомненным достоинством ADO.NET является возможность работы с отсоединенными источниками данных, представляющих собой структуры, организующие данные в оперативной памяти компьютера, работать с которыми возможно с использованием ставших уже привычными средств доступа к данным. Таким образом, ADO.NET можно использовать в качестве надежного, иерархически организованного отсоединенного от источника кэша данных для автономной работы, что незаменимо при построении масштабируемых приложений, особенно ориентированных на Web. На сегодняшний день, ADO.NET является наиболее развитой технологией доступа к данным, среди технологий, разработанных корпорацией Microsoft. Она развивает те принципы, которые были заложены в таких технологиях как DAO и ADO, делая их более простыми в применении, более мощными и универсальными. В то же время, ADO.NET является другой технологией доступа к данным. Рассмотрим архитектуру ADO.NET. Так как ADO.NET представляет собой набор классов для организации взаимодействия клиентского приложения с базой данных, рассмотрим объектную модель ADO.NET. На рис. 10.1 показаны классы, составляющие объектную модель ADO.NET. 201 Connection DataSet Transaction DataAdapter Command DataTable DataView DataRow DataColumn Constraint Parameter DataReader DataRelation Рис. 10.1. Иерархия объектов ADO.NET Объекты, расположенные в левой части называются подсоединенными и необходимы для управления соединением, транзакциями, выборкой данных и передачей изменений данных в БД. Они непосредственно взаимодействуют с базой данных. Объекты, расположенные в правой части называются отсоединенными. Они позволяют работать с данными автономно. Отсоединенные объекты не взаимодействуют непосредственно с подсоединенными для получения данных из БД. Точнее, можно сказать, что они не взаимодействуют со всеми объектами левой части иерархии объектов. И, хотя вопросы заполнения данными отсоединенных объектов, будут обсуждаться позднее, можно сказать, что одной из форм взаимодействия этих двух групп является использованием объектом DataAdapter объекта DataSet для заполнения последнего набором данных, извлеченных непосредственно из БД. Одной из основных идей, лежащих в основе ADO.NET является наличие поставщиков данных. Поставщик данных – это набор классов, предназначенных для взаимодействия с хранилищем данных определенного типа. За счет этого, модель ADO.NET является чрезвычайно гибкой и расширяемой. Рассмотрим уровни модели поставщиков ADO.NET (Рис. 10.2). 202 Приложение .NET Поставщик SQL Server для .NET Поставщик OleDb для .NET Поставщик Oracle для .NET Поставщик OleDb База данных SQL Server Источник данных База данных Oracle Рис. 10.2. Уровни моделей поставщиков ADO.NET. Как видно из рис. 10.2, приложение .NET взаимодействует с базой данных посредством поставщиков данных. Каждый поставщик данных может обеспечивать доступ только к базе данных определенного формата. Так, для доступа к БД Microsoft SQL Server используется поставщик SQL Server для .NET, для доступа к БД Oracle – поставщик Oracle для .NET и т.д. Названные базы данных являются одними из самых распространенных во всем мире, поэтому поставщики данных для них выделены в отдельные элементы модели ADO.NET, тем не менее, существует множество баз данных другого формата, к которым необходимо осуществлять доступ из приложения .NET. Для этого может применяться поставщик данных OleDb для .NET или ODBC для .NET, которые обеспечивают доступ к любым данным, для которых существует драйвер OleDb, либо ODBC соответственно. При построении приложения, использующего доступ к данным, необходимо сперва попытаться найти «родного» поставщика данных для .NET. Если такового не существует, можно воспользоваться OleDb, если существует соответствующий драйвер для источника данных, к которому устанавливается подключение. Т.к. технология OleDb существует достаточно давно, существует много драйверов для различных источников данных, поддерживающих ее. Если в системе не установлен соответствующий драйвер OleDb, его можно попытаться найти на сайте разработчика той базы данных, к которой осуществляется доступ, либо на сайте Microsoft. То же самое справедливо и для технологи ODBC. В ряде случаев, существует несколько альтернативных вариантов организации доступа к данным определенного формата. Например, доступ к источнику данных на основе SQL Server можно организовать используя либо поставщик SQL Server для 203 .NET, либо поставщик OleDb. Тем не менее, всегда предпочтительнее использовать тот поставщик данных, который специально предназначен для обеспечения доступа к данному источнику данных, т.к. он учитывает его особенности. Каждый поставщик .NET реализует одинаковые базовые классы – Connection, Transaction, DataAdapter, Command, Parameter, DataReader имена которых зависят от поставщика. Например, у поставщика SQL Server существует объект SqlDataAdapter, у поставщика OleDb – OleDbDataAdapter и т.д. У каждого поставщика данных существует собственное пространство имен. Хотя все поставщики относятся к пространству имен System.Data, каждый из них содержит свой подраздел этого пространства, который содержит объекты, специфичные для данного поставщика. Например, объект SqlDataAdapter находится в пространстве имен System.Data.SqlClient. Все поставщики данных .NET реализуют одинаковые базовые функции, поэтому код создаваемый для доступа к данным выглядит приблизительно одинаково независимо от поставщика. Это означает, что рассматривая использование интерфейсов отдельного поставщика, мы фактически рассматриваем возможность использования этих же интерфейсов применительно и к другим поставщикам данных. При организации доступа к данным с помощью ADO.NET исключительно важную роль играют объекты, изображенные на рис. 10.1. Рассмотрим их более подробно. Объект Connection представляет соединение с источником данных. С его помощью можно задать тип источника данных, его местонахождения, параметры доступа и ряд других атрибутов. Перед тем, как начать взаимодействие с источником данных, необходимо установить подключение к нему с помощью объекта Connection. Объект Command представляет запрос к источнику данных, вызов хранимой процедуры или прямой запрос на возврат содержимого конкретной таблицы. Как известно, существует несколько типов запросов. Часть из них возвращают данные, извлекаемые из источника данных, другие – изменяют записи, третьи – управляют структурой БД. С помощью объекта Command возможно выполнить любой из перечисленных типов запросов. Различия в поведении объекта Command начинают проявляться тогда, когда необходимо исполнить тот или иной запрос. Так, например, при необходимости исполнения запроса, не возвращающего записи, необходимо использовать метод ExecuteNonQuery объекта Command. При извлечении данных из БД – метод ExecuteReader, который в свою очередь возвращает объект DataReader, позволяющий просматривать полученные в результате запроса записи. Объект DataReader предназначен для максимально быстрой выборки и просмотра возвращаемых запросом записей. Однако, он позволяет просматривать весь результирующий набор записей путем перемещения от одной записи к другой, и, таким образом, просматривать только одну запись за раз. Кроме того, DataReader не имеет возможности для обновления 204 значений записей, вследствие чего имеет возможность работать в режиме только для чтения, за счет чего обладает высокой производительностью. Объект Transaction позволяет осуществлять группировку записей в логическую единицу работы, называемой транзакцией. Транзакция логически объединяет несколько различных действий, связанных с манипулированием данными в единое целое. В процессе выполнения действий, осуществляемых в рамках транзакции, СУБД обычно кэширует изменения, вносимые этими действиями в данные до момента завершения транзакции. Это позволяет производить отмену любых изменений, выполненных в рамках транзакции в случае, если хотя бы одно из действий транзакции завершилось неудачно. Объект Parameter позволяет вводить в запрос элемент, значение которого может быть задано непосредственно перед исполнением запроса. За счет этого отпадает необходимость каждый раз изменять текст самого запроса. Объект DataAdapter представляет собой связующее звено между отсоединенными объектами ADO.NET и базой данных. С его помощью осуществляется заполнение таких объектов как DataSet или DataTable значениями, полученными в результате выполнения запроса к базе данных для последующей автономной работы с ними. Помимо этого, DataAdapter реализует эффективный механизм выполнения обновления данных, хранимых в базе данных изменениями, внесенными в данные объектов DataSet и DataTable. Объект DataTable позволяет просматривать данные в виде наборов записей и столбцов. Фактически, он представляет собой аналог таблицы БД, размещенный в памяти. Достоинством такой организации является возможность автономной работы с данными. Это означает, что после установления соединения с базой данных, чтения данных и заполнения ими объекта DataTable можно отключиться от источника данных и продолжать работать с ним в автономном режиме. Такая возможность чрезвычайно полезна при организации Web приложений, которые должны быть хорошо масштабируемыми и ориентированы на многопользовательский режим работы. При этом, однако, возникают и побочные эффекты, один из которых связан с тем, что человек, работающий с автономным набором данных не может увидеть изменений, вносимых в данные в этот момент другими пользователями. Объект DataColumn представляет собой столбец объекта DataTable. Набор же всех столбов объекта DataTable представляет собой коллекцию Columns. Постредством этого объекта можно получить доступ к значению ячейки соответствующего столбца. Объект DataRow представляет собой строку объекта DataTable. Набор всех строк этого объекта представляет собой коллекцию Rows. DataRow очень часто используется для доступа к значению конкретного поля определенной записи. При этом применяется свойство Item. Объект DataSet представляет собой отсоединенный набор данных, который может рассматриваться как контейнер для объектов DataTable. 205 позволяет организовывать внутри себя структуру, полностью соответствующую реальной структуре таблиц и связей между ними в БД. Это удобно в том случае, когда при работе с базой данных необходимы данные из разных таблиц. В этом случае, вместо того, чтобы многократно обращаться к серверу и выбирать данные из одной таблицы за раз, можно поместить все данные в объект DataSet, а затем передать его клиентскому приложению. DataSet является очень мощным инструментом для работы с отсоединенными наборами данных. Все изменения, вносимые в данные, хранящиеся в DataSet кэшируются в объектах DataRow. Когда возникает необходимость передачи изменений из DataSet в БД возможно осуществить передачу только измененных данных, вместо того, чтобы передавать все данные объекта, что значительно снижает объем данных, передаваемых между клиентским компьютером и сервером. Объект DataRelation представляет собой описание связей между таблицами реляционной базы данных. Он предоставляется объектом DataSet и позволяет организовывать взаимосвязи между таблицами отсоединенного набора данных объекта DataSet. Объект DataRelation выполняет функцию аналогичную той, которую выполняют связи, определяемые в СУБД между таблицами при создании структур данных. Это касается и соблюдение принципов ссылочной целостности информации. Например, DataRelation можно настроить таким образом, чтобы изменение значение первичного ключа родительской таблицы автоматически передавались (каскадировались) дочерним записям, а при удалении записи в родительской таблице, автоматически удалялись записи в дочерних таблицах, связанных с ней. Объект DataView предназначен для организации возможности просмотра содержимого DataTable различными способами. Это относится к таким операциям, как сортировка и фильтрация записей. С помощью DataView допускается просматривать содержимое одного объекта DataTable с различными установками фильтрации и сортировки. Для этого необходимо использовать два различных объекта DataView, связанных с одним объектом DataTable. Такая возможность исключает необходимость хранения одного набора данных в двух разных структурах. Более подробно использование всех объектов модели ADO.NET будет рассмотрено ниже. DataSet Организация взаимодействия с БД Для того, чтобы приложение .NET могло осуществлять взаимодействие с источником данных, необходимо установить соединение с ним. Наиболее типичным сценарием работы Web приложения является следующий. 1. Устанавливается соединение, открывается подключение к базе данных. 2. Выполняется один или несколько запросов, осуществляющих внесение изменений в наборы данных источника данных, а также выборку данных из БД. 206 3. Осуществляется отключение от источника данных. При этом пользователь работает с отсоединенным набором данных, просматривая его, выполняя фильтрацию, внося изменения и т.д. 4. При необходимости переноса изменений, из отсоединенного набора данных в БД, а также при необходимости просмотра изменений, внесенных в БД другими пользователями, осуществляется подключение к источнику данных, выполняются необходимые действия, после чего производится отключение от БД. Рассмотрим описанные выше этапы более подробно. Подключение к БД Как уже говорилось выше, для подключения к источнику данных с использованием ADO.NET необходимо воспользоваться объектом Connection. Для рассмотрения примеров подключений к источникам данных, прежде всего, необходимо выбрать поставщика данных, с которым будет работать приложение. Затем необходимо подключить соответствующие пространства имен, содержащие определения объектов выбранного поставщика. В качестве примеров подключения к источникам данных будем рассматривать базы данных Access и SQL Server 2005 Express Edition. Такой выбор продиктован прежде всего тем, что Access очень хорошо подходит для построения небольших информационных систем, нетребователен к ресурсам компьютера, не требует установки специального программного обеспечения. SQL Server Express является свободно распространяемой СУБД, обладающей всеми достоинствами коммерческого SQL Server 2005 и обладающего рядом ограничений, в том числе на максимальный объем базы данных (не более 4 Гб). Все приемы работы с базами данных, описываемые ниже могут быть использованы также для работы с коммерческими версиями продуктов Microsoft, и другими СУБД. Итак, для того, чтобы подключиться к базе данных во время выполнения программы, необходимо создать объект Connection, а также задать его свойства, определяющие текущие параметры подключения. Основным параметром, устанавливающим необходимые опции для подключения к БД является строка соединения, которая представляет собой набор пар «имя-значение», разделенных точкой с запятой. Порядок следования значений этих параметров, а также их регистр не важны. Строка соединения зависит от СУБД, к которой осуществляется подключение, а также от используемого поставщика данных. Тем не менее, существует несколько фрагментов информации, указываемой в строке подключения, необходимых практически всегда. Перечислим их и прокомментируем их назначение. 1. Сервер, на котором находится база данных. Если СУБД, к которой осуществляется подключение расположена на клиентском компьютере (так будет во всех примерах, рассматриваемых в рамках данного курса), вместо имени сервера необходимо указывать имя localhost, либо IP адрес 127.0.0.1. 207 2. Имя базы данных, к которой производится подключение. 3. Способ аутентификации пользователя. Существующие клиентсерверные СУБД (к которым относится SQL Server, Oracle и ряд других) позволяют указывать в строке подключения имя пользователя и пароль, которые будут использоваться для проверки возможности доступа к базе данных, либо использовать параметры текущего пользователя. В качестве примера рассмотрим создание небольшого Web приложения для работы с базой данных, ER модель которой представлена на рис. 10.3. Товары Закупки КодТовара входят в НаименованиеТовара Цена осущ ествляют Контрагенты КодОперации ДатаОперации Количество Цена КодКонтрагента (FK) КодТовара (FK) КодКонтрагента НаименованиеКонтрагента Рис. 10.3. ER модель фрагмента базы данных. Создадим базы данных в соответствии с моделью, изображенной на рис. 10.3 в формате SQL Server Express и Access 2003. Для этого исполним следующие SQL запросы. CREATE DATABASE TEST_DB CREATE TABLE Товары (КодТовара INTEGER NOT NULL, НаименованиеТовара VARCHAR (50) NOT NULL, Цена FLOAT, PRIMARY KEY (КодТовара)) CREATE TABLE Контрагенты (КодКонтрагента INTEGER NOT NULL, НаименованиеКонтрагента VARCHAR (50) NOT NULL, PRIMARY KEY (КодКонтрагента)) CREATE TABLE Закупки (КодОперации INTEGER NOT NULL, ДатаОперации DATETIME NOT NULL, Количество FLOAT, Цена FLOAT, КодКонтрагента INTEGER NOT NULL, КодТовара INTEGER NOT NULL, PRIMARY KEY (КодОперации), 208 CONSTRAINT Входят_в FOREIGN KEY (КодТовара) REFERENCES Товары, CONSTRAINT Осуществляют FOREIGN KEY (КодКонтрагента) REFERENCES Контрагенты) Рис. 10.4. Диаграмма базы данных в SQL Server 2005 Express. Создание Web приложения для работы с базами данных различных видов практически идентичны. Основное различие заключается в способе организации доступа к самой базе, т.е. в способе подключения к ней. Для подключения к БД используется объект Connection из пространства имен System.Data.SqlClient в случае с SQL Server и System.Data.OleDb в случае с другими источниками данных, например Access. Строки соединения с базами данных при этом будут выглядеть следующим образом. string strSqlConnection = "Data Source=localhost\\sqlexpress;Initial Catalog=TEST_DB;Integrated Security=SSPI"; string strOleDbConnection = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\\Projects\\Ex_Db\\App_Data\\TEST_DB.mdb"; Для окрытия соединения с базой данных необходимо вызвать метод Open объекта Connection. При этом, строку соединения с БД можно передать как в качестве параметра конструктора объекта, так и потом с помощью установки соответствующего свойства. SqlConnection sqlCon=new SqlConnection(strSqlConnection); sqlCon.Open(); OleDbConnection oleDbCon=new OleDbConnection(); oleDbCon.ConnectionString=strOleDbConnection; oleDbCon.Open(); Строку соединения с базой данных можно жестко прописать в исходном коде приложения, однако, это не самый лучший вариант, т.к. при изменении пути к базе данных, либо других параметров соединения придется вносить изменения в исходный код приложения и перекомпилировать его. В связи с этим, лучше всего использовать строку соединения, сохраненную в файле web.config. В последствии эту строку можно извлечь из файла web.config. string strOleDbConnection = WebConfigurationManager.ConnectionStrings ["TEST_DBConnectionString"].ConnectionString; Управление соединением осуществляется очень легко. Методы Open и Close объекта Connection выполняют всю работу. Однако, следует учитывать, что при подключении к базе данных может произойти сбой, в результате которого установить соединение с ней окажется невозможно. Это может быть особенно актуальным при размещении базы данных на другом сервере, который в момент подключения может оказаться недоступен. Для того, чтобы неудавшаяся попытка соединения с базой данных не приводила к фатальным последствиям при работе приложения, необходимо использовать конструкции try catch, позволяющие адекватно реагировать на возникшую ошибку. Следующий пример демонстрирует возможность использования такого подхода. try { sqlCon.Open(); lbl_DB.Text = "Сервер:"+sqlCon.ServerVersion; lbl_DB.Text += "Соединение:" + sqlCon.ToString(); } catch(Exception ex) { lbl_DB.Text = "При соединении с БД произошла ошибка "; lbl_DB.Text += ex.Message; } finally { sqlCon.Close(); lbl_DB.Text += "Соединение:"; lbl_DB.Text += sqlCon.State.ToString(); } Результатом работы этого фрагмента кода в случае успешного установления соединения будет сообщение, изображенное на рис. 10.5. На рис. 10.6 изображено окно, содержащее сообщение об ошибке установления соединения. 210 Рис. 10.5. Окно сообщения о параметрах и состоянии текущего соединения с БД. Рис. 10.6. Окно сообщения об ошибке в момент подключения к БД. Для обслуживания соединения с базой данных расходуются ресурсы компьютера, на котором выполняется Web приложение. В связи с этим, рекомендуется открывать соединение как можно позже, а закрывать как можно раньше, сразу после того, как все необходимые действия с объектами базы данных были выполнены. Кроме того, желательно строить программный код так, чтобы соединение с базой данных закрывалось при любом исходе соединения с ней. В предыдущем примере, код, расположенный в блоке finally выполнится при любом исходе попытки подключения к ней, что гарантирует освобождение ресурсов сервера не дожидаясь момента, когда память будет освобождена сборщиком мусора. Выполнение команд над наборами данных Установление соединения является необходимым условием подключения к базе данных. Однако, подключение необходимо для того, 211 чтобы выполнять определенные команды над элементами базы. Рассмотрим возможности выполнения команд над источниками данных. Одним из основных элементов из набора классов ADO.NET, способным выполнять любой SQL оператор является класс Command. Для того, чтобы использовать класс Command необходимо установить тип команды, установить текст запроса SQL и привязать ее к соединению с БД. Существует 3 типа команд класса Command. CommandType.Text CommandType. StoredProcedure CommandType. TableDirect Выполнение прямого оператора SQL, текст которого устанавливается в свойстве CommandText. Это значение по умолчанию. Выполнение хранимой процедуры, имя которой установлено в свойстве CommandText. Выполняет опрос всей таблицы базы данных, имя которой задается в свойстве CommandText. Этот тип команды используется для обратной совместимости с некоторыми драйверами OLE DB и не поддерживается поставщиком данных SQL Server. Пример создания объекта Command для выполнения SQL запроса и хранимой процедуры представлен ниже. SqlCommand cmd_SQL = new SqlCommand("Select * From Товары",sqlCon); cmd_SQL.CommandType = CommandType.Text; SqlCommand cmd_Proc=new SqlCommand("GetGoods",sqlCon); cmd_Proc.CommandType = CommandType.StoredProcedure; Для выполнения созданной команды необходимо использовать один из следующих методов. ExecuteReader() ExecuteNonQuery() ExecuteScalar() Выполнение запроса SQL и возврат объекта DataReader, представляющего однонаправленный курсор, с доступом только для чтения. Выполнение SQL команд, предназначенных для вставки, изменения, удаления записей БД. Результатом работы команды является количество строк, обработанных командой. Выполнение SQL команды и возврат первой строки результата запроса. Обычно используется для выполнения команды, содержащей агрегирующие функции типа COUNT(), MAX() и т.д. При использовании метода ExecuteReader() создается объект DataReader, с помощью которого можно организовать перебор всех строк возвращенного набора данных. Для этого необходимо использовать цикл по строкам 212 результирующего набора данных. Объект DataReader представляет собой один из самых простых и быстрых способов получения доступа к данным БД. В следующем примере демонстрируется способ отображения содержимого таблицы Товары в виде списка. SqlDataReader rdr_SQL = cmd_SQL.ExecuteReader(); StringBuilder strResult=new StringBuilder(""); while (rdr_SQL.Read()) { strResult.Append(" Обращение к полям таблицы может происходить как по имени, так и с помощью индекса. В случае с индексом, необходимо применять метод, соответствующий типу данных того поля, к которому происходит обращение. После завершения обращения к данным с помощью объекта DataReader необходимо закрыть модуль чтения. В данном примере используется класс StringBuilder, который, как уже демонстрировалось ранее более эффективно работает со строками. Те же самые операции со строками возможны и с использованием стандартной операции конкатенации. Результат работы программы представлен на рис.??? Рис. 10.7. Построение списка на основе содержимого таблицы Товары. Использованный подход достаточно трудоемок для того, чтобы его применять на практике для вывода информации, содержащейся в базе данных. В ASP.NET существует несколько более мощных классов, 213 способных производить данную операцию. Одним из таких классов является класс GridView. Применение данного класса позволяет осуществлять вывод информации на экран очень простым способом. В следующем примере показан способ вывода информации, получаемой из БД посредством объекта DataReader. GridView1.DataSource = rdr_SQL; GridView1.DataBind(); Результат работы данного кода представлен на рис. 10.8. Рис. 10.8. Результат работы программы, отображающей информацию с помощью GridView. Метод ExecuteNonQuery() используется для выполнения команд, которые не возвращают результирующих наборов данных. К таким командам относятся команды вставки, удаления и обновления данных. Результатом работы метода ExecuteNonQuery() является количество обработанных записей. В следующем примере демонстрируется возможность удаления товара из таблицы товары. string strSqlConnection = "Data Source=localhost\\sqlexpress;Initial Catalog=TEST_DB;Integrated Security=SSPI"; sqlCon = new SqlConnection(strSqlConnection); SqlCommand cmdDelete = new SqlCommand("DELETE FROM Товары WHERE КодТовара=5",sqlCon); try { sqlCon.Open(); int n = cmdDelete.ExecuteNonQuery(); lbl_Delete.Text += String.Format("Удалено {0} записей",n); } catch (SqlException ex) { lbl_Delete.Text += String.Format("Ошибка: {0}",ex.Message); } finally { 214 } sqlCon.Close(); В результате работы данного примера, в случае успешного выполнения запроса, будет сформировано следующее сообщение. Рис. 10.9. Результат работы программы, удаляющей запись из таблицы Товары. В реальных ситуациях практически всегда пользователю требуется ввести некоторое условие, в соответствии с которым будет выполняться запрос SQL. Таким условием, например, может быть ввод наименования товара, подлежащего удалению. Для реализации такой возможности в предыдущем примере, добавим элемент управления TextBox, предназначенный для ввода наименования товара, который необходимо удалить, а также изменим текст строки, формирующей команду следующим образом. SqlCommand cmdDelete = new SqlCommand("DELETE FROM Товары WHERE НаименованиеТовара='"+tb_Delete.Text+"'",sqlCon); Как видно, в данном случае, строка SQL запроса удаления записи из таблицы Товары формируется динамически в зависимости от введенного в элемент tb_Delete значения. Такая практика динамического формирования запросов в реальных приложениях допустима, однако, при этом могут возникать проблемы, связанные с безопасностью Web приложения, например атаки внедрением SQL. Подробнее о такого рода атаках можно прочитать в [1]. Суть этого вида нарушения безопасности приложения состоит в том, что пользователь может ввести в поле ввода параметра текст, отличный от того, чего от него ожидает приложение. Это может приводить к тому, что текст SQL запроса фактически изменяется и в результате выполняет не то действие, на которое рассчитывал программист при его реализации. Например, если в предыдущем примере в элемент tb_Delete ввести следующий текст «Какой-то товар' OR '1'='1», то в результате будут удалены 215 все записи из таблицы Товары, т.к. текст SQL запроса в этом случае, после подстановки значения введенного в элемент tb_Delete будет "DELETE '1'='1'" FROM Товары WHERE НаименованиеТовара='Какой-то товар' OR Как видно из этого примера, наименование введенного товара становится неважным, т.к. условие всегда будет истинным за счет добавления оператора OR с заведомо верным условием. Можно привести примеры и более сложных атак внедрением SQL, приводящих к еще более серьезным и масштабным последствиям и создающих реальную угрозу функционирования всего Web приложения. Решением данной проблемы может быть несколько. Во-первых, можно постараться проанализировать текст, введенный пользователем до того, как он будет подставлен в текст SQL запроса. При этом можно анализировать длину введенного текста (если длина превышает стандартную длину вводимых данных, ожидаемых от пользователя, следует предпринять определенные действия и возможно заблокировать выполнение запроса), можно заменить все одиночные кавычки двумя одиночными кавычками. Хорошей практикой в этом случае считается обработка исключительных ситуаций, возникающих при работе с базой данных и выдача соответствующих сообщений об ошибке. При этом текст сообщения об ошибке должен содержать только общую формулировку возникшей ошибки, т.е. не стоит выводить текст ошибки, передаваемый в объекте Exception. Можно придумать и массу других алгоритмов анализа вводимых данных, но все равно они не могут гарантировать сто процентной безопасности Вашего приложения. Лучшей практикой защиты от атаки внедрением SQL является использование параметризованных команд. Кроме того, параметризованные команды SQL более просты при формировании и применении. Использование параметризованных команд Параметризованная команда – это обычная SQL команда, в тексте которой используются специальные символы, указывающие место для динамической подстановки значений, передаваемых объекту Command через коллекцию Parameters. Например, команда удаления записи из таблицы Товары, использованная выше, будет выглядеть следующим образом при использовании параметра. DELETE FROM Товары WHERE НаименованиеТовара=@ProductName Здесь @ProductName представляет собой параметр, значение которого должно быть установлено до того, как будет запущено выполнение запроса. Синтаксис параметризованных команд отличается в разных поставщиках данных. Приведенный выше пример справедлив для взаимодействия с SQL 216 Server. Для использования той же команды при взаимодействии с Access тот же запрос должен выглядеть следующим образом. DELETE FROM Товары WHERE НаименованиеТовара=? Текст программы, реализующей подключение к наборам данных SQL Server и Access, установку значений параметров запросов и их исполнение приведен ниже. OleDbConnection AccessCon=new OleDbConnection(strOleDbConnection); string strSQLServer = "DELETE FROM Товары WHERE НаименованиеТовара=@ProductName"; string strAccess = "DELETE FROM Товары WHERE НаименованиеТовара=?"; SqlCommand cmdDeleteSQLServer = new SqlCommand(strSQLServer,sqlCon); OleDbCommand cmdDeleteAccess=new OleDbCommand(strAccess,AccessCon); try { cmdDeleteSQLServer.Parameters.Add("@ProductName", tb_Delete.Text); sqlCon.Open(); int n = cmdDeleteSQLServer.ExecuteNonQuery(); lbl_Delete.Text += String.Format("Из базы данных SQL Server удалено {0} записей",n); cmdDeleteAccess.Parameters.Add("ProductName",tb_Delete.Text); AccessCon.Open(); int k = cmdDeleteAccess.ExecuteNonQuery(); lbl_Delete.Text += String.Format("Из базы данных Access удалено {0} записей", k); } catch (Exception ex) { lbl_Delete.Text += String.Format("Ошибка: {0}",ex.Message); } finally { sqlCon.Close(); AccessCon.Close(); } Результат работы программы изображен на рис. 10.10. 217 Рис. 10.10. Результат работы программы удаления записи из таблицы Товары баз данных SQL Server и Access. Кроме того, что работа с параметризованными запросами проще с точки зрения динамического формирования текста самого запроса, она еще обеспечивает защиту от атаки внедрением SQL. Так, предыдущий пример использования такой атаки в данном случае не работает. Использование хранимых процедур При использовании сложный СУБД, реализующий полный набор функций клиент-серверной СУБД, разработчик приложения, взаимодействующего с базой данных получает в свое распоряжение несколько дополнительных очень мощных инструментов, позволяющих облегчить, ускорить и обезопасить процесс взаимодействия с базой данных по сравнению с локальными базами данных. Одними из основных таких возможностей являются способы организации и использования хранимых процедур и триггеров. Вопросы создания и программирования хранимых процедур и триггеров, а также всевозможные вопросы, связанные с особенностями их реализации в конкретных СУБД выходят за рамки данного курса. Желающим более подробно ознакомиться с особенностями реализации такого рода элементов рекомендуется ознакомиться с [5,6]. Здесь же будут в основном рассмотрены вопросы использования хранимых процедур при работе с SQL Server без объяснения того, как именно создавалась та или иная хранимая процедура. Хранимая процедура представляет собой набор операторов SQL, сохраненный в базе данных (на стороне сервера), имеющий имя, а также набор параметров, посредством которых могут быть переданы значения в хранимую процедуру. Они подобны процедурам, используемых в языках программирования с той лишь разницей, что в данном случае, предназначены для обработки данных, содержащихся в базе данных. Хранимые процедуры обладают рядом преимуществ, главными из которых являются такие, как повышение быстродействия приложения при 218 использовании хранимых процедур для обработки данных, повышение безопасности приложения при работе с данными. В реальных приложениях, если используется СУБД, поддерживающая возможности использования хранимых процедур, рекомендуется воспользоваться этим и создавать приложение с их использованием. Кроме того, рекомендуется все наиболее типичные и часто возникающие действия, связанные с обработкой данных, содержащихся в базе данных, а также операции вставки, удаления и изменения производить с помощью вызова предварительно созданных хранимых процедур. Рассмотрим пример создания и использования простой хранимой процедуры, позволяющей добавить новый товар в таблицу Товары базы данных. Текст хранимой процедуры, реализованной в СУБД SQL Server 2005 выглядит следующим образом. CREATE PROCEDURE AddProduct @ProductID int, @ProductName varchar(100), @ProductPrice float AS INSERT INTO Товары VALUES(@ProductID,@ProductName,@ProductPrice) Данная хранимая процедура использует три значения переменных для формирования запроса на добавление данных в таблицу Товары. Для ее вызова можно воспользоваться объектом SqlCommand. Текст программы, реализующей вызов данной хранимой процедуры приведен ниже. 4)); SqlCommand cmd_SQL=new SqlCommand("AddProduct",sqlCon); cmd_SQL.CommandType = CommandType.StoredProcedure; string[] strProduct = tb_AddProduct.Text.Split(new char[] {','}); cmd_SQL.Parameters.Add(new SqlParameter("@ProductID", SqlDbType.Int, cmd_SQL.Parameters["@ProductID"].Value = Convert.ToInt32(strProduct[0]); cmd_SQL.Parameters.Add(new SqlParameter("@ProductName", SqlDbType.NVarChar, 100)); cmd_SQL.Parameters["@ProductName"].Value = strProduct[1]; cmd_SQL.Parameters.Add(new SqlParameter("@ProductPrice", SqlDbType.Float, 8)); cmd_SQL.Parameters["@ProductPrice"].Value = Convert.ToDouble(strProduct[2]); try { sqlCon.Open(); int k = cmd_SQL.ExecuteNonQuery(); } finally { sqlCon.Close(); } 219 В процессе работы данной программы, введенный пользователем текст в элемент TextBox преобразуется в массив строк. В коллекцию Parameters объекта SqlConnection добавляется три параметра, соответствующих параметрам хранимой процедуры. Для них устанавливаются значения, ранее помещенные в массив строк. После чего открывается соединение и посылается запрос на исполнение хранимой процедуры постредством команды ExecuteNonQuery(). Из данного примера видно, что при добавлении нового параметра, выступающего в роли параметра хранимой процедуры, необходимо обязательно указывать его тип, а также длину (для строковых типов это количество символов, для числовых – количество байт). Отсоединенные наборы данных Рассмотренная ранее логика взаимодействия клиентского приложения с базой данных, основанная на соединении, оправдана для реализации односторонней связи с базой данных. Это может быть либо получение данных, либо выполнение запросов, связанных с внесением изменений в нее. Реализация же сложных операций взаимодействия с БД при этом является очень трудоемким процессом и требует написания большого количества программного кода, его отладки и т.д. ADO.NET предоставляет более совершенные способы организации двустороннего взаимодействия приложения с базой данных, основанной на отсоединенных наборах данных. Основной идеей использования отсоединенных наборов данных является изменения алгоритмов взаимодействия приложения с базой данных за счет подключения к набору данных, выполнения запроса и создания копии данных на стороне клиента, отключения от БД, осуществление манипуляций с данными на стороне клиента, при необходимости внесения изменений в базу данных, подключение к ней, передача изменений и отключение. Таким образом, все основные манипуляции с данными происходят в отсоединенной наборе данных, представляющем собой копию данных, хранящихся в БД, а внесение изменений происходит в одной пакетной операции. Все это уменьшает время, в течение которого должно быть открыто соединение с БД, ускоряет работу и упрощает логику взаимодействия приложения с данными. Рассмотрим объекта ADO.NET, реализующие данный механизм. Класс DataSet Объект DataSet представляет собой контейнер, содержащий объекты DataTable и Relation. DataTable представляет собой таблицу, состоящую из строк и столбцов. Строки таблицы представлены объектом DataRow, который, в свою очередь, представляет собой коллекцию столбцов таблицы (объект DataColumn). Данные в DataSet отсоединены от БД. Все изменения данных кэшируются в объектах DataRow. При возникновении необходимости передачи изменений в данных объекта DataSet существует возможность передачи только изменившейся части данных, что позволяет значительно экономить ресурсы канала связи, т.к. в этом случае передается гораздо меньший объем 220 данных. Обобщая все вышесказанное можно сделать вывод о том, что использование объекта DataSet в ряде случаев оказывается более эффективным, чем DataReader. Наиболее типичными ситуациями, в которых рекомендуется применять объект DataSet являются. 1. Необходимость реализации сериализации данных на диск. DataSet позволяет легко сохранять данные в файле XML. При этом возможны варианты сохранения только данных, только структуры данных, либо и того и другого. 2. Необходимость организации навигации по набору данных в двух направлениях. Как уже упоминалось, DataReader обеспечивает перемещение по набору только вперед. С помощью же DataSet возможна организация постраничного просмотра данных. 3. Необходима привязка нескольких элементов управления к одному набору данных. DataSet, в отличие от DataReader содержит средства организации сортировки и фильтрации данных. Более подробная информация относительно сценариев использования DataSet находится в [1]. Важно!!! Большинство Web приложений использует DataSet для извлечения данных из базы данных и показа их на странице. Для обновления же данных в БД используются прямые команды. Использование DataSet Выше уже говорилось о том, что DataSet состоит из таблиц, которые в свою очередь состоят из строк, а они, в свою очередь, из столбцов. Каждый из перечисленных элементов реализован в виде класса. Для управления автономными изменениями DataSet отслеживает информацию о версии каждого объекта DataRow. Это означает, что когда происходит редактирование строки, ее исходное значение сохраняется в памяти, а строка помечается как измененная. Аналогичные действия происходят и при добавлении и изменении строк DataTable. В дальнейшем, в базу данных можно перенести значения только тех строк, которые были затронуты изменениями. Таким образом, DataSet никогда не поддерживает постоянного соединения с базой данных. Для извлечения данных из базы данных и наполнения ими объекта DataSet необходимо использовать еще один объект DataAdapter, который помимо прочего позволяет обновлять данные БД на основе внесенных в DataSet изменений. Класс DataAdapter является связующим звеном между базой данных и DataSet. Точнее, он связывает БД и объект DataTable, расположенный внутри DataSet. DataAdapter содержит три основных метода, позволяющих ему выполнять все необходимые операции, связанные с извлечением и обновлением данных. DataAdapter 221 Fill FillSchema() Update() Выполнение запроса типа Select, определенного в свойстве SelectCommand и добавление таблицы, получаемой в результате данного запроса в DataSet. Выполнение запроса типа Select, текст которого расположен в свойстве SelectCommand и добавление таблицы, содержащей только структуру данных полученных в результате выполнения запроса в DataSet. Применяет все изменения, внесенные в DataTable к источнику данных. При этом исполняются команды вставки, обновления и удаления, расположенные в свойствах InsertCommand, UpdateCommand, DeleteCommand. Рассмотрим пример использования объектов DataSet и DataAdapter для извлечения данных из БД, наполнения DataSet и отображения данных на странице Web приложения. Прежде всего необходимо установить подключение к источнику данных. Для этого необходимо использовать строку подключения, а также объект Connection. В данном примере подключение будет происходить к базе данных Test_Db, расположенной на локальном сервере SQL Server Express 2005. string strCon = "Server=.\SQLEXPRESS;Integrated Security=SSPI;Initial Catalog=Test_Db"; string sqlString = "SELECT * FROM Товары"; SqlConnection sqlCon = new SqlConnection(strCon); Создадим объект DataAdapter и передадим ему в качестве параметров строку запроса, а также строку подключения к БД. SqlDataAdapter da = new SqlDataAdapter(sqlString,sqlCon); Теперь необходимо создать объект помощью DataAdapter. DataSet и заполнить его данными с DataSet ds = new DataSet(); da.Fill(ds, "Goods"); Из приведенного выше примера видно, что метод Fill объекта da, в качестве параметров использует имя объекта DataSet, в который необходимо поместить данные, возвращаемые в результате выполнения запроса, определенного в строке sqlString. Во втором параметре можно указать имя, которое будет сопоставлено с таблицей, созданной в DataSet. Из примера видно, что в данном случае не используется явный вызов метода Open объекта Connection. Это объясняется тем, что данный метод вызывается неявно при исполнении метода Fill. Таким образом, DataAdapter сперва открывает 222 соединение с БД, затем выполняет необходимые манипуляции с данными, после чего закрывает открытое ранее соединение. Если по каким-то причинам такой алгоритм взаимодействия с базой данных требуется изменить, необходимо до вызова метода Fill открыть соединение с БД. В этом случае DataAdapter будет использовать уже существующее соединение. После того, как необходимый набор данных был извлечен из базы данных, его необходимо отобразить на Web странице. Для этого можно воспользоваться способом, описанным ранее, при рассмотрении объекта DataReader. Однако, в данном случае, гораздо более эффективного использования можно добиться с помощью использования возможностей привязки данных к визуальным объектам, способным выводить данные на экран. Основная идея, лежащая в основе привязки данных, заключается в создании связи между объектом данных и элементом управления. Всю дальнейшую работу, связанную с извлечением данных и выводом их на экран выполняет ASP.NET. Рассмотрим основные способы привязки данных и особенности их вывода на экран, реализованные в различных элементах управления. Привязка и отображение данных Привязка данных является очень мощным средством организации автоматического отображения данных в элементе управления без использования программирования. Большинство элементов управления ASP.NET поддерживают привязку данных. При этом привязка данных может быть как данных с одним значением, так и с множественными значениями. Привязка с одним значением означает, что элемент управления может отображать единственное значение, извлекаемое из источника данных. Такие принципы используются элементами управления TextBox, LinkButton, Image, HyperLink. Привязка с множественными значениями означает, что элемент управления может отображать несколько значений, извлекаемых из источника данных. Элементы управления, поддерживающие привязку с множественными значениями строятся на основе списков и электронных таблиц. Типичными представителями таких элементов управления являются ListBox и GridView. Для привязки элементов управления к источнику данных необходимо установить значение свойства DataSource, в котором указать наименование объекта, содержащего данные, необходимые для отображения. При установке свойства DataSource создается логическая связь между элементом управления, способным отображать данные и объектом данных, подлежащих отображению. После того, как источник данных определен, необходимо заполнить его данными. Для этого используется метод DataBind() элемента управления, заполняемого данными, который извлекает данные из источника, проходит в цикле по источнику, определенному в DataSource и обновляет страницу. Привязка с множественными значениями является наиболее мощным типом привязки, так как в этом случае не требуется 223 программировать циклический перебор значений источника данных и вывод их на экран, эта логика уже реализована в элементе управления, поддерживающего множественную привязку. Рассмотрим основные принципы использования обоих типов привязки данных. Привязка с одним значением Элементы управления, поддерживающие привязку данных с одним значением позволяют осуществить привязку некоторых из их свойств к данным с помощью выражения привязки данных. Выражение привязки данных вводится в тексте страницы .aspx и заключается внутри ограничителей . В качестве выражения привязки данных может быть использована общедоступная, либо защищенная переменная, а также любое другое выражение, которое может быть вычислено в момент выполнения страницы. Так, в качестве выражения привязки данных допустимо использовать функции, свойства, объекты, определенные в классе страницы. Для того, чтобы вычислить выражение привязки, необходимо в коде приложения вызвать метод Page.DataBind(). В момент вызова данного метода, ASP.NET проверяет все выражения на текущей странице, при необходимости производит вычисления и заменяет их соответствующими значениями. В качестве примера рассмотрим привязку элемента Label к данным таблицы Товары. Для этого создадим метод GetProductName(), возвращающий в качестве значения содержимое первой строки столбца НаименованиеТовара таблицы Товары. public string GetProductName() { return ds.Tables["Goods"].Rows[0]["НаименованиеТовара"].ToString(); } Теперь необходимо добавить элемент Label на страницу .aspx и установить выражение привязки для свойства Text данного элемента. "> ID="Label1" runat="server" Text=" Следует обратить внимание на то, что свойство AutoPostBack элемента DropDownList установлено равным true. Это необходимо для обеспечения автоматического инициирования обратной отсылки при изменении текущего элемента списка. Это необходимо для того, чтобы страница изменялась при выборе другого элемента списка. Необходимо сделать так, чтобы при выборе города из списка, содержимое элемента GridView, отображающего закупки обновлялось. Для реализации данного механизма поместим на форму элементы GridView и еще один SqlDataSource. Настроим SqlDataSource в соответствии со следующим определением. <SelectParameters> Ключевыми элементами настройки SqlDataSource являются строка запроса, определенная в свойстве SelectCommand и параметры, определенные в разделе <SelectParameters>. В строке запроса определен параметр @Product, который используется для фильтрации списка закупок по значению кода товара. В разделе параметров определен один параметр, для которого установлено имя Product. Значение для этого параметра извлекается из свойства SelectedValue объекта DropDownList1. Пример экрана браузера, получаемого после запуска данного приложения представлен на рис. 10.24. 236 Рис. 10.24. Результат работы приложения, содержащего главную и подчиненную таблицы. В данном примере значение параметра Product подставляется автоматически из свойства SelectedValue элемента DropDownList. Тем не менее, существует возможность использования и других элементов управления, значения свойств которых можно использовать для этих целей. Для того, чтобы настроить извлечение параметра из элемента управления и подстановку его значения в качестве параметра запроса, определенного в SqlDataSource можно воспользоваться визуальными средствами Visual Studio 2005. Для вызова диалогового окна редактора параметров (рис. 10.25), необходимо щелкнуть по кнопке . Тип источника данных параметра Список параметров Элемент управления Значение параметра по умолчанию Рис. 10.25. Диалоговое окно редактирования параметров запроса. 237 Допустимые типы источников данных параметров можно выбирать из списка. При этом допустимы следующие значения списка. Control Cookie Form Profile QueryString Session Свойство элемента управления. Значение Cookie набора. Переменная формы. С помощью данного типа источника данных параметра можно извлекать значения, отправленные странице элементом ввода. Значение текущего профиля пользователя. Строковое значение запроса. Позволяет извлекать значение из текущей строки запроса. Значение состояния сеанса. Обновление данных предоставляет возможность изменения данных, содержащихся в источниках данных. Для того, чтобы воспользоваться этой возможностью необходимо определить соответствующие запросы, выполняющие операции вставки, обновления и удаления записей. Все они создаются по аналогии с запросом Select на основе параметризованного запроса. Выше уже упоминалось об имеющихся в SqlDataSource возможностях построения запросов данного типа. В качестве примера, добавим возможность изменения данных в страницу просмотра товаров и заказов, созданную ранее. Для выполним следующие операции. Определим в SqlDataSource запрос на обновление данных (UpdateQuery). Текст запроса будет выглядеть следующим образом. SqlDataSource UPDATE Закупки SET ДатаОперации = @ДатаОперации, Количество = @Количество, Цена = @Цена, КодКонтрагента = @Контрагент WHERE (КодОперации = @КодОперации) При составлении запроса важно учитывать тот факт, что имена параметрам необходимо давать таким образом, чтобы они совпадали с именами столбцов таблицы. Это объясняется тем, что ASP.NET автоматически отправляет значения параметрам, взятым из столбцов, у которых имена совпадают с именами параметров. В данном же примере присутствует еще одна особенность, которую следует учитывать. Она состоит в том, что ранее для удобства отображения данных был использован запрос, в котором происходит внутреннее соединение данных из таблиц Контрагенты и Закупки, необходимый для подмены номеров контрагентов их наименованиями. Поэтому при отображении информации в GridView мы видим наименования контрагентов вместо их номеров. Однако, это создаст некоторые трудности при реализации обновления данных. Дело в том, что теперь мы не можем реализовать обновление данных контрагентов, так как они находятся в таблице 238 Контрагенты, а SqlDataSource связан с таблицей Закупки. Все что мы можем сделать – это изменить номер текущего контрагента в редактируемой операции закупки. При этом, необходимо будет вводить не наименование контрагента, а его номер. Именно этим объясняется наличие в запросе параметра КодКонтрагента = @Контрагент, обновляющего информацию о контрагенте текущей операции закупки. Добавим в GridView новый столбец. Для этого в интеллектуальном дескрипторе выполним команду Add New Column… (рис. 10.26). Рис. 10.26. Добавление столбца в GridView с помощью интеллектульного дескриптора. В открывшемся окне (Рис. 10.27) установим тип поля CommandField, а также убедимся, что установлен флажок Edit/Update. Если требуется добавить также и возможность удаления элемента, необходимо установить флажок Delete. 239 Рис. 10.27. Окно добавления нового столбца к GridView. После этого в GridView будет добавлен столбец, в каждой строке которого, рядом со значениями элементов строк будет добавлена ссылка Edit. После щелчка по этой ссылке, соответствующая строка GridView будет переведена в режим редактирования, в результате чего все ячейки, за исключением столбцов, доступных только для чтения и не определенных в команде Update, превратятся в текстовые поля, а ссылка Edit будет заменена ссылками Update и Cancel, предназначенных для подтверждения внесения изменений в БД, либо для их отмены (Рис. 10.28). Рис. 10.28. Редактирование содержимого таблицы Закупки с помощью GridView. 240 При обновлении данных в базе данных могут возникать серьезные затруднения, вызванные возможностью одновременной работы большого количества пользователей с одними и теми же данными. Это особенно актуально для Web приложений, количество пользователей которых может быть в несколько раз больше чем для традиционных win32 приложений. Достаточно подробное описание возможных проблем и путей их решения можно найти в [7]. Остановимся лишь на некоторых из них, наиболее актуальных и востребованных. Особые случаи обновления данных Если посмотреть на текст запроса на обновление данных таблицы Закупки, использованный в предыдущем примере, можно заметить, что очень важным его элементом является строка WHERE (КодОперации = @КодОперации). Она позволяет обновить только ту строку, которой соответствует КодОперации, определенный в параметре. Этот код хорошо работает если в один и тот же момент времени одну запись пытается обновить только один человек. Если же представить, что в момент редактирования нами этой записи другой пользователь сможет изменить значение поля КодОперации, то попытка обновления таблицы Закупки, предпринимаемая нами завершится неудачей, т.к. в таблице больше не существует строки с тем значением кода операции, которое было взято в качестве параметра в момент начала редактирования. Поэтому, чтобы быть уверенным в том, что попытка обновления завершится успешно, необходимо использовать только те значения первичных ключей, которые доступны только для чтения. Кроме того, параметр, используемый для хранения исходного значения ключевого поля, необходимого для обновления записи рекомендуется помечать префиксом. Задать префикс можно с помощью свойства OldValuesParameterFormatString объекта SqlDataSource. Обычно в качестве префикса используется значение original_. Так, в приведенном выше примере строка WHERE с учетом префикса может быть записана следующим образом. WHERE КодОперации=@original_КодОперации Даже при использовании ключей доступных только для чтения, в ситуации, описанной выше, когда два пользователя одновременно редактируют одну и ту же запись в базе данных, может возникнуть конфликт. Суть его состоит в том, что изменения того пользователя, который позже других выполнит команду переноса сделанных им изменений в базу данных перезапишут изменения других пользователей редактировавших эту запись вместе с ним, но выполнивших команду обновления чуточку раньше. Для разрешения этой ситуации можно реализовать строгую проверку параллелизма при выполнении обновления данных в БД. Одним из таких способов является выполнение обновления только в том случае, если значения каждого из полей обновляемой таблицы совпадают с 241 оригинальными значениями, взятыми в момент начала редактирования. Запрос, который реализует такой подход для примера, приведенного выше выглядит следующим образом. UPDATE Закупки SET ДатаОперации = @ДатаОперации, Количество = @Количество, Цена = @Цена, КодКонтрагента = @Контрагент WHERE (КодОперации = @original_КодОперации AND ДатаОперации=@original_ДатаОперации AND Количество=@originalКоличество AND Цена=@original_Цена AND КодКонтрагента=@original_Контрагент) Проблема в данном случае состоит в том, что использование префикса возможно только для ключевых полей таблицы, определенных в свойстве DataKeyNames объекта GridView. Понятно, что невозможно определить все поля таблицы в качестве ключевых. Поэтому лучшим решением будет использование свойства ConflictDetection объекта SqlDataSource равным значению CompareAllValues. Рассмотренные способы привязки элементов отображения к источникам данных, а также возможности объекта SqlDataSource целесообразно применять только в относительно небольших приложениях. При необходимости создания сложного Web приложения использование SqlDataSource может привести к возникновению массы проблем, вызванных недостатками данного элемента. К недостаткам SqlDataSource можно отнести следующие: • все запросы, предназначенные для манипулирования данными и их извлечения встроены в страницу. Это приводит к тому, что при необходимости изменения логики доступа к данным, необходимо вносить изменения в страницу, что приводит к необходимости ее повторного тестирования и отладке. • каждая страница и даже каждый элемент, связанный с данными нуждается в отдельном элементе SqlDataSource. Это, с одной стороны, крайне не эффективно при использовании одинаковых запросов на разных страницах, т.к. в этом случае необходимо дублировать одинаковые элементы на разных страницах. С другой стороны, поддерживать такой код крайне неудобно, т.к. может возникнуть ситуация, в которой изменения придется вносить в несколько элементов SqlDataSource. Для решения этих и многих других проблем, возникающих при использовании SqlDataSource необходимо при построении приложения использовать принципы построения трехуровневой архитектуры доступа к данным. Эти принципы рассматриваются ниже. Использование DataView для фильтрации и сортировки данных При создании сложных приложений, ориентированных на активное взаимодействие с пользователями, большое значение играет отображение данных и возможности настройки этого отображения. ASP.NET 242 предоставляет возможность такой настройки отображения извлекаемой из базы данных информации посредством использования класса DataView. Этот класс снабжен возможностью фильтрации и сортировки отображаемых данных, причем эти режимы никак не затрагивают реальные данные. Таким образом, DataView очень удобен в случаях, когда необходимо настраивать возможность показа только части данных таблицы, извлекаемые из БД без необходимости изменять или обрабатывать данные, необходимые для других задач. Каждый объект DataTable по умолчанию имеет ассоциированный с ним объект DataView. При этом, допускается создание множества объектов DataView для создания множества представлений одной и той же таблицы. Для обращения к представлению по умолчанию объекта DataTable необходимо обратиться к свойству DataTable.DefaultView. Рассмотрим примеры использования основных функций DataView. Сортировка данных Сценарий использования объекта DataView выглядит следующим образом. 1. Создание объекта DataView. 2. Настроить его параметры сортировки или фильтрации. 3. Привязать его к элементу отображения данных. 4. Инициировать процесс привязки данных. Рассмотрим пример реализации сортировки данных о товарах, извлеченных из таблицы Товары БД. Полный текст кода метода Page_Load, осуществляющего подключение к источнику данных, сортировку и отображение содержимого таблицы Товары на странице Web приложения приведен ниже. protected void Page_Load(object sender, EventArgs e) { string ConnectionString = WebConfigurationManager.ConnectionStrings["TEST_DB"].ConnectionString; SqlConnection con = new SqlConnection(ConnectionString); string query = "SELECT * FROM Товары"; SqlDataAdapter da = new SqlDataAdapter(query, con); DataSet ds = new DataSet(); da.Fill(ds, "Products"); } DataView dv = new DataView(ds.Tables["Products"]); dv.Sort = "НаименованиеТовара"; GridView1.DataSource = dv; Page.DataBind(); Как видно из примера, сортировка данных заключается просто в присваивании свойству Sort объекта DataView выражения сортировки. В данном примере, выражение сортировки состоит из имени одного столбца таблицы. В общем случае, оно может включать несколько столбцов, разделенных запятыми. По умолчанию производится сортировка в порядке 243 возрастания значения элементов столбца. При необходимости, возможно изменение порядка сортировки. Фильтрация данных Для выполнения фильтрации данных при помощи DataView необходимо воспользоваться свойством RowFilter. Содержимое RowFilter очень напоминает команду Where запроса SQL. Здесь записывается условие фильтрации набора данных. При этом, возможно использование тех же операций сравнения и логических операций, которые используются в языке SQL. Кроме того, здесь возможно выполнение простейших вычислительных операций, таких как сложение, вычитание, умножение, деление, вычисление остатка от деления. Рассмотрим пример фильтрации данных таблицы Товары. Для отображения информации о товаре Товар1 необходимо выполнить следующую команду. dv.RowFilter = "НаименованиеТовара='Товар1'"; Следующий пример отобразит товары, цена которых лежит в диапазоне от 10 до 100. dv.RowFilter = "Цена>10 AND Цена50"; GridView1.DataSource = dv; Как видно из примера, для подсчета средней цены используется функция AVG(). Обращение Child позволяет сослаться на подчиненную таблицу, установленную ранее в свойстве связи DataRelation. Для определения подчиненной и главной таблиц используется созданная ранее связь ProductsPurchase. Реализация трехуровневой архитектуры доступа к данным в ASP.NET Логика взаимодействия сложных информационных систем с базами данных обычно бывает достаточно сложной. Сложность эта многократно увеличивается вместе с увеличением сложности информационной системы. Если не обеспечить унифицированные механизмы и интерфейсы доступа к данным, поддержание и развитие информационной системы становится очень трудоемким, а подчас и просто невозможным. Выходом из данной ситуации является создание промежуточного слоя, обеспечивающего взаимодействие приложения с базой данных на основе принципов трехуровневой архитектуры баз данных. В сложных профессиональных приложениях код доступа к базе данных не встраивается в само приложение, а инкапсулируется в отдельный выделенный класс. Для выполнения операции с базой данных приложению необходимо создать экземпляр этого класса и вызвать соответствующий метод. При создании такого класса необходимо следовать ряду рекомендаций. 1. Необходимо открывать соединение с базой данных перед выполнением операции с ней и закрывать сразу после завершения этой операции. 2. Необходимо реализовывать обработку ошибок для гарантированного закрытия подключения даже если в процессе выполнения операции возникла исключительная ситуация. 3. Так как реализация логики взаимодействия с базой данных описана в методах класса, необходимо передавать исходные данные методам посредством параметров, а результаты возвращать посредством возвращаемого значения. 245 4. Нельзя позволять клиенту указывать и изменять строку соединения с базой данных, т.к. это может стать причиной нарушения безопасности приложения. 5. Нельзя подключаться к источнику данных с использованием клиентского идентификатора пользователя. Для более эффективной организации взаимодействия приложения с БД лучше использовать систему безопасности на основе ролей, либо билетов. 6. При выборе данных из БД необходимо выбирать лишь те строки, которые действительно необходимы приложению для работы. Если не следовать данному принципу, быстродействие приложения может заметно падать по мере роста базы данных, т.к. объемы передаваемой по сети информации могут значительно увеличиваться. Современные приложения создаются на основе объектноориентированного подхода. Основным строительным блоком приложения в данном подходе является класс. Все обрабатываемые приложением данные хранятся в объектах, которые также содержат и методы для их обработки. Тем не менее, большинство используемых СУБД сегодня построены на принципах реляционной модели данных. Принципы обработки данных в объектных и реляционных моделях отличаются, это необходимо учитывать при организации взаимодействия с базой данных. Ниже рассматриваются некоторые вопросы организации такого взаимодействия. В хорошо организованном приложении создается отдельный класс для каждой таблицы, либо логически взаимосвязанной группы таблиц базы данных. Этот класс используется для организации взаимодействия приложения с базой данных, реализуя все необходимые операции манипуляции этими данными. Совокупность таких классов представляет собой как бы промежуточный программный слой, находящийся между классами, содержащими данные и методы для их обработки и данными, сохраненными в базе данных. Набор таких классов, реализующих логику взаимодействия приложения с базой данных, может быть реализован в виде отдельной сборки, которая может быть откомпилирована. Это целесообразно в том случае, когда аналогичную логику необходимо реализовать в нескольких приложениях. Рассмотрим пример реализации взаимодействия Web приложения с таблицей Товары базы данных Test_Db с использованием принципов трехуровневой архитектуры. Прежде всего, создадим класс Product, содержащий поля, соответствующие полям таблицы Товары. Доступ к этим полям должен осуществляться посредством соответствующих свойств. В типичном объектно-ориентированном приложении такие классы выполняют роль хранилища данных, доступ к которым предоставляют методам, осуществляющим их обработку. В достаточно большом и сложном приложении классы желательно размещать отдельно от основного кода программы. Это позволяет легко отделять код основной программы от кода, 246 который может быть перенесен в другие приложения и повторно использоваться для облегчения у ускорения создания приложения. Для создания класса рекомендуется выполнить команду Website → Add New Item. В появившемся диалоговом окне выбрать в качестве типа создаваемого элемента Class, ввести имя класса и нажать кнопку Add. Появившееся диалоговое окно, изображенное на рис. 10.29 сообщает, что классы, используемые приложением, должны быть помещены в папку App_Code. Рекомендуется поместить создаваемый класс в эту папку. Рис. 10.29. Диалоговое окно создания нового класса в папке App_Code. Текст класса Product приведен ниже. public class Product { private int productID; private string productName; private double productCost; public int ProductID { get { return productID; } set { productID = value; } } public string ProductName { get { return productName; } set { productName = value; } } public double ProductCost { get { return productCost; } set { productCost = value; } } } public Product(int productID, string productName, double productCost) { this.productID = productID; this.productName = productName; this.productCost = productCost; } Вся обработка данных о товарах приложением должна производиться с использованием класса Product. Для этого необходимо сначала загрузить в этот класс необходимую информацию из базы данных, произвести необходимые действия, а затем обновить информацию о товарах в базе 247 данных. Конечно, вопросы организации универсальных способов взаимодействия объектов приложения с базой данных достаточно сложны и выходят за рамки данного курса, поэтому ограничимся лишь наиболее общими принципами создания такого механизма. Создадим класс, предназначенный для организации взаимодействия приложения с таблицей Товары базы данных. Этот класс должен содержать необходимые методы для выполнения всех тех операций, которые необходимо выполнять клиентскому приложению с информацией, расположенной в базе данных. В данном примере, создадим класс, который «умеет» извлекать информацию о товарах из таблицы Товары, а также добавлять, удалять и обновлять эту информацию. Добавление и удаление данных являются самыми простыми процедурами, поэтому начнем с их реализации. Создадим класс ProductsDB как было описано выше. Определим в классе закрытую переменную connectionString, содержащую строку подключения, которая в момент создания класса (в конструкторе) считывается из файла web.config и помещается в connectionString. В web.config параметр, содержащий строку подключения может называться по-разному, поэтому можно предусмотреть два конструктора: один – настроенный на извлечение строки подключения из жестко заданного параметра, второй – принимающий имя данного параметра и использующий его для чтения строки подключения. public class ProductsDB { private string connectionString; public ProductsDB() { connectionString = WebConfigurationManager.ConnectionStrings["Test_Db"].ConnectionString; } public ProductsDB(string conString) { connectionString = WebConfigurationManager.ConnectionStrings[conString].ConnectionString; } } Определим метод, предназначенный для добавления новой записи в таблицу Товары. Для этого создадим объект соединения с базой данных. В данном примере используется СУБД SQL Server Express 2005, но аналогичный код (с небольшими изменениями) можно использовать для подключения практически к любой СУБД. Создадим запрос с параметрами, заполним эти параметры значениями, извлеченными из полей объекта Product, переданного в данный метод в качестве параметра и исполним запрос. Текст метода AddProduct, выполняющего вышеописанные действия, представлен ниже. public int AddProduct(Product prod) { SqlConnection con = new SqlConnection(connectionString); 248 string query = "INSERT INTO Товары (КодТовара,НаименованиеТовара,Цена) VALUES (@id,@name,@cost)"; SqlCommand cmd = new SqlCommand(query, con); cmd.CommandType = CommandType.Text; cmd.Parameters.Add(new SqlParameter("@id", SqlDbType.Int, 4)); cmd.Parameters.Add(new SqlParameter("@name", SqlDbType.VarChar, 50)); cmd.Parameters.Add(new SqlParameter("@cost", SqlDbType.Float, 8)); cmd.Parameters["@id"].Value = prod.ProductID; cmd.Parameters["@name"].Value = prod.ProductName; cmd.Parameters["@cost"].Value = prod.ProductCost; try { con.Open(); return cmd.ExecuteNonQuery(); } catch (SqlException e) { throw new ApplicationException("Ошибка добавления нового товара в таблицу Товары"); } finally { con.Close(); } } Здесь стоит обратить внимание на то, что открытие соединения с базой данных, а также выполнение запроса выполняются в блоке try, позволяющего обрабатывать возникающие исключительные ситуации и гарантирующего, что каким бы ни был результат выполнения операции, соединение будет закрыто. В случае возникновения ошибки формируется исключительная ситуация, выводящая сообщение об ошибке. В формулировке этого сообщения следует избегать излишних подробностей о причинах возникновения ошибки, т.к. некоторые пользователи могут использовать данную информацию для поиска брешей в защите информационной системы и использования их для нарушения безопасности системы. Аналогичным образом создаются остальные методы работы с базой данных. Создадим метод для удаления товара из таблицы Товары. Код метода DeleteProduct, осуществляющего эту операцию приведен ниже. public void DeleteProduct(int productID) { SqlConnection con = new SqlConnection(connectionString); string query = "DELETE FROM Товары WHERE КодТовара=@ID"; SqlCommand cmd = new SqlCommand(query, con); cmd.CommandType = CommandType.Text; cmd.Parameters.Add("@ID", SqlDbType.Int, 4); cmd.Parameters["@ID"].Value = productID; try { con.Open(); cmd.ExecuteNonQuery(); } catch (SqlException e) { 249 throw new ApplicationException("Ошибка удаления товара из таблицы Товары"); } finally { con.Close(); } } Отличие данного метода от предыдущего заключается в том, что выполняется запрос на удаление записи, а в качестве параметра передается номер (код) товара, который надо удалить. При создании метода, обновляющего данные в таблице, необходимо учесть принятую стратегию параллелизма, используемую в приложении. Эта тема уже обсуждалась выше. Код метода UpdateProduct, реализующего механизм обновления информации о товаре в БД приведен ниже. public void UpdateProduct(Product prod) { SqlConnection con = new SqlConnection(connectionString); string query = "UPDATE Товары SET НаименованиеТовара=@pName, Цена=@pCost WHERE КодТовара=@ID"; SqlCommand cmd = new SqlCommand(query, con); cmd.CommandType = CommandType.Text; cmd.Parameters.Add("@ID",SqlDbType.Int,4); cmd.Parameters.Add("@pName", SqlDbType.VarChar, 50); cmd.Parameters.Add("@pCost", SqlDbType.Float, 8); cmd.Parameters["@ID"].Value = prod.ProductID; cmd.Parameters["@pName"].Value = prod.ProductName; cmd.Parameters["@pCost"].Value = prod.ProductCost; try { con.Open(); cmd.ExecuteNonQuery(); } catch (SqlException e) { throw new ApplicationException("Ошибка обновления информации о товаре в таблице Товары"); } finally { con.Close(); } } При создании методов, извлекающих данные из БД необходимо учитывать как и для чего будут применяться данные методы. Дело в том, что может существовать множество различных потребностей в извлекаемых из БД данных. Отличия в них сводятся к представлению извлеченных данных в различных форматах. Например, если необходимо организовывать вывод информации с использованием таких элементов как GridView, удобнее всего сделать так, чтобы метод вернул такую структуру, которую можно использовать для привязки данных к этому элементу. К таким структурам, как уже было сказано выше, относятся DataReader, DataTable, DataSet и т.д. 250 Если необходимо извлекать данные об одном товаре, необходимо, чтобы метод возвращал объект Product, если же необходимо, чтобы метод извлекал список существующих товаров, представленных в БД и необходимых для их обработки, удобнее, чтобы метод вернул массив объектов Product. В реальных приложениях может потребоваться несколько методов, предназначенных для реализации различных режимов работы с БД. Например, для отображения вызывается один метод, извлекающий данные из БД, а для их редактирования – другой. Создадим метод GetProductsTable(), предназначенный для извлечения данных о товарах из таблицы Товары и помещения их в объект DataTable. Исходный код этого метода приведен ниже. public DataTable GetProductsTable() { SqlConnection con = new SqlConnection(connectionString); string query = "SELECT КодТовара,НаименованиеТовара,Цена FROM Товары"; SqlCommand cmd = new SqlCommand(query, con); cmd.CommandType = CommandType.Text; SqlDataAdapter da = new SqlDataAdapter(query, con); DataTable dt = new DataTable("Product"); try { con.Open(); da.Fill(dt); return dt; } catch (SqlException e) { throw new ApplicationException("Ошибка чтения списка товаров из таблицы Товары"); } finally { con.Close(); } } В дальнейшем полученный в результате такой операции результат может быть использован для отображения данных в объекте GridView. В качестве примера, создадим также метод для извлечения данных из таблицы Товары и предоставлении вызывающей программе объекта DataReader, с помощью которого осуществляется чтение данных из БД. Исходный код метода GetProductsReader(), реализующего данную операцию, представлен ниже. public SqlDataReader GetProductsReader() { SqlConnection con = new SqlConnection(connectionString); string query = "SELECT КодТовара,НаименованиеТовара,Цена FROM Товары"; SqlCommand cmd = new SqlCommand(query, con); cmd.CommandType = CommandType.Text; try { 251 con.Open(); return cmd.ExecuteReader(); } catch (SqlException e) { throw new ApplicationException("Ошибка чтения списка товаров из таблицы Товары"); } } Особенностью метода является то, что в нем открывается соединение с базой данных, но не закрывается. Это объясняется тем, что основная программа, которой передается объект DataReader должна осуществлять чтение данных из него, а для этого необходимо открытое соединение. Описанные выше методы позволяют получать данные о товарах из БД в формате, лучше всего приспособленном для их отображения на экране, но плохо подходящего для редактирования данных. Для получения данных о товарах в удобном для программной работы с ними формате создадим еще два метода. Первый предназначен для извлечения из БД информации об одном товаре и передаче ее в вызывающую программу в виде объекта Product. Второй – для формирования массива объектов Product. Оба метода используют объект DataReader для чтения информации из БД. public Product GetProduct(int productID) { SqlConnection con = new SqlConnection(connectionString); string query = "SELECT КодТовара,НаименованиеТовара,Цена FROM Товары WHERE КодТовара=@ID"; SqlCommand cmd = new SqlCommand(query, con); cmd.CommandType = CommandType.Text; cmd.Parameters.Add("@ID", SqlDbType.Int, 4); cmd.Parameters["@ID"].Value = productID; try { con.Open(); SqlDataReader rdr = cmd.ExecuteReader(CommandBehavior.SingleRow); rdr.Read(); int pID=(int)rdr["КодТовара"]; string pName = rdr["НаименованиеТовара"].ToString(); double pCost = Convert. ToDouble(rdr["Цена"]); Product prod = new Product(pID,pName,pCost); rdr.Close(); return prod; } catch (SqlException e) { throw new ApplicationException("Ошибка извлечения товара из таблицы Товары"); } finally { con.Close(); } } public List GetProducts() { SqlConnection con = new SqlConnection(connectionString); 252 string query = "SELECT КодТовара,НаименованиеТовара,Цена FROM Товары"; SqlCommand cmd = new SqlCommand(query, con); cmd.CommandType = CommandType.Text; List products = new List(); try { con.Open(); SqlDataReader rdr = cmd.ExecuteReader(); while (rdr.Read()) { Product prod = new Product((int) rdr["КодТовара"], rdr["НаименованиеТовара"].ToString(), (double) rdr["Цена"]); products.Add(prod); } rdr.Close(); return products; } catch (SqlException e) { throw new ApplicationException("Ошибка извлечения списка товаров из таблицы Товары"); } finally { con.Close(); } } Для демонстрации использования созданного слоя доступа к данным поместим на форму объект ProductsView класса GridView, а также создадим следующий код, использующий возможности класса ProductsDB. protected void Page_Load(object sender, EventArgs e) { //Создание нового объекта доступа к данным ProductsDB prodDB = new ProductsDB("TEST_DB"); if (!Page.IsPostBack) { //Создание нового объекта Product Product prod = new Product(31, "Товар31", 99); //Добавление созданного объекта Product в таблицу БД prodDB.AddProduct(prod); //Получение из БД информации о товаре с кодом 5 Product product = prodDB.GetProduct(5); //Изменение наименование полученного товара product.ProductName = "Товар1000"; //Изменение цены полученного товара product.ProductCost = 10.5; //Обновление информации о товаре в БД prodDB.UpdateProduct(product); //Получение массива объектов Product. Количество объектов //равно количеству записей в таблице Товары List products = prodDB.GetProducts(); } //Установить источник данных и осуществить их привязку //для элемента GridView ProductsView.DataSource = prodDB.GetProductsTable(); Page.DataBind(); } 253 Для демонстрации возможности удаления данных из таблицы Товары поместим на форму кнопку и создадим следующий обработчик события нажатия на нее. protected void Button1_Click(object sender, EventArgs e) { ProductsDB prodDB = new ProductsDB("TEST_DB"); prodDB.DeleteProduct(31); ProductsView.DataSource = prodDB.GetProductsReader(); Page.DataBind(); } В этом метода вновь осуществляется установка источника данных для объекта GridView, а также их привязка. Это необходимо для того, чтобы отобразить изменения в данных, произведенных ранее выполненными действиями. Результат работы программы представлен на рис. 10.30. Рис. 10.30. Результат работы программы трехуровневого взаимодействия с БД. Использование объекта ObjectDataSource Создание пользовательского кода, реализующего возможность взаимодействия с базой данных достаточно трудоемкий процесс. К тому же такой способ позволяет связывать визуальные элементы с данными только в программном коде. Для получения возможности создания такой связи в режиме редактирования страницы, можно использовать объект ObjectDataSource. Этот объект позволяет создавать связь между элементами управления, расположенными на Web странице и компонентами доступа к данным, реализованными в виде пользовательских классов. Но для этого необходимо, чтобы пользовательский класс доступа к данным подчинялся следующим правилам: 1. Он не должен сохранять состояние. 2. Он должен иметь конструктор по умолчанию, без аргументов. 254 3. Вся логика должна быть сосредоточена в единственном классе. 4. Он не должен содержать статических методов, предназначенных для извлечения и обновления записей. 5. Он должен предоставлять результаты запроса при вызове единственного метода. 6. Результатом запроса должна быть одни или несколько записей, которые могут быть представлены в виде коллекции, массива, либо спискового объекта. Главное, чтобы он реализовывал интерфейс IEnumerable. Использование ObjetDataSource в ряде случае бывает гораздо удобнее, использования таких элементов доступа к данным как SqlDataSource или AccessDataSource. Например, в предыдущем примере, для доступа к таблице Товары можно воспользоваться пользовательским классом и объектом ObjectDataSource. Для этого необходимо выполнить следующие шаги. Поместить на форму объект ObjectDataSource, перетащив его с панели Toolbox, после чего прикрепить к нему класс, отвечающий за извлечение данных, нашем случае это ProductsDB. Для этого достаточно установить значение свойства TypeName равным ProductsDB. После этого необходимо определить свойства SelectMethod, DeleteMethod, UpdateMethod и InsertMethod, используемые для выполнения соответствующих операций над данными в БД. В качестве значений этих свойств необходимо установить имя метода, выполняющего соответствующие операции. Так, в качестве значения свойства SelectMethod в данном примере необходимо установить имя метода, извлекающего данные из БД – GetProducts. Этот метод удовлетворяет всем критериям ObjectDataSource – он возвращает объект, представляющий все данные через общедоступные свойства. Имена этих свойств и будут использованы в качестве имен столбцов при выводе информации на экран в табличной форме. После того, как связь ObjectDataSource с классом, извлекающим данные из базы данных установлена, необходимо добавить на Web форму элемент GridView и связать их установив в свойстве DataSourceID этого элемента имя объекта ObjectDataSource. Определение ObjectDataSource и GridView таким образом будет выглядеть следующим образом. 255 Результат работы программы показан на рис. 10.31. Рис. 10.31. Окно результата использования объекта ObjectDataSource для организации доступа к БД. Как видно из данного примера, с точки зрения пользователя использование ObjectDataSource и принципов трехуровневой архитектуры построения приложений доступа к данным аналогично использованию объекта SqlDataSource. Однако, это сходство скрывает целый ряд деталей, сильно отличающих принципы построения трехуровневых приложений от обычной архитектуры. Наиболее значимый эффект от использования этих принципов заключается в том, что при трехуровневой архитектуре организации доступа к данным Web страница не содержит никакого кода SQL. Вместо этого, вся работа выполняется классом ProductsDB. За счет этого приложение оказывается более гибким и легко модифицируемым при необходимости изменения механизмов доступа к данным. Более подробную информацию об использовании принципов построения трехуровневой архитектуры доступа к данным в приложении можно найти в справочной системе MSDN, а также в [1]. 256 Список использованной литературы 1. Мак-Дональд, Мэтью. Шпушта, Марио. Microsoft ASP.NET 2.0 с примерами на С# 2005 для профессионалов. : Пер. с англ. – М.: ООО «И.Д. Вильямс», 2006. – 1408 с.; ил. – Парал. тит. англ. 2. Microsoft Corporation. Разработка Web приложений на Microsoft Visual Basic .NET и Microsoft Visual C# .NET. Учебный курс MCAD/MCSD/Пер. с англ. – М.: Издательский дом «Русская Редакция», 2003. – 704 стр.: ил. 3. Троелсен Э. C# и платформа .NET. Библиотека программиста. – СПб.: Питер, 2004. – 796 с.: ил. 4. Дубаков М.А. Веб-мастеринг средствами CSS. – СПб.: БХВПетербург, 2002. – 544 с.: ил. 5. www.asp.net. 6. Браст Эндрю Дж., Форте Стивен Разработка приложений на основе Microsoft SQL Server 2005. Мастер-класс. / Пер. с англ. – М.: Издательство «Русская редакция», 2007. – 880 стр. ил. 7. Пирогов В.Ю. SQL Server 2005: программирование клиентсерверных приложений. – СПб.: BHV-СПб, 2006. – 336 стр. 8. Сеппа Д. Microsoft ADO.NET/Пер. с англ. – М.: Издательскоторговый дом «Русская Редакция», 2003. – 640 стр.: ил. Recommend DocumentsCYAN MAGENTA
YELLOW BLACK PANTONE 123 C
BOOKS FOR PROFESSIONALS BY PROFESSIONALS ®
THE EXPERT’S VOICE ® IN .NET Compa...
MOON’S WEB By
Cathy Clamp & CT Adams CHAPTER 1 The scent of snow on the wind raised the hairs on my skin like distant l...
Tangled Web By Terry O’Reilly
Published by JMS Books LLC Visit jms-books.com for more information.
Copyright 2011 Ter...
Sign In
Our partners will collect data and use cookies for ad personalization and measurement. Learn how we and our ad partner Google, collect and use data. Agree & close
|
---|