02 июня 2020 года    
Вторник | 13:12    
Главная
 Новости
Базы данных
Безопасность PC
Всё о компьютерах
Графика и дизайн
Интернет-технологии
Мобильные устройства
Операционные системы
Программирование
Программы
Связь
Сети
 Документация
Статьи
Самоучители
 Общение
Форум







Разделы / Интернет-технологии / ASP.NET

Понимание состояния отображения ASP.NET

Введение

В двух словах, состояние отображения Microsoft® ASP.NET— это техника, используемая ASP.NET Web-страницей для сохранения изменений состояния Web-формы между возвратами данных обратно. По моему опыту инструктора и консультанта, состояние отображения является причиной основной части непонимания у разработчиков. При создании специальных элементов управления или реализации более сложных техник, отсутствие твердого понимания того, чем является состояние отображения и как оно работает, может выйти вам боком. Web-разработчики, фокусирующие внимание на создании не требующих высокой производительности тонких страниц, тоже часто разочаровываются в состоянии отображения. Состояние отображения страницы, по умолчанию, размещается в скрытом поле формы __VIEWSTATE. Это скрытое поле формы может запросто стать очень большим, порядка десятков килобайт. Причиной более долгих загрузок является не только поле __VIEWSTATE формы. Всякий раз, когда пользователь возвращает Web-страницу, содержимое этого скрытого поля формы должно возвращаться в HTTP-запросе, таким образом, также растягивается время запроса.

В этой статье поставлена цель — провести всесторонний анализ состояния отображения ASP.NET. Мы рассмотрим, что именно сохраняет состояние отображения и как оно сериализуется в скрытое поле формы и десериализуется при возврате данных обратно. Мы также обсудим методы сокращения производительности, необходимой состоянию отображения.

Комментарий Комментарий: Эта статья ориентирована на разработчика страниц ASP.NET, а не серверных элементов управления ASP.NET. Поэтому она не включает обсуждение того, как разработчик элемента управления должен реализовывать сохранение состояния. Всестороннее обсуждение этого вопроса вы может найти в книге Developing Microsoft ASP.NET Server Controls and Components.

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

 
Жизненный цикл страницы ASP.NET

Каждый раз, когда на Web-сервер приходит запрос на ASP.NET Web-страницу, первое, что он делает — передает запрос ядру ASP.NET. Ядро ASP.NET принимает запрос через конвейер, состоящий из множества этапов, включая проверку достоверности прав доступа к файлу для ASP.NET Web-страницы, восстановление состояния сеанса пользователя и т.д. В конце конвейера создается экземпляр соответствующего запрашиваемой ASP.NET Web-странице класса и вызывается метод ProcessRequest() (смотрите рис. 1).

Рис. 1. Обработка страницы ASP.NET

Рис. 1. Обработка страницы ASP.NET

Жизненный цикл страницы ASP.NET начинается с вызова метода ProcessRequest(). Этот метод начинается с инициализации иерархии элементов управления страницы. Далее страница и ее серверные элементы управления проходят различные строго определенные этапы, присущие выполнению ASP.NET Web-страницы. Эти шаги включают управление состоянием отображения, обработку событий возврата данных обратно и генерирование HTML-разметки страницы. На рис. 2 приведено графическое представление жизненного цикла ASP.NET Web-страницы. Этот жизненный цикл завершается передачей HTML-разметки Web-страницы Web-серверу, который возвращает ее клиенту, запрашивающему страницу.

Комментарий Комментарий: Подробное обсуждение шагов, предшествующих жизненному циклу страницы ASP.NET, выходит за рамки данной статьи. Подробнее читайте в книге Михеля Лерукс-Бастаманте (Michele Leroux-Bustamante) Inside IIS & ASP.NET. Более детально об обработчиках HTTP, которые являются конечными точками конвейера ASP.NET, смотрите в моей предыдущей статье HTTP Handlers.

Важно понимать, что каждый раз, когда запрашивается ASP.NET Web-страница, она проходит одни и те же этапы жизненного цикла (показанные на рис. 2).

Рис. 2. События жизненного цикла страницы

Рис. 2. События жизненного цикла страницы

 
Этап 0 – Создание экземпляра

Жизненный цикл страницы ASP.NET начинается с создания экземпляра класса, который представляет запрашиваемую ASP.NET Web-страницу. Но как создается этот класс? Где он хранится?

Как вы знаете, ASP.NET Web-страницы состоят частично из HTML и частично из кода. Блок HTML содержит разметку HTML и синтаксис Web-элемента управления. Ядро ASP.NET преобразовывает блок HTML из его текстового представления в свободной форме в ряд программно созданных Web-элементов управления.

При первом посещении ASP.NET Web-страницы после изменения разметки HTML или синтаксиса Web-элемента управления страницы .aspx, ядро ASP.NET автоматически генерирует класс. Если вы создавали свою ASP.NET Web-страницу, используя технику отделенного кода (code-behind technique), этот автоматически генерируемый класс наследуется от ассоциированного со страницей класса отделенного кода (обратите внимание, что класс отделенного кода сам должен быть унаследован, прямо или косвенно, от класса System.Web.UI.Page); если вы создавали свою страницу с помощью подставляемого в строку серверного блока <script>, класс происходит непосредственно от System.Web.UI.Page. В любом случае, автоматически сгенерированный класс вместе с откомпилированным экземпляром класса хранятся в папке WINDOWS\Microsoft.NET\Framework\version\Temporary ASP.NET Files, отчасти для того, чтобы не надо было бы создавать его вновь для каждого запроса страницы.

