Программирование на ASP.NET

Управление состоянием ASP.NET

Разбить на страницы
Показывать лекцию целиком
Файлы к лекции Вы можете скачать здесь

Главная особенность удаленных приложений состоит в том, что они могут одновременно обрабатывать множество пользователей. К тому же сервер может выполнять одновременно несколько разных приложений. Поэтому среда исполнения устроена так, что после обработки запроса очередного пользователя память сервера немедленно очищается от кода запрошенной страницы, освобождая ресурсы для новых возможных запросов. Измененное состояние страницы, информация о пользователе и другие оперативные данные приложения теряется безвозвратно, а при следующем запросе страница загружается в память сервера как при первом. Такой механизм называется работой без сохранения состояния.

Такой факт вынуждает разработчиков Web-приложений изыскивать другие способы хранения информации о состоянии между запросами, с последующим ее извлечением и использованием. В ASP.NET существует два способа хранения информации между запросами: на клиенте и на сервере. Разработчик сайта сам решает, какую группу методов хранения данных состояния ему выбрать.

Хранение информации на клиенте имеет следующие преимущества:

  • Лучшая масштабируемость - не расходуется память сервера для хранения состояния, что позволяет обслужить больше клиентов
  • Поддержка множества Web-серверов - обработку поступающих запросов можно распараллелить между несколькими серверами. При этом код приложения модифицировать не нужно, поскольку вся необходимая информация о состоянии поступает вместе с каждым запросом
  • Хранение информации на сервере имеет следующие преимущества:

  • Безопасность - защищенность данных состояния от злоумышленников. Информация управления состоянием на клиенте может быть похищена с клиентского компьютера или перехвачена в пути. А хранение на сервере конфиденциальных данных, таких как пароль, уровень доступа, состояние авторизации делают это безопасным
  • Снижение нагрузки на каналы связи - снижение времени загрузки страницы и увеличение масштабируемости, особенно для мобильных клиентов
  • Управление состоянием на клиенте

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

    Различают следующие методы хранения данных на клиенте:

  • Состояние вида ( view state ) - используется для сохранения значений элементов управления, добавленных конечным пользователем. Применение состояния вида основано на свойстве ViewState типа System.Web.UI.StateBag, наследуемом от класса Control любым элементом управления и самой страницей.
  • Скрытые поля ( hidden fields ) - сохранение данных в скрытых полях HTML-вывода. Они также хранят информацию об одной странице, но в отличие от состояния вида скрытые поля не поддерживают хэширование, шифрование, сжатие и хранение по частям. При использовании скрытых полей данные формы на сервер должны отправляться методом POST (по щелчку кнопки Submit ), а не методом GET (по щелчку на гиперссылке).
  • Строки запроса ( query strings ) - сохранение данных в URL. Видимы для пользователя в поле адреса
  • Межстраничные обратные отсылки - присланные вызывающей страницей данные остаются в памяти сервера на момент работы страницы-приемника
  • Куки-наборы ( cookie -файлы) - созданные на сервере данные записываются на компьютер клиента в небольших файлах (в количестве до 20 файлов и до 4 Кб каждый) и автоматически пересылаются броузером на сервер вместе с каждым запросом. Это лучший способ хранения состояния, доступного многим страницам приложения
  • Рассмотрим последовательно перечисленные методы хранения состояния на клиенте.

    Состояние вида

    Это простой способ хранения информации на клиенте между обратными отсылками для одной и той же страницы. Вспомним, как обрабатывается страница на сервере. Для этого рассмотрим свежий отклик сервера, только что полученный броузером. В нем броузеру вместе с отображаемым HTML-кодом в скрытых полях была прислана информация со значениями состояния элементов управления страницы на момент отправки с сервера. Пользователь через броузер меняет состояние некоторых элементов и инициирует обратную отсылку.

    При поступлении обратной отсылки на сервер в память загружается адресуемая страница в своем первозданном виде, инициницализируются ее интерфейсные элементы начальными настройками, затем извлекается поступившая в скрытых полях информация о состоянии на момент предыдущего отклика и свойства элементов корректируются. После этого срабатывают события элементов управления о последних действиях пользователя и состояние элементов управления еще раз корректируются. Таким образом, состояние интерфейсных элементов при создании страницы перезаписывается по меньшей мере три раза: первоначальными настройками, последними сохранными значениями и новыми значениями, инициированными пользователем.

    Все серверные Web-элементы управления используют состояние вида по умолчанию (не обязательно включать EnableViewState="true" ). В состав каждого из них входит свойство-коллекция ViewState, наследуемое от класса System.Web.UI.Control. Это же свойство напрямую наследует и класс Page, который мы расширяем при проектировании активной страницы. Оно создается и заполняется поступившими данными вместе с объектом страницы, а значит мы программно можем использовать это страничное свойство для сохранения и извлечения информации между запросами одной и той же страницы.

    После обработки страницы ее текущее состояние, а также состояние элементов управления, автоматически хэшируется (hash - шифруется, превращается в мусор; не путайте с кэшем - сохранением в памяти) и строка с хэшем пересылается броузеру в скрытом поле страницы вместе с отображаемым HTML. Если данные получаются очень объемные и не помещаются в одном поле, то ASP.NET автоматически делит их на части и записывает в нескольких скрытых полях. Максимальный объем одного скрытого поля определяется свойством maxPageStateFieldLength класса Page (по умолчанию равен -1 ). Для изменения начальных настроек объекта страницы значение этого свойства нужно поместить в конфигурационный файл, например

    <?xml version="1.0" encoding="utf-8"?>
    <configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
        <system.web>
            <compilation debug="true" />
            <pages maxPageStateFieldLength="1024" />
        </system.web>
    </configuration>

    Коллекция ViewState построена по типу словаря (ассоциативного массива), в котором значения адресуются строковыми уникальными ключами. Элементами коллекции являются ссылки на объекты обобщенного типа Object, поэтому она способна хранить объекты любого типа. При извлечении из коллекции состояния вида данные нужно приводить к соответствующему типу. Если в коде попытаться извлечь значение, которого нет в коллекции, будет выдано исключение NullReferenceException, поэтому необходима предварительная проверка возвращаемой ссылки на null -значение.

    Не все типы объектов могут быть сохранены в состоянии вида. Простые типы сохраняются безусловно, а более сложные типы должны поддаваться сериализации (преобразовываться в поток байтов). Если объявлению класса предшествует атрибут [Serializable], то порожденный этим классом объект можно сохранять в состоянии вида. В противном случае, при попытке сохранить такой объект в состоянии вида ASP.NET выдаст ошибку выполнения. Библиотечный тип Hashtable из пространства имен System.Collections также представляет собой словарь с полями Key и Value. Он поддается сериализации, поэтому настроенный объект такого типа можно целиком сохранять и восстанавливать в ViewState.

    В качестве иллюстрации сказанного приведем 2 примера, где используется программирование словаря ViewState самой страницы.

    Упражнение 1

    В этом примере мы создадим форму с интерфейсными элементами, в которых отключим автоматическое сохранение состояния вида, а будем его сохранять и восстанавливать вручную. Интерфейсные элементы мы разместим в серверной таблице и будем использовать рекурсивную логику для опроса всех ее дочерних элементов, размещенных в коллекции Controls. В качестве ключа для доступа к элементам словаря ViewState уровня страницы будем использовать идентификаторы элементов.

  • Создайте пустой проект с именем
  • Добавьте к проекту новую Web-страницу с именем
  • Запустите командой Website/ASP.NET Configuration страницу WAT (Web Site Administration Tool) и выполните команду Application Configuration/Configure debugging and tracing. На появившейся странице включите флажок Enable debugging
  • Таким действием мы создадим файл конфигурации Web.config с настройкой отладки.

  • В панели Solution Explorer щелкните на кнопке Refresh, чтобы увидеть файл конфигурации в составе проекта
  • Откройте интерфейсную часть страницы ViewStateTest.aspx в режиме Source и добавьте в открывающую часть дескриптора <div> внутренний стиль
    <div style="text-align: center">
    и заголовок
    <h1 style="color: Red">Заполните анкету</h1>
  • Откройте интерфейсную часть страницы ViewStateTest.aspx в режиме Design и поместите на нее элемент Table из вкладки HTML панели Toolbox
  • Щелкните на элементе Table правой кнопкой мыши и выполните опцию Run As Server Control, чтобы превратить его в серверный
  • Откорректируйте интерфейсный HTML-код заготовки таблицы следующим образом
    (рис ) <table id="Table1" width="100%" runat="server" style="text-align: left; width: 500px;">
        <tr>
            <td style="width: 200px">
            </td>
            <td>
            </td>
        </tr>
        <tr>
            <td style="width: 200px">
            </td>
            <td>
            </td>
        </tr>
        <tr>
            <td style="width: 200px">
            </td>
            <td>
            </td>
        </tr>
        <tr>
            <td style="width: 200px">
            </td>
            <td>
            </td>
        </tr>
        <tr>
            <td style="width: 200px">
            </td>
            <td>
            </td>
        </tr>
        <tr>
            <td style="width: 200px">
            </td>
            <td>
            </td>
        </tr>
    </table>
  • Созданная таблица имеет 6 строк и 2 столбца.

  • Поместите в первую строку и первый столбец таблицы приведенный ниже текст, а в остальные ячейки второго столбца - элемент TextBox из вкладки Standard панели Toolbox
  • Поместите после таблицы три кнопки Button из вкладки Standard панели Toolbox и присвойте им имена cmdSubmit, cmdSave и cmdRestore соответственно
  • После этих действий интерфейс страницы в режиме проектирования будет таким

    Ему соответствует такой дескрипторный код

    <%@ Page Language="C#" AutoEventWireup="true" 
             CodeFile="ViewStateTest.aspx.cs" Inherits="ViewStateTest" %>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        <div style="text-align: center">
        <h1 style="color: Red">Заполните анкету</h1>
            <table id="Table1" runat="server" style="text-align: left; width: 500px;">
                <tr>
                    <td style="width: 200px">
                        <b>Наименование</b></td>
                    <td>
                        <b>Значение</b></td>
                </tr>
                <tr>
                    <td style="width: 200px">
                        Имя:</td>
                    <td>
                        <asp:TextBox ID="TextBox1" runat="server"></asp:TextBox></td>
                </tr>
                <tr>
                    <td style="width: 200px">
                        Учетный номер:</td>
                    <td>
                        <asp:TextBox ID="TextBox2" runat="server"></asp:TextBox></td>
                </tr>
                <tr>
                    <td style="width: 200px">
                        Возраст:</td>
                    <td>
                        <asp:TextBox ID="TextBox3" runat="server"></asp:TextBox></td>
                </tr>
                <tr>
                    <td style="width: 200px">
                        E-mail:</td>
                    <td>
                        <asp:TextBox ID="TextBox4" runat="server"></asp:TextBox></td>
                </tr>
                <tr>
                    <td style="width: 200px">
                        Пароль:</td>
                    <td>
                        <asp:TextBox ID="TextBox5" runat="server"></asp:TextBox></td>
                </tr>
            </table>
            <br />
            <asp:Button ID="cmdSubmit" runat="server" Text="Отправить" />
            nbsp;nbsp;nbsp; nbsp;
            <asp:Button ID="cmdSave" runat="server" Text="Сохранить" />
            nbsp;nbsp;nbsp; nbsp;	
            <asp:Button ID="cmdRestore" runat="server" Text="Восстановить" />
        </div>
        </form>
    </body>
    </html>
  • Двойным щелчком на каждой из двух последних кнопок cmdSave и cmdRestore в интерфейсной части страницы создайте для них заготовки обработчиков с именами по умолчанию
  • Откройте файл отделенного кода и заполните его следующим кодом
    (рис ) Содержимое файла ViewStateTest.aspx.csusing System;
    using System.Data;
    using System.Configuration;
    using System.Collections;
    using System.Web;
    using System.Web.Security;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    using System.Web.UI.WebControls.WebParts;
    using System.Web.UI.HtmlControls;
        
    public partial class ViewStateTest : System.Web.UI.Page
    {
        private bool executeFlag = true;
        
        protected void cmdSave_Click(object sender, EventArgs e)
        {
            // Сохраняем присланные значения текстовых полей
            SaveAllText(Table1.Controls, executeFlag);
        }
        
        private void SaveAllText(ControlCollection controls, bool saveNested)
        {
            if (!saveNested)
                return;
        
            // Перебираем все дочерние элементы в переданной коллекции таблицы 
            foreach (Control control in controls)
            {
                // Выбираем элементы текстовых полей и сохраняем
                // их содержимое в свойстве ViewState объекта страницы,
                // используя в качестве ключа идентификатор элемента
                if (control is TextBox)
                {
                    this.ViewState[control.ID] = ((TextBox)control).Text;
                }
        
                // На верхних уровнях объекта Table1 дочерними элементами
                // являются строки и ячейки таблицы, поэтому продолжаем
                // рекурсивно спускаться ниже, пока не встретим объекты TextBox.
                // Флаг saveNested разрешает проверять вложенные уровни коллекций 
                if (control.Controls != null)
                {
                    SaveAllText(control.Controls, saveNested);
                }
            }
        }
        
        protected void cmdRestore_Click(object sender, EventArgs e)
        {
            // Извлекаем пришедший с обратной отсылкой сохраненный текст предыдущей
            // обратной отсылки и вставляем его в текстовые поля, готовя новый отклик
            RestoreAllText(Table1.Controls, executeFlag);
        }
        
        void RestoreAllText(ControlCollection controls, bool restoreNested)
        {
            if (!restoreNested)
                return;
        
            // Перебираем все элементы таблицы, включая вложенные, чтобы
            // использовать их идентификаторы как ключи для извлечения
            // возможно сохраненных значений из словаря ViewState
            foreach (Control control in controls)
            {
                // Нашли текстовое поле 
                if (control is TextBox)
                {
                    // Убеждаемся, что есть в словаре
                    if (this.ViewState[control.ID] != null)
                    {
                        // Приводим типы и восстанавливаем
                        ((TextBox)control).Text = (string)this.ViewState[control.ID];
                    }
                }
        
                // Рекурсивно спускаемся ниже к дочерним элементам
                if (control.Controls != null)
                {
                    RestoreAllText(control.Controls, restoreNested);
                }
            }
        }
    }
  • Испытайте работу страницы и убедитесь, что в любое время можно восстановить состояние элементов управления, сохраненное ранее в словаре ViewState страницы
  • Упражнение 2

    Этот пример подобен предыдущему, но в нем значения текстовых полей прежде поэлементно сохраняются в промежуточном объекте Hashtable, а затем этот объект целиком сохраняется в свойстве объекта страницы ViewState. Интерфейсная часть страницы остается в точности такой-же, как в предыдущем примере, а кодовая часть изменится. При этом, чтобы не менять интерфейсную часть, мы динамически (в кодовой части) добавим в конце коллекции формы form1 текстовую метку для отображения на клиенте содержимого промежуточного объекта-словаря.

  • Через панель Solution Explorer создайте копию страницы ViewStateTest.aspx и присвойте ей имя ViewStateObjects.aspx
  • Измените имя класса ViewStateTest на ViewStateObjects в кодовой и интерфейсной частях скопированной страницы
  • Заполните кодовую часть следующим образом
  • using System;
    using System.Data;
    using System.Configuration;
    using System.Collections;
    using System.Web;
    using System.Web.Security;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    using System.Web.UI.WebControls.WebParts;
    using System.Web.UI.HtmlControls;
        
    public partial class ViewStateObjects : System.Web.UI.Page
    {
        private bool executeFlag = true;
        
        // Создаем экземпляр промежуточного словаря
        Hashtable dictionary = new Hashtable();
        
        protected void cmdSave_Click(object sender, EventArgs e)
        {
            // Сохраняем присланные значения текстовых полей в словаре
            SaveAllText(Table1.Controls, executeFlag);
        
            // Сохраняем помеченный словарь целиком в состоянии вида
            this.ViewState["contentsState"] = dictionary;
        }
        
        private void SaveAllText(ControlCollection controls, bool saveNested)
        {
            if (!saveNested)
                return;
        
            // Перебираем все дочерние элементы в переданной коллекции таблицы 
            foreach (Control control in controls)
            {
                // Выбираем элементы только текстовых полей и сохраняем
                // их содержимое в промежуточном объекте, используя
                // в качестве ключа идентификатор элемента
                if (control is TextBox)
                {
                    // Значения полей перед сохранением на всякий случай
                    // URL-кодируем как поступившие от ненадежного источника
                    // (предохраняемся от возможного ввода зловредных скриптов).
                    // Чтобы система не реагировала на угловые скобки в полях ввода,
                    // нужно в директиву @ Page интерфейсной части вставить атрибут
                    //  ValidateRequest="false" или в секцию 
                    // <pages validateRequest="false"> файла web.config
                    // Внимание!!! XML-файл web.config чувствителен к регистру символов
                    string text = ((TextBox)control).Text;
                    text = Page.Server.HtmlEncode(text);
        
                    // То же самое...
                    // text = HttpUtility.HtmlEncode(text);
        
                    // Добавляем в словарь обезвреженный текст
                    dictionary.Add(control.ID, text);
                }
        
                // На верхних уровнях объекта Table1 дочерними элементами
                // являются строки и ячейки таблицы, поэтому продолжаем
                // рекурсивно спускаться ниже, пока не встретим объекты TextBox.
                // Флаг saveNested разрешает проверять вложенные уровни коллекций 
                if (control.Controls != null)
                {
                    SaveAllText(control.Controls, saveNested);
                }
            }
        }
        
        protected void cmdRestore_Click(object sender, EventArgs e)
        {
            // Извлекаем из состояния вида пришедший с 
            // обратной отсылкой промежуточный объект и заполняем
            // из него текстовые поля сохраненными значениями
            RestoreAllText(executeFlag);
        }
        
        // Если доступ к функции явно не объявлен, то по умолчанию считается private
        void RestoreAllText(bool restoreNested)
        {
            if (!restoreNested)
                return;
        
            // Извлекаем помеченные ранее данные из состояния
            // вида в промежуточный объект-словарь
            dictionary = (Hashtable)this.ViewState["contentsState"];
        
            // Перебираем все элементы заполненного словаря и ищем на 
            // странице соответствующее текстовое поле. Заодно заполняем
            // парами словаря текстовую метку для отсылки клиенту. Текстовую
            // метку создадим динамически и добавим в конец коллекции формы
            Label label = new Label();
            form1.Controls.Add(label);
            label.Text = "<hr />" + "<h2>Значения словаря</h2>";
        
            foreach (DictionaryEntry pair in dictionary)
            {
                // Выделяем ключ и значение для очередной извлеченной пары
                string key = pair.Key.ToString();
                string value = pair.Value.ToString();
        
                // Ищем на странице текстовое поле с соответствующим 
                // идентификатором, уникальным в пределах страницы
                TextBox item = (TextBox)Page.FindControl(key);
                if (item != null)
                {
                    // Если нашли, заполняем поле и текстовую метку
                    item.Text = value;
        
                    label.Text += string.Format("Key={0}, Value={1}", key, value);
                    label.Text += "<br />";
                }
            }
        }
    }

    Поскольку мы взяли на себя проверку достоверности данных, введенных пользователем, то нужно отключить такую проверку системой ASP.NET при считывании запроса, поместив соответствующую инструкцию в директиву @ Page страницы или в файл конфигурации в секцию <pages>. При этом нужно строго учитывать регистр символов и в директиву @ Page атрибут помещать таким ValidateRequest="false", а в конфигурационный файл таким validateRequest="false". Это общее правило ASP.NET для случая, когда настройки допускают двойственное размещение.

  • Дополните файл конфигурации web.config секцией <pages> с отключенной проверкой достоверности
    (рис ) Файл web.config с отключенной проверкой достоверности<?xml version="1.0" encoding="utf-8"?>
    <configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
        <system.web>
            <compilation debug="true" />
            <pages validateRequest="false" />
        </system.web>
    </configuration>
  • Запустите приложение и убедитесь в его работоспособности
  • Результат после восстановления ранее сохраненной в состоянии вида информации будет примерно таким

  • Удалите из секции <system.web> файла Web.config параметр отключения проверки достоверности <pages validateRequest="false" /> и включите его как атрибут ValidateRequest="false" в состав директивы @ Page страницы ViewStateObjects.aspx, чтобы он действовал только на одну эту страницу
  • Восстановление состояния вида элемента списка через коллекцию запроса

    Иногда состояние вида в элементе полезно отключить, чтобы уменьшить нагрузку на каналы связи. Но для некоторых элементов это нарушает их нормальную работу. Возьмем, например, списки ListBox и DropDownList. Когда они заполнены большим количеством элементов, объем пересылаемой информации состояния вида очень большой и это свойство лучше отключить. Для восстановления состояния выделенного элемента списка в этом случае можно воспользоваться информацией из коллекции Request.Form страницы. Проверим это на примере.

    Упражнение 3

  • Добавьте к проекту страницу с именем ViewStateList.aspx без файла отделенного кода и назначьте ее стартовой
  • Оформите страницу так, чтобы код ее интерфейсной части был следующим
    (рис ) Код интерфейсной части страницы ViewStateList.aspx<%@ Page Language="C#" %>
    	
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        <div style="text-align: center">
        <h2>Отключение состояния вида в списках</h2>
            <asp:ListBox ID="ListBox1" runat="server" Height="200px" Width="150px" />
            nbsp; nbsp; nbsp; nbsp;
            <asp:DropDownList ID="DropDownList1"
                runat="server" Width="150px">
            </asp:DropDownList><br />
            <br />
            <asp:Button ID="Button1" runat="server" Text="Отправить" /></div>
        </form>
    </body>
    </html>
  • Этому коду в режиме проектирования соответствует следующее представление
  • Двойным щелчком на свободном месте страницы в режиме Design создайте блок скриптов с заготовкой обработчика Page_Load()
  • Поместите в обработчик Page_Load() код заполнения списков видимым содержимым, который будет срабатывать при каждом запросе страницы
    (рис ) Обобработчик Page_Load() для заполнения списков<script runat="server">
        
        // Поле класса страницы
        private const int maxCount = 1000;
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Заполняем списки программной генерацией значений
            for (int i = 0; i < maxCount; i++)
            {
                ListBox1.Items.Add("Опция " + (i + 1).ToString());
                DropDownList1.Items.Add("Опция " + (i + 1).ToString());
            }
        }
    </script>
  • Выполните страницу и убедитесь, что при каждом новом запросе по щелчку на кнопке Submit отмеченные пользователем опции списков возвращаются сервером сохраненными
  • Пока что это работает механизм автоматического сохранения состояния вида ASP.NET. Но давайте посмотрим, какой при этом получается объем пересылаемой информации, именно касающийся работы этого механизма. Для этого включим работу ASP.NET в режиме выдачи трассировки на индивидуальных страницах. Это можно сделать, если добавить атрибут Trace="true" в директиву @ Page страницы, либо через утилиту WAT, запустив ее командой Website/ASP.NET Configuration (или щелкнуть на пиктограмме в панели Solution Explorer ).

  • Запустите утилиту WAT, пойдите последовательно по ссылкам Application Configuration/Configure debugging and tracing и включите флажки "Capture tracing information" и "Display tracing information on ondividual pages". Закройте утилиту
  • В результате этих действий файл конфигурации примет вид

    <?xml version="1.0" encoding="utf-8"?>
    <configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
        <system.web>
            <trace enabled="true" pageOutput="true" />
            <compilation debug="true" />
        </system.web>
    </configuration>
  • Вновь запустите страницу ViewStateList.aspx, поманипулируйте с интерфейсными элементами, выполните обратную отсылку и проанализируйте секцию Control Tree в результатах трассировки, которая будет, примерно, такой
  • Control Tree
    Control UniqueID Type Render Size Bytes (including children) ViewState Size Bytes (excluding children) ControlState Size Bytes (excluding children)
    __Page ASP.viewstatelist_aspx 910866 0 0
    ctl02 System.Web.UI.LiteralControl 50 0 0
    ctl00 System.Web.UI.HtmlControls.HtmlHead 46 0 0
    ctl01 System.Web.UI.HtmlControls.HtmlTitle 33 0 0
    ctl03 System.Web.UI.LiteralControl 14 0 0
    form1 System.Web.UI.HtmlControls.HtmlForm 910736 0 0
    ctl04 System.Web.UI.LiteralControl 129 0 0
    ListBox1 System.Web.UI.WebControls.ListBox 227258 196036 0
    ctl05 System.Web.UI.LiteralControl 47 0 0
    DropDownList1 System.Web.UI.WebControls.DropDownList 227246 196036 0
    ctl06 System.Web.UI.LiteralControl 32 0 0
    Button1 System.Web.UI.WebControls.Button 78 0 0
    ctl07 System.Web.UI.LiteralControl 12 0 0
    ctl08 System.Web.UI.LiteralControl 20 0 0

    Объем пересылаемой по каналам связи в оба конца информации получается огромным до неприличия. Причем с каждой новой отсылкой он катастрофически нарастает, добавляя к прежним сохраненным значениям сгенерированные в коде новые значения списков. Отключим сохранение состояния вида в списках, а для восстановления позиций выделенных пользователем опций воспользуемся тем фактом, что с каждым запросом эти значения передаются на сервер в словарь Request.Form объекта страницы.

  • Перейдите в режим Design редактора страницы ViewStateList.aspx, выделите одновременно оба списка и через панель Properties сбросьте свойства EnableViewState
  • Запустите страницу ViewStateList.aspx и убедитесь, что объем сохраняемого состояния вида для списков стал нулевым
  • Замечание. Если количество запросов превысит заданное параметром трассировки requestLimit значение (по умолчанию 10), то ASP.NET при исполнении не будет включать результаты трассировки в страницу. В этом случае нужно при очередном запуске тестового браузера вызвать через строку адреса страницу trace.axd и щелкнуть на ссылке "clear current trace" этой страницы. Включить безлимитную трассировку можно более просто - добавить в директиву @ Page страницы параметр Trace=true

    Мы видим, что при отключении сохранения состояния вида резко уменьшился и общий объем страницы. Но сервер в своих откликах не сохраняет выбранные клиентом опции списков. Исправим этот недостаток программно.

  • Добавьте к странице код восстановления выбора пользователя в списках
  • <script runat="server">
        
        // Поле класса страницы
        private const int maxCount = 1000;
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Заполняем списки программной генерацией значений
            for (int i = 0; i < maxCount; i++)
            {
                ListBox1.Items.Add("Опция " + (i + 1).ToString());
                DropDownList1.Items.Add("Опция " + (i + 1).ToString());
            }
            
            // Отслеживание выделенных пользователем опций списков 
            if (this.IsPostBack)
            {
                string text = this.Request.Form["ListBox1"];
                ListBox1.Items.FindByText(text).Selected = true;
                text = this.Request.Form["DropDownList1"];
                DropDownList1.Items.FindByText(text).Selected = true;
            }
            else
                ListBox1.SelectedIndex = DropDownList1.SelectedIndex = 0;
        }
    </script>

    Броузер с обратной отсылкой присылает на сервер значения выделенных элементов списка, собираемых дескриптором <form> из HTML-кода, представленного примерно таким фрагментом

    <select size="4" name="ListBox1" id="ListBox1" style="height:200px;width:150px;">
    <option selected="selected" value="Опция 1">Опция 1</option>
    <option value="Опция 2">Опция 2</option>
    <option value="Опция 3">Опция 3</option>
    ..................................................
    </select>

    Фактически броузер посылает пару ListBox1=Опция 1, которая попадает в словарь Request.Form, доступный для извлечения и использования в коде страницы на стороне сервера. Мы находим это значение в сгенерированной коллекции значений списка и настраиваем этот элемент коллекции как выделенный. После таких настроек список сгенерирует в текущем отклике сервера нужный HTML-код, соответствующий состоянию списка на клиенте перед обратной отсылкой.

  • Запустите страницу ViewStateList.aspx с включенной трассировкой и убедитесь, что выбранные опции списков сохраняют свои позиции и ASP.NET генерирует небольшой объем клиентского кода
  • Таким образом мы выяснили, что состояние вида элементов управления, которое включено по умолчанию, не всегда является полезным либо просто не используется, и тогда его лучше отключать. Отключить состояние вида можно следующим образом:

  • На уровне приложения для всех страниц в файле web.config корня web-дерева сайта
    <?xml version="1.0" encoding="utf-8"?>
    <configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
        <system.web>
            <compilation debug="true" />
            <pages enableViewState="false" />
        </system.web>
    </configuration>
  • На уровне страницы, добавив в директиву @ Page параметр EnableViewState="false" (в коде свойство Page.EnableViewState = false )
  • На уровне элемента управления, добавив в дескриптор элемента атрибут EnableViewState="false" (в коде свойство Элемент.EnableViewState = false )
  • Если более общая настройка сохранения состояния вида выключена, а для каких-то страниц или элементов управления ее нужно включить, то в соответствующее место следует добавить атрибут EnableViewState="true".

    Защита информации в состоянии вида

    Состояние вида передается клиенту системой ASP.NET в скрытых полях частично зашифрованным. Но это шифрование поверхностное, чтобы не требовало много процессорного времени. При желании злоумышленник может частично восстановить и прочитать эту информацию.

    Упражнение 4

  • Добавьте к проекту новую страницу (без файла отделенного кода) с именем DisplayViewState.aspx и установите ее стартовой
  • Удалите из интерфейсной части страницы дескриптор <div></div>
  • Включите трассировку, добавив в директиву @ Page страницы параметр Trace=true
  • Добавьте в блок скриптов кодовую часть, чтобы окончательно страница стала такой
    (рис ) Окончательный код страницы DisplayViewState.aspx<%@ Page Language="C#" Trace="true" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Объекты создаем каждый раз заново
            Label label = new Label();
            this.form1.Controls.Add(label);
            
            this.form1.Controls.Add(new HtmlGenericControl("br"));
            
            Button button = new Button();
            this.form1.Controls.Add(button);
            
            if (!this.IsPostBack)
            {
                // Один раз настроили, а дальше работает состояние вида
                label.Text = "Привет всем!!!";
                button.Text = "Отправить";
            }
        }
        
        protected void Page_PreRender(object sender, EventArgs e)
        {
            // Вывод на страницу строки состояния
            if (this.IsPostBack)
            {
                // Извлекаем строку состояния
                string strViewState = this.Request.Form["__VIEWSTATE"];
        
                // Добавляем вывод к родительской форме 
                Label label = new Label();
                this.form1.Controls.Add(label);
                label.Text = "<p><b>Строка состояния как есть:</b><br />" 
                    + strViewState + "</p>";
                label.EnableViewState = false;// Чтобы не влияла на результат
        
                // Преобразуем строку из Base64 в массив ASCII-символов
                byte[] ascii = Convert.FromBase64String(strViewState);
        
                // Вставляем в отклик
                Response.Write("<b>Строка состояния в формате ASCII:</b><br />");
                Response.BinaryWrite(ascii);
        
                // То же самое, только по турецки...
                // Десериализация и отображение строки
                //string decodedViewState = System.Text.Encoding.ASCII.GetString(ascii);
                //Response.Write(decodedViewState);
            }
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        </form>
    </body>
    </html>
  • Запустите страницу DisplayViewState.aspx и повыполните обратную отсылку
  • HTML-отклик на стороне клиента, сгенерированный страницей DisplayViewState.aspx при обратной отсылке, будет выглядеть примерно так

    Мы видим, что некоторая информация, сохраняемая на клиенте в строке состояния вида, может быть частично расшифрована. Злоумышленник может изменить ее нужным для себя образом и попытаться отправить на сервер.

    Поскольку состояние вида формирует только серверная сторона, то перед включением строки в скрытое поле HTML-вывода ASP.NET вычисляет контрольную сумму и прячет ее в ту же самую строку. При получении этой хэшированной строки от клиента система десериализует ее, вновь вычисляет контрольную сумму и сравнивает с ранее спрятанной. Несовпадение легко обнаружить. Подсчет контрольной суммы включается в директиве @ Page параметром EnableViewStateMac="true".

    Для большей защиты строки состояния вида можно использовать встроенное шифрование, которое включается на странице параметром ViewStateEncryptionMode="Always" директивы @ Page или установкой в конфигурационном файле секции

    <pages viewStateEncryptionMode="Always">
  • Включите шифрование и выполните страницу DisplayViewState.aspx. Сгенерированный отклик на обратную отсылку станет примерно таким
  • Теперь строка состояния стала зашифрованной и ее трудно расшифровать. Однако не рекомендуется без надобности шифровать данные состояния вида. Это негативно скажется на производительности, поскольку сервер должен будет выполнять операцию шифрования и дешифрования при каждой отсылке и получении данных.

    Скрытые поля

    Во вкладке Standard панели Toolbox имеется элемент управления HiddenField, который генерирует скрытое поле c единственной переменной, определяемой его свойством Value. Этот элемент не поддерживает шифрование, хэширование и хранение по частям. Поэтому клиенты в исходном коде HTML-вывода могут просматривать эти данные. Данные в скрытом поле хранятся до тех пор, пока пользователь не перейдет к другой странице.

    Упражнение 5

  • Добавьте к проекту новую страницу с именем HiddenFieldTest.aspx без файла отделенного кода и назначьте ее стартовой
  • Переведите страницу в режим Design и двойным щелчком создайте в кодовой части обработчик Page_Load()
  • Поместите на Web-форму из вкладки Standard панели Toolbox два элемента управления HiddenField и присвойте им имена beginTime и countRequest
  • Отключите для всех элементов страницы механизм сохранения состояния вида, добавив в директиву @ Page параметр EnableViewState="false"
  • Заполните страницу следующим кодом
    (рис ) Код страницы HiddenFieldTest.aspx<%@ Page Language="C#" EnableViewState="false" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Создаем текстовую метку
            lblTime = new Label();
            div1.Controls.Add(lblTime);
            div1.Controls.Add(new HtmlGenericControl("br"));
        
            // Создаем кнопку CurrentTime
            Button button = new Button();
            div1.Controls.Add(button);
            // Подписываемся на событие кнопки
            button.Click += CurrentTimeClick;
            button.Text = "Текущее время";
            
            // Добавляем промежуток между кнопками
            Literal literal = new Literal();
            literal.Text = "nbsp;nbsp;nbsp;nbsp;nbsp;";
            div1.Controls.Add(literal);
        
            // Создаем кнопку OldTime
            button = new Button();
            div1.Controls.Add(button);
            // Подписываемся на событие кнопки
            button.Click += OldTimeClick;
            button.Text = "Начальное время";
        
            // Инициализируем скрытые поля и метку при первом запросе
            if (!this.IsPostBack)
            {
                beginTime.Value = System.DateTime.Now.ToLongTimeString();
                OldTimeClick(null, EventArgs.Empty);
            }
            else
            {
                // Ведение счетчика запросов страницы 
                int.TryParse(countRequest.Value, out count);
                count++;
                // Или то же самое
                // count = int.Parse(countRequest.Value) + 1;
            }
            
            countRequest.Value = count.ToString();
        }
        
        // Вынесены в поля класса для видимости в обработчиках
        int count = 0;
        Label lblTime;
        
        void CurrentTimeClick(object sender, EventArgs e)
        {
            lblTime.Text = "Текущее время: " + System.DateTime.Now.ToLongTimeString();
            lblTime.Text += "<br />Ответ № " + count.ToString();
        }
        
        void OldTimeClick(object sender, EventArgs e)
        {
            lblTime.Text = "Начальное время: " + beginTime.Value;
            lblTime.Text += "<br />Ответ № " + count.ToString();
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
            <div style="text-align: center" id="div1" runat="server">
                <h1 style="color: Red">
                    Испытание элемента HiddenField
                </h1>
                <asp:HiddenField ID="beginTime" runat="server" />
                <asp:HiddenField ID="countRequest" runat="server" />
            </div>
        </form>
    </body>
    </html>
  • Исполните страницу и убедитесь, что скрытые поля, генерируемые элементом HiddenField, в пределах одной страницы работают исправно
  • Строки запроса

    Одно из самых серьезных ограничений состояния вида состоит в том, что оно работает только на одной странице. При переходе пользователя на другую страницу состояние вида прежней страницы утрачивается. Одним из выходов является передача информации в строке запроса, которая может переносить информацию со страницы на страницу. Но этот способ имеет несколько недостатков:

  • Информация является видимой для пользователя и доступна для вмешательства с его стороны. В результате страница может получить такие данные, от которых она не имеет защиты
  • Информация должна кодироваться допустимыми для URL символами
  • Броузеры устанавливают ограничение на длину URL, которая должна быть в пределах 1-2 Кб. Поэтому в строку запроса нельзя помещать большое количество информации
  • Форма отправляющей страницы должна использовать метод передачи GET вместо POST
  • Формирование строки запроса и извлечение из нее информации программист выполняет вручную
  • Отправляющая запрос страница инициирует передачу либо с помощью гиперссылки (элемент HyperLink ), либо с помощью кнопки Submit, в обработчик которой вставлен код Response.Redirect(URL). Для извлечения информации из строки запроса на адресуемой (целевой) странице используется встроенный в нее объект-словарь Request.QueryString.

    Упражнение 6

  • Добавьте к проекту страницу без файла отделенного кода с именем SourceQueryString.aspx и назначьте ее стартовой
  • Добавьте внутрь контейнера <div></div> дескриптор строки-заголовка
    <h1 style="color: Red">Введите учетную запись</h1>
  • Дополните открывающий дескриптор <div> стилем центрирования
    <div style="text-align: center">
  • Создайте на Web-форме позиционирующую таблицу размером 3x2 и поместите в ее ячейки из вкладки Standard панели Toolbox серверные элементы с настройками согласно таблице
  • Таблица свойств элементов управления страницы SourceQueryString.aspx
    Элемент Свойство
    Label ID="Label1"
    Text="Имя:"
    TextBox ID="Name"
    Label ID="Label2"
    Text="Пароль:"
    TextBox ID="Password"
    TextMode="Password"
    Button ID="Button1"
    Text="Отправить"

    Интерфейс страницы в режиме проектирования будет таким

  • Переведите страницу в режим Design и двойным щелчком на элементе Button1 создайте для него обработчик. Заполните обработчик следующим кодом
  • protected void Button1_Click(object sender, EventArgs e)
    {
        string Url = "TargetQueryString.aspx?Name="
                                + Server.UrlEncode(Name.Text)
                                + "Password="
                                + Server.UrlEncode(Password.Text);
        this.Response.Redirect(Url);
        
        // То же самое, только с прямой переадресацией
        // this.Server.Transfer(Url);
    }

    Кнопка будет инициировать обратную отсылку, а ее обработчик сформирует адрес целевой страницы со значениями параметров, снятых с элементов текстовых полей. Перед включением в строку адреса введенных пользователем значений мы применяем URL-кодирование, преобразуя тем самым ввод в допустимые символы. К сожалению, строку адреса приходится формировать вручную.

  • Переведите страницу в режим Design и двойным щелчком создайте в кодовой части обработчик Page_Load(). Заполните обработчик кодом, восстанавливающий состояние текстового поля Name сохраненным в строке запроса значением
  • protected void Page_Load(object sender, EventArgs e)
    {
        if (this.Request.QueryString["Name"] != null  !this.IsPostBack)
            Name.Text = this.Request.QueryString["Name"];
    }

    Страница SourceQueryString.aspx, отправляющая строку запроса, готова. Вот ее полный код

    <%@ Page Language="C#" %>
        
    <script runat="server">
        
        protected void Button1_Click(object sender, EventArgs e)
        {
            string Url = "TargetQueryString.aspx?Name="
                                    + Server.UrlEncode(Name.Text)
                                    + "Password="
                                    + Server.UrlEncode(Password.Text);
            this.Response.Redirect(Url);
            
            // То же самое с прямой переадресацией
            // this.Server.Transfer(Url);
        }
        
        protected void Page_Load(object sender, EventArgs e)
        {
            if (this.Request.QueryString["Name"] != null  !this.IsPostBack)
                Name.Text = this.Request.QueryString["Name"];
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
            <div style="text-align: center">
                <h1 style="color: Red">
                    Введите учетную запись</h1>
                <table border="0" width="100%">
                    <tr>
                        <td style="text-align: right; width: 412px;">
                            nbsp;<asp:Label ID="Label1" runat="server" Text="Имя:" />
                        </td>
                        <td>
                            nbsp;<asp:TextBox ID="Name" runat="server" />
                        </td>
                    </tr>
                    <tr>
                        <td style="width: 412px; text-align: right">
                            nbsp;<asp:Label ID="Label2" runat="server" Text="Пароль:" />
                        </td>
                        <td>
                            nbsp;<asp:TextBox ID="Password" runat="server" TextMode="Password" />
                        </td>
                    </tr>
                    <tr>
                        <td colspan="2" style="text-align: center">
                            nbsp;<asp:Button ID="Button1" runat="server" Text="Отправить" 
                            OnClick="Button1_Click" />
                        </td>
                    </tr>
                </table>
            </div>
        </form>
    </body>
    </html>

    Теперь нужно создать принимающую запрос (целевую) страницу. На целевой странице значения поступивших параметров, извлекаемые из строки запроса, следует предварительно URL-раскодировать. Затем можно принять решение, что делать с поступившими данными.

  • Создайте страницу с совмещенным кодом и именем TargetQueryString.aspx
  • Заполните страницу следующим кодом
  • <%@ Page Language="C#" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Критерии оценки зарегистрированного пользователя
            const string NAME = "USER", PASSWORD = "ROOT";
        
            // Извлекаем и раскодируем поступившие данные из строки запроса
            string name = this.Request.QueryString["Name"];
            name = this.Server.UrlDecode(name);
            string password = this.Request.QueryString["Password"];
            password = this.Server.UrlDecode(password);
        
            // Создаем текстовую метку для отображения решения
            Label label = new Label();
            form1.Controls.Add(label);
        
            // Резервируем место для размещения гиперссылки
            PlaceHolder placeHiperLink = new PlaceHolder();
            form1.Controls.Add(placeHiperLink);
        
            // Создаем муляж кнопки
            Button button = new Button();
            form1.Controls.Add(button);
        
            // Анализируем данные и принимаем решение
            StringBuilder message = new StringBuilder();
            if (name.ToUpper() == NAME  password.ToUpper() == PASSWORD)
            {
                // Просто выведем хвалебное приветствие 
                message.Append("<h1 style=\"color: Red\">");
                message.Append("Приветствуем Вас, дорогой " + name + "!");
                message.Append("</h1>");
        
                // Отключаем у кнопки способность инициировать отсылку
                // Лень посылать дальше зарегистрированного пользователя 
                button.PostBackUrl = "javascript: void(0)";
                button.Text = "Дальше >";
        
                // Добавляем в зарезервированное место гиперссылку
                HyperLink link = new HyperLink();
                placeHiperLink.Controls.Add(link);
                link.Text = "< Назад";
                link.NavigateUrl = "~/SourceQueryString.aspx?"
                    + "Name=" + name;
        
                // Добавляем в зарезервированное место пробелы
                Label lblSpace = new Label();
                placeHiperLink.Controls.Add(lblSpace);
                lblSpace.Text = "nbsp;nbsp;nbsp;nbsp;";
            }
            else
            {
                // Поругаем и отправим на повторную регистрацию
                message.Append("<h1>");
                message.Append("Вы неопознаны!<br />Повторите регистрацию...");
                message.Append("</h1>");
        
                button.PostBackUrl = "~/SourceQueryString.aspx?"
                    + "Name=" + name;
                button.Text = "< Назад";
            }
        
            // Выводим сформированное сообщение
            label.Text = message.ToString();
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        </form>
    </body>
    </html>

    Обратите внимание, что интерфейсная часть целевой страницы совершенно пустая - все создается динамически.

  • Запустите страницу SourceQueryString.aspx на выполнение и убедитесь, что механизм межстраничного обмена информацией через строку запроса работает
  • Страница TargetQueryString.aspx на клиенте при наведении курсора мыши на гиперссылке будет выглядеть так

    Межстраничная обратная отсылка данных

    В классах кнопочных элементов управления Button, ImageButton и LinkButton имеется свойство PostBackUrl, позволяющее сразу адресоваться к целевой странице с доступом в ней ко всем данным текущей страницы. Свойству PostBackUrl кнопки обратной отсылки присваивается строка с URL целевой страницы, которая и будет загружена на выполнение по щелчку пользователя на этой кнопке. При этом на целевой странице, благодаря ее свойству PreviousPage, будет полностью доступен объект страницы, инициировавшей обратную отсылку.

    Идея реализации механизма межстраничной отсылки достаточно проста. Любая страница на этапе описания является классом (инструкцией) с именем, равном имени страницы, которая при загрузке в память сервера превращается в объект (экземпляр класса), адресуемый средой исполнения. Исполняемый объект проходит все необходимые этапы жизни, начиная с инициализации элементов управления, корректировкой состояния, выполнением обработчиков, и заканчивая генерацией HTML-кода (рендерингом).

    То же самое происходит и со страницей, загруженной по кнопке с ненулевым свойством PostBackUrl. Она также проходит все начальные стадии жизни, включая выполнение обработчиков. Непосредственно перед генерацией HTML-кода выполнение исходной страницы прерывается, но она продолжает оставаться в памяти сервера. Одновременно запускается объект целевой страницы, которому в свойство PreviousPage среда исполнения записывает адрес объекта первой страницы.

    Теперь код целевой страницы может адресоваться к общедоступным данным исходной страницы, но рендеринг для клиента будет генерироваться только второй страницей. Когда целевая страница заканчивает свой цикл жизни, вместе с ней из памяти выгружается и исходная страница. Для того, чтобы закрытые или защищенные члены класса исходной страницы стали доступными в классе целевой страницы, их нужно обернуть в общедоступные свойства или функции доступа.

    Рассмотренная межстраничная связь может быть реализована не только как "один к одному", но и в других сочетаниях: "один ко многим", "многие к одному" или "многие ко многим". Множественность на исходной странице может быть организована программным формированием значения свойства PostBackUrl в зависимости от выбранных на этой странице предпочтений. Множественность на целевой странице реализуется проверкой того, к какому типу (к какой странице) относится записанный в свойство PreviousPage объект.

    Межстраничная обратная отсылка целевой страницы может быть инициирована не только по щелчку на кнопке с ненулевым значением свойства PostBackUrl, но и в любом месте кода вызовом соответствующей перегрузки метода

    public void Page.Server.Transfer(string path, bool preserveForm)

    где

  • path - вызываемая целевая страница
  • preserveForm - флаг передачи в целевую страницу данных QueryString и Form
  • Оптимальным будет вызов

    Server.Transfer(Url, true);

    Упражнение 7

    Проиллюстрируем межстраничную отсылку на примере двух страниц-источников и двух страниц-приемников.

  • Добавьте к проекту страницу с совмещенным кодом и именем SourcePostBack1.aspx
  • Назначьте страницу стартовой и оформите пользовательский интерфейс на этапе проектирования так, как показано на рисунке
  • Здесь использованы серверные элементы Label, TextBox, ListBox, Button и HyperLink.

  • Для элемента управления ListBox включите свойство AutoPostBack
  • Заполните страницу следующим кодом
    (рис ) Полный код страницы SourcePostBack1.aspx<%@ Page Language="C#" %>
        
    <script runat="server">
        
        // Функция доступа к закрытому полю класса из внешнего кода
        public string GetName()
        {
            return txtName.Text;
        }
        
        // Свойство доступа к закрытым данным класса из внешнего кода
        public string GetTitle
        {
            get { return lblTitle.Text; }
        }
        
        string text;
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Просто отображаем страницу
            if (lstPost.SelectedItem == null)
                return;
            
            // Выбираем целевую страницу при AutoPostBack списка
            text = lstPost.SelectedItem.Text;
            if (String.Compare(text, "Студент", true) == 0)
                btnPostBackUrl.PostBackUrl = "~/CrossPageStudent.aspx";
            else 
                btnPostBackUrl.PostBackUrl = "~/CrossPageLecturer.aspx";
        }
        
        protected void btn_Click(object sender, EventArgs e)
        {
            // Контролируем наличие выбора при щелчках на кнопках
            if (lstPost.SelectedItem == null)
            {
                String mess = "<script>alert('Выберите сотрудника')</" + "script>";
                this.Response.Write(mess);
                return;
            }
        
            text = lstPost.SelectedItem.Text;
            
            // Различаем кнопки
            Button btn = (Button)sender;
            if (String.Compare(btn.Text, "PostBackUrl", true) != 0)
            {
                if (String.Compare(text, "Студент", true) == 0)
                    this.Server.Transfer("~/CrossPageStudent.aspx", true);
                else
                    this.Server.Transfer("~/CrossPageLecturer.aspx", true);
            }
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
            <div style="text-align: center">
                <asp:Label ID="lblTitle" runat="server" Text="Это первая страница-источник"></asp:Label>
                <br />
                <br />
                Фамилия:nbsp;
                <asp:TextBox ID="txtName" runat="server">Иванов</asp:TextBox>nbsp;
                <asp:ListBox ID="lstPost" runat="server" Height="45px" AutoPostBack="True">
                    <asp:ListItem>Студент</asp:ListItem>
                    <asp:ListItem>Доцент</asp:ListItem>
                </asp:ListBox>
                <br />
                <br />
                <asp:Button ID="btnPostBackUrl" runat="server" Text="PostBackUrl" OnClick="btn_Click" />nbsp;
                <asp:Button ID="btnServerTransfer" runat="server" OnClick="btn_Click"
                    Text="Server.Transfer()" />
                <br />
                <br />
                <asp:HyperLink ID="HyperLink1" runat="server" NavigateUrl="SourcePostBack2.aspx">
                На вторую страницу-источник</asp:HyperLink>
            </div>
        </form>
    </body>
    </html>
  • Получите копию страницы SourcePostBack1.aspx под именем SourcePostBack2.aspx и скорректируйте в ней элементы lblTitle и HyperLink1
  • Создайте новую страницу с именем CrossPageStudent.aspx без файла отделенного кода, в которой блок скриптов заполните так
    (рис ) Полный код страницы-приемника данных CrossPageStudent.aspx<%@ Page Language="C#" %>
        
    <script runat="server">
        
        // Поля, заполняемые данными вызвавшей страницы
        String name, title, className;
        
        // Наша функция, использующая доступ с помощью механизма отражения типов
        void GetInfoPreviousPage()
        {
            // Получаем тип запускающей страницы для
            // динамического вызова членов представляющего класса
            System.Type typePreviousPage = this.PreviousPage.GetType();
            // Находим общедоступный метод класса 
            System.Reflection.MethodInfo methodGetName =
                typePreviousPage.GetMethod("GetName");
            // Выполняем метод предыдущей страницы
            name = methodGetName.Invoke(this.PreviousPage, null).ToString();
            // Находим общедоступное свойство класса 
            System.Reflection.PropertyInfo propertyGetTitle =
                typePreviousPage.GetProperty("GetTitle");
            // Выполняем свойство предыдущей страницы
            title = propertyGetTitle.GetGetMethod().
                Invoke(this.PreviousPage, null).ToString();
            // Извлекаем имя класса предыдущей страницы
            className = this.PreviousPage.GetType().Name;
        }
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Получаем информацию с предыдущей страницы
            GetInfoPreviousPage();
        
            Label label = new Label();
            form1.Controls.Add(label);
            string text = "<h1 style='text-align: center; color: Red'>";
        
            if (this.PreviousPage == null)
            {
                text += "Страница запущена напрямую</h1>";
                label.Text = text;
                return;
            }
        
            text += "Методом межстраничной обратной отсылки получены данные</h1>";
            text += "<ul>";
            text += "<li>Заголовок: " + title + "</li>";
            
            if (String.Compare(className, "SourcePostBack1_aspx", true) == 0)
                text += "<li>Данные прислала первая страница</li>";
            else if (String.Compare(className, "SourcePostBack2_aspx", true) == 0)
                text += "<li>Данные прислала вторая страница</li>";
            text += "<li>Фамилия студента: " + name + "</li>";
            
            if (this.PreviousPage.IsCrossPagePostBack)
                text += "<li>Обратная отсылка инициирована свойством PostBackUrl</li>";
            else
                text += "<li>Обратная отсылка инициирована методом Server.Transfer()</li>";
            text += "</ul>";
            label.Text = text;
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        </form>
    </body>
    </html>
  • Получите копию страницы CrossPageStudent.aspx под именем CrossPageLecturer.aspx
  • Эти обе страницы-приемники имеют совершенно одинаковый код и просто демонстрируют работу схемы межстраничной передачи данных "многие ко многим". В обеих страницах используется наша функция GetInfoPreviousPage(), в которую мы упаковали код, извлекающий данные из класса страницы-источника на этапе выполнения. Этот прием применен потому, что при прямой адресации к членам предыдущей страницы компилятор ищет их в текущей странице и выдает ошибку компиляции (и правильно делает). А при динамическом доступе он работает только со строковыми именами свойств и методов предыдущей страницы.

  • Разберитесь с кодом отсылающей и принимающей сторон и испытайте их работу
  • Вариант результата на принимающей странице будет примерно таким

    Cookie-наборы

    Куки-наборы являются еще одним распространенным способом хранения информации на клиенте. Они, подобно состоянию вида, также включают в себя текстовую информацию, но хранят ее не в скрытых полях HTML-документа, а в небольших файлах, хранящихся на клиенте в специальной временной папке ( ...\Temporary Internet Files\ ). В зависимости от версии броузера для каждого сайта у клиента таких файлов может быть до 20 с размером до 4 Кб каждый. Клиент может даже и не подозревать, что при взаимодействии с сайтом его броузер автоматически принимает и отсылает связанные с ним куки.

    Однако клиент имеет возможность удалить куки, отключить в броузере поддержку куков, прочитать информацию в любом из них и опасно ее модифицировать. Такую возможность разработчик страницы должен учитывать и применять соответствующие методы. В свою очередь, разработчик страницы может устанавливать время жизни куков и шифровать сохраняемую в них информацию. Использование куков опирается на два объекта-свойства страницы Request и Response, каждый из которых имеет коллекцию (набор) Cookies типа HttpCookieCollection. Поступившие от клиента куки нужно извлекать из коллекции Request.Cookies, а отправляемые на клиента новые данные для их сохранения в куках необходимо добавлять в коллекцию Response.Cookies.

    Коллекции Cookies состоят из отдельных объектов-экземпляров класса HttpCookie, каждый из которых является словарем. При создании объекта-словаря в конструкторе HttpCookie(string) задается имя кука, под которым он будет адресоваться в коллекции. Перегруженная версия конструктора HttpCookie(string, string) позволяет при создании имени кука определять и его единственное значение.

    Важным свойством объекта cookie является Expires, которое определяет срок хранения куки-файла на клиенте. Если броузер при соединении с сайтом обнаружит связанный файл с просроченным сроком хранения, то он его немедленно уничтожит. Если это свойство не установлено, то cookie будет храниться в памяти, пока пользователь не закроет броузер. Для немедленного удаления cookie на клиенте с длительным сроком хранения нужно перезаписать в него просроченную дату.

    Упражнение 8

    В качестве примера разработаем страницу, на которой значения некоторых интерфейсных элементов управления будем хранить в cookies.

  • Добавьте к проекту страницу без отделенного кода с именем CookiesPage1.aspx и назначьте ее стартовой
  • Никакие элементы в режиме проектирования мы на странице размещать не будем, а все создадим и настроим динамически, чтобы еще раз подчеркнуть возможности программирования. В итоге код нашей страницы будет выглядеть так

    <%@ Page Language="C#" ValidateRequest="false" %>
        
    <script runat="server">
            
        // Вынесли как поле для видимости в обработчиках
        // Инициализировать необязательно, компилятор сам справится
        TextBox txtName;
        String result = "";
        int count = 0;
        
        protected void Page_Load(object sender, EventArgs e)
        {
            this.Response.Write("<h1 style='color: Red;'>Испытание Cookies</h1>");
        
            Label label = new Label();
            label.Text = "Введите ФИО:nbsp;nbsp;";
            form1.Controls.Add(label);
        
            txtName = new TextBox();
            txtName.Text = "Снетков В.М.";
            form1.Controls.Add(txtName);
        
            form1.Controls.Add(new HtmlGenericControl("br"));
        
            Button button = new Button();
            button.Text = "Отправить";
            form1.Controls.Add(button);
        
            button = new Button();
            button.Text = "Сохранить";
            form1.Controls.Add(button);
            button.Click += btnSaveClick;
        
            button = new Button();
            button.Text = "Восстановить";
            form1.Controls.Add(button);
            button.Click += btnRestoreClick;
        
            form1.Controls.Add(new HtmlGenericControl("br"));
        
            button = new Button();
            button.Text = "Удалить";
            form1.Controls.Add(button);
            button.Click += btnDeleteClick;
        
            form1.Controls.Add(new HtmlGenericControl("br"));
        
            HyperLink link = new HyperLink();
            form1.Controls.Add(link);
            // Определяем имя класса текущей страницы и формируем гиперссылку
            String nameCurrentPage = this.GetType().Name;
            // Разбиваем строку на части
            String[] parts = nameCurrentPage.Split('_');
            // Сравниваем первую часть без учета регистра
            if (String.Compare(parts[0], "CookiesPage1", true) == 0)
                link.NavigateUrl = "CookiesPage2" + ".aspx";
            else
                link.NavigateUrl = "CookiesPage1" + ".aspx";
            link.Text = "Перейти к " + link.NavigateUrl;
        
            // Отключаем сохранение состояния вида всех элементов страницы
            DisabledViewState(form1.Controls);
        
            // Извлекаем общее число посещений 
            HttpCookie cookie = this.Request.Cookies["count"];
            if (cookie != null)
            {
                int.TryParse(cookie.Value, out count);
            }
            count++;
            // Адресуемся к отклику и сохраняем общее число посещений
            cookie = this.Response.Cookies["count"];
            cookie.Value = count.ToString();
            // Устанавливаем срок хранения на клиенте: от текущего 1 день 
            cookie.Expires = DateTime.Now.AddDays(1);
            // Отображаем сообщение на странице пользователя
            result = "Текущий запрос: " + count.ToString();
        }
        
        // Отключаем механизм сохранения состояния вида для всех элементов
        void DisabledViewState(ControlCollection controls)
        {
            foreach (Control ctrl in controls)
            {
                // Отключаем сохранение состояния вида элемента
                if (ctrl.EnableViewState)
                    ctrl.EnableViewState = false;
        
                // Продолжаем опрашивать рекурсивно, если есть дочерние элементы
                if (ctrl.Controls != null)
                    DisabledViewState(ctrl.Controls);
            }
        }
        
        // Переопределяем обработчик генерации HTML-вывода страницы
        protected override void Render(HtmlTextWriter output)
        {
            // Центрируем динамически созданные элементы управления
            output.RenderBeginTag(HtmlTextWriterTag.Center);
            base.Render(output);
            output.Write("<hr />");
            output.Write(result);
            output.RenderEndTag();
        }
        
        // Сохраняем данные в куке "oldInfo"
        void btnSaveClick(object sender, EventArgs e)
        {
            // Создаем словарь и заполняем текущей информацией 
            HttpCookie cookie = new HttpCookie("oldInfo");
            // Кодировать необязательно, направляется в файл
            cookie["name"] = txtName.Text;
            cookie["clicks"] = count.ToString();
            // Присоединяем к ответу
            // Срок хранения - текущий сеанс
            // cookie.Expires = DateTime.Now.AddYears(1);
            this.Response.Cookies.Add(cookie);
        }
        
        // Извлекаем данные из кука "oldInfo"
        void btnRestoreClick(object sender, EventArgs e)
        {
            // Подключаемся к кукам поступившего запроса
            HttpCookieCollection cookies = this.Request.Cookies;
            // Извлекаем нужный словарь
            HttpCookie cookie = cookies["oldInfo"];
            if (cookie != null)
            {
                // Извлекаем значения словаря
                string name, clicks;
                name = cookie["name"];
                clicks = cookie["clicks"];
                
                // Альтернативный синтаксис
                // name = cookies["oldInfo"]["name"];
                // clicks = cookies["oldInfo"]["clicks"];
                // Если готовим к отправке, то можно...
                // cookies["oldInfo"].Expires = DateTime.Now.AddYears(1);
                
                // Безопасно отображаем сохраненные значения,
                // кодируя интерпретируемые броузером символы
                name = Server.HtmlEncode(name);
                clicks = Server.HtmlEncode(clicks);
                result = "Сохраненное ФИО: " + name;
                result += "; cохраненный запрос: " + clicks;
            }
            else
            {
                result = "Нечего восстанавливать! ";
                result += "Текущий запрос: " + count.ToString();
            }
        }
        
        // Удаляем кук "count"
        void btnDeleteClick(object sender, EventArgs e)
        {
            // Удаляем кук количества запросов, посылая просроченную дату
            this.Response.Cookies["count"].Expires = 
                DateTime.Now.AddDays(-1);
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        <div>
            
        </div>
        </form>
    </body>
    </html>

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

  • Создайте в проекте копию страницы CookiesPage1.aspx и присвойте ей имя CookiesPage2.aspx
  • Запустите первую страницу CookiesPage1.aspx и испытайте ее работу совместно со второй страницей
  • Вид страницы на клиенте будет таким

    Проверка поддержки cookies клиентом

    Механизм cookies является очень удобным, но неустойчивым, поскольку может либо не поддерживаться броузером, либо пользователь может эту поддержку отключить. На этот случай разработчик может применить другие схемы сохранения данных при межстраничных отсылках. Но прежде всего такую проблему нужно обнаружить.

    Информация о функциональности броузера автоматически поступает на сервер с каждым запросом пользователя и записывается в объект типа System.Web.HttpBrowserCapabilities, ссылка на который находится в свойстве Page.Request.Browser объекта страницы. Эта информация включает в себя тип броузера, его версию, поддержку на стороне клиента сценариев, стилей, фреймов, cookie, таблиц и другое.

    К сожалению эта информация свидетельствует только о потенциальных возможностях броузера, но никак не о том, как сконфигурирован (настроен) броузер в данный момент. В такой ситуации у разработчика имеется два выхода: положиться на присланную информацию и считать, что в броузере эта функциональность не отключена, либо выполнить дополнительные проверки.

    Что касается cookie-наборов, то при первом запросе страницы после того, как мы удостоверимся, что такая функциональность у броузера имеется, можно сделать пробный отклик с посылкой cookie-набора с помощью метода Response.Redirect(), а затем в поступившем запросе попытаться прочитать его. Если такая попытка не удастся, значит поддержка cookies-наборов недоступна и нужно переключать логику выполнение страницы на другие методы сохранения межстраничной информации.

    Упражнение 9

  • В качестве иллюстрации к сказанному создайте страницу с именем IsCookiesSupported.aspx и заполните ее следующим кодом
  • <%@ Page Language="C#" ClassName="IsCookiesSupported" EnableViewState="false" %>
        
    <script runat="server">
        
        bool CookiesSupportedFlag;
        int clicks = 1;
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Создаем объекты пользовательского интерфейса
            // Текстовая метка заголовка
            Label label = new Label();
            form1.Controls.Add(label);
            label.Text = "<h2>Поддерживаются ли Cookies?</h2>";
        
            // Кнопка Submit
            Button submit = new Button();
            form1.Controls.Add(submit);
            submit.Text = "Отправить";
        
            // Горизонтальная линия
            form1.Controls.Add(new HtmlGenericControl("hr"));
        
            // Текстовая метка результатов
            label = new Label();
            form1.Controls.Add(label);
        
            if (!this.IsPostBack)
            {
                // Проверяем функциональность броузера на поддержку Cookies
                CookiesSupportedFlag = this.Request.Browser.Cookies;
                if (CookiesSupportedFlag)
                {
                    // Для дополнительной проверки пробуем отправить cookie
                    this.Response.Cookies["tmp"].Value = String.Empty;
                }
        
                // this.Response.Redirect(Request.Url.ToString());// Зацикливает
        
                // Дублируем сохранение в состоянии вида
                this.ViewState["clicks"] = clicks.ToString(); ;
                label.Text = "Пробный запрос страницы";
                return;
            }
            else if (this.Request.Cookies["tmp"] != null)
            {
                // Поднимаем флаг поддержки cookies
                CookiesSupportedFlag = true;
            }
            else
                CookiesSupportedFlag = false;
        
            // Принимаем решение, откуда извлекать и где сохранять данные
            if (CookiesSupportedFlag)
            {
                if (this.Request.Cookies["clicks"] != null)
                    clicks = int.Parse(this.Request.Cookies["clicks"].Value) + 1;
                this.Response.Cookies["clicks"].Value = clicks.ToString();
                // Устанавливаем срок хранения
                this.Response.Cookies["clicks"].Expires =
                    DateTime.Now.AddMinutes(1.0);
            }
            else
            {
                clicks = Convert.ToInt32(this.ViewState["clicks"]) + 1;
                this.ViewState["clicks"] = clicks.ToString();
            }  
            
            // Показываем пользователю количество щелчков и механизм хранения
            label.Text = "Запрос страницы № " + clicks.ToString();
            if (CookiesSupportedFlag)
                label.Text += "<br />Хранение в cookies";
            else
                label.Text += "<br />Хранение в ViewState";   
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        </form>
    </body>
    </html>

    Cookie-набору можно назначить область видимости в пределах сайта. В этом случае броузер будет следить за тем, при работе с какими страницами отправлять на них куки, а на какие не отправлять. Так, например, если кук "clicks" при записи пометить

    Response.Cookies["clicks"].Path = "~/MyFolder";

    то кук будет доступен только страницам папки MyFolder. По умолчанию кук доступен любым страницам домена, как и, например, при установке

    Response.Cookies["clicks"].Domain = "mysite.ru";

    Перейдите ко второй части ...

    Сохранение данных в состоянии сеанса

    Любой пользователь при первом обращении к приложению получает свой сеанс, который выражается в выделении некоторого объема памяти на стороне сервера для хранения данных любого типа. Доступ к этим совместным данным осуществляется любой страницей через присоединенный к ней объект-словарь Session. За счет этого можно сохранять информацию на одной странице, а получать к ней доступ с другой страницы.

    При поступлении первого клиентского запроса на объект страницы ASP.NET генерирует идентификатор сеанса размером 42 байта. Этот идентификатор сеанса включается в отклик сервера и является уникальным в пределах приложения. Если ни одна из страниц приложения, к которым обращается пользователь, не записывает в объект Session данные, то броузер не возвращает его в запросе серверу. Вместо этого сервер при каждом запросе генерирует новый идентификатор сеанса и включает его в отклик. Стоит только странице записать какие-нибудь данные в объект Session, идентификатор сеанса сразу запоминается броузером и начинается обратный процесс. Теперь уже броузер с каждым запросом начинает посылаться на сервер сохраненный идентификатор сеанса, даже если в дальнейшем мы полностью очистим объект Session.

    Когда клиент присылает идентификатор сеанса, сервер по нему автоматически извлекает из места хранения данные сеанса и заполняет ими объект Session созданной страницы. После чего данные сеанса становятся доступными в коде этой страницы.

    Идентификатор сеанса может пересылаться серверу с запросом двумя способами:

  • С помощью cookie-набора с именем ASP.NET_SessionId
  • С помощью включения в URL для клиентов, не поддерживающих cookie-наборы (использование измененных URL - адресов)
  • Идентификатор сеанса можно получить как значение Response.Cookies["ASP.NET_SessionID"] - при формировании первого отклика, или Session.SessionID - всегда. Но конкретное его значение нас, как правило, не интересует, поскольку процесс распознавания выполняется автоматически.

    Данные сеанса могут сохраняться ASP.NET в одном из трех мест на сервере, определяемых свойством Mode объекта Session:

  • В оперативной памяти сервера внутри процесса - текущего домена приложения (режим InProc - установлен по умолчанию)
  • В оперативной памяти сервера вне процесса в специальной службе Windows под названием ASP.NET State Service (режим StateServer )
  • Вне процесса на жестком диске в базе данных SQL Server (режим SQLServer )
  • Состояние сеанса утрачивается в следующих случаях:

  • Пользователь закрывает и вновь запускает броузер
  • Пользователь получает доступ к приложению через другой броузер
  • Из-за простоя сеанса больше определенного времени по причине отсутствия запросов от клиента (по умолчанию 20 минут)
  • Программист завершает сеанс методом Session.Abandon()
  • Если сеанс создается внутри текущего домена, то при переходе приложения в новый домен сеанс будет потерян
  • Сеанс является экземпляром класса System.Web.SessionState.HttpSessionState и может частично управляться из кода. В таблице приведены некоторые свойства и методы класса HttpSessionState

    Некоторые свойства и методы класса HttpSessionState
    Член Описание
    Count Количество элементов в коллекции текущего сеанса. Только для чтения
    IsCookieless Булев флаг, показывающий способ хранения идентификатора сеанса: в куках или в модифицированном URL. Только для чтения
    IsNewSession Булево значение, которое указывает, был ли сеанс создан с текущим запросом. Если коллекция пуста или сеанс создан ранее, то возвращается false. Только для чтения
    Mode Свойство (только для чтения) содержит одно из значений перечисления SessionStateMode, указывающее способ хранения данных сеанса:
  • Off = 0 - сеанс не используется
  • InProc = 1 - данные сеанса хранятся в памяти домена
  • StateServer = 2 - сеанс хранится в специальной службе Windows под названием ASP.NET State Service
  • SQLServer = 3 - сеанс хранится в базе данных SQL Server
  • Custom = 4 - сеанс хранится на основе нестандартного поставщика
  • SessionID Строка, содержащая уникальный идентификатор сеанса. Только для чтения
    Timeout Максимальный срок простоя сеанса в минутах. Для чтения и записи
    Abandon() Отменяет текущий сеанс
    Add(string name, object value) Добавляет новый элемент в коллекцию сеанса. Допускается альтернативный синтаксис в виде индексатора
    Clear() Очищает коллекцию сеанса
    RemoveAll() Очищает коллекцию сеанса
    Remove(string name) Удаляет элемент данных из коллекции сеанса по ключевому имени
    RemoveAt(int index) Удаляет элемент данных из коллекции сеанса по ключевому индексу

    Начальное состояние сеанса устанавливается в конфигурационном файле web.config в секции <sessionState>. Настройки секции <sessionState> поддерживаются классом System.Web.Configuration.SessionStateSection через его свойства, но имена свойств в кофигурационном файле начинаются с символа нижнего регистра. Вот некоторые наиболее важные из них

    Некоторые свойства SessionStateSection
    Свойство Описание
    Cookieless Задает режим использования cookie-набора для пересылки идентификатора сеанса. Определяется значениями перечисления System.Web.HttpCookieMode:
  • UseUri = 0 - никогда не использовать cookie-наборы, а включать идентификатор сеанса в URL (в файле web.config допускаются варианты синтаксиса: cookieless="UseUri" или cookieless="true" )
  • UseCookies = 1 - всегда использовать cookie-наборы, даже если броузер их не поддерживает или эта функциональность в нем отключена. Этот режим установлен по умолчанию. Для неподдерживающих cookie-наборы броузеров данные сеанса будут теряться, поскольку идентификатор сеанса возвращаться на сервер не будет
  • AutoDetect = 2 - наиболее надежный режим, в котором сервер сам определяет возможности броузера по поддержке куков и автоматически применяет нужной вариант пересылки идентификатора сеанса
  • UseDeviceProfile = 3 - способ пересылки идентификатора сеанса основывается только на потенциальной функциональности броузера по поддержке cookie-наборов, но без учета возможности фактического отключения этой функциональности пользователем
  • Mode Задает одно из значений перечисления SessionStateMode, указывающее способ хранения данных сеанса:
  • Off = 0 - указывает, что сеанс не задействуется для хранения данных. Такой режим может существенно увеличить быстродействие, когда необходимости в сеансе нет
  • InProc = 1 - данные сеанса хранятся в памяти домена
  • StateServer = 2 - сеанс хранится в специальной службе Windows под названием ASP.NET State Service
  • SQLServer = 3 - сеанс хранится в базе данных SQL Server
  • Custom = 4 - сеанс хранится на основе нестандартного поставщика
  • Timeout Устанавливают время простоя сеанса в минутах
    RegenerateExpiredSessionId Булева переменная, запрещающая ASP.NET использовать поступивший от клиента идентификатор сеанса с просроченной меткой времени, а генерирующая новый. Это особенно важно при включенном режиме cookieless="true", когда идентификатор сеанса пересылается броузером в составе URL, куда его может специально встроить злонамеренный пользователь
    CookieName Устанавливает имя идентификатора сеанса в cookie-наборе. По умолчанию используется имя "ASP.NET_SessionID"

    При включенном режиме cookieless="true" броузер автоматически встраивает идентификатор сеанса в URL всех относительных ссылок. Это же справедливо и при автоматической переадресации на новую страницу методом Response.Redirect("Относительный_адрес") с полным циклом. Для того, чтобы указать системе, что идентификатор сеанса должен передаваться секретным протоколом, нужно установить Response.Cookies["ASP.NET_SessionID"].Secure = true;

    Приведем пример использования сеанса.

    Упражнение 10

  • Отредактируйте файл web.config приложения как показано ниже
    (рис ) Файл web.config с включенным режимом Cookieless<?xml version="1.0" encoding="utf-8"?>
    <configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
        <system.web>
            <compilation debug="true" />
            <sessionState cookieless="true" regenerateExpiredSessionId="true" />
    	</system.web>
    </configuration>
  • Добавьте страницу с именем SessionCookieless.aspx с совмещенным кодом, которую заполните так
    (рис ) Код страницы SessionCookieless.aspx<%@ Page Language="C#" EnableViewState="false" %>
        
    <script runat="server">
        
        int clicks = 1;
        const int maxCol=2;
        HtmlTable table;
        
        protected void Page_Load(object sender, EventArgs e)
        {
            string[] headers = {
                    // Имена свойств секции sessionState 
                    "Cookieless",
                    "Mode",
                    "Timeout",
                    "RegenerateExpiredSessionId",
                    "CookieName",
                    // Дополнительная информация
                    "SessionID",
                    "numRequest",
                    "currentTime"
                                };
        
            // Создаем объекты пользовательского интерфейса
            // Текстовая метка заголовка
            Label label = new Label();
            form1.Controls.Add(label);
            label.Text = "<h2>Использование сеансов Session</h2>";
        
            if (!IsPostBack)
            {
                // Получаем всю информацию о параметрах конфигурации
                System.Configuration.Configuration config;
                config =
                    System.Web.Configuration.WebConfigurationManager.
                    OpenWebConfiguration(this.Request.ApplicationPath);
        
                // Объявляем ссылку на объект SessionStateSection
                System.Web.Configuration.SessionStateSection sessionState;
        
                // Поиск элемента <sessionState> в элементе <system.web>
                sessionState = (System.Web.Configuration.SessionStateSection)
                    config.GetSection(@"system.web/sessionState");
                //sessionState.Timeout = TimeSpan.FromMinutes(1);
        
                // Формируем таблицу и сохраняем ее в сеансе
                table = new HtmlTable();
                table.Border = 1;
                for (int i = 0; i < headers.Length; i++)
                {
                    // Создаем строку
                    HtmlTableRow row = new HtmlTableRow();
                    table.Rows.Add(row);
                    // Создаем ячейки в строке
                    for (int j = 0; j < maxCol; j++)
                    {
                        HtmlTableCell col = new HtmlTableCell();
                        row.Cells.Add(col);
                        if (j == 0)// Левый столбец
                        {
                            col.InnerHtml = headers[i] + ":nbsp;";
                            col.Align = "right";
                            col.BgColor = "Yellow";// Цвет фона
                        }
                        else // Правый столбец
                        {
                            // Применяем методику отражения типов
                            System.Type type = typeof(System.Web.Configuration.
                                SessionStateSection);
                            // Извлекаем публичное свойство по его имени
                            System.Reflection.PropertyInfo property =
                                type.GetProperty(headers[i]);
                            if (property != null)
                            {
                                col.InnerHtml = "nbsp;";
                                // Извлекаем значение свойства из экземпляра класса 
                                col.InnerHtml += property.GetGetMethod().
                                    Invoke(sessionState, null).ToString();
                            }
                            else
                            {
                                // Маркируем ячейку, чтобы потом найти
                                col.ID = headers[i];
                            }
                            col.BgColor = "#CCFFFF";// Цвет фона
                        }
                    }
                }
        
                // Сохраняем информацию в сеансе при первом запросе
                this.Session.Add("table", table);
                this.Session["clicks"] = clicks;
            }
            else
            {
                // Извлекаем информацию из сеанса при каждой обратной отсылке
                table = (HtmlTable)this.Session["table"];
                clicks = (int)this.Session["clicks"];
                // Сохраняем новое значение
                this.Session["clicks"] = ++clicks;
            }
        
            // Добавляем таблицу на форму
            form1.Controls.Add(table);
        
            // Корректируем таблицу
            ((HtmlTableCell)Page.FindControl("SessionID")).InnerHtml =
                "nbsp;" + this.Session.SessionID;
            ((HtmlTableCell)Page.FindControl("numRequest")).InnerHtml =
                "nbsp;" + clicks.ToString();
            ((HtmlTableCell)Page.FindControl("currentTime")).InnerHtml =
                "nbsp;" + DateTime.Now.ToLocalTime();
        
            // Новая строка HTML
            HtmlGenericControl br = new HtmlGenericControl();
            br.InnerHtml = "<br />";
            form1.Controls.Add(br);
        
            // Кнопка Submit
            Button submit = new Button();
            form1.Controls.Add(submit);
            submit.Text = "Отправить";
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server" style="text-align: center;">
        </form>
    </body>
    </html>
  • Клиентский результат будет таким

    Обратите внимание, что идентификатор сеанса встроен в URL при значении параметра Cookieless="UseUri". Отметьте также, что интерфейсная часть страницы пустая - все создается динамически. Часть выводимой в таблицу информации мы добыли из объекта класса SessionStateSection, который создается по настройкам конфигурационного файла. Для более лаконичного кода мы применили методику отражения типов, позволившую опосредованно вызывать свойства объекта. В сессиях можно сохранять объекты любого типа, а не только строки. Но при извлечении их нужно явно приводить к соответствующему типу.

    Сохранение данных в состоянии приложения

    Состояние приложения - это область памяти сервера, выделяемая при запуске приложения для хранения глобальных данных, доступных с любой страницы приложения. На эти данные в каждой странице ссылается объект-словарь Application типа System.Web.HttpApplicationState. Как и при состоянии сеанса, состояние приложения хранит данные в виде объектов, поэтому при их извлечении необходимо явное преобразование типов.

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

    Упражнение 11

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

  • Добавьте к приложению файл Global.asax и заполните его так
    (рис ) Файл Global.asax<%@ Application Language="C#" %>
        
    <script runat="server">
        
        void Application_Start(object sender, EventArgs e) 
        {
            this.Application["clicks"] = (int)0;
        }
    </script>
  • Создайте страницу без отделенного кода с именем ApplicationState1.aspx, назначьте ее стартовой и заполните так
    (рис ) Файл ApplicationState1.aspx<%@ Page Language="C#" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Читаем без проверки - инициализирована в global.asax
            int visit = (int)this.Application["clicks"] + 1;
            // Обновляем состояние приложения с блокировкой 
            this.Application.Lock();
            this.Application["clicks"] = visit;
            this.Application.UnLock();
            
            // Создаем текстовую метку
            Label label = new Label();
            form1.Controls.Add(label);
            label.Text = "Сегодня вы посетили нас в " 
                + visit.ToString() + " раз!<br />";
            
            // Создаем гиперссылку
            HyperLink link = new HyperLink();
            form1.Controls.Add(link);
            link.NavigateUrl = "~/ApplicationState2.aspx";
            link.Text = "На другую страницу...";
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        </form>
    </body>
    </html>
  • Создайте копию для страницы ApplicationState1.aspx с именем ApplicationState2.aspx и поменяйте в ней гиперссылку на ApplicationState1.aspx, чтобы страницы ссылались друг на друга
  • Запустите само приложение несколько раз и пощелкайте по ссылкам чтобы убедиться, что счетчик количества визитов увеличивается
  • В панели задач нижней части экрана щелкните правой кнопкой мыши на пиктограмме тестового сервера и остановите его командой
  • Вновь запустите страницу ApplicationState1.aspx, чтобы убедиться, что счетчик сбросился в начальное значение
  • Хранение данных в статических переменных приложения

    Для увеличения масштабируемости приложение загружается в память сервера в виде множества копий (пула). Каждый экземпляр класса приложения имеет свой набор данных и только объект-тип хранит все методы и статические данные в единственном экземпляре. Если в файле Global.asax объявить общедоступные статические поля, то в них можно хранить данные, которые будут видимы через имя класса приложения из любой страницы сайта. Но при этом нужно явно определить в директиве @ Application файла Global.asax параметр ClassName, в котором указывается имя класса приложения для последующего обращения из кода страниц.

    При изменении в страницах глобальных данных класса приложения механизм автоматической блокировки отсутствует. Поэтому нужно пользоваться встроенным в язык C# оператором lock, чтобы на время позволить воздействовать на переменную только со стороны одного потока. Такой подход удобен тем, что мы можем обернуть сохраняемые данные в статические свойства и назначить для них контролирующий код, прежде чем сохранять их в памяти сервера. Мы можем объявлять статические переменные для хранения объектов любого типа и для доступа к ним явных преобразований типа не потребуется.

    Упражнение 12

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

  • Отредактируйте код файла Global.asax следующим образом
    (рис ) Код файла Global.asax со статическими членами<%@ Application Language="C#" ClassName="MyAppClass" %>
        
    <script runat="server">
        
        // Список файлов
        private static string[] fileList;// Базовое поле
        // Общедоступное статическое свойство для обертки базового поля
        public static string[] FileList
        {
            get
            {
                // Заполнить список только при первом запросе 
                if (fileList == null)
                {
                    fileList = System.IO.Directory.GetFiles(
                        HttpContext.Current.Request.PhysicalApplicationPath);
                }
                return fileList;
            }
        }
        
        // Число посещений 
        private static int visit = 1;// Базовое поле
        // Общедоступное статическое свойство для обертки базового поля
        public static int Visit
        {
            get { return visit; }
            set 
            {
                lock (typeof(int))// Контейнер обеспечения синхронизации потоков
                {
                    visit = value;
                }
            }
        }
    </script>
  • Добавьте к приложению страницу без отделенного кода с именем AppClassStaticMembers.aspx и назначьте ее стартовой
  • Заполните страницу AppClassStaticMembers.aspx следующим кодом
    (рис ) Код страницы AppClassStaticMembers.aspx <%@ Page Language="C#" EnableViewState="false" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Заголовок
            Label title = new Label();
            form1.Controls.Add(title);
            title.Text = "<h2 style='text-align: center; color: Red'>"
                + "Использование статических"
                + " полей класса приложения</h2>"; 
        
            // Число посещений
            Label visit = new Label();
            form1.Controls.Add(visit);
            int clicks = MyAppClass.Visit;
            visit.Text = "<b>Количество посещений: " 
                + clicks.ToString() + "</b>";
            MyAppClass.Visit = ++clicks;
            
            // Горизонтальная черта
            form1.Controls.Add(new HtmlGenericControl("hr"));
        
            // Извлекаем и готовим список файлов 
            // текущего каталога приложения
            StringBuilder builder = new StringBuilder();
            builder.Append("<ol>");
            // Счетчик для сокращения листинга 
            int count = 0;
            const int maxCount = 5;
            foreach (string file in MyAppClass.FileList)
            {
                if (++count > maxCount)
                    break;
                
                string str;
                //int pos = file.LastIndexOf(@"\");
                //str = file.Substring(pos + 1);
                // То же самое
                str = System.IO.Path.GetFileName(file);
                builder.Append("<li>" + str + "</li>");
            }
            builder.Append("</ol>");
            
            // Готовим текстовую метку для отображения списка
            Label fileInfo = new Label();
            form1.Controls.Add(fileInfo);
            fileInfo.Text = builder.ToString();
        
            // Горизонтальная черта
            form1.Controls.Add(new HtmlGenericControl("hr"));
            
            // Создаем дескриптор центрирования кнопки
            HtmlGenericControl center = new HtmlGenericControl("center");
            form1.Controls.Add(center);
                
            // Кнопка Submit добавляется в контейнер центрирования
            Button button = new Button();
            button.Text = "Отправить";
            center.Controls.Add(button);
        }
        
        // Переопределяем обработчик рендеринга
        protected override void Render(HtmlTextWriter writer)
        {
            // Сгенерировать основное
            base.Render(writer);
            
            // Добавить в конец еще HTML-вывод
            // Настроить атрибуты будущего дескриптора 
            writer.AddAttribute(HtmlTextWriterAttribute.Align, "Center");
            writer.AddStyleAttribute(HtmlTextWriterStyle.Color, "Blue");
            // Создать дескриптор
            writer.RenderBeginTag(HtmlTextWriterTag.H2);
            // Писать внутрь дескриптора
            writer.Write("Щелкайте на кнопке и наблюдайте счетчик посещений");
            // Закрыть дескриптор
            writer.RenderEndTag();
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        </form>
    </body>
    </html>
  • Запустите страницу и убедитесь, что все работает и объявленные в классе приложения статические члены сохраняют свои значения даже при отключении клиента от сервера до тех пор, пока сервер остается запущенным
  • В панели задач нижней части экрана щелкните правой кнопкой мыши на пиктограмме тестового сервера и остановите его командой
  • Вновь запустите страницу AppClassStaticMembers.aspx, чтобы убедиться, что память сервера очистилась и счетчик сбросился в начальное значение
  • А сейчас мы убедимся, что при модификации любого файла приложения, включая web.config, оно автоматически перекомпилируется при первом запросе и загружается в новый домен.

  • Не нарушая кода, модифицируйте любой исходный файл приложения, например, введите пробел после точки с запятой в конце любой строки (или добавьте пустую строку) и сохраните изменения
  • Запустите приложение и убедитесь, что счетчик сбросился за счет загрузки приложения в новый домен
  • Интерфейс клиента для данного примера будет таким

    Рассмотренные в данной теме способы хранения данных относятся, в основном, к внутрипроцессным, так как сохраняются в оперативной памяти сервера или передаются по каналам связи на компьютер клиента для временного хранения. Другие методы хранения информации в службах сервера, включая базы данных и файлы, являются внепроцессными, поскольку не зависят от среды исполнения ASP.NET.

    Страницы:
    Файлы к лекции Вы можете скачать здесь

    Главная особенность удаленных приложений состоит в том, что они могут одновременно обрабатывать множество пользователей. К тому же сервер может выполнять одновременно несколько разных приложений. Поэтому среда исполнения устроена так, что после обработки запроса очередного пользователя память сервера немедленно очищается от кода запрошенной страницы, освобождая ресурсы для новых возможных запросов. Измененное состояние страницы, информация о пользователе и другие оперативные данные приложения теряется безвозвратно, а при следующем запросе страница загружается в память сервера как при первом. Такой механизм называется работой без сохранения состояния.

    Такой факт вынуждает разработчиков Web-приложений изыскивать другие способы хранения информации о состоянии между запросами, с последующим ее извлечением и использованием. В ASP.NET существует два способа хранения информации между запросами: на клиенте и на сервере. Разработчик сайта сам решает, какую группу методов хранения данных состояния ему выбрать.

    Хранение информации на клиенте имеет следующие преимущества:

  • Лучшая масштабируемость - не расходуется память сервера для хранения состояния, что позволяет обслужить больше клиентов
  • Поддержка множества Web-серверов - обработку поступающих запросов можно распараллелить между несколькими серверами. При этом код приложения модифицировать не нужно, поскольку вся необходимая информация о состоянии поступает вместе с каждым запросом
  • Хранение информации на сервере имеет следующие преимущества:

  • Безопасность - защищенность данных состояния от злоумышленников. Информация управления состоянием на клиенте может быть похищена с клиентского компьютера или перехвачена в пути. А хранение на сервере конфиденциальных данных, таких как пароль, уровень доступа, состояние авторизации делают это безопасным
  • Снижение нагрузки на каналы связи - снижение времени загрузки страницы и увеличение масштабируемости, особенно для мобильных клиентов
  • Управление состоянием на клиенте

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

    Различают следующие методы хранения данных на клиенте:

  • Состояние вида ( view state ) - используется для сохранения значений элементов управления, добавленных конечным пользователем. Применение состояния вида основано на свойстве ViewState типа System.Web.UI.StateBag, наследуемом от класса Control любым элементом управления и самой страницей.
  • Скрытые поля ( hidden fields ) - сохранение данных в скрытых полях HTML-вывода. Они также хранят информацию об одной странице, но в отличие от состояния вида скрытые поля не поддерживают хэширование, шифрование, сжатие и хранение по частям. При использовании скрытых полей данные формы на сервер должны отправляться методом POST (по щелчку кнопки Submit ), а не методом GET (по щелчку на гиперссылке).
  • Строки запроса ( query strings ) - сохранение данных в URL. Видимы для пользователя в поле адреса
  • Межстраничные обратные отсылки - присланные вызывающей страницей данные остаются в памяти сервера на момент работы страницы-приемника
  • Куки-наборы ( cookie -файлы) - созданные на сервере данные записываются на компьютер клиента в небольших файлах (в количестве до 20 файлов и до 4 Кб каждый) и автоматически пересылаются броузером на сервер вместе с каждым запросом. Это лучший способ хранения состояния, доступного многим страницам приложения
  • Рассмотрим последовательно перечисленные методы хранения состояния на клиенте.

    Состояние вида

    Это простой способ хранения информации на клиенте между обратными отсылками для одной и той же страницы. Вспомним, как обрабатывается страница на сервере. Для этого рассмотрим свежий отклик сервера, только что полученный броузером. В нем броузеру вместе с отображаемым HTML-кодом в скрытых полях была прислана информация со значениями состояния элементов управления страницы на момент отправки с сервера. Пользователь через броузер меняет состояние некоторых элементов и инициирует обратную отсылку.

    При поступлении обратной отсылки на сервер в память загружается адресуемая страница в своем первозданном виде, инициницализируются ее интерфейсные элементы начальными настройками, затем извлекается поступившая в скрытых полях информация о состоянии на момент предыдущего отклика и свойства элементов корректируются. После этого срабатывают события элементов управления о последних действиях пользователя и состояние элементов управления еще раз корректируются. Таким образом, состояние интерфейсных элементов при создании страницы перезаписывается по меньшей мере три раза: первоначальными настройками, последними сохранными значениями и новыми значениями, инициированными пользователем.

    Все серверные Web-элементы управления используют состояние вида по умолчанию (не обязательно включать EnableViewState="true" ). В состав каждого из них входит свойство-коллекция ViewState, наследуемое от класса System.Web.UI.Control. Это же свойство напрямую наследует и класс Page, который мы расширяем при проектировании активной страницы. Оно создается и заполняется поступившими данными вместе с объектом страницы, а значит мы программно можем использовать это страничное свойство для сохранения и извлечения информации между запросами одной и той же страницы.

    После обработки страницы ее текущее состояние, а также состояние элементов управления, автоматически хэшируется (hash - шифруется, превращается в мусор; не путайте с кэшем - сохранением в памяти) и строка с хэшем пересылается броузеру в скрытом поле страницы вместе с отображаемым HTML. Если данные получаются очень объемные и не помещаются в одном поле, то ASP.NET автоматически делит их на части и записывает в нескольких скрытых полях. Максимальный объем одного скрытого поля определяется свойством maxPageStateFieldLength класса Page (по умолчанию равен -1 ). Для изменения начальных настроек объекта страницы значение этого свойства нужно поместить в конфигурационный файл, например

    <?xml version="1.0" encoding="utf-8"?>
    <configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
        <system.web>
            <compilation debug="true" />
            <pages maxPageStateFieldLength="1024" />
        </system.web>
    </configuration>

    Коллекция ViewState построена по типу словаря (ассоциативного массива), в котором значения адресуются строковыми уникальными ключами. Элементами коллекции являются ссылки на объекты обобщенного типа Object, поэтому она способна хранить объекты любого типа. При извлечении из коллекции состояния вида данные нужно приводить к соответствующему типу. Если в коде попытаться извлечь значение, которого нет в коллекции, будет выдано исключение NullReferenceException, поэтому необходима предварительная проверка возвращаемой ссылки на null -значение.

    Не все типы объектов могут быть сохранены в состоянии вида. Простые типы сохраняются безусловно, а более сложные типы должны поддаваться сериализации (преобразовываться в поток байтов). Если объявлению класса предшествует атрибут [Serializable], то порожденный этим классом объект можно сохранять в состоянии вида. В противном случае, при попытке сохранить такой объект в состоянии вида ASP.NET выдаст ошибку выполнения. Библиотечный тип Hashtable из пространства имен System.Collections также представляет собой словарь с полями Key и Value. Он поддается сериализации, поэтому настроенный объект такого типа можно целиком сохранять и восстанавливать в ViewState.

    В качестве иллюстрации сказанного приведем 2 примера, где используется программирование словаря ViewState самой страницы.

    Упражнение 1

    В этом примере мы создадим форму с интерфейсными элементами, в которых отключим автоматическое сохранение состояния вида, а будем его сохранять и восстанавливать вручную. Интерфейсные элементы мы разместим в серверной таблице и будем использовать рекурсивную логику для опроса всех ее дочерних элементов, размещенных в коллекции Controls. В качестве ключа для доступа к элементам словаря ViewState уровня страницы будем использовать идентификаторы элементов.

  • Создайте пустой проект с именем
  • Добавьте к проекту новую Web-страницу с именем
  • Запустите командой Website/ASP.NET Configuration страницу WAT (Web Site Administration Tool) и выполните команду Application Configuration/Configure debugging and tracing. На появившейся странице включите флажок Enable debugging
  • Таким действием мы создадим файл конфигурации Web.config с настройкой отладки.

  • В панели Solution Explorer щелкните на кнопке Refresh, чтобы увидеть файл конфигурации в составе проекта
  • Откройте интерфейсную часть страницы ViewStateTest.aspx в режиме Source и добавьте в открывающую часть дескриптора <div> внутренний стиль
    <div style="text-align: center">
    и заголовок
    <h1 style="color: Red">Заполните анкету</h1>
  • Откройте интерфейсную часть страницы ViewStateTest.aspx в режиме Design и поместите на нее элемент Table из вкладки HTML панели Toolbox
  • Щелкните на элементе Table правой кнопкой мыши и выполните опцию Run As Server Control, чтобы превратить его в серверный
  • Откорректируйте интерфейсный HTML-код заготовки таблицы следующим образом
    (рис ) <table id="Table1" width="100%" runat="server" style="text-align: left; width: 500px;">
        <tr>
            <td style="width: 200px">
            </td>
            <td>
            </td>
        </tr>
        <tr>
            <td style="width: 200px">
            </td>
            <td>
            </td>
        </tr>
        <tr>
            <td style="width: 200px">
            </td>
            <td>
            </td>
        </tr>
        <tr>
            <td style="width: 200px">
            </td>
            <td>
            </td>
        </tr>
        <tr>
            <td style="width: 200px">
            </td>
            <td>
            </td>
        </tr>
        <tr>
            <td style="width: 200px">
            </td>
            <td>
            </td>
        </tr>
    </table>
  • Созданная таблица имеет 6 строк и 2 столбца.

  • Поместите в первую строку и первый столбец таблицы приведенный ниже текст, а в остальные ячейки второго столбца - элемент TextBox из вкладки Standard панели Toolbox
  • Поместите после таблицы три кнопки Button из вкладки Standard панели Toolbox и присвойте им имена cmdSubmit, cmdSave и cmdRestore соответственно
  • После этих действий интерфейс страницы в режиме проектирования будет таким

    Ему соответствует такой дескрипторный код

    <%@ Page Language="C#" AutoEventWireup="true" 
             CodeFile="ViewStateTest.aspx.cs" Inherits="ViewStateTest" %>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        <div style="text-align: center">
        <h1 style="color: Red">Заполните анкету</h1>
            <table id="Table1" runat="server" style="text-align: left; width: 500px;">
                <tr>
                    <td style="width: 200px">
                        <b>Наименование</b></td>
                    <td>
                        <b>Значение</b></td>
                </tr>
                <tr>
                    <td style="width: 200px">
                        Имя:</td>
                    <td>
                        <asp:TextBox ID="TextBox1" runat="server"></asp:TextBox></td>
                </tr>
                <tr>
                    <td style="width: 200px">
                        Учетный номер:</td>
                    <td>
                        <asp:TextBox ID="TextBox2" runat="server"></asp:TextBox></td>
                </tr>
                <tr>
                    <td style="width: 200px">
                        Возраст:</td>
                    <td>
                        <asp:TextBox ID="TextBox3" runat="server"></asp:TextBox></td>
                </tr>
                <tr>
                    <td style="width: 200px">
                        E-mail:</td>
                    <td>
                        <asp:TextBox ID="TextBox4" runat="server"></asp:TextBox></td>
                </tr>
                <tr>
                    <td style="width: 200px">
                        Пароль:</td>
                    <td>
                        <asp:TextBox ID="TextBox5" runat="server"></asp:TextBox></td>
                </tr>
            </table>
            <br />
            <asp:Button ID="cmdSubmit" runat="server" Text="Отправить" />
            nbsp;nbsp;nbsp; nbsp;
            <asp:Button ID="cmdSave" runat="server" Text="Сохранить" />
            nbsp;nbsp;nbsp; nbsp;	
            <asp:Button ID="cmdRestore" runat="server" Text="Восстановить" />
        </div>
        </form>
    </body>
    </html>
  • Двойным щелчком на каждой из двух последних кнопок cmdSave и cmdRestore в интерфейсной части страницы создайте для них заготовки обработчиков с именами по умолчанию
  • Откройте файл отделенного кода и заполните его следующим кодом
    (рис ) Содержимое файла ViewStateTest.aspx.csusing System;
    using System.Data;
    using System.Configuration;
    using System.Collections;
    using System.Web;
    using System.Web.Security;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    using System.Web.UI.WebControls.WebParts;
    using System.Web.UI.HtmlControls;
        
    public partial class ViewStateTest : System.Web.UI.Page
    {
        private bool executeFlag = true;
        
        protected void cmdSave_Click(object sender, EventArgs e)
        {
            // Сохраняем присланные значения текстовых полей
            SaveAllText(Table1.Controls, executeFlag);
        }
        
        private void SaveAllText(ControlCollection controls, bool saveNested)
        {
            if (!saveNested)
                return;
        
            // Перебираем все дочерние элементы в переданной коллекции таблицы 
            foreach (Control control in controls)
            {
                // Выбираем элементы текстовых полей и сохраняем
                // их содержимое в свойстве ViewState объекта страницы,
                // используя в качестве ключа идентификатор элемента
                if (control is TextBox)
                {
                    this.ViewState[control.ID] = ((TextBox)control).Text;
                }
        
                // На верхних уровнях объекта Table1 дочерними элементами
                // являются строки и ячейки таблицы, поэтому продолжаем
                // рекурсивно спускаться ниже, пока не встретим объекты TextBox.
                // Флаг saveNested разрешает проверять вложенные уровни коллекций 
                if (control.Controls != null)
                {
                    SaveAllText(control.Controls, saveNested);
                }
            }
        }
        
        protected void cmdRestore_Click(object sender, EventArgs e)
        {
            // Извлекаем пришедший с обратной отсылкой сохраненный текст предыдущей
            // обратной отсылки и вставляем его в текстовые поля, готовя новый отклик
            RestoreAllText(Table1.Controls, executeFlag);
        }
        
        void RestoreAllText(ControlCollection controls, bool restoreNested)
        {
            if (!restoreNested)
                return;
        
            // Перебираем все элементы таблицы, включая вложенные, чтобы
            // использовать их идентификаторы как ключи для извлечения
            // возможно сохраненных значений из словаря ViewState
            foreach (Control control in controls)
            {
                // Нашли текстовое поле 
                if (control is TextBox)
                {
                    // Убеждаемся, что есть в словаре
                    if (this.ViewState[control.ID] != null)
                    {
                        // Приводим типы и восстанавливаем
                        ((TextBox)control).Text = (string)this.ViewState[control.ID];
                    }
                }
        
                // Рекурсивно спускаемся ниже к дочерним элементам
                if (control.Controls != null)
                {
                    RestoreAllText(control.Controls, restoreNested);
                }
            }
        }
    }
  • Испытайте работу страницы и убедитесь, что в любое время можно восстановить состояние элементов управления, сохраненное ранее в словаре ViewState страницы
  • Упражнение 2

    Этот пример подобен предыдущему, но в нем значения текстовых полей прежде поэлементно сохраняются в промежуточном объекте Hashtable, а затем этот объект целиком сохраняется в свойстве объекта страницы ViewState. Интерфейсная часть страницы остается в точности такой-же, как в предыдущем примере, а кодовая часть изменится. При этом, чтобы не менять интерфейсную часть, мы динамически (в кодовой части) добавим в конце коллекции формы form1 текстовую метку для отображения на клиенте содержимого промежуточного объекта-словаря.

  • Через панель Solution Explorer создайте копию страницы ViewStateTest.aspx и присвойте ей имя ViewStateObjects.aspx
  • Измените имя класса ViewStateTest на ViewStateObjects в кодовой и интерфейсной частях скопированной страницы
  • Заполните кодовую часть следующим образом
  • using System;
    using System.Data;
    using System.Configuration;
    using System.Collections;
    using System.Web;
    using System.Web.Security;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    using System.Web.UI.WebControls.WebParts;
    using System.Web.UI.HtmlControls;
        
    public partial class ViewStateObjects : System.Web.UI.Page
    {
        private bool executeFlag = true;
        
        // Создаем экземпляр промежуточного словаря
        Hashtable dictionary = new Hashtable();
        
        protected void cmdSave_Click(object sender, EventArgs e)
        {
            // Сохраняем присланные значения текстовых полей в словаре
            SaveAllText(Table1.Controls, executeFlag);
        
            // Сохраняем помеченный словарь целиком в состоянии вида
            this.ViewState["contentsState"] = dictionary;
        }
        
        private void SaveAllText(ControlCollection controls, bool saveNested)
        {
            if (!saveNested)
                return;
        
            // Перебираем все дочерние элементы в переданной коллекции таблицы 
            foreach (Control control in controls)
            {
                // Выбираем элементы только текстовых полей и сохраняем
                // их содержимое в промежуточном объекте, используя
                // в качестве ключа идентификатор элемента
                if (control is TextBox)
                {
                    // Значения полей перед сохранением на всякий случай
                    // URL-кодируем как поступившие от ненадежного источника
                    // (предохраняемся от возможного ввода зловредных скриптов).
                    // Чтобы система не реагировала на угловые скобки в полях ввода,
                    // нужно в директиву @ Page интерфейсной части вставить атрибут
                    //  ValidateRequest="false" или в секцию 
                    // <pages validateRequest="false"> файла web.config
                    // Внимание!!! XML-файл web.config чувствителен к регистру символов
                    string text = ((TextBox)control).Text;
                    text = Page.Server.HtmlEncode(text);
        
                    // То же самое...
                    // text = HttpUtility.HtmlEncode(text);
        
                    // Добавляем в словарь обезвреженный текст
                    dictionary.Add(control.ID, text);
                }
        
                // На верхних уровнях объекта Table1 дочерними элементами
                // являются строки и ячейки таблицы, поэтому продолжаем
                // рекурсивно спускаться ниже, пока не встретим объекты TextBox.
                // Флаг saveNested разрешает проверять вложенные уровни коллекций 
                if (control.Controls != null)
                {
                    SaveAllText(control.Controls, saveNested);
                }
            }
        }
        
        protected void cmdRestore_Click(object sender, EventArgs e)
        {
            // Извлекаем из состояния вида пришедший с 
            // обратной отсылкой промежуточный объект и заполняем
            // из него текстовые поля сохраненными значениями
            RestoreAllText(executeFlag);
        }
        
        // Если доступ к функции явно не объявлен, то по умолчанию считается private
        void RestoreAllText(bool restoreNested)
        {
            if (!restoreNested)
                return;
        
            // Извлекаем помеченные ранее данные из состояния
            // вида в промежуточный объект-словарь
            dictionary = (Hashtable)this.ViewState["contentsState"];
        
            // Перебираем все элементы заполненного словаря и ищем на 
            // странице соответствующее текстовое поле. Заодно заполняем
            // парами словаря текстовую метку для отсылки клиенту. Текстовую
            // метку создадим динамически и добавим в конец коллекции формы
            Label label = new Label();
            form1.Controls.Add(label);
            label.Text = "<hr />" + "<h2>Значения словаря</h2>";
        
            foreach (DictionaryEntry pair in dictionary)
            {
                // Выделяем ключ и значение для очередной извлеченной пары
                string key = pair.Key.ToString();
                string value = pair.Value.ToString();
        
                // Ищем на странице текстовое поле с соответствующим 
                // идентификатором, уникальным в пределах страницы
                TextBox item = (TextBox)Page.FindControl(key);
                if (item != null)
                {
                    // Если нашли, заполняем поле и текстовую метку
                    item.Text = value;
        
                    label.Text += string.Format("Key={0}, Value={1}", key, value);
                    label.Text += "<br />";
                }
            }
        }
    }

    Поскольку мы взяли на себя проверку достоверности данных, введенных пользователем, то нужно отключить такую проверку системой ASP.NET при считывании запроса, поместив соответствующую инструкцию в директиву @ Page страницы или в файл конфигурации в секцию <pages>. При этом нужно строго учитывать регистр символов и в директиву @ Page атрибут помещать таким ValidateRequest="false", а в конфигурационный файл таким validateRequest="false". Это общее правило ASP.NET для случая, когда настройки допускают двойственное размещение.

  • Дополните файл конфигурации web.config секцией <pages> с отключенной проверкой достоверности
    (рис ) Файл web.config с отключенной проверкой достоверности<?xml version="1.0" encoding="utf-8"?>
    <configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
        <system.web>
            <compilation debug="true" />
            <pages validateRequest="false" />
        </system.web>
    </configuration>
  • Запустите приложение и убедитесь в его работоспособности
  • Результат после восстановления ранее сохраненной в состоянии вида информации будет примерно таким

  • Удалите из секции <system.web> файла Web.config параметр отключения проверки достоверности <pages validateRequest="false" /> и включите его как атрибут ValidateRequest="false" в состав директивы @ Page страницы ViewStateObjects.aspx, чтобы он действовал только на одну эту страницу
  • Восстановление состояния вида элемента списка через коллекцию запроса

    Иногда состояние вида в элементе полезно отключить, чтобы уменьшить нагрузку на каналы связи. Но для некоторых элементов это нарушает их нормальную работу. Возьмем, например, списки ListBox и DropDownList. Когда они заполнены большим количеством элементов, объем пересылаемой информации состояния вида очень большой и это свойство лучше отключить. Для восстановления состояния выделенного элемента списка в этом случае можно воспользоваться информацией из коллекции Request.Form страницы. Проверим это на примере.

    Упражнение 3

  • Добавьте к проекту страницу с именем ViewStateList.aspx без файла отделенного кода и назначьте ее стартовой
  • Оформите страницу так, чтобы код ее интерфейсной части был следующим
    (рис ) Код интерфейсной части страницы ViewStateList.aspx<%@ Page Language="C#" %>
    	
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        <div style="text-align: center">
        <h2>Отключение состояния вида в списках</h2>
            <asp:ListBox ID="ListBox1" runat="server" Height="200px" Width="150px" />
            nbsp; nbsp; nbsp; nbsp;
            <asp:DropDownList ID="DropDownList1"
                runat="server" Width="150px">
            </asp:DropDownList><br />
            <br />
            <asp:Button ID="Button1" runat="server" Text="Отправить" /></div>
        </form>
    </body>
    </html>
  • Этому коду в режиме проектирования соответствует следующее представление
  • Двойным щелчком на свободном месте страницы в режиме Design создайте блок скриптов с заготовкой обработчика Page_Load()
  • Поместите в обработчик Page_Load() код заполнения списков видимым содержимым, который будет срабатывать при каждом запросе страницы
    (рис ) Обобработчик Page_Load() для заполнения списков<script runat="server">
        
        // Поле класса страницы
        private const int maxCount = 1000;
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Заполняем списки программной генерацией значений
            for (int i = 0; i < maxCount; i++)
            {
                ListBox1.Items.Add("Опция " + (i + 1).ToString());
                DropDownList1.Items.Add("Опция " + (i + 1).ToString());
            }
        }
    </script>
  • Выполните страницу и убедитесь, что при каждом новом запросе по щелчку на кнопке Submit отмеченные пользователем опции списков возвращаются сервером сохраненными
  • Пока что это работает механизм автоматического сохранения состояния вида ASP.NET. Но давайте посмотрим, какой при этом получается объем пересылаемой информации, именно касающийся работы этого механизма. Для этого включим работу ASP.NET в режиме выдачи трассировки на индивидуальных страницах. Это можно сделать, если добавить атрибут Trace="true" в директиву @ Page страницы, либо через утилиту WAT, запустив ее командой Website/ASP.NET Configuration (или щелкнуть на пиктограмме в панели Solution Explorer ).

  • Запустите утилиту WAT, пойдите последовательно по ссылкам Application Configuration/Configure debugging and tracing и включите флажки "Capture tracing information" и "Display tracing information on ondividual pages". Закройте утилиту
  • В результате этих действий файл конфигурации примет вид

    <?xml version="1.0" encoding="utf-8"?>
    <configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
        <system.web>
            <trace enabled="true" pageOutput="true" />
            <compilation debug="true" />
        </system.web>
    </configuration>
  • Вновь запустите страницу ViewStateList.aspx, поманипулируйте с интерфейсными элементами, выполните обратную отсылку и проанализируйте секцию Control Tree в результатах трассировки, которая будет, примерно, такой
  • Control Tree
    Control UniqueID Type Render Size Bytes (including children) ViewState Size Bytes (excluding children) ControlState Size Bytes (excluding children)
    __Page ASP.viewstatelist_aspx 910866 0 0
    ctl02 System.Web.UI.LiteralControl 50 0 0
    ctl00 System.Web.UI.HtmlControls.HtmlHead 46 0 0
    ctl01 System.Web.UI.HtmlControls.HtmlTitle 33 0 0
    ctl03 System.Web.UI.LiteralControl 14 0 0
    form1 System.Web.UI.HtmlControls.HtmlForm 910736 0 0
    ctl04 System.Web.UI.LiteralControl 129 0 0
    ListBox1 System.Web.UI.WebControls.ListBox 227258 196036 0
    ctl05 System.Web.UI.LiteralControl 47 0 0
    DropDownList1 System.Web.UI.WebControls.DropDownList 227246 196036 0
    ctl06 System.Web.UI.LiteralControl 32 0 0
    Button1 System.Web.UI.WebControls.Button 78 0 0
    ctl07 System.Web.UI.LiteralControl 12 0 0
    ctl08 System.Web.UI.LiteralControl 20 0 0

    Объем пересылаемой по каналам связи в оба конца информации получается огромным до неприличия. Причем с каждой новой отсылкой он катастрофически нарастает, добавляя к прежним сохраненным значениям сгенерированные в коде новые значения списков. Отключим сохранение состояния вида в списках, а для восстановления позиций выделенных пользователем опций воспользуемся тем фактом, что с каждым запросом эти значения передаются на сервер в словарь Request.Form объекта страницы.

  • Перейдите в режим Design редактора страницы ViewStateList.aspx, выделите одновременно оба списка и через панель Properties сбросьте свойства EnableViewState
  • Запустите страницу ViewStateList.aspx и убедитесь, что объем сохраняемого состояния вида для списков стал нулевым
  • Замечание. Если количество запросов превысит заданное параметром трассировки requestLimit значение (по умолчанию 10), то ASP.NET при исполнении не будет включать результаты трассировки в страницу. В этом случае нужно при очередном запуске тестового браузера вызвать через строку адреса страницу trace.axd и щелкнуть на ссылке "clear current trace" этой страницы. Включить безлимитную трассировку можно более просто - добавить в директиву @ Page страницы параметр Trace=true

    Мы видим, что при отключении сохранения состояния вида резко уменьшился и общий объем страницы. Но сервер в своих откликах не сохраняет выбранные клиентом опции списков. Исправим этот недостаток программно.

  • Добавьте к странице код восстановления выбора пользователя в списках
  • <script runat="server">
        
        // Поле класса страницы
        private const int maxCount = 1000;
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Заполняем списки программной генерацией значений
            for (int i = 0; i < maxCount; i++)
            {
                ListBox1.Items.Add("Опция " + (i + 1).ToString());
                DropDownList1.Items.Add("Опция " + (i + 1).ToString());
            }
            
            // Отслеживание выделенных пользователем опций списков 
            if (this.IsPostBack)
            {
                string text = this.Request.Form["ListBox1"];
                ListBox1.Items.FindByText(text).Selected = true;
                text = this.Request.Form["DropDownList1"];
                DropDownList1.Items.FindByText(text).Selected = true;
            }
            else
                ListBox1.SelectedIndex = DropDownList1.SelectedIndex = 0;
        }
    </script>

    Броузер с обратной отсылкой присылает на сервер значения выделенных элементов списка, собираемых дескриптором <form> из HTML-кода, представленного примерно таким фрагментом

    <select size="4" name="ListBox1" id="ListBox1" style="height:200px;width:150px;">
    <option selected="selected" value="Опция 1">Опция 1</option>
    <option value="Опция 2">Опция 2</option>
    <option value="Опция 3">Опция 3</option>
    ..................................................
    </select>

    Фактически броузер посылает пару ListBox1=Опция 1, которая попадает в словарь Request.Form, доступный для извлечения и использования в коде страницы на стороне сервера. Мы находим это значение в сгенерированной коллекции значений списка и настраиваем этот элемент коллекции как выделенный. После таких настроек список сгенерирует в текущем отклике сервера нужный HTML-код, соответствующий состоянию списка на клиенте перед обратной отсылкой.

  • Запустите страницу ViewStateList.aspx с включенной трассировкой и убедитесь, что выбранные опции списков сохраняют свои позиции и ASP.NET генерирует небольшой объем клиентского кода
  • Таким образом мы выяснили, что состояние вида элементов управления, которое включено по умолчанию, не всегда является полезным либо просто не используется, и тогда его лучше отключать. Отключить состояние вида можно следующим образом:

  • На уровне приложения для всех страниц в файле web.config корня web-дерева сайта
    <?xml version="1.0" encoding="utf-8"?>
    <configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
        <system.web>
            <compilation debug="true" />
            <pages enableViewState="false" />
        </system.web>
    </configuration>
  • На уровне страницы, добавив в директиву @ Page параметр EnableViewState="false" (в коде свойство Page.EnableViewState = false )
  • На уровне элемента управления, добавив в дескриптор элемента атрибут EnableViewState="false" (в коде свойство Элемент.EnableViewState = false )
  • Если более общая настройка сохранения состояния вида выключена, а для каких-то страниц или элементов управления ее нужно включить, то в соответствующее место следует добавить атрибут EnableViewState="true".

    Защита информации в состоянии вида

    Состояние вида передается клиенту системой ASP.NET в скрытых полях частично зашифрованным. Но это шифрование поверхностное, чтобы не требовало много процессорного времени. При желании злоумышленник может частично восстановить и прочитать эту информацию.

    Упражнение 4

  • Добавьте к проекту новую страницу (без файла отделенного кода) с именем DisplayViewState.aspx и установите ее стартовой
  • Удалите из интерфейсной части страницы дескриптор <div></div>
  • Включите трассировку, добавив в директиву @ Page страницы параметр Trace=true
  • Добавьте в блок скриптов кодовую часть, чтобы окончательно страница стала такой
    (рис ) Окончательный код страницы DisplayViewState.aspx<%@ Page Language="C#" Trace="true" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Объекты создаем каждый раз заново
            Label label = new Label();
            this.form1.Controls.Add(label);
            
            this.form1.Controls.Add(new HtmlGenericControl("br"));
            
            Button button = new Button();
            this.form1.Controls.Add(button);
            
            if (!this.IsPostBack)
            {
                // Один раз настроили, а дальше работает состояние вида
                label.Text = "Привет всем!!!";
                button.Text = "Отправить";
            }
        }
        
        protected void Page_PreRender(object sender, EventArgs e)
        {
            // Вывод на страницу строки состояния
            if (this.IsPostBack)
            {
                // Извлекаем строку состояния
                string strViewState = this.Request.Form["__VIEWSTATE"];
        
                // Добавляем вывод к родительской форме 
                Label label = new Label();
                this.form1.Controls.Add(label);
                label.Text = "<p><b>Строка состояния как есть:</b><br />" 
                    + strViewState + "</p>";
                label.EnableViewState = false;// Чтобы не влияла на результат
        
                // Преобразуем строку из Base64 в массив ASCII-символов
                byte[] ascii = Convert.FromBase64String(strViewState);
        
                // Вставляем в отклик
                Response.Write("<b>Строка состояния в формате ASCII:</b><br />");
                Response.BinaryWrite(ascii);
        
                // То же самое, только по турецки...
                // Десериализация и отображение строки
                //string decodedViewState = System.Text.Encoding.ASCII.GetString(ascii);
                //Response.Write(decodedViewState);
            }
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        </form>
    </body>
    </html>
  • Запустите страницу DisplayViewState.aspx и повыполните обратную отсылку
  • HTML-отклик на стороне клиента, сгенерированный страницей DisplayViewState.aspx при обратной отсылке, будет выглядеть примерно так

    Мы видим, что некоторая информация, сохраняемая на клиенте в строке состояния вида, может быть частично расшифрована. Злоумышленник может изменить ее нужным для себя образом и попытаться отправить на сервер.

    Поскольку состояние вида формирует только серверная сторона, то перед включением строки в скрытое поле HTML-вывода ASP.NET вычисляет контрольную сумму и прячет ее в ту же самую строку. При получении этой хэшированной строки от клиента система десериализует ее, вновь вычисляет контрольную сумму и сравнивает с ранее спрятанной. Несовпадение легко обнаружить. Подсчет контрольной суммы включается в директиве @ Page параметром EnableViewStateMac="true".

    Для большей защиты строки состояния вида можно использовать встроенное шифрование, которое включается на странице параметром ViewStateEncryptionMode="Always" директивы @ Page или установкой в конфигурационном файле секции

    <pages viewStateEncryptionMode="Always">
  • Включите шифрование и выполните страницу DisplayViewState.aspx. Сгенерированный отклик на обратную отсылку станет примерно таким
  • Теперь строка состояния стала зашифрованной и ее трудно расшифровать. Однако не рекомендуется без надобности шифровать данные состояния вида. Это негативно скажется на производительности, поскольку сервер должен будет выполнять операцию шифрования и дешифрования при каждой отсылке и получении данных.

    Скрытые поля

    Во вкладке Standard панели Toolbox имеется элемент управления HiddenField, который генерирует скрытое поле c единственной переменной, определяемой его свойством Value. Этот элемент не поддерживает шифрование, хэширование и хранение по частям. Поэтому клиенты в исходном коде HTML-вывода могут просматривать эти данные. Данные в скрытом поле хранятся до тех пор, пока пользователь не перейдет к другой странице.

    Упражнение 5

  • Добавьте к проекту новую страницу с именем HiddenFieldTest.aspx без файла отделенного кода и назначьте ее стартовой
  • Переведите страницу в режим Design и двойным щелчком создайте в кодовой части обработчик Page_Load()
  • Поместите на Web-форму из вкладки Standard панели Toolbox два элемента управления HiddenField и присвойте им имена beginTime и countRequest
  • Отключите для всех элементов страницы механизм сохранения состояния вида, добавив в директиву @ Page параметр EnableViewState="false"
  • Заполните страницу следующим кодом
    (рис ) Код страницы HiddenFieldTest.aspx<%@ Page Language="C#" EnableViewState="false" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Создаем текстовую метку
            lblTime = new Label();
            div1.Controls.Add(lblTime);
            div1.Controls.Add(new HtmlGenericControl("br"));
        
            // Создаем кнопку CurrentTime
            Button button = new Button();
            div1.Controls.Add(button);
            // Подписываемся на событие кнопки
            button.Click += CurrentTimeClick;
            button.Text = "Текущее время";
            
            // Добавляем промежуток между кнопками
            Literal literal = new Literal();
            literal.Text = "nbsp;nbsp;nbsp;nbsp;nbsp;";
            div1.Controls.Add(literal);
        
            // Создаем кнопку OldTime
            button = new Button();
            div1.Controls.Add(button);
            // Подписываемся на событие кнопки
            button.Click += OldTimeClick;
            button.Text = "Начальное время";
        
            // Инициализируем скрытые поля и метку при первом запросе
            if (!this.IsPostBack)
            {
                beginTime.Value = System.DateTime.Now.ToLongTimeString();
                OldTimeClick(null, EventArgs.Empty);
            }
            else
            {
                // Ведение счетчика запросов страницы 
                int.TryParse(countRequest.Value, out count);
                count++;
                // Или то же самое
                // count = int.Parse(countRequest.Value) + 1;
            }
            
            countRequest.Value = count.ToString();
        }
        
        // Вынесены в поля класса для видимости в обработчиках
        int count = 0;
        Label lblTime;
        
        void CurrentTimeClick(object sender, EventArgs e)
        {
            lblTime.Text = "Текущее время: " + System.DateTime.Now.ToLongTimeString();
            lblTime.Text += "<br />Ответ № " + count.ToString();
        }
        
        void OldTimeClick(object sender, EventArgs e)
        {
            lblTime.Text = "Начальное время: " + beginTime.Value;
            lblTime.Text += "<br />Ответ № " + count.ToString();
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
            <div style="text-align: center" id="div1" runat="server">
                <h1 style="color: Red">
                    Испытание элемента HiddenField
                </h1>
                <asp:HiddenField ID="beginTime" runat="server" />
                <asp:HiddenField ID="countRequest" runat="server" />
            </div>
        </form>
    </body>
    </html>
  • Исполните страницу и убедитесь, что скрытые поля, генерируемые элементом HiddenField, в пределах одной страницы работают исправно
  • Строки запроса

    Одно из самых серьезных ограничений состояния вида состоит в том, что оно работает только на одной странице. При переходе пользователя на другую страницу состояние вида прежней страницы утрачивается. Одним из выходов является передача информации в строке запроса, которая может переносить информацию со страницы на страницу. Но этот способ имеет несколько недостатков:

  • Информация является видимой для пользователя и доступна для вмешательства с его стороны. В результате страница может получить такие данные, от которых она не имеет защиты
  • Информация должна кодироваться допустимыми для URL символами
  • Броузеры устанавливают ограничение на длину URL, которая должна быть в пределах 1-2 Кб. Поэтому в строку запроса нельзя помещать большое количество информации
  • Форма отправляющей страницы должна использовать метод передачи GET вместо POST
  • Формирование строки запроса и извлечение из нее информации программист выполняет вручную
  • Отправляющая запрос страница инициирует передачу либо с помощью гиперссылки (элемент HyperLink ), либо с помощью кнопки Submit, в обработчик которой вставлен код Response.Redirect(URL). Для извлечения информации из строки запроса на адресуемой (целевой) странице используется встроенный в нее объект-словарь Request.QueryString.

    Упражнение 6

  • Добавьте к проекту страницу без файла отделенного кода с именем SourceQueryString.aspx и назначьте ее стартовой
  • Добавьте внутрь контейнера <div></div> дескриптор строки-заголовка
    <h1 style="color: Red">Введите учетную запись</h1>
  • Дополните открывающий дескриптор <div> стилем центрирования
    <div style="text-align: center">
  • Создайте на Web-форме позиционирующую таблицу размером 3x2 и поместите в ее ячейки из вкладки Standard панели Toolbox серверные элементы с настройками согласно таблице
  • Таблица свойств элементов управления страницы SourceQueryString.aspx
    Элемент Свойство
    Label ID="Label1"
    Text="Имя:"
    TextBox ID="Name"
    Label ID="Label2"
    Text="Пароль:"
    TextBox ID="Password"
    TextMode="Password"
    Button ID="Button1"
    Text="Отправить"

    Интерфейс страницы в режиме проектирования будет таким

  • Переведите страницу в режим Design и двойным щелчком на элементе Button1 создайте для него обработчик. Заполните обработчик следующим кодом
  • protected void Button1_Click(object sender, EventArgs e)
    {
        string Url = "TargetQueryString.aspx?Name="
                                + Server.UrlEncode(Name.Text)
                                + "Password="
                                + Server.UrlEncode(Password.Text);
        this.Response.Redirect(Url);
        
        // То же самое, только с прямой переадресацией
        // this.Server.Transfer(Url);
    }

    Кнопка будет инициировать обратную отсылку, а ее обработчик сформирует адрес целевой страницы со значениями параметров, снятых с элементов текстовых полей. Перед включением в строку адреса введенных пользователем значений мы применяем URL-кодирование, преобразуя тем самым ввод в допустимые символы. К сожалению, строку адреса приходится формировать вручную.

  • Переведите страницу в режим Design и двойным щелчком создайте в кодовой части обработчик Page_Load(). Заполните обработчик кодом, восстанавливающий состояние текстового поля Name сохраненным в строке запроса значением
  • protected void Page_Load(object sender, EventArgs e)
    {
        if (this.Request.QueryString["Name"] != null  !this.IsPostBack)
            Name.Text = this.Request.QueryString["Name"];
    }

    Страница SourceQueryString.aspx, отправляющая строку запроса, готова. Вот ее полный код

    <%@ Page Language="C#" %>
        
    <script runat="server">
        
        protected void Button1_Click(object sender, EventArgs e)
        {
            string Url = "TargetQueryString.aspx?Name="
                                    + Server.UrlEncode(Name.Text)
                                    + "Password="
                                    + Server.UrlEncode(Password.Text);
            this.Response.Redirect(Url);
            
            // То же самое с прямой переадресацией
            // this.Server.Transfer(Url);
        }
        
        protected void Page_Load(object sender, EventArgs e)
        {
            if (this.Request.QueryString["Name"] != null  !this.IsPostBack)
                Name.Text = this.Request.QueryString["Name"];
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
            <div style="text-align: center">
                <h1 style="color: Red">
                    Введите учетную запись</h1>
                <table border="0" width="100%">
                    <tr>
                        <td style="text-align: right; width: 412px;">
                            nbsp;<asp:Label ID="Label1" runat="server" Text="Имя:" />
                        </td>
                        <td>
                            nbsp;<asp:TextBox ID="Name" runat="server" />
                        </td>
                    </tr>
                    <tr>
                        <td style="width: 412px; text-align: right">
                            nbsp;<asp:Label ID="Label2" runat="server" Text="Пароль:" />
                        </td>
                        <td>
                            nbsp;<asp:TextBox ID="Password" runat="server" TextMode="Password" />
                        </td>
                    </tr>
                    <tr>
                        <td colspan="2" style="text-align: center">
                            nbsp;<asp:Button ID="Button1" runat="server" Text="Отправить" 
                            OnClick="Button1_Click" />
                        </td>
                    </tr>
                </table>
            </div>
        </form>
    </body>
    </html>

    Теперь нужно создать принимающую запрос (целевую) страницу. На целевой странице значения поступивших параметров, извлекаемые из строки запроса, следует предварительно URL-раскодировать. Затем можно принять решение, что делать с поступившими данными.

  • Создайте страницу с совмещенным кодом и именем TargetQueryString.aspx
  • Заполните страницу следующим кодом
  • <%@ Page Language="C#" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Критерии оценки зарегистрированного пользователя
            const string NAME = "USER", PASSWORD = "ROOT";
        
            // Извлекаем и раскодируем поступившие данные из строки запроса
            string name = this.Request.QueryString["Name"];
            name = this.Server.UrlDecode(name);
            string password = this.Request.QueryString["Password"];
            password = this.Server.UrlDecode(password);
        
            // Создаем текстовую метку для отображения решения
            Label label = new Label();
            form1.Controls.Add(label);
        
            // Резервируем место для размещения гиперссылки
            PlaceHolder placeHiperLink = new PlaceHolder();
            form1.Controls.Add(placeHiperLink);
        
            // Создаем муляж кнопки
            Button button = new Button();
            form1.Controls.Add(button);
        
            // Анализируем данные и принимаем решение
            StringBuilder message = new StringBuilder();
            if (name.ToUpper() == NAME  password.ToUpper() == PASSWORD)
            {
                // Просто выведем хвалебное приветствие 
                message.Append("<h1 style=\"color: Red\">");
                message.Append("Приветствуем Вас, дорогой " + name + "!");
                message.Append("</h1>");
        
                // Отключаем у кнопки способность инициировать отсылку
                // Лень посылать дальше зарегистрированного пользователя 
                button.PostBackUrl = "javascript: void(0)";
                button.Text = "Дальше >";
        
                // Добавляем в зарезервированное место гиперссылку
                HyperLink link = new HyperLink();
                placeHiperLink.Controls.Add(link);
                link.Text = "< Назад";
                link.NavigateUrl = "~/SourceQueryString.aspx?"
                    + "Name=" + name;
        
                // Добавляем в зарезервированное место пробелы
                Label lblSpace = new Label();
                placeHiperLink.Controls.Add(lblSpace);
                lblSpace.Text = "nbsp;nbsp;nbsp;nbsp;";
            }
            else
            {
                // Поругаем и отправим на повторную регистрацию
                message.Append("<h1>");
                message.Append("Вы неопознаны!<br />Повторите регистрацию...");
                message.Append("</h1>");
        
                button.PostBackUrl = "~/SourceQueryString.aspx?"
                    + "Name=" + name;
                button.Text = "< Назад";
            }
        
            // Выводим сформированное сообщение
            label.Text = message.ToString();
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        </form>
    </body>
    </html>

    Обратите внимание, что интерфейсная часть целевой страницы совершенно пустая - все создается динамически.

  • Запустите страницу SourceQueryString.aspx на выполнение и убедитесь, что механизм межстраничного обмена информацией через строку запроса работает
  • Страница TargetQueryString.aspx на клиенте при наведении курсора мыши на гиперссылке будет выглядеть так

    Межстраничная обратная отсылка данных

    В классах кнопочных элементов управления Button, ImageButton и LinkButton имеется свойство PostBackUrl, позволяющее сразу адресоваться к целевой странице с доступом в ней ко всем данным текущей страницы. Свойству PostBackUrl кнопки обратной отсылки присваивается строка с URL целевой страницы, которая и будет загружена на выполнение по щелчку пользователя на этой кнопке. При этом на целевой странице, благодаря ее свойству PreviousPage, будет полностью доступен объект страницы, инициировавшей обратную отсылку.

    Идея реализации механизма межстраничной отсылки достаточно проста. Любая страница на этапе описания является классом (инструкцией) с именем, равном имени страницы, которая при загрузке в память сервера превращается в объект (экземпляр класса), адресуемый средой исполнения. Исполняемый объект проходит все необходимые этапы жизни, начиная с инициализации элементов управления, корректировкой состояния, выполнением обработчиков, и заканчивая генерацией HTML-кода (рендерингом).

    То же самое происходит и со страницей, загруженной по кнопке с ненулевым свойством PostBackUrl. Она также проходит все начальные стадии жизни, включая выполнение обработчиков. Непосредственно перед генерацией HTML-кода выполнение исходной страницы прерывается, но она продолжает оставаться в памяти сервера. Одновременно запускается объект целевой страницы, которому в свойство PreviousPage среда исполнения записывает адрес объекта первой страницы.

    Теперь код целевой страницы может адресоваться к общедоступным данным исходной страницы, но рендеринг для клиента будет генерироваться только второй страницей. Когда целевая страница заканчивает свой цикл жизни, вместе с ней из памяти выгружается и исходная страница. Для того, чтобы закрытые или защищенные члены класса исходной страницы стали доступными в классе целевой страницы, их нужно обернуть в общедоступные свойства или функции доступа.

    Рассмотренная межстраничная связь может быть реализована не только как "один к одному", но и в других сочетаниях: "один ко многим", "многие к одному" или "многие ко многим". Множественность на исходной странице может быть организована программным формированием значения свойства PostBackUrl в зависимости от выбранных на этой странице предпочтений. Множественность на целевой странице реализуется проверкой того, к какому типу (к какой странице) относится записанный в свойство PreviousPage объект.

    Межстраничная обратная отсылка целевой страницы может быть инициирована не только по щелчку на кнопке с ненулевым значением свойства PostBackUrl, но и в любом месте кода вызовом соответствующей перегрузки метода

    public void Page.Server.Transfer(string path, bool preserveForm)

    где

  • path - вызываемая целевая страница
  • preserveForm - флаг передачи в целевую страницу данных QueryString и Form
  • Оптимальным будет вызов

    Server.Transfer(Url, true);

    Упражнение 7

    Проиллюстрируем межстраничную отсылку на примере двух страниц-источников и двух страниц-приемников.

  • Добавьте к проекту страницу с совмещенным кодом и именем SourcePostBack1.aspx
  • Назначьте страницу стартовой и оформите пользовательский интерфейс на этапе проектирования так, как показано на рисунке
  • Здесь использованы серверные элементы Label, TextBox, ListBox, Button и HyperLink.

  • Для элемента управления ListBox включите свойство AutoPostBack
  • Заполните страницу следующим кодом
    (рис ) Полный код страницы SourcePostBack1.aspx<%@ Page Language="C#" %>
        
    <script runat="server">
        
        // Функция доступа к закрытому полю класса из внешнего кода
        public string GetName()
        {
            return txtName.Text;
        }
        
        // Свойство доступа к закрытым данным класса из внешнего кода
        public string GetTitle
        {
            get { return lblTitle.Text; }
        }
        
        string text;
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Просто отображаем страницу
            if (lstPost.SelectedItem == null)
                return;
            
            // Выбираем целевую страницу при AutoPostBack списка
            text = lstPost.SelectedItem.Text;
            if (String.Compare(text, "Студент", true) == 0)
                btnPostBackUrl.PostBackUrl = "~/CrossPageStudent.aspx";
            else 
                btnPostBackUrl.PostBackUrl = "~/CrossPageLecturer.aspx";
        }
        
        protected void btn_Click(object sender, EventArgs e)
        {
            // Контролируем наличие выбора при щелчках на кнопках
            if (lstPost.SelectedItem == null)
            {
                String mess = "<script>alert('Выберите сотрудника')</" + "script>";
                this.Response.Write(mess);
                return;
            }
        
            text = lstPost.SelectedItem.Text;
            
            // Различаем кнопки
            Button btn = (Button)sender;
            if (String.Compare(btn.Text, "PostBackUrl", true) != 0)
            {
                if (String.Compare(text, "Студент", true) == 0)
                    this.Server.Transfer("~/CrossPageStudent.aspx", true);
                else
                    this.Server.Transfer("~/CrossPageLecturer.aspx", true);
            }
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
            <div style="text-align: center">
                <asp:Label ID="lblTitle" runat="server" Text="Это первая страница-источник"></asp:Label>
                <br />
                <br />
                Фамилия:nbsp;
                <asp:TextBox ID="txtName" runat="server">Иванов</asp:TextBox>nbsp;
                <asp:ListBox ID="lstPost" runat="server" Height="45px" AutoPostBack="True">
                    <asp:ListItem>Студент</asp:ListItem>
                    <asp:ListItem>Доцент</asp:ListItem>
                </asp:ListBox>
                <br />
                <br />
                <asp:Button ID="btnPostBackUrl" runat="server" Text="PostBackUrl" OnClick="btn_Click" />nbsp;
                <asp:Button ID="btnServerTransfer" runat="server" OnClick="btn_Click"
                    Text="Server.Transfer()" />
                <br />
                <br />
                <asp:HyperLink ID="HyperLink1" runat="server" NavigateUrl="SourcePostBack2.aspx">
                На вторую страницу-источник</asp:HyperLink>
            </div>
        </form>
    </body>
    </html>
  • Получите копию страницы SourcePostBack1.aspx под именем SourcePostBack2.aspx и скорректируйте в ней элементы lblTitle и HyperLink1
  • Создайте новую страницу с именем CrossPageStudent.aspx без файла отделенного кода, в которой блок скриптов заполните так
    (рис ) Полный код страницы-приемника данных CrossPageStudent.aspx<%@ Page Language="C#" %>
        
    <script runat="server">
        
        // Поля, заполняемые данными вызвавшей страницы
        String name, title, className;
        
        // Наша функция, использующая доступ с помощью механизма отражения типов
        void GetInfoPreviousPage()
        {
            // Получаем тип запускающей страницы для
            // динамического вызова членов представляющего класса
            System.Type typePreviousPage = this.PreviousPage.GetType();
            // Находим общедоступный метод класса 
            System.Reflection.MethodInfo methodGetName =
                typePreviousPage.GetMethod("GetName");
            // Выполняем метод предыдущей страницы
            name = methodGetName.Invoke(this.PreviousPage, null).ToString();
            // Находим общедоступное свойство класса 
            System.Reflection.PropertyInfo propertyGetTitle =
                typePreviousPage.GetProperty("GetTitle");
            // Выполняем свойство предыдущей страницы
            title = propertyGetTitle.GetGetMethod().
                Invoke(this.PreviousPage, null).ToString();
            // Извлекаем имя класса предыдущей страницы
            className = this.PreviousPage.GetType().Name;
        }
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Получаем информацию с предыдущей страницы
            GetInfoPreviousPage();
        
            Label label = new Label();
            form1.Controls.Add(label);
            string text = "<h1 style='text-align: center; color: Red'>";
        
            if (this.PreviousPage == null)
            {
                text += "Страница запущена напрямую</h1>";
                label.Text = text;
                return;
            }
        
            text += "Методом межстраничной обратной отсылки получены данные</h1>";
            text += "<ul>";
            text += "<li>Заголовок: " + title + "</li>";
            
            if (String.Compare(className, "SourcePostBack1_aspx", true) == 0)
                text += "<li>Данные прислала первая страница</li>";
            else if (String.Compare(className, "SourcePostBack2_aspx", true) == 0)
                text += "<li>Данные прислала вторая страница</li>";
            text += "<li>Фамилия студента: " + name + "</li>";
            
            if (this.PreviousPage.IsCrossPagePostBack)
                text += "<li>Обратная отсылка инициирована свойством PostBackUrl</li>";
            else
                text += "<li>Обратная отсылка инициирована методом Server.Transfer()</li>";
            text += "</ul>";
            label.Text = text;
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        </form>
    </body>
    </html>
  • Получите копию страницы CrossPageStudent.aspx под именем CrossPageLecturer.aspx
  • Эти обе страницы-приемники имеют совершенно одинаковый код и просто демонстрируют работу схемы межстраничной передачи данных "многие ко многим". В обеих страницах используется наша функция GetInfoPreviousPage(), в которую мы упаковали код, извлекающий данные из класса страницы-источника на этапе выполнения. Этот прием применен потому, что при прямой адресации к членам предыдущей страницы компилятор ищет их в текущей странице и выдает ошибку компиляции (и правильно делает). А при динамическом доступе он работает только со строковыми именами свойств и методов предыдущей страницы.

  • Разберитесь с кодом отсылающей и принимающей сторон и испытайте их работу
  • Вариант результата на принимающей странице будет примерно таким

    Cookie-наборы

    Куки-наборы являются еще одним распространенным способом хранения информации на клиенте. Они, подобно состоянию вида, также включают в себя текстовую информацию, но хранят ее не в скрытых полях HTML-документа, а в небольших файлах, хранящихся на клиенте в специальной временной папке ( ...\Temporary Internet Files\ ). В зависимости от версии броузера для каждого сайта у клиента таких файлов может быть до 20 с размером до 4 Кб каждый. Клиент может даже и не подозревать, что при взаимодействии с сайтом его броузер автоматически принимает и отсылает связанные с ним куки.

    Однако клиент имеет возможность удалить куки, отключить в броузере поддержку куков, прочитать информацию в любом из них и опасно ее модифицировать. Такую возможность разработчик страницы должен учитывать и применять соответствующие методы. В свою очередь, разработчик страницы может устанавливать время жизни куков и шифровать сохраняемую в них информацию. Использование куков опирается на два объекта-свойства страницы Request и Response, каждый из которых имеет коллекцию (набор) Cookies типа HttpCookieCollection. Поступившие от клиента куки нужно извлекать из коллекции Request.Cookies, а отправляемые на клиента новые данные для их сохранения в куках необходимо добавлять в коллекцию Response.Cookies.

    Коллекции Cookies состоят из отдельных объектов-экземпляров класса HttpCookie, каждый из которых является словарем. При создании объекта-словаря в конструкторе HttpCookie(string) задается имя кука, под которым он будет адресоваться в коллекции. Перегруженная версия конструктора HttpCookie(string, string) позволяет при создании имени кука определять и его единственное значение.

    Важным свойством объекта cookie является Expires, которое определяет срок хранения куки-файла на клиенте. Если броузер при соединении с сайтом обнаружит связанный файл с просроченным сроком хранения, то он его немедленно уничтожит. Если это свойство не установлено, то cookie будет храниться в памяти, пока пользователь не закроет броузер. Для немедленного удаления cookie на клиенте с длительным сроком хранения нужно перезаписать в него просроченную дату.

    Упражнение 8

    В качестве примера разработаем страницу, на которой значения некоторых интерфейсных элементов управления будем хранить в cookies.

  • Добавьте к проекту страницу без отделенного кода с именем CookiesPage1.aspx и назначьте ее стартовой
  • Никакие элементы в режиме проектирования мы на странице размещать не будем, а все создадим и настроим динамически, чтобы еще раз подчеркнуть возможности программирования. В итоге код нашей страницы будет выглядеть так

    <%@ Page Language="C#" ValidateRequest="false" %>
        
    <script runat="server">
            
        // Вынесли как поле для видимости в обработчиках
        // Инициализировать необязательно, компилятор сам справится
        TextBox txtName;
        String result = "";
        int count = 0;
        
        protected void Page_Load(object sender, EventArgs e)
        {
            this.Response.Write("<h1 style='color: Red;'>Испытание Cookies</h1>");
        
            Label label = new Label();
            label.Text = "Введите ФИО:nbsp;nbsp;";
            form1.Controls.Add(label);
        
            txtName = new TextBox();
            txtName.Text = "Снетков В.М.";
            form1.Controls.Add(txtName);
        
            form1.Controls.Add(new HtmlGenericControl("br"));
        
            Button button = new Button();
            button.Text = "Отправить";
            form1.Controls.Add(button);
        
            button = new Button();
            button.Text = "Сохранить";
            form1.Controls.Add(button);
            button.Click += btnSaveClick;
        
            button = new Button();
            button.Text = "Восстановить";
            form1.Controls.Add(button);
            button.Click += btnRestoreClick;
        
            form1.Controls.Add(new HtmlGenericControl("br"));
        
            button = new Button();
            button.Text = "Удалить";
            form1.Controls.Add(button);
            button.Click += btnDeleteClick;
        
            form1.Controls.Add(new HtmlGenericControl("br"));
        
            HyperLink link = new HyperLink();
            form1.Controls.Add(link);
            // Определяем имя класса текущей страницы и формируем гиперссылку
            String nameCurrentPage = this.GetType().Name;
            // Разбиваем строку на части
            String[] parts = nameCurrentPage.Split('_');
            // Сравниваем первую часть без учета регистра
            if (String.Compare(parts[0], "CookiesPage1", true) == 0)
                link.NavigateUrl = "CookiesPage2" + ".aspx";
            else
                link.NavigateUrl = "CookiesPage1" + ".aspx";
            link.Text = "Перейти к " + link.NavigateUrl;
        
            // Отключаем сохранение состояния вида всех элементов страницы
            DisabledViewState(form1.Controls);
        
            // Извлекаем общее число посещений 
            HttpCookie cookie = this.Request.Cookies["count"];
            if (cookie != null)
            {
                int.TryParse(cookie.Value, out count);
            }
            count++;
            // Адресуемся к отклику и сохраняем общее число посещений
            cookie = this.Response.Cookies["count"];
            cookie.Value = count.ToString();
            // Устанавливаем срок хранения на клиенте: от текущего 1 день 
            cookie.Expires = DateTime.Now.AddDays(1);
            // Отображаем сообщение на странице пользователя
            result = "Текущий запрос: " + count.ToString();
        }
        
        // Отключаем механизм сохранения состояния вида для всех элементов
        void DisabledViewState(ControlCollection controls)
        {
            foreach (Control ctrl in controls)
            {
                // Отключаем сохранение состояния вида элемента
                if (ctrl.EnableViewState)
                    ctrl.EnableViewState = false;
        
                // Продолжаем опрашивать рекурсивно, если есть дочерние элементы
                if (ctrl.Controls != null)
                    DisabledViewState(ctrl.Controls);
            }
        }
        
        // Переопределяем обработчик генерации HTML-вывода страницы
        protected override void Render(HtmlTextWriter output)
        {
            // Центрируем динамически созданные элементы управления
            output.RenderBeginTag(HtmlTextWriterTag.Center);
            base.Render(output);
            output.Write("<hr />");
            output.Write(result);
            output.RenderEndTag();
        }
        
        // Сохраняем данные в куке "oldInfo"
        void btnSaveClick(object sender, EventArgs e)
        {
            // Создаем словарь и заполняем текущей информацией 
            HttpCookie cookie = new HttpCookie("oldInfo");
            // Кодировать необязательно, направляется в файл
            cookie["name"] = txtName.Text;
            cookie["clicks"] = count.ToString();
            // Присоединяем к ответу
            // Срок хранения - текущий сеанс
            // cookie.Expires = DateTime.Now.AddYears(1);
            this.Response.Cookies.Add(cookie);
        }
        
        // Извлекаем данные из кука "oldInfo"
        void btnRestoreClick(object sender, EventArgs e)
        {
            // Подключаемся к кукам поступившего запроса
            HttpCookieCollection cookies = this.Request.Cookies;
            // Извлекаем нужный словарь
            HttpCookie cookie = cookies["oldInfo"];
            if (cookie != null)
            {
                // Извлекаем значения словаря
                string name, clicks;
                name = cookie["name"];
                clicks = cookie["clicks"];
                
                // Альтернативный синтаксис
                // name = cookies["oldInfo"]["name"];
                // clicks = cookies["oldInfo"]["clicks"];
                // Если готовим к отправке, то можно...
                // cookies["oldInfo"].Expires = DateTime.Now.AddYears(1);
                
                // Безопасно отображаем сохраненные значения,
                // кодируя интерпретируемые броузером символы
                name = Server.HtmlEncode(name);
                clicks = Server.HtmlEncode(clicks);
                result = "Сохраненное ФИО: " + name;
                result += "; cохраненный запрос: " + clicks;
            }
            else
            {
                result = "Нечего восстанавливать! ";
                result += "Текущий запрос: " + count.ToString();
            }
        }
        
        // Удаляем кук "count"
        void btnDeleteClick(object sender, EventArgs e)
        {
            // Удаляем кук количества запросов, посылая просроченную дату
            this.Response.Cookies["count"].Expires = 
                DateTime.Now.AddDays(-1);
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        <div>
            
        </div>
        </form>
    </body>
    </html>

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

  • Создайте в проекте копию страницы CookiesPage1.aspx и присвойте ей имя CookiesPage2.aspx
  • Запустите первую страницу CookiesPage1.aspx и испытайте ее работу совместно со второй страницей
  • Вид страницы на клиенте будет таким

    Проверка поддержки cookies клиентом

    Механизм cookies является очень удобным, но неустойчивым, поскольку может либо не поддерживаться броузером, либо пользователь может эту поддержку отключить. На этот случай разработчик может применить другие схемы сохранения данных при межстраничных отсылках. Но прежде всего такую проблему нужно обнаружить.

    Информация о функциональности броузера автоматически поступает на сервер с каждым запросом пользователя и записывается в объект типа System.Web.HttpBrowserCapabilities, ссылка на который находится в свойстве Page.Request.Browser объекта страницы. Эта информация включает в себя тип броузера, его версию, поддержку на стороне клиента сценариев, стилей, фреймов, cookie, таблиц и другое.

    К сожалению эта информация свидетельствует только о потенциальных возможностях броузера, но никак не о том, как сконфигурирован (настроен) броузер в данный момент. В такой ситуации у разработчика имеется два выхода: положиться на присланную информацию и считать, что в броузере эта функциональность не отключена, либо выполнить дополнительные проверки.

    Что касается cookie-наборов, то при первом запросе страницы после того, как мы удостоверимся, что такая функциональность у броузера имеется, можно сделать пробный отклик с посылкой cookie-набора с помощью метода Response.Redirect(), а затем в поступившем запросе попытаться прочитать его. Если такая попытка не удастся, значит поддержка cookies-наборов недоступна и нужно переключать логику выполнение страницы на другие методы сохранения межстраничной информации.

    Упражнение 9

  • В качестве иллюстрации к сказанному создайте страницу с именем IsCookiesSupported.aspx и заполните ее следующим кодом
  • <%@ Page Language="C#" ClassName="IsCookiesSupported" EnableViewState="false" %>
        
    <script runat="server">
        
        bool CookiesSupportedFlag;
        int clicks = 1;
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Создаем объекты пользовательского интерфейса
            // Текстовая метка заголовка
            Label label = new Label();
            form1.Controls.Add(label);
            label.Text = "<h2>Поддерживаются ли Cookies?</h2>";
        
            // Кнопка Submit
            Button submit = new Button();
            form1.Controls.Add(submit);
            submit.Text = "Отправить";
        
            // Горизонтальная линия
            form1.Controls.Add(new HtmlGenericControl("hr"));
        
            // Текстовая метка результатов
            label = new Label();
            form1.Controls.Add(label);
        
            if (!this.IsPostBack)
            {
                // Проверяем функциональность броузера на поддержку Cookies
                CookiesSupportedFlag = this.Request.Browser.Cookies;
                if (CookiesSupportedFlag)
                {
                    // Для дополнительной проверки пробуем отправить cookie
                    this.Response.Cookies["tmp"].Value = String.Empty;
                }
        
                // this.Response.Redirect(Request.Url.ToString());// Зацикливает
        
                // Дублируем сохранение в состоянии вида
                this.ViewState["clicks"] = clicks.ToString(); ;
                label.Text = "Пробный запрос страницы";
                return;
            }
            else if (this.Request.Cookies["tmp"] != null)
            {
                // Поднимаем флаг поддержки cookies
                CookiesSupportedFlag = true;
            }
            else
                CookiesSupportedFlag = false;
        
            // Принимаем решение, откуда извлекать и где сохранять данные
            if (CookiesSupportedFlag)
            {
                if (this.Request.Cookies["clicks"] != null)
                    clicks = int.Parse(this.Request.Cookies["clicks"].Value) + 1;
                this.Response.Cookies["clicks"].Value = clicks.ToString();
                // Устанавливаем срок хранения
                this.Response.Cookies["clicks"].Expires =
                    DateTime.Now.AddMinutes(1.0);
            }
            else
            {
                clicks = Convert.ToInt32(this.ViewState["clicks"]) + 1;
                this.ViewState["clicks"] = clicks.ToString();
            }  
            
            // Показываем пользователю количество щелчков и механизм хранения
            label.Text = "Запрос страницы № " + clicks.ToString();
            if (CookiesSupportedFlag)
                label.Text += "<br />Хранение в cookies";
            else
                label.Text += "<br />Хранение в ViewState";   
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        </form>
    </body>
    </html>

    Cookie-набору можно назначить область видимости в пределах сайта. В этом случае броузер будет следить за тем, при работе с какими страницами отправлять на них куки, а на какие не отправлять. Так, например, если кук "clicks" при записи пометить

    Response.Cookies["clicks"].Path = "~/MyFolder";

    то кук будет доступен только страницам папки MyFolder. По умолчанию кук доступен любым страницам домена, как и, например, при установке

    Response.Cookies["clicks"].Domain = "mysite.ru";

    Перейдите ко второй части ...

    Сохранение данных в состоянии сеанса

    Любой пользователь при первом обращении к приложению получает свой сеанс, который выражается в выделении некоторого объема памяти на стороне сервера для хранения данных любого типа. Доступ к этим совместным данным осуществляется любой страницей через присоединенный к ней объект-словарь Session. За счет этого можно сохранять информацию на одной странице, а получать к ней доступ с другой страницы.

    При поступлении первого клиентского запроса на объект страницы ASP.NET генерирует идентификатор сеанса размером 42 байта. Этот идентификатор сеанса включается в отклик сервера и является уникальным в пределах приложения. Если ни одна из страниц приложения, к которым обращается пользователь, не записывает в объект Session данные, то броузер не возвращает его в запросе серверу. Вместо этого сервер при каждом запросе генерирует новый идентификатор сеанса и включает его в отклик. Стоит только странице записать какие-нибудь данные в объект Session, идентификатор сеанса сразу запоминается броузером и начинается обратный процесс. Теперь уже броузер с каждым запросом начинает посылаться на сервер сохраненный идентификатор сеанса, даже если в дальнейшем мы полностью очистим объект Session.

    Когда клиент присылает идентификатор сеанса, сервер по нему автоматически извлекает из места хранения данные сеанса и заполняет ими объект Session созданной страницы. После чего данные сеанса становятся доступными в коде этой страницы.

    Идентификатор сеанса может пересылаться серверу с запросом двумя способами:

  • С помощью cookie-набора с именем ASP.NET_SessionId
  • С помощью включения в URL для клиентов, не поддерживающих cookie-наборы (использование измененных URL - адресов)
  • Идентификатор сеанса можно получить как значение Response.Cookies["ASP.NET_SessionID"] - при формировании первого отклика, или Session.SessionID - всегда. Но конкретное его значение нас, как правило, не интересует, поскольку процесс распознавания выполняется автоматически.

    Данные сеанса могут сохраняться ASP.NET в одном из трех мест на сервере, определяемых свойством Mode объекта Session:

  • В оперативной памяти сервера внутри процесса - текущего домена приложения (режим InProc - установлен по умолчанию)
  • В оперативной памяти сервера вне процесса в специальной службе Windows под названием ASP.NET State Service (режим StateServer )
  • Вне процесса на жестком диске в базе данных SQL Server (режим SQLServer )
  • Состояние сеанса утрачивается в следующих случаях:

  • Пользователь закрывает и вновь запускает броузер
  • Пользователь получает доступ к приложению через другой броузер
  • Из-за простоя сеанса больше определенного времени по причине отсутствия запросов от клиента (по умолчанию 20 минут)
  • Программист завершает сеанс методом Session.Abandon()
  • Если сеанс создается внутри текущего домена, то при переходе приложения в новый домен сеанс будет потерян
  • Сеанс является экземпляром класса System.Web.SessionState.HttpSessionState и может частично управляться из кода. В таблице приведены некоторые свойства и методы класса HttpSessionState

    Некоторые свойства и методы класса HttpSessionState
    Член Описание
    Count Количество элементов в коллекции текущего сеанса. Только для чтения
    IsCookieless Булев флаг, показывающий способ хранения идентификатора сеанса: в куках или в модифицированном URL. Только для чтения
    IsNewSession Булево значение, которое указывает, был ли сеанс создан с текущим запросом. Если коллекция пуста или сеанс создан ранее, то возвращается false. Только для чтения
    Mode Свойство (только для чтения) содержит одно из значений перечисления SessionStateMode, указывающее способ хранения данных сеанса:
  • Off = 0 - сеанс не используется
  • InProc = 1 - данные сеанса хранятся в памяти домена
  • StateServer = 2 - сеанс хранится в специальной службе Windows под названием ASP.NET State Service
  • SQLServer = 3 - сеанс хранится в базе данных SQL Server
  • Custom = 4 - сеанс хранится на основе нестандартного поставщика
  • SessionID Строка, содержащая уникальный идентификатор сеанса. Только для чтения
    Timeout Максимальный срок простоя сеанса в минутах. Для чтения и записи
    Abandon() Отменяет текущий сеанс
    Add(string name, object value) Добавляет новый элемент в коллекцию сеанса. Допускается альтернативный синтаксис в виде индексатора
    Clear() Очищает коллекцию сеанса
    RemoveAll() Очищает коллекцию сеанса
    Remove(string name) Удаляет элемент данных из коллекции сеанса по ключевому имени
    RemoveAt(int index) Удаляет элемент данных из коллекции сеанса по ключевому индексу

    Начальное состояние сеанса устанавливается в конфигурационном файле web.config в секции <sessionState>. Настройки секции <sessionState> поддерживаются классом System.Web.Configuration.SessionStateSection через его свойства, но имена свойств в кофигурационном файле начинаются с символа нижнего регистра. Вот некоторые наиболее важные из них

    Некоторые свойства SessionStateSection
    Свойство Описание
    Cookieless Задает режим использования cookie-набора для пересылки идентификатора сеанса. Определяется значениями перечисления System.Web.HttpCookieMode:
  • UseUri = 0 - никогда не использовать cookie-наборы, а включать идентификатор сеанса в URL (в файле web.config допускаются варианты синтаксиса: cookieless="UseUri" или cookieless="true" )
  • UseCookies = 1 - всегда использовать cookie-наборы, даже если броузер их не поддерживает или эта функциональность в нем отключена. Этот режим установлен по умолчанию. Для неподдерживающих cookie-наборы броузеров данные сеанса будут теряться, поскольку идентификатор сеанса возвращаться на сервер не будет
  • AutoDetect = 2 - наиболее надежный режим, в котором сервер сам определяет возможности броузера по поддержке куков и автоматически применяет нужной вариант пересылки идентификатора сеанса
  • UseDeviceProfile = 3 - способ пересылки идентификатора сеанса основывается только на потенциальной функциональности броузера по поддержке cookie-наборов, но без учета возможности фактического отключения этой функциональности пользователем
  • Mode Задает одно из значений перечисления SessionStateMode, указывающее способ хранения данных сеанса:
  • Off = 0 - указывает, что сеанс не задействуется для хранения данных. Такой режим может существенно увеличить быстродействие, когда необходимости в сеансе нет
  • InProc = 1 - данные сеанса хранятся в памяти домена
  • StateServer = 2 - сеанс хранится в специальной службе Windows под названием ASP.NET State Service
  • SQLServer = 3 - сеанс хранится в базе данных SQL Server
  • Custom = 4 - сеанс хранится на основе нестандартного поставщика
  • Timeout Устанавливают время простоя сеанса в минутах
    RegenerateExpiredSessionId Булева переменная, запрещающая ASP.NET использовать поступивший от клиента идентификатор сеанса с просроченной меткой времени, а генерирующая новый. Это особенно важно при включенном режиме cookieless="true", когда идентификатор сеанса пересылается броузером в составе URL, куда его может специально встроить злонамеренный пользователь
    CookieName Устанавливает имя идентификатора сеанса в cookie-наборе. По умолчанию используется имя "ASP.NET_SessionID"

    При включенном режиме cookieless="true" броузер автоматически встраивает идентификатор сеанса в URL всех относительных ссылок. Это же справедливо и при автоматической переадресации на новую страницу методом Response.Redirect("Относительный_адрес") с полным циклом. Для того, чтобы указать системе, что идентификатор сеанса должен передаваться секретным протоколом, нужно установить Response.Cookies["ASP.NET_SessionID"].Secure = true;

    Приведем пример использования сеанса.

    Упражнение 10

  • Отредактируйте файл web.config приложения как показано ниже
    (рис ) Файл web.config с включенным режимом Cookieless<?xml version="1.0" encoding="utf-8"?>
    <configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
        <system.web>
            <compilation debug="true" />
            <sessionState cookieless="true" regenerateExpiredSessionId="true" />
    	</system.web>
    </configuration>
  • Добавьте страницу с именем SessionCookieless.aspx с совмещенным кодом, которую заполните так
    (рис ) Код страницы SessionCookieless.aspx<%@ Page Language="C#" EnableViewState="false" %>
        
    <script runat="server">
        
        int clicks = 1;
        const int maxCol=2;
        HtmlTable table;
        
        protected void Page_Load(object sender, EventArgs e)
        {
            string[] headers = {
                    // Имена свойств секции sessionState 
                    "Cookieless",
                    "Mode",
                    "Timeout",
                    "RegenerateExpiredSessionId",
                    "CookieName",
                    // Дополнительная информация
                    "SessionID",
                    "numRequest",
                    "currentTime"
                                };
        
            // Создаем объекты пользовательского интерфейса
            // Текстовая метка заголовка
            Label label = new Label();
            form1.Controls.Add(label);
            label.Text = "<h2>Использование сеансов Session</h2>";
        
            if (!IsPostBack)
            {
                // Получаем всю информацию о параметрах конфигурации
                System.Configuration.Configuration config;
                config =
                    System.Web.Configuration.WebConfigurationManager.
                    OpenWebConfiguration(this.Request.ApplicationPath);
        
                // Объявляем ссылку на объект SessionStateSection
                System.Web.Configuration.SessionStateSection sessionState;
        
                // Поиск элемента <sessionState> в элементе <system.web>
                sessionState = (System.Web.Configuration.SessionStateSection)
                    config.GetSection(@"system.web/sessionState");
                //sessionState.Timeout = TimeSpan.FromMinutes(1);
        
                // Формируем таблицу и сохраняем ее в сеансе
                table = new HtmlTable();
                table.Border = 1;
                for (int i = 0; i < headers.Length; i++)
                {
                    // Создаем строку
                    HtmlTableRow row = new HtmlTableRow();
                    table.Rows.Add(row);
                    // Создаем ячейки в строке
                    for (int j = 0; j < maxCol; j++)
                    {
                        HtmlTableCell col = new HtmlTableCell();
                        row.Cells.Add(col);
                        if (j == 0)// Левый столбец
                        {
                            col.InnerHtml = headers[i] + ":nbsp;";
                            col.Align = "right";
                            col.BgColor = "Yellow";// Цвет фона
                        }
                        else // Правый столбец
                        {
                            // Применяем методику отражения типов
                            System.Type type = typeof(System.Web.Configuration.
                                SessionStateSection);
                            // Извлекаем публичное свойство по его имени
                            System.Reflection.PropertyInfo property =
                                type.GetProperty(headers[i]);
                            if (property != null)
                            {
                                col.InnerHtml = "nbsp;";
                                // Извлекаем значение свойства из экземпляра класса 
                                col.InnerHtml += property.GetGetMethod().
                                    Invoke(sessionState, null).ToString();
                            }
                            else
                            {
                                // Маркируем ячейку, чтобы потом найти
                                col.ID = headers[i];
                            }
                            col.BgColor = "#CCFFFF";// Цвет фона
                        }
                    }
                }
        
                // Сохраняем информацию в сеансе при первом запросе
                this.Session.Add("table", table);
                this.Session["clicks"] = clicks;
            }
            else
            {
                // Извлекаем информацию из сеанса при каждой обратной отсылке
                table = (HtmlTable)this.Session["table"];
                clicks = (int)this.Session["clicks"];
                // Сохраняем новое значение
                this.Session["clicks"] = ++clicks;
            }
        
            // Добавляем таблицу на форму
            form1.Controls.Add(table);
        
            // Корректируем таблицу
            ((HtmlTableCell)Page.FindControl("SessionID")).InnerHtml =
                "nbsp;" + this.Session.SessionID;
            ((HtmlTableCell)Page.FindControl("numRequest")).InnerHtml =
                "nbsp;" + clicks.ToString();
            ((HtmlTableCell)Page.FindControl("currentTime")).InnerHtml =
                "nbsp;" + DateTime.Now.ToLocalTime();
        
            // Новая строка HTML
            HtmlGenericControl br = new HtmlGenericControl();
            br.InnerHtml = "<br />";
            form1.Controls.Add(br);
        
            // Кнопка Submit
            Button submit = new Button();
            form1.Controls.Add(submit);
            submit.Text = "Отправить";
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server" style="text-align: center;">
        </form>
    </body>
    </html>
  • Клиентский результат будет таким

    Обратите внимание, что идентификатор сеанса встроен в URL при значении параметра Cookieless="UseUri". Отметьте также, что интерфейсная часть страницы пустая - все создается динамически. Часть выводимой в таблицу информации мы добыли из объекта класса SessionStateSection, который создается по настройкам конфигурационного файла. Для более лаконичного кода мы применили методику отражения типов, позволившую опосредованно вызывать свойства объекта. В сессиях можно сохранять объекты любого типа, а не только строки. Но при извлечении их нужно явно приводить к соответствующему типу.

    Сохранение данных в состоянии приложения

    Состояние приложения - это область памяти сервера, выделяемая при запуске приложения для хранения глобальных данных, доступных с любой страницы приложения. На эти данные в каждой странице ссылается объект-словарь Application типа System.Web.HttpApplicationState. Как и при состоянии сеанса, состояние приложения хранит данные в виде объектов, поэтому при их извлечении необходимо явное преобразование типов.

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

    Упражнение 11

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

  • Добавьте к приложению файл Global.asax и заполните его так
    (рис ) Файл Global.asax<%@ Application Language="C#" %>
        
    <script runat="server">
        
        void Application_Start(object sender, EventArgs e) 
        {
            this.Application["clicks"] = (int)0;
        }
    </script>
  • Создайте страницу без отделенного кода с именем ApplicationState1.aspx, назначьте ее стартовой и заполните так
    (рис ) Файл ApplicationState1.aspx<%@ Page Language="C#" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Читаем без проверки - инициализирована в global.asax
            int visit = (int)this.Application["clicks"] + 1;
            // Обновляем состояние приложения с блокировкой 
            this.Application.Lock();
            this.Application["clicks"] = visit;
            this.Application.UnLock();
            
            // Создаем текстовую метку
            Label label = new Label();
            form1.Controls.Add(label);
            label.Text = "Сегодня вы посетили нас в " 
                + visit.ToString() + " раз!<br />";
            
            // Создаем гиперссылку
            HyperLink link = new HyperLink();
            form1.Controls.Add(link);
            link.NavigateUrl = "~/ApplicationState2.aspx";
            link.Text = "На другую страницу...";
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        </form>
    </body>
    </html>
  • Создайте копию для страницы ApplicationState1.aspx с именем ApplicationState2.aspx и поменяйте в ней гиперссылку на ApplicationState1.aspx, чтобы страницы ссылались друг на друга
  • Запустите само приложение несколько раз и пощелкайте по ссылкам чтобы убедиться, что счетчик количества визитов увеличивается
  • В панели задач нижней части экрана щелкните правой кнопкой мыши на пиктограмме тестового сервера и остановите его командой
  • Вновь запустите страницу ApplicationState1.aspx, чтобы убедиться, что счетчик сбросился в начальное значение
  • Хранение данных в статических переменных приложения

    Для увеличения масштабируемости приложение загружается в память сервера в виде множества копий (пула). Каждый экземпляр класса приложения имеет свой набор данных и только объект-тип хранит все методы и статические данные в единственном экземпляре. Если в файле Global.asax объявить общедоступные статические поля, то в них можно хранить данные, которые будут видимы через имя класса приложения из любой страницы сайта. Но при этом нужно явно определить в директиве @ Application файла Global.asax параметр ClassName, в котором указывается имя класса приложения для последующего обращения из кода страниц.

    При изменении в страницах глобальных данных класса приложения механизм автоматической блокировки отсутствует. Поэтому нужно пользоваться встроенным в язык C# оператором lock, чтобы на время позволить воздействовать на переменную только со стороны одного потока. Такой подход удобен тем, что мы можем обернуть сохраняемые данные в статические свойства и назначить для них контролирующий код, прежде чем сохранять их в памяти сервера. Мы можем объявлять статические переменные для хранения объектов любого типа и для доступа к ним явных преобразований типа не потребуется.

    Упражнение 12

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

  • Отредактируйте код файла Global.asax следующим образом
    (рис ) Код файла Global.asax со статическими членами<%@ Application Language="C#" ClassName="MyAppClass" %>
        
    <script runat="server">
        
        // Список файлов
        private static string[] fileList;// Базовое поле
        // Общедоступное статическое свойство для обертки базового поля
        public static string[] FileList
        {
            get
            {
                // Заполнить список только при первом запросе 
                if (fileList == null)
                {
                    fileList = System.IO.Directory.GetFiles(
                        HttpContext.Current.Request.PhysicalApplicationPath);
                }
                return fileList;
            }
        }
        
        // Число посещений 
        private static int visit = 1;// Базовое поле
        // Общедоступное статическое свойство для обертки базового поля
        public static int Visit
        {
            get { return visit; }
            set 
            {
                lock (typeof(int))// Контейнер обеспечения синхронизации потоков
                {
                    visit = value;
                }
            }
        }
    </script>
  • Добавьте к приложению страницу без отделенного кода с именем AppClassStaticMembers.aspx и назначьте ее стартовой
  • Заполните страницу AppClassStaticMembers.aspx следующим кодом
    (рис ) Код страницы AppClassStaticMembers.aspx <%@ Page Language="C#" EnableViewState="false" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Заголовок
            Label title = new Label();
            form1.Controls.Add(title);
            title.Text = "<h2 style='text-align: center; color: Red'>"
                + "Использование статических"
                + " полей класса приложения</h2>"; 
        
            // Число посещений
            Label visit = new Label();
            form1.Controls.Add(visit);
            int clicks = MyAppClass.Visit;
            visit.Text = "<b>Количество посещений: " 
                + clicks.ToString() + "</b>";
            MyAppClass.Visit = ++clicks;
            
            // Горизонтальная черта
            form1.Controls.Add(new HtmlGenericControl("hr"));
        
            // Извлекаем и готовим список файлов 
            // текущего каталога приложения
            StringBuilder builder = new StringBuilder();
            builder.Append("<ol>");
            // Счетчик для сокращения листинга 
            int count = 0;
            const int maxCount = 5;
            foreach (string file in MyAppClass.FileList)
            {
                if (++count > maxCount)
                    break;
                
                string str;
                //int pos = file.LastIndexOf(@"\");
                //str = file.Substring(pos + 1);
                // То же самое
                str = System.IO.Path.GetFileName(file);
                builder.Append("<li>" + str + "</li>");
            }
            builder.Append("</ol>");
            
            // Готовим текстовую метку для отображения списка
            Label fileInfo = new Label();
            form1.Controls.Add(fileInfo);
            fileInfo.Text = builder.ToString();
        
            // Горизонтальная черта
            form1.Controls.Add(new HtmlGenericControl("hr"));
            
            // Создаем дескриптор центрирования кнопки
            HtmlGenericControl center = new HtmlGenericControl("center");
            form1.Controls.Add(center);
                
            // Кнопка Submit добавляется в контейнер центрирования
            Button button = new Button();
            button.Text = "Отправить";
            center.Controls.Add(button);
        }
        
        // Переопределяем обработчик рендеринга
        protected override void Render(HtmlTextWriter writer)
        {
            // Сгенерировать основное
            base.Render(writer);
            
            // Добавить в конец еще HTML-вывод
            // Настроить атрибуты будущего дескриптора 
            writer.AddAttribute(HtmlTextWriterAttribute.Align, "Center");
            writer.AddStyleAttribute(HtmlTextWriterStyle.Color, "Blue");
            // Создать дескриптор
            writer.RenderBeginTag(HtmlTextWriterTag.H2);
            // Писать внутрь дескриптора
            writer.Write("Щелкайте на кнопке и наблюдайте счетчик посещений");
            // Закрыть дескриптор
            writer.RenderEndTag();
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        </form>
    </body>
    </html>
  • Запустите страницу и убедитесь, что все работает и объявленные в классе приложения статические члены сохраняют свои значения даже при отключении клиента от сервера до тех пор, пока сервер остается запущенным
  • В панели задач нижней части экрана щелкните правой кнопкой мыши на пиктограмме тестового сервера и остановите его командой
  • Вновь запустите страницу AppClassStaticMembers.aspx, чтобы убедиться, что память сервера очистилась и счетчик сбросился в начальное значение
  • А сейчас мы убедимся, что при модификации любого файла приложения, включая web.config, оно автоматически перекомпилируется при первом запросе и загружается в новый домен.

  • Не нарушая кода, модифицируйте любой исходный файл приложения, например, введите пробел после точки с запятой в конце любой строки (или добавьте пустую строку) и сохраните изменения
  • Запустите приложение и убедитесь, что счетчик сбросился за счет загрузки приложения в новый домен
  • Интерфейс клиента для данного примера будет таким

    Рассмотренные в данной теме способы хранения данных относятся, в основном, к внутрипроцессным, так как сохраняются в оперативной памяти сервера или передаются по каналам связи на компьютер клиента для временного хранения. Другие методы хранения информации в службах сервера, включая базы данных и файлы, являются внепроцессными, поскольку не зависят от среды исполнения ASP.NET.

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