Назначение автоматически генерируемого класса — программное создание иерархии элементов управления (control hierarchy) страницы. Т.е. этот класс отвечает за программное создание Web-элементов управления, определенных в HTML-блоке страницы. Это осуществляется путем трансляции синтаксиса Web-элемента управления — <asp:WebControlName Prop1="Value1" ... /> — в язык программирования класса (обычно C# или Microsoft® Visual Basic® .NET). Кроме преобразования синтаксиса Web-элемента управления в соответствующий код, разметка HTML, присутствующая в блоке HTML ASP.NET Web-страницы, преобразовывается в Литеральные элементы управления.

У всех серверных элементов управления ASP.NET может быть родительский элемент управления, а также различное число дочерних элементов управления. Класс System.Web.UI.Page происходит от базового класса элементов управления (System.Web.UI.Control) и, следовательно, тоже может иметь ряд дочерних элементов управления. Элементы управления верхнего уровня, объявленные в блоке HTML ASP.NET Web-страницы являются прямыми потомками автоматически сгенерированного класса Page. Элементы управления Web также могут быть вложены друг в друга. Например, большинство ASP.NET Web-страниц содержат одну серверную Web Form с множеством Web-элементов управления на ней. Web Form — это элемент управления HTML (System.Web.UI.HtmlControls.HtmlForm). Web-элементы управления, находящиеся в Web Form, являются потомками Web Form.

Поскольку серверные элементы управления могут иметь потомков, каждый из которых тоже может иметь дочерние элементы управления, и т.д., элемент управления и его потомки образуют дерево элементов управления. Это дерево называется иерархией элементов управления. Корень иерархии элементов управления для ASP.NET Web-страницы — это производный от Page класс, автоматически сгенерированный ядром ASP.NET.

Ух! Эти несколько последних абзацев, возможно, несколько сбивают с толку, поскольку это не самый простой предмет для обсуждения или понимания. Чтобы устранить любое потенциальное недопонимание, давайте рассмотрим коротенький пример. Представьте, что у вас есть ASP.NET Web-страница со следующим HTML-блоком:

<html>
<body>
<h1>Welcome to my Homepage!</h1>
<form runat="server"> What is your name?
  <asp:TextBox runat="server" ID="txtName"></asp:TextBox>
    <br />What is your gender?
       <asp:DropDownList runat="server" ID="ddlGender">
         <asp:ListItem Select="True" Value="M">Male</asp:ListItem>
         <asp:ListItem Value="F">Female</asp:ListItem>
         <asp:ListItem Value="U">Undecided</asp:ListItem>
       </asp:DropDownList>
    <br />
    <asp:Button runat="server" Text="Submit!"></asp:Button>
</form>
</body>
</html>

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

Рис. 3. Иерархия элементов управления для страницы-примера

Рис. 3. Иерархия элементов управления для страницы-примера

Затем эта иерархия элементов управления преобразовывается в подобный код:

Page.Controls.Add( new LiteralControl(@"<html>\r\n<body>\r\n <h1>Welcome to my Homepage!</h1>\r\n"));
HtmlForm Form1 = new HtmlForm();
Form1.ID = "Form1";
Form1.Method = "post";
Form1.Controls.Add( new LiteralControl("\r\nWhat is your name?\r\n"));
TextBox TextBox1 = new TextBox();
TextBox1.ID = "txtName";
Form1.Controls.Add(TextBox1);
Form1.Controls.Add( new LiteralControl("\r\n<br />What is your gender?\r\n"));
DropDownList DropDownList1 = new DropDownList();
DropDownList1.ID = "ddlGender";
ListItem ListItem1 = new ListItem();
ListItem1.Selected = true;
ListItem1.Value = "M";
ListItem1.Text = "Male";
DropDownList1.Items.Add(ListItem1);
ListItem ListItem2 = new ListItem();
ListItem2.Value = "F";
ListItem2.Text = "Female";
DropDownList1.Items.Add(ListItem2);
ListItem ListItem3 = new ListItem();
ListItem3.Value = "U";
ListItem3.Text = "Undecided";
DropDownList1.Items.Add(ListItem3);
Form1.Controls.Add( new LiteralControl("\r\n<br /> \r\n"));
Button Button1 = new Button();
Button1.Text = "Submit!";
Form1.Controls.Add(Button1);
Form1.Controls.Add( new LiteralControl("\r\n</body>\r\n</html>"));
Controls.Add(Form1);
Комментарий Комментарий: Приведенный выше исходный код на C# не является именно тем кодом, который автоматически генерируется ядром ASP.NET. Вернее, это освобожденная от лишнего и более простая для восприятия версия автоматически сгенерированного кода. Чтобы увидеть автоматически генерируемый код полностью — он совершенно непригоден для чтения — отправляйтесь в папку WINDOWS\Microsoft.NET\Framework\Version\Temporary ASP.NET Files и откройте один из файлов .cs или .vb.

Единственное, что хотелось бы отметить: когда иерархия элемента управления создана, свойства, которые были явно заданы в декларативном синтаксисе Web-элемента управления, присваиваются в коде. (Например, свойству Text Web-элемента управления в декларативном синтаксисе было присвоено значение "Submit!" — Text="Submit!" — так же как и в сгенерированном автоматически классе — Button1.Text = "Submit!";).

 
Этап 1 - Инициализация

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

Более детально этап инициализации мы рассмотрим в этой статье позже. Относительно состояния отображения он важен по двум причинам: во-первых, серверные элементы управления не начинают отслеживать изменения состояния отображения как раз до завершения этапа инициализации. Во-вторых, при добавлении динамических элементов управления, которые нуждаются в инициализации состояния отображения, понадобится добавить эти элементы управления во время события Init страницы в противоположность событию Load, как мы вскоре увидим.

 
Этап 2 – Загрузка состояния отображения

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

 
Этап 3 – Загрузка возвращаемых данных

Этап загрузки возвращаемых данных также имеет место только при возврате страницы. Серверный элемент управления может показать, что он заинтересован в проверке возвращенных данных, реализовывая интерфейс IPostBackDataHandler. На этом этапе жизненного цикла страницы класс Page перебирает возвращенные поля формы и ищет соответствующий серверный элемент управления. Если он находит элемент управления, он проводит проверку на то, реализует ли этот элемент управления интерфейс IPostBackDataHandler. Если да, он передает соответствующие возвращаемые данные серверному элементу управления через вызов метода LoadPostData() элемента управления. Затем серверный элемент управления должен обновить свое состояние на основании этих возвращенных данных.

Чтобы прояснить все это, давайте рассмотрим простой пример. В ASP.NET замечательно то, что Web-элементы управления в Web Form помнят свои значения между возвратами данных обратно. Т.е. если у вас на странице был Web-элемент управления TextBox и пользователь вводит в него некоторое значение и возвращает страницу, свойство Text TextBox автоматически обновляется до введенного пользователем значения. Это происходит потому, что Web-элемент управления TextBox реализует интерфейс IPostBackDataHandler, и класс Page передает соответствующее значение классу TextBox, который обновляет свое свойство Text.

Чтобы конкретизировать это, представьте, что у нас есть ASP.NET Web-страницы с TextBox, свойству ID которого присвоено значение txtName. При первом посещении страницы для TextBox будет сгенерирован следующий HTML: <input type="text" id="txtName" name="txtName" />. Когда пользователь вводит значение в этот TextBox (например, «Здравствуй, Мир!») и делает отправку формы, браузер будет делать запрос к той же ASP.NET Web-странице, возвращая значения полей формы в заголовках HTTP POST. Это касается значений как скрытых полей формы (таких как __VIEWSTATE), так и значений из txtName TextBox.

Когда на этапе загрузки возвращаемых данных возвращается ASP.NET Web-страница, класс Page видит, что одно из полей возвращенной формы поставлено в соответствие интерфейсу IPostBackDataHandler. В иерархии есть такой элемент управления, таким образом, инициируется метод LoadPostData()TextBox, передающий значение, введенное пользователем в TextBox («Здравствуй, Мир!»). Метод LoadPostData() TextBox просто присваивает это переданное значение своему свойству Text.

Заметьте, что в нашем обсуждении этапа загрузки возвращаемых данных состояние отображения не упоминалось. Поэтому вы, должно быть, удивлены, почему мне взбрело в голову говорить об этапе загрузки возвращаемых данных в статье, посвященной состоянию отображения. Причина в том, чтобы обратить внимание на отсутствие состояния отображения на этом этапе. Это общая ошибка, встречающаяся среди разработчиков, считать, что состояние отображения каким-то образом ответственно за то, что TextBoxe, CheckBoxe, DropDownList и другие элементы управления помнят свои значения между возвратами данных обратно. Причина не в состоянии отображения, поскольку значения определяются посредством значений полей возвращенной формы и присваиваются в методе LoadPostData() тем элементам управления, которые реализуют IPostBackDataHandler.

 
Этап 4 - Загрузка

Это этап, с которым все разработчики ASP.NET хорошо знакомы, поскольку все мы создавали обработчик события для события Load страницы (Page_Load). Когда инициируется событие Load, состояние отображения уже загружено (этап 2, загрузка состояния отображения), как и возвращаемые данные (этап 3, загрузка возвращаемых данных). Если страница возвращена, при инициировании события Load мы знаем, что она восстановила свое состояние, сохраненное с момента предыдущего ее посещения.

 
Этап 5 – Инициация события возврата данных обратно

Определенные серверные элементы управления инициируют события с учетом изменений, произошедших между возвратами. Например, Web-элемент управления DropDownList имеет событие SelectedIndexChanged, которое происходит, если значение SelectedIndex DropDownList изменилось по сравнению с его значением при предыдущей загрузке страницы. Другой пример: если Web Form была возвращена по щелчку Web-элемента управления Button, во время этого этапа инициируется событие Click Button.

Существует две разновидности событий возврата данных обратно. Первая — событие изменения (changed). Это событие инициируется, когда какая-то часть данных изменяется между возвратами данных обратно. Примером является событие SelectedIndexChanged элемента управления DropDownLists или событие TextChanged элемента управления TextBox. Серверные элементы управления, которые предоставляют события изменения, должны реализовывать интерфейс IPostBackDataHandler. Другой вид событий возврата данных обратно — инициированное (raised) событие. Это такие события, которые инициируются серверным элементом управления по любому поводу, который элемент управления считает подходящим. Например, Web-элемент управления Button инициирует событие Click при нажатии, а элемент управления Calendar формирует событие VisibleMonthChanged, когда пользователь переходит к другому месяцу. Элементы управления, которые запускают события инициации, должны реализовывать интерфейс IPostBackEventHandler.

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

 
Этап 6 – Сохранение состояния отображения

На этапе сохранения состояния отображения класс Page создает состояние отображения страницы, которое представляет, что должно сохраниться между возвратами данных обратно. Делает это страница, рекурсивно вызывая метод SaveViewState() элементов управления, составляющих ее иерархию элементов управления. Затем это обобщенное, сохраненное состояние сериализуется в строку, кодированную по основанию base-64. На этапе 7, когда формируется страница Web Form, состояние отображения сохраняется в странице как скрытое поле формы.

Этап 7 – Формирование изображения (Рендеринг)

На этапе формирования изображения для клиента, запрашивающего страницу, происходит генерирование HTML. Класс Page осуществляет это, рекурсивно вызывая метод RenderControl() каждого элемента управления своей иерархии.

Этот седьмой этап — самая важная стадия относительно понимания состояния отображения. (Заметьте, я пропустил пару этапов, такие как этап перед формированием (PreRender) и этап выгрузки (Unload)). Продолжая чтение этой статьи, помните, что каждый раз, когда происходит запрос ASP.NET Web-страницы, она проходит эти этапы.

 
Роль состояния отображения

Назначение состояния отображения в жизненном цикле просто: здесь сохраняется состояние между возвратами данных обратно. (Для ASP.NET Web-страницы ее состояние — это значения свойств элементов управления, которые составляют ее иерархию элементов управления.) Напрашивается вопрос: «Состояние какого типа нуждается в сохранении?» чтобы ответить на этот вопрос, давайте начнем с рассмотрения того, какое состояние не нуждается в сохранении между возвратами данных обратно. Вспомним, что на этапе создания экземпляра жизненного цикла страницы создается иерархия элементов управления и задаются те свойства, которые указаны в декларативном синтаксисе. Поскольку эти декларативные свойства при каждом возврате данных обратно переприсваиваются автоматически, нет необходимости сохранять их значения в состоянии отображения.

Например, представим, что в блоке HTML имеем Web-элемент управления Label со следующим декларативным синтаксисом:

<asp:Label runat="server" Font-Name="Verdana" Text="Hello, World!"></asp:Label> 

При построении иерархии элементов управления на этапе создания экземпляра свойству Text Label будет присвоено «Hello, World!» и свойству Name свойства Font будет присвоено значение Verdana. Поскольку значения этим свойствам будут присваиваться при всяком и каждом посещении страницы во время этапа создания экземпляра, нет необходимости сохранять эту информацию в состоянии отображения.

Что надо сохранять в состоянии отображения, это любые программные изменения состояния страницы. Например, предположим, что кроме этого Web-элемента управления Label страница также включает два Web-элемента управления Button, кнопки Change Message Button и Empty Postback. Change Message Button имеет обработчик события Click, который присваивает свойству Text Label значение «Goodbye, Everyone!»; кнопка Empty Postback Button просто вызывает возврат данных обратно, но не выполняет никакого кода. Изменение свойства Text Label в Change Message Button понадобится сохранить в состоянии отображения. Чтобы увидеть, как и когда это изменение должно быть сделано, давайте пробежимся по короткому примеру. Принимаем, что HTML-блок страницы содержит следующую разметку:

<asp:Label runat="server" ID="lblMessage" Font-Name="Verdana" Text="Hello, World!"></asp:Label>
<br />
<asp:Button runat="server" Text="Change Message" ID="btnSubmit">
</asp:Button>
<br />
<asp:Button runat="server" Text="Empty Postback">
</asp:Button> 

И класс отделенного кода содержит следующий обработчик для события Click элемента управления Button:

private void btnSubmit_Click(object sender, EventArgs e)
{
lblMessage.Text = "Goodbye, Everyone!";
}

Рис. 4 иллюстрирует происходящие события, выделяя, почему изменения свойства Text Label должны сохраняться в состоянии отображения.

Рис. 4. События и состояние отображения

Рис. 4. События и состояние отображения

Чтобы понять, почему важно сохранять измененное свойство Text элемента управления Label в состоянии отображения, рассмотрим, что бы могло произойти, если бы эта информация не была бы сохранена там. Т.е. представьте, что на этапе сохранения состояния отображения второго шага не было сохранено никакой информации состояния. В таком случае в шаге 3 свойству Text элемента управления Label на этапе создания экземпляра было бы присвоено значение «Hello, World!», но не было бы переприсвоено значение «Goodbye, Everyone!» на этапе загрузки состояния отображения. Следовательно, для конечного пользователя свойство Text элемента управления Label было бы «Goodbye, Everyone!» в шаге 2, но, по-видимому, вновь вернулось бы к исходному значению («Hello, World!») в шаге 3 после нажатия кнопки Empty Postback.

 
Состояние отображения и динамически добавляемые элементы управления

Поскольку все ASP.NET серверные элементы управления включают набор дочерних элементов управления, раскрываемых через свойство Controls, элементы управления могут быть добавлены в иерархию элементов управления динамически путем введения новых элементов управления в набор Controls серверного элемента управления. Всестороннее обсуждение динамических элементов управления немного выходит за рамки данной статьи, поэтому здесь мы не будем детально рассматривать этот вопрос; вместо этого мы сосредоточим внимание на том, как управлять состоянием отображения для элементов управления, которые добавляются динамически. (Более детальный урок по использованию динамических элементов управления смотрите в статьях Dynamic Controls in ASP.NET и Working with Dynamically Created Controls.)

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

  1. Поскольку состояние отображения сохраняет между возвратами данных обратно состояние только измененных элементов управления, а не сами элементы управления, динамически добавляемые элементы управления должны добавляться на ASP.NET Web-страницу как при исходном посещении, так и при всех последующих возвратах данных обратно.
  2. Динамические элементы управления добавляются в иерархию элементов управления в классе отделенного кода и, следовательно, это происходит в некоторый момент после этапа создания экземпляра.
  3. Состояние отображения этих динамически добавляемых элементов управления автоматически сохраняется на этапе сохранения состояния отображения. (Что, однако, происходит при возврате данных обратно, если динамические элементы управления еще не были добавлены на момент начала этапа загрузки состояния отображения?)

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

Комментарий Комментарий: Возможно, у вас получится загрузить ваши элементы управления в обработчике события Page_Load и правильно сохранить состояние отображения. Все зависит от того, задаете ли вы или нет какие либо свойства динамически загружаемых элементов управления программно и, если да, когда вы делаете это относительно строки Controls.Add(dynamicControl). Подробное обсуждение этого выходит за рамки этой статьи, но причина, по которой этот вариант может сработать в том, что метод Add() свойства Controls рекурсивно загружает родительское состояние отображения его потомкам, даже если этап загрузки состояния отображения уже пройден.

При добавлении динамического элемента управления c некоторому родительскому элементу управления p на основании некоторого условия (т.е. они загружаются не при каждом посещении страницы), вам надо убедиться, что вы добавляете c в конец набора Controls элемента управления p. Причина в том, что состояние отображения для p содержит также и состояние отображения для потомков p, и, как мы будем говорить в разделе «Синтаксический анализ состояния отображения», состояние отображения p определяет состояние отображения для его потомков по индексу. (Рис. 5 иллюстрирует, как введение динамического элемента управления не в конец набора Controls может привести к повреждению состояния отображения.)

Рис. 5. Какое влияние оказывает введение элементов управления на состояние отображения

Рис. 5. Какое влияние оказывает введение элементов управления на состояние отображения

 
Свойство ViewState

Каждый элемент управления отвечает за сохранение собственного состояния отображения. Это выполняется путем добавления измененного состояния в его свойство ViewState. Свойство ViewState определено в классе System.Web.UI.Control, что означает, что оно доступно всем серверным элементам управления ASP.NET.

Если вы проверите простые свойства любого серверного элемента управления ASP.NET, то увидите, что свойства считываются и записываются непосредственно в состояние отображения. (Вы можете посмотреть декомпилированный исходный код для .NET-сборки с помощью инструмента Relfector.) Например, рассмотрим свойство NavigateUrl Web-элемента управления HyperLink. Код для этого свойства выглядит так:

public string NavigateUrl
{
  get
    {
       string text = (string) ViewState["NavigateUrl"];
       if (text != null) return text;
       else return string.Empty;
    }
  set
    {
       ViewState["NavigateUrl"] = value;
    }
}

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

Комментарий Комментарий: Все Web-элементы управления используют приведенный выше шаблон для простых свойств. Простые свойства — это такие свойства, которые являются скалярными значениями, как string, integer, Boolean и т.д. Комплексные свойства, такие как свойство Font элемента управления Label, которые сами могут быть классами, используют другой подход. Более подробная информация по работе с состоянием серверных элементов управления ASP.NET приведена в книге Developing Microsoft ASP.NET Server Controls and Components.

Свойство ViewState типа System.Web.UI.StateBag. Класс StateBag предоставляет средства для сохранения пар имя и значение, используя «за сценой» System.Collections.Specialized.HybridDictionary. Как иллюстрирует синтаксис свойства NavigateUrl, элементы могут быть добавлены в и запрашиваться из StateBag с помощью такого же синтаксиса, который вы могли бы использовать для доступа к элементам из Hashtable.

 
Начало отслеживания состояния отображения

Помните, ранее я говорил, что состояние отображения запоминает только состояние, которое должно быть сохранено между возвратами данных обратно. Часть состояния, которую не надо сохранять между возвратами данных обратно — это свойства элемента управления, определенные в декларативном синтаксисе, поскольку они автоматически восстанавливаются на этапе создания экземпляра страницы. Например, если у нас на ASP.NET Web-странице есть Web-элемент управления HyperLink, и свойству NavigateUrl декларативно присвоено значение http://www.ScottOnWriting.NET, эту информацию не надо сохранять в состоянии отображения.

Однако если посмотреть на код свойства NavigateUrl элемента управления HyperLink, кажется, что ViewState элемента управления записывается независимо о того, задано ли значение свойства. Следовательно, на этапе создания экземпляра, где мы бы имели примерно следующее HyperLink1.NavigateUrl = http://www.ScottOnWriting.NET;, это могло бы иметь смысл, только если бы эта информация сохранялась бы в состоянии отображения.

Хотя это может казаться очевидным, это неверно. Причина в том, что класс StateBag всего лишь отслеживает изменения своих членов после вызова его метода TrackViewState(). Т.е. если у вас есть StateBag, любые и все дополнения или изменения, сделанные до вызова TrackViewState(), не будут сохранены при инициации метода SaveViewState(). Метод TrackViewState() вызывается в конце этапа инициализации, который имеет место после фазы создания экземпляра. Следовательно, исходные присваивания свойств на этапе создания экземпляра — несмотря на то, что они записаны во ViewState в аксессорах set свойств — не сохраняются во время вызова метода SaveViewState() на этапе сохранения состояния отображения, потому что метод TrackViewState() еще не был вызван.

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

Поскольку класс Page происходит от класса System.Web.UI.Control, у него также есть свойство ViewState. Кстати, вы можете использовать его для сохранения между возвратами данных обратно характерной для страницы и пользователя информации. Синтаксис использования очень простой:

ViewState[keyName] = value

Существует несколько сценариев, когда возможность сохранения информации во ViewState класса Page полезна. Канонический пример — создание сортируемой DataGrid со страничной организацией и (или сортируемой редактируемой DataGrid), поскольку выражение сортировки должно сохраняться между возвратами данных обратно. Т.е. если данные DataGrid сначала сортируются, а затем разбиваются на страницы, при связывании следующей страницы данных с DataGrid важно, чтобы вы получили следующую страницу данных, когда она отсортирована назначенным пользователем выражением сортировки. Таким образом, выражение сортировки надо каким-то образом сохранять. Существуют методы сортировки, но проще всего, по-моему, сохранять выражение сортировки во ViewState класса Page.

Чтобы получить более подробную информацию по созданию сортируемых DataGrid со страничной организацией (или сортируемых, редактируемых DataGrid с возможностью страничной организации), найдите копию моей книги ASP.NET Data Web Controls Kick Start.

 
Цена использования состояния отображения

Ничего не дается бесплатно, и состояние отображения не исключение. Состояние отображения ASP.NET наносит два удара по производительности при любом запросе ASP.NET Web-страницы:

  1. При всех посещениях страницы во время этапа сохранения состояния отображения класс Page собирает коллективное состояние отображения для всех элементов управления своей иерархии элементов управления и сериализует состояние в строку, кодированную по основанию base-64. (Это строка, которая записывается в скрытое поле __VIEWSTATE формы.) Аналогично, при возвратах данных обратно фазе загрузки состояния отображения надо десериализовать сохраненные данные состояния отображения и обновить соответствующие элементы управления в иерархии элементов управления.
  2. Скрытое поле __VIEWSTATE формы увеличивает размер Web-страницы, которую должен загрузить клиент. Для некоторых страницы с большим состоянием отображения эта величина достигает десятков килобайт, что может потребовать нескольких дополнительных секунд (или минут!) при загрузке через модем. Также при возврате данных обратно поле __VIEWSTATE должно быть возвращено Web-серверу в заголовках HTTP POST, таким образом, увеличивается время запроса возврата данных обратно.

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

 
Отключение состояния отображения

На этапе сохранения состояния отображения жизненного цикла страницы ASP.NET класс Page рекурсивно перебирает элементы управления своей иерархии элементов управления, вызывая метод SaveViewState() каждого из них. Это коллективное состояние и сохраняется в скрытом поле__VIEWSTATE формы. По умолчанию все элементы управления в иерархии будут записывать свое состояние отображения при вызове их метода SaveViewState(). Однако как разработчик страницы вы можете определить, что элемент управления не должен сохранять свое состояние отображения или состояния отображения его дочерних элементов управления, присваивая свойству EnableViewState элемента управления значение False (по умолчанию оно равно True).

Свойство EnableViewState определено в классе System.Web.UI.Control, таким образом, все серверные элементы управления имеют это свойство, включая класс Page. Следовательно, вы можете обозначить, что нет необходимости сохранять состояние отображения всей страницы, задав свойству EnableViewState класса Page значение False. (Это можно сделать или в классе отделенного кода строкой Page.EnableViewState = false;, или с помощью директивы уровня @Page — <%@Page EnableViewState="False" %>.)

Не все Web-элементы управления записывают одинаковое количество информации в своем состоянии отображения. Например, Web-элемент управления Label записывает только программные изменения своих свойств, что не сильно увеличивает размер состояния отображения. Однако DataGrid сохраняет в состоянии отображения все свое содержимое. Для DataGrid со множеством столбцов и строк размер состояния отображения может быстро разрастись! Например, размер показанного на рис. 6 DataGrid (и включенный в код примера этой статьи как HeavyDataGrid.aspx) составляет примерно 2.8 килобайт, а общий размер страницы — 5,791 байт. (Почти половину размера страницы занимает скрытое поле __VIEWSTATE формы!) Рис. 7 показывает моментальный снимок экрана состояния отображения, которое можно увидеть, посетив ASP.NET Web-страницы, выбрав в меню View\Source и затем раскрыв скрытое поле __VIEWSTATE формы.

Рис. 6. Элемент управления DataGrid

Рис. 6. Элемент управления DataGrid

Рис. 7. Состояние отображения элемента управления DataGrid

Рис. 7. Состояние отображения элемента управления DataGrid

Пример для этой статьи также включает ASP.NET Web-страницу LightDataGrid.aspx, которая имеет такой же DataGrid, как показан на рис. 6, но свойству EnableViewState в нем задано значение False. Общий размер состояния отображения для этой страницы? 96 байт. Размер всей страницы составляет 3,014 байта. LightDataGrid.aspx сократила размер состояния отображения примерно в 30 раз по сравнению с HeavyDataGrid.aspx, и ее общий размер загрузки составляет примерно половину HeavyDataGrid.aspx. Для DataGrid большего размера с большим количеством строк эта разница могла бы быть даже еще более ярко выраженной. (Более подробная информация по сравнению производительности DataGrid с включенным и выключенным состоянием отображения представлена в статье Deciding When to Use the DataGrid, DataList, or Repeater.)

Надеюсь, последний абзац доказал вам преимущества разумного использования свойства EnableViewState, особенно для элементов управления с «тяжелым» состоянием отображения, как у DataGrid. Теперь вопрос в том, «когда я могу благополучно присвоить свойству EnableViewState значение False?» Чтобы ответить на этот вопрос, вспомните, когда требуется состояние отображения — только когда вам надо сохранить состояние между возвратами данных обратно. DataGrid сохраняет свое содержимое в состоянии отображения, таким образом, разработчику не надо повторно связывать данные базы данных с DataGrid при каждой загрузке страницы, только при первой. Преимущество в том, что нет надобности обращаться к базе данных так часто. Если, однако, вы присвоили свойству EnableViewState DataGrid значение False, вам понадобится повторно связывать данные базы данных с DataGrid как при первой загрузке страницы, так и при каждом последующем возврате данных обратно.

Для Web-страницы, DataGrid которой предназначена только для чтения, как представленная на рис. 6, вы конечно же захотите задать свойству EnableViewState DataGrid значение False. Вы даже можете создать DataGrid с возможностью сортировки и страничной организации и отключить для них состояние отображения (о чем свидетельствует страница LightDataGrid-WithFeatures.aspx, входящая в пример), но, опять же, вы должны наверняка связывать данные базы данных с DataGrid при первом посещении страницы, а также и при последующих возвратах данных обратно.

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

После того, как на этапе сохранения состояния отображения страница собрала информацию состояния отображения для всех элементов управления своей иерархии элементов управления, она сохраняет его в скрытое поле __VIEWSTATE формы. Это скрытое поле формы может, конечно же, существенно увеличить общий размер Web-страницы. Состояние отображение сериализуется в скрытое поле формы в методе SavePageStateToPersistenceMedium() класса Page на этапе сохранения состояния отображения и десериализуется методом LoadPageStateFromPersistenceMedium()класса Page на этапе загрузки состояния отображения. Приложив небольшие усилия, мы можем сохранять состояние отображения не как скрытое поле формы, которое «утяжеляет» страницу, а в файловой системе Web-сервера. Для этого нам понадобится создать класс, который происходит от класса Page и переопределяет методы SavePageStateToPersistenceMedium() и LoadPageStateFromPersistenceMedium().

Комментарий Комментарий: Существует продукт сторонних производителей Flesk.ViewStateOptimizer, который сокращает размер состояния отображения, используя подобную технику.

Сериализация и десериализация состояния отображения осуществляется классом System.Web.UI.LosFormatter — LOS обозначает ограниченную сериализацию объекта — он разработан для эффективной сериализации определенных типов объектов в строку, кодированную по основанию base-64. LosFormatter может сериализовать любой тип объекта, который может быть сериализован классом BinaryFormatter, но создан для эффективной сериализации объектов следующих типов:

  • String
  • Integer
  • Boolean
  • Array
  • ArrayList
  • Hashtable
  • Pair
  • Triplet

Комментарий Комментарий: Pair и Triplet — это два класса, находящиеся в пространстве имен System.Web.UI и предоставляющие один класс для хранения двух или трех объектов. Для организации доступа к своим двум элементам класс Pair имеет свойства First и Second, тогда как Triplet имеет свойства First, Second и Third.

Метод SavePageStateToPersistenceMedium() вызывается из класса Page и передает обобщенное состояние отображения иерархии элементов управления страницы. При переопределении этого метода нам надо использовать LosFormatter() для сериализации состояния отображения в строку, кодированную по основанию base-64, и затем сохранить эту строку в файле файловой системы Web-сервера. Две основные проблемы, связанные с этим подходом:

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

Чтобы справиться с первой проблемой, мы назовем файл сохраненного состояния отображения, используя SessionID пользователя и URL страницы. Этот подход будет прекрасно работать для всех пользователей, чьи браузеры принимают cookies сеанса. Однако те, которые не принимают cookies, будут иметь уникальный ID сеанса, создаваемый для них при каждом посещении страницы, что, таким образом, делает этот метод присваивания имен невозможным для них. В этой статье я собираюсь продемонстрировать только использование схемы SessionID / URL, хотя она не годится для тех, чьи браузеры конфигурированы так, чтобы не принимать cookies. Также этот метод не подходит для Web-фермы, если только все серверы не хранят файлы состояния отображения централизованно.

Комментарий Комментарий: Одним из вариантов решения может быть использование в качестве имени файла для сохраняемого состояния отображения глобально уникального идентификатора (GUID), сохраняя этот GUID в скрытом поле формы на ASP.NET Web-странице. Этот подход, к сожалению, потребует намного больших усилий, чем применение схемы SessionID / URL, поскольку он требует введения скрытого поля формы в Web Form. По этой причине я буду придерживаться более простого подхода в качестве иллюстрации для этой статьи.

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

Чтобы сохранить информацию состояния отображения в файл, мы начнем с создания класса, происходящего от класса Page. Затем этому производному классу понадобится переопределить методы SavePageStateToPersistenceMedium() и LoadPageStateFromPersistenceMedium(). Следующий код представляет подобный класс:

public class PersistViewStateToFileSystem : Page
{
  protected override void SavePageStateToPersistenceMedium(object viewState)
   {
     // сериализуем состояние отображения в строку, кодированную по основанию base-64
     LosFormatter los = new LosFormatter();
     StringWriter writer = new StringWriter();
     los.Serialize(writer, viewState);
     // сохраняем строку на диск 
     StreamWriter sw = File.CreateText(ViewStateFilePath);
     sw.Write(writer.ToString());
     sw.Close();
   }
  protected override object LoadPageStateFromPersistenceMedium()
   {
     // определяем, к какому файлу организовывать доступ 
     if (!File.Exists(ViewStateFilePath)) return null;
     else
       {
       // открываем файл 
       StreamReader sr = File.OpenText(ViewStateFilePath);
       string viewStateString = sr.ReadToEnd();
       sr.Close();
       // десериализуем строку 
       LosFormatter los = new LosFormatter();
       return los.Deserialize(viewStateString);
       }
   }
  public string ViewStateFilePath
   {
     get
     {
       string folderName = Path.Combine(Request.PhysicalApplicationPath, "PersistedViewState");
       string fileName = Session.SessionID + "-" + Path.GetFileNameWithoutExtension(Request.Path).Replace("/", "-") + ".vs";
       return Path.Combine(folderName, fileName);
     }
   }
}

Класс включает открытое свойство ViewStateFilePath, которое возвращает физический путь к файлу, в котором будет храниться конкретная информация состояния отображения. Этот путь к файлу зависит от SessionID пользователя и URL запрашиваемой страницы.

Обратите внимание, что метод SavePageStateToPersistenceMedium() принимает входной параметр object. Этот object — это объект состояния отображения, который создан на этапе сохранения состояния отображения. Работа SavePageStateToPersistenceMedium() заключается в сериализации этого объекта и сохранении его определенным образом. Код метода просто создает экземпляр объекта LosFormatter и вызывает его метод Serialize(), сериализующий переданную информацию состояния отображения в StringWriter writer. Вслед за этим создается указанный файл (или перезаписывается, если он уже существует) содержащий сериализованное в строку, кодированную по основанию base-64, состояние отображения.

Метод LoadPageStateFromPersistenceMedium() вызывается в начале этапа загрузки состояния отображения. Его работа заключается в извлечении сохраненного состояния отображения и десериализации его в объект, который может быть распространен в иерархию элементов управления страницы. Для этого открывается тот же файл, в котором было сохранено состояние отображения при последнем посещении страницы, и с помощью метода Deserialize()в LosFormatter() возвращается десериализованная версия.

Опять же, этот подход не подходит пользователям, которые не принимают cookies, но для всех остальных состояние отображения полностью сохраняется в файловой системе Web-сервера. Таким образом, общий размер страницы не увеличивается ни на байт!

Комментарий Комментарий: Другим подходом для сокращения влияния состояния отображения на размеры страницы является сжатие потока сериализованного состояния отображения в методе SavePageStateToPersistenceMedium(), а затем развертывание его в исходную форму в методе LoadPageStateFromPersistenceMedium(). У Скотта Гэлловея (Scott Galloway) есть форум, на котором он делится опытом по использованию библиотеки #ziplib для сжатия состояния отображения.
 
Синтаксический разбор состояния отображения

При формировании изображения страницы она с помощью класса LosFormatter сериализует свое состояние отображения в строку, кодированную по основанию base-64, и (по умолчанию) сохраняет его в скрытом поле формы. При возврате данных обратно содержимое скрытого поля формы извлекается и десериализуется в объектное представление состояния отображения, которое затем используется для восстановления состояния элементов управления в иерархии элементов управления. Мы упустили одну деталь — что конкретно представляет собой структура объекта состояния отображения класса Page?

Как обсуждалось ранее, полное состояние отображения Page — это сумма состояний отображения элементов управления ее иерархии элементов управления. Скажем по-другому, в любой точке иерархии элементов управления состояние отображения данного элемента управления представляет состояние отображения данного элемента управления наряду с состояниями отображения всех его дочерних элементов управления. Поскольку класс Page является корнем иерархии элементов управления, его состояние отображения представляет состояние отображения всей иерархии элементов управления.

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

  1. Хэш-код страницы. Этот хэш-код используется как гарантия того, что состояние отображения не было изменено между возвратами данных обратно. Подробнее о хэшировании состояния отображения мы поговорим в разделе «Состояние отображения и вопросы безопасности».
  2. Коллективное состояние отображения иерархии элементов управления Page.
  3. ArrayList элементов управления иерархии элементов управления, который классу страницы надо вызывать явно на этапе инициации события возврата данных обратно жизненного цикла.

Элементы First и Third в Triplet относительно ясны; элемент Second — это то место, где удерживается состояние отображения для иерархии элементов управления Page. Элемент Second генерируется Page путем вызова метода SaveViewStateRecursive(), который определен в классе System.Web.UI.Control. SaveViewStateRecursive() сохраняет состояние отображения элемента управления и его потомков, возвращая Triplet со следующей информацией:

  1. Состояние, находящееся в свойстве ViewState объекта StageBag класса Control.
  2. ArrayList целых. Этот ArrayList сохраняет индексы дочерних элементов управления Control, которые имеют определенное (non-null) состояние отображения.
  3. ArrayList состояний отображения для дочерних элементов управления. –n-ное состояние отображения в этом ArrayList проецируется на индекс дочернего элемента управления в –n-ном элементе ArrayList элемента Second в Triplet.

Класс Control обрабатывает состояние отображения, возвращая Triplet. Элемент Second в Triplet содержит состояние отображения потомков Control. В конечном итоге состояние отображения охватывает множество ArrayLists внутри Triplets в Triplets в Triplets в... (Точно содержимое состояния отображения зависит от элементов управления в иерархии. Более сложные элементы управления могут сериализовать свое состояние, используя Pairs или массивы object. Как мы вскоре увидим, состояние отображения образуется из большого числа Triplets и ArrayLists, с такой же глубиной вложенности, как и в иерархии элементов управления.)

 
Программный разбор состояния отображения

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

  1. Текущий объект состояния отображения.
  2. Глубину уровней вложенности состояния отображения.
  3. Текстовую метку для вывода на экран.

Два последних входных параметра служат только целям отображения. Код этого метода, показанный ниже, определяет тип текущего объекта состояния отображения и, соответственно, выводит на экран содержимое состояния отображения, рекурсивно вызывая себя для каждого члена текущего объекта. (Переменная tw — это экземпляр TextWriter, в который записываются выходные данные.)

protected virtual void ParseViewStateGraph( object node, int depth, string label)
{
  tw.Write(System.Environment.NewLine);
  if (node == null)
   {
     tw.Write(String.Concat(Indent(depth), label, "NODE IS NULL"));
   }
  else if (node is Triplet)
   {
     tw.Write(String.Concat(Indent(depth), label, "TRIPLET"));
     ParseViewStateGraph( ((Triplet) node).First, depth+1, "First: ");
     ParseViewStateGraph( ((Triplet) node).Second, depth+1, "Second: ");
     ParseViewStateGraph( ((Triplet) node).Third, depth+1, "Third: ");
   }
  else if (node is Pair)
   {
     tw.Write(String.Concat(Indent(depth), label, "PAIR"));
     ParseViewStateGraph(((Pair) node).First, depth+1, "First: ");
     ParseViewStateGraph(((Pair) node).Second, depth+1, "Second: ");
   }
  else if (node is ArrayList)
   {
     tw.Write(String.Concat(Indent(depth), label, "ARRAYLIST"));
     // display array values
     for (int i = 0; i < ((ArrayList) node).Count; i++)
     ParseViewStateGraph( ((ArrayList) node)[i], depth+1, String.Format("({0}) ", i));
   }
  else if (node.GetType().IsArray)
   {
     tw.Write(String.Concat(Indent(depth), label, "ARRAY "));
     tw.Write(String.Concat("(", node.GetType().ToString(), ")"));
     IEnumerator e = ((Array) node).GetEnumerator();
     int count = 0;
     while (e.MoveNext())
     ParseViewStateGraph( e.Current, depth+1, String.Format("({0}) ", count++));
   }
  else if (node.GetType().IsPrimitive || node is string)
   {
     tw.Write(String.Concat(Indent(depth), label));
     tw.Write(node.ToString() + " (" + node.GetType().ToString() + ")");
   }
  else
   {
     tw.Write(String.Concat(Indent(depth), label, "OTHER - "));
     tw.Write(node.GetType().ToString());
   }
}

Как показывает код, метод ParseViewState() перебирает ожидаемые типы — Triplet, Pair, ArrayList, arrays и элементарные типы. Для скалярных значений — integer, string и т.д. — на экран выводятся тип и значение; для составных типов — array, Pair, Triplet и т.д. — на экран выводятся члены путем рекурсивного вызова метода ParseViewState().

Класс ViewStateParser может использоваться из ASP.NET Web-страницы (смотрите демонстрационную версию ParseViewState.aspx) или к нему может быть организован доступ непосредственно из метода SavePageStateToPersistenceMedium() в классе, унаследованном от класса Page (смотрите класс ShowViewState). Рис. 8 и 9 показывают демонстрационную версию ParseViewState.aspx в действии. Как видно на рис. 8, пользователю предлагается многостроковое текстовое окно, в которое они могут вставить скрытое поле __VIEWSTATE формы некоторой Web-страницы. На рис. 9 показан фрагмент разобранного состояния отображения для страницы, отображающей информацию файловой системы в DataGrid.

Рис. 8. Декодирование ViewState

Рис. 8. Декодирование ViewState

Рис. 9. Декодированный ViewState

Рис. 9. Декодированный ViewState

Кроме синтаксического анализатора состояния отображения, предоставленного в примере для этой статьи, Пол Вилсон (Paul Wilson) предлагает синтаксический анализатор состояния отображения на своем сайте. На сайте Фрица Онион (Fritz Onion) в разделе Resources есть декодер состояния отображения в виде приложения WinForms, доступный для скачивания.

 
Состояние отображения и вопросы безопасности

Состояние отображения для ASP.NET Web-страницы хранится по умолчанию как строка, кодированная по основанию base-64. Как мы видели в предыдущем разделе, эту строку можно без труда декодировать и провести ее синтаксический разбор, выводя на экран содержимое состояния отображения для всеобщего обозрения. Это подымает два вопроса, связанных с обеспечением безопасности:

  1. Поскольку может быть проведен синтаксический разбор состояния отображения, что может защитить от изменения значений, повторной сериализации и использования измененного состояния отображения?
  2. Поскольку может быть проведен синтаксический разбор состояния отображения, не означает ли это, что я не могу разместить в нем никакой уязвимой информации (такой как пароли, строки подключения и т.д.)?

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

Защита состояния отображения от вмешательства

Даже несмотря на то, что состояние отображения будет сохранять только состояние Web-элементов управления страницы и другие неуязвимые данные, подлые пользователи могут доставить вам головную боль, если у них получится изменить состояние отображения страницы. Например, представьте, что вы запустили Web-сайт электронной торговли, который использует DataGrid для отображения списка продаваемых товаров и их стоимости. Если вы не зададите свойству EnableViewState DataGrid значения False, содержимое DataGrid — названия и цены ваших товаров — будет сохраняться в состоянии отображения.

Злонамеренные пользователи могут провести синтаксический разбор состояния отображения, заменить все цены суммой $0.01 и затем десериализовать состояние отображения опять в строку, кодированную по основанию base-64. Потом они могут разослать сообщения электронной почты или ссылки, по щелчку кнопки формы, которая отправляет пользователя на вашу страницу со списком товаров, передавая при этом измененное состояние отображения в заголовки HTTP POST. Ваша страница считает состояние отображения и выведет на экран данные DataGrid на основании этого состояния отображения. И что получится? У вас будет куча покупателей, полагающих, что они могут купить ваши товары всего лишь за пенни!

Простое средство защиты от подобных фальсификаций — использование машинного контроля аутентификации (machine authentication check) или MAC. Машинный контроль аутентификации разработан для обеспечения гарантии того, что полученные компьютером данные являются именно теми данными, которые были переданы, в частности, что они не были фальсифицированы. Это именно то, что нам требовалось для состояния отображения. Для состояния отображения ASP.NET LosFormatter осуществляет MAC путем хэширования сериализованных данных состояния отображения и добавления этого хэша в конец состояния отображения. (Хэш — это быстро вычисляемый дайджест, который обычно используется в сценариях симметричной безопасности для обеспечения целостности сообщения.) При возврате Web-страницы LosFormatter проводит проверку, чтобы гарантировать, что прикрепленный хэш совпадает с хэшированным значением десериализованного состояния отображения. Если он не совпадает, состояние отображения было изменено по дороге.

По умолчанию класс LosFormatter применяет MAC. Однако вы можете настроить то, проводить MAC или нет, через свойство EnableViewStateMac класса Page. По умолчанию оно имеет значение True, которое указывает, что MAC будет иметь место; значение False показывает, что MAC не должен проводиться. Вы можете далее настроить, какой алгоритм хэширования должен применяться при MAC. В файле machine.config найдите атрибут validation элемента <machineKey>. По умолчанию применяется алгоритм хэширования SHA1, но вы по желанию можете заменить его алгоритмом MD5. (Более подробная информация по SHA1 представлена в RFC 3174; более подробная информация по MD5 представлена в RFC 1321.)

Комментарий Комментарий: При использовании Server.Transfer() вы, возможно, столкнетесь с проблемой аутентификации состояния отображения. Во многих статьях в сети упоминается о том, что единственный выход в этом случае — задать свойству EnableViewStateMac значение False. Тогда как это, конечно же, решает проблему, но и открывает брешь в системе безопасности. Более подробную информацию, включая безопасные методы решения, вы можете найти в статье KB.
вернуться к содержанию
Кодирование состояния отображения

В идеале состояние отображения не должно кодироваться, поскольку оно никогда не должно содержать уязвимой информации. Однако в случае необходимости LosFormatter предоставляет ограниченную поддержку кодирования. LosFormatter допускает только один тип кодирования: Triple DES. Чтобы показать, что состояние отображения должно быть закодировано, присвойте атрибуту validation элемента <machineKey> в файле machine.config значение 3DES.

Кроме атрибута validation, элемент<machineKey> включает также атрибуты validationKey и decryptionKey. Атрибут validationKey определяет ключ, используемый для MAC; decryptionKey указывает ключ, используемый в кодировании Triple DES. По умолчанию этим атрибутам присваивается значение «AutoGenerate,IsolateApp», при котором автоматически генерируются ключи для каждого Web-приложения на сервере. Эти настройки хороши для среды с одним Web-сервером, но если вы работаете с Web-фермой, жизненно важно чтобы все Web-серверы использовали одинаковые ключи для MAC и/или кодирования/декодирования. В этом случае вам понадобится вручную вводить общий ключ для серверов Web-фермы. Более подробно об этом и элементе <machineKey> вообще смотрите в технической документации по <machineKey> и статье Сьюзан Воррен (Susan Warren) Taking a Bite Out of ASP.NET ViewState.

Свойство ViewStateUserKey

В Microsoft® ASP.NET версии 1.1 классу Page добавлено свойство — ViewStateUserKey. Этому свойству, если оно используется, на этапе инициализации жизненного цикла страницы должно быть присвоено значение типа string (в обработчике события Page_Init). Назначение этого свойства — присвоение состоянию отображения некоторого характерного для пользователя ключа, такого как имя пользователя. ViewStateUserKey, если предоставляется, используется во время MAC как «изюминка» хэша.

Свойство ViewStateUserKey защищает от случаев, когда злонамеренные пользователи посещают страницу, собирают состояние отображения и затем заманивают пользователя посетить такую же страницу, передавая ему свое состояние отображения . Более подробно об этом свойстве и его применении читайте в Building Secure ASP.NET Pages and Controls

Заключение

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

Тогда как состояние отображения дает возможность без труда сохранять состояние между возвратами данных обратно, оно все-таки имеет и свой недостаток, и он заключается в увеличении размера страницы. Поскольку данные состояния отображения сохраняются в скрытом поле формы, состояние отображения может запросто добавить десятки килобайт данных Web-странице, таким образом увеличивая и время ее загрузки, и время закачивания. Чтобы «облегчить» страницу, отягощенную состоянием отображения, вы можете выборочно указать Web-элементам управления не записывать их состояние отображения, присваивая их свойству EnableViewState значение False. Кстати, состояние отображения можно отключить для всей страницы, задав свойству EnableViewState значение false в директиве @Page. Кроме отключения состояния отображения на уровне страницы или элемента управления, вы также можете указать для него альтернативное вспомогательное запоминающее устройство, такое как файловая система Web-сервера.

Эта статья завершается рассмотрением вопросов безопасности, связанных с состоянием отображения. По умолчанию состояние отображения осуществляет MAC, чтобы гарантировать, что оно не было фальсифицировано между возвратами данных обратно. ASP.NET 1.1 предоставляет свойство ViewStateUserKey, чтобы добавить дополнительный уровень защиты. Данные состояния отображения также могут быть закодированы с помощью алгоритма шифрования Triple DES.Счастливого программирования!

Понимание состояния отображения ASP.NET
Лента новостей


2006 (c) Copyright Hardline.ru