Pull to refresh

Настоящее понимание ViewState'а

Reading time 30 min
Views 62K
Original author: Dave Reed
От переводчика: Это перевод статьи от одного из разработчиков ASP.NET, в которой подробно рассказывается о механизме управления состоянием страницы — ViewState'е. Несмотря на то, что статья написана в 2006 году, она до сих пор не потеряла своей актуальности.

ViewState — очень непонятное существо. Я попытаюсь положить конец всяческим кривотолкам, и постараюсь объяснить, как на самом деле работает механизм ViewState'а, от начала до конца, посмотрев на него с различных точек зрения.

Есть множество статей, авторы которых пытаются развеять мифы о ViewState'е. Можно даже подумать, что это все — борьба с ветряными мельницами (где ViewState – ветряные мельницы, а Интернет – инструмент борьбы). Но, я вам доложу, мельницы ещё не остановились. Как раз наоборот, они вертятся и заполняют собой вашу гостиную. Пора бы нанести по ним ещё один удар. Не тревожьтесь, при написании этой статьи ни одна ветряная мельница не пострадала.

Не то чтобы вокруг не было хороших источников информации о ViewState'е, просто все они что-то упускают, и таким образом только вносят свой вклад в общую сумятицу вокруг ViewState'а. Например, один из ключевых моментов — понимание того, как работает трекинг ViewState'а. И вот очень хорошая и глубокая статья о ViewState, которая даже не касается этого! Или вот статья на W3Schools, которая наводит на мысль, будто бы параметры формы, отправляемые на сервер, хранятся во ViewState'е, что не есть правда. (Не верите? Отключите ViewState textbox'а на их примере и запустите его еще раз). А вот документация на MSDN, которая описывает то, как элементы управления хранят свое состояние между postback'ами. Нельзя сказать, что эта документация неверна, но в ней содержится утверждение, которое не до конца правильно:

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

Похоже, что здесь подразумевается что все, что пихается во ViewState, будет отправляться клиенту. НЕПРАВДА! Понятно, откуда берется такая суматоха вокруг ViewState'а. Нигде в Интернете я не смог найти 100%-но полного и точно описания его работы! Лучшая статья, которую я видел, это статья Скотта Митчелла. Она обязательна к прочтению. Однако она не объясняет взаимодействие родительских и дочерних элементов управления во время инициализации и трекинга ViewState'а, и уже это вызывает множество непониманий ViewState'а, по крайней мере исходя из моего опыта.

Так что первая цель этой статьи — дать исчерпывающее объяснение того, как функционирует ViewState, от начала до конца, по возможности закрывая белые пятна, оставленные другими статьями. После подробного описания процесса ViewState'а я покажу несколько ошибок, которые допускают разработчики при использовании ViewState'а, как правило даже не догадываясь об этом, и как эти ошибки исправлять. Следует отметить, что я писал эту статью на основе ASP.NET 1.x. Однако изменений в механизме ViewState'а в ASP.NET 2.0 очень мало. В частности, появился новый тип ViewState'а — ControlState, однако он используется точно также, как и ViewState, поэтому мы можем его просто проигнорировать.

Сперва позвольте мне объяснить, почему я считаю, что понимание сути ViewState'а очень важно:

Непонимание ViewState'а ведет к...


  1. Потере важной информации
  2. Атакам на ViewState
  3. Плохой производительности — вплоть до ОТСУТСТВИЯ ПРОИЗВОДИТЕЛЬНОСТИ
  4. Плохой расширяемости — как много пользователей вы сможете обслужить, если каждый из них будет отправлять 50k с каждым запросом?
  5. Плохому дизайну вообще
  6. Головной боли, тошноте, головокружениям и необратимому искривлению формы надбровных дуг.


А теперь давайте начнем с самого начала.

Что делает ViewState?


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

  1. Сохраняет данные элементов управления по ключу, как хэш-таблица
  2. Отслеживает изменения состояния ViewState'а
  3. Сериализирует и десериализирует сохраненные данные в скрытое поле на клиенте
  4. Автоматически восстанавливает данные на postback'ах


Еще более важно понимать, что ViewState НЕ делает.

Что не делает ViewState?

  1. Автоматически сохраняет состояние полей класса (скрытых, защищенных или открытых)
  2. Запоминает какую-либо информацию при загрузке страницы (только postback'и)
  3. Исключает необходимость загружать данные при каждом запросе
  4. Отвечает за загрузку данных, которые были отправлены на сервер, например введенных в текстовое поле (хотя ViewState и играет здесь важную роль)
  5. Варит вам кофе


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

1. ViewState сохраняет данные


Если вы когда-нибудь использовали хэш-таблицы, вы уже все знаете. Ничего экстраординарного, у ViewState'а есть индексатор, который принимает строку как ключ и любой объект как значение. Например:
ViewState["Key1"] = 123.45M; // сохраняем decimal

ViewState["Key2"] = "abc"; // сохраняем string

ViewState["Key3"] = DateTime.Now; // сохраняем DateTime


* This source code was highlighted with Source Code Highlighter.

На самом деле здесь «ViewState» — всего лишь название. ViewState — это защищенное свойство, определенное у класса System.Web.UI.Control, от которого наследуются все элементы управления, включая серверные элементы управления, пользовательские элементы управления, и страницы. Тип этого свойства — System.Web.UI.StateBag. Строго говоря, класс StateBag не имеет никакого отношения к ASP.NET. Он определен в сборке System.Web, но кроме как зависимость от State Formatter'а, определенного в System.Web.UI, нет никаких предпосылок к тому, чтобы StateBag не был определен там же, где и скажем ArrayList, в пространстве имен System.Collections. На практике, серверные элементы управления используют ViewState для хранения большинства, если не всех, своих свойств. Это верно практически для всех готовых элементов управления от Microsoft (таких как Label, TextBox, Button). Это важно! Вы должны знать такие вещи о элементах управления, которые вы используете. Прочитайте это предложение еще раз. Серьезно… и третий раз: Серверные элементы управления используют ViewState для хранения большинства, если не всех, своих свойств. Основываясь на своем опыте, думая о свойствах вы наверное представляете себе что-то типа:
public string Text
{
  get { return _text; }
  set { _text = value; }
}

* This source code was highlighted with Source Code Highlighter.

Важно знать, что большинство свойств в элементах управления ASP.NET выглядят не так. Вместо этого они используют ViewState, а не скрытую переменную:
public string Text
{
  get { return (string)ViewState["Text"]; }
  set { ViewState["Text"] = value; }
}

* This source code was highlighted with Source Code Highlighter.

И еще раз акцентирую ваше внимание — это верно практически для ВСЕХ СВОЙСТВ, даже Style (вообще говоря Style делает это, реализуя IStateManager, но суть та же). Когда вы пишете свои элементы управления, обычно следует придерживаться этого шаблона, но сначала следует подумать, что можно, а что нельзя динамически менять на postback'ах. Но это несколько иная тема для разговора. Также важно понимать как в рамках этой техники реализованы значения по умолчанию. Когда вы думаете об обычных свойствах, имеющих значения по умолчанию, вы наверное представляете себе что-то типа:
public class MyClass
{
  private string _text = "Default Value!";

  public string Text
  {
    get { return _text; }
    set { _text = value; }
  }
}

* This source code was highlighted with Source Code Highlighter.

Значение по умолчанию является таковым, поскольку именно его возвращает свойство, если ему ни разу ничего не присваивали. Как же нам добиться такого же поведения, но используя ViewState? А вот как:
public string Text
{
  get
  {
    return ViewState["Text"] == null ?
       "Default Value!" :
       (string)ViewState["Text"];
  }
  set { ViewState["Text"] = value; }
}

* This source code was highlighted with Source Code Highlighter.

Как и хэш-таблица, StateBag возвращает null по ключу, если он не содержит записи с таким ключом. То есть, если значение записи null, значит ее не создавали и надо вернуть значение по умолчанию, в противном случае возвращаем значение записи. Самые внимательные наверняка заметили разницу между этими двумя реализациями. В случае с ViewState присвоение свойству значения null отправит свойство назад в состояние «по умолчанию». А если обычному свойству присвоить null, то оно просто будет содержать null. Это одна из причин почему в ASP.NET постоянно используется String.Empty (""), а не null. Это также не очень-то важно для готовых элементов управления, поскольку те их свойства, которые могут быть null, и так имеют значение по умолчанию, равное null. Все что я могу сказать — имейте это в виду, когда пишете свои элементы управления. И наконец, хотя для хранения значений свойств используется ViewState, он не ограничен только этим. В элементе управления или на странице, вы всегда можете использовать ViewState в любой момент для любой цели, а не только как контейнер свойств. Иногда бывает полезно запоминать таким образом какую-то информацию, но это, опять же, тема для другого разговора.

2. ViewState отслеживает изменения


Бывало ли с вами так, что вы присваивали какое-то значение свойству элемента управления и потом чувствовали себя как будто… испачкавшимся? Со мной точно бывало. Более того, после двадцати часов установки свойств в офисе я становился таким грязным, что моя жена отказывалась меня целовать, если только я не приносил цветы чтобы замаскировать запах. Честное слово! Ну ладно, ладно, установка свойств не делает вас грязным. Но она делает грязным StateBag! StateBag — это не просто тупая коллекция ключей и значений, как хэш-таблица (только не говорите хэш-таблице, что я ее так назвал, она страшна в гневе). В дополнение к хранению значений по ключу, StateBag умеет ОТСЛЕЖИВАТЬ изменения (здесь и далее «трекинг» — прим. пер.). Трекинг или включен, или выключен. Он может быть включен вызовом метода TrackViewState(), но если его включить, то выключить уже нельзя. КОГДА И ТОЛЬКО КОГДА трекинг ВКЛЮЧЕН, любое изменение любой записи StateBag'а будет помечать эту запись как «Dirty» (здесь и далее «грязную» — прим. пер.). У StateBag'а есть даже специальный метод, который нужен для проверки, является и запись грязной — IsItemDirty(string key). Вы также можете вручную пометить запись, вызвав метод SetItemDirty(string key). Чтобы проиллюстрировать это, давайте предположим, что у нас есть неотслеживаемый StateBag:
stateBag.IsItemDirty("key"); // вернет false
stateBag["key"] = "abc";
stateBag.IsItemDirty("key"); // опять вернет false

stateBag["key"] = "def";
stateBag.IsItemDirty("key"); // ОПЯТЬ вернет false

stateBag.TrackViewState();
stateBag.IsItemDirty("key"); // верно, опять вернет false

stateBag["key"] = "ghi";
stateBag.IsItemDirty("key"); // TRUE!

stateBag.SetItemDirty("key", false);
stateBag.IsItemDirty("key"); // FALSE!

* This source code was highlighted with Source Code Highlighter.

Вообще говоря, трекинг позволяет StateBag'у отслеживать, какие записи менялись после того, как был вызван TrackViewState(). Те значения, которые были присвоены до вызова этого метода, отслежены не будут. Важно понимать, что любая операция присваивания пометит запись как грязную, даже если присваиваемое значение совпадает с текущим!
stateBag["key"] = "abc";
stateBag.IsItemDirty("key"); // вернет false
stateBag.TrackViewState();
stateBag["key"] = "abc";
stateBag.IsItemDirty("key"); // вернет true

* This source code was highlighted with Source Code Highlighter.

Можно было бы написать ViewState так, чтобы он проверял новые и старые значения перед там как определять, помечать ли запись. Но помните, что ViewState позволяет использовать любые объекты в качестве значений, и значит речь идет не о банальном сравнении строк, и не всякий объект реализует IComparable. Увы, поскольку имеют место сериализация и десериализация, объект который вы кладете во ViewState не будет тем же объектом после postback'а. Такие сравнения не нужны ViewState'у, поэтому он их и не делает. Вот что такое трекинг по сути.

Но вам наверное интересно, зачем StateBag'у эта функция. С чего бы это вдруг кому-то может понадобиться знать, имели ли место какие-либо изменения после того, как был вызван TrackViewState()? Почему бы просто не пользоваться обычной коллекцией и проблем не знать? И это одно из мест, которые вызывают всю путаницу вокруг ViewState'а. Я проводил интервью со многими специалистами по ASP.NET, у которых в резюме записаны годы использования этой технологии, и они не смогли доказать, что разбираются в этом вопросе. Более того, я ни разу не собеседовал человека, который разбирался в этом! Прежде всего, для того чтобы понять зачем нужно отслеживание, нужно несколько глубже понять как ASP.NET работает с декларативными элементами управления. Декларативные — это такие элементы управления, которые определяются в ASPX или ASCX файлах:
<asp:Label id="lbl1" runat="server" Text="Hello World" />

* This source code was highlighted with Source Code Highlighter.

Вот Label, который определен в вашем файле. Следующая вещь, которую нужно понять, это возможность ASP.NET связывать атрибуты со свойствами элемента управления. Когда ASP.NET разбирает файл, и обнаруживает тэг с runat=server, она создает экземпляр объявленного элемента управления. Соответствующую переменную она называет согласно тому, какое значение вы задали атрибуту ID (кстати говоря, не все знают, что ID задавать необязательно, ASP.NET сгенерирует его сама. Это может быть полезным, но сейчас речь не об этом). Но это не все. Тэг элемента управления может иметь кучу атрибутов. В нашем примере с Label, у нас есть атрибут Text, и его значение «Hello World». Используя рефлексию, ASP.NET может определить, есть ли у элемента управления соответствующее свойство, и присвоить ему объявленное значение. Конечно же, значение атрибута объявлено как строка (в конце концов, оно объявлено в текстовом файле разметки), поэтому если соответствующее свойство имеет тип не string, нужно определить, как привести строку к нужному типу перед тем, как вызывать сеттер. Как это происходит — тема отдельная (здесь используются TypeConverter'ы и статические методы Parse). Достаточно того, что конвертация как-то происходит, и вызывается сеттер свойства.

Помните то важное утверждение из первого пункта? Вот оно еще раз: Cерверные элементы управления используют ViewState для хранения большинства, если не всех, своих свойств. Это значит, что когда вы определяете атрибут серверного элемента управления, это значение как правило сохраняется как запись во ViewState'те. Теперь вспомните как работает трекинг. Помните, что если StateBag отслеживает изменения, присвоение записям значений пометит их как грязные. А если отслеживание отключено, записи помечены не будут. Возникает вопрос — когда ASP.NET вызывает сеттер свойства, которое соответствует объявленному атрибуту, отслеживает ли StateBag изменения? Ответ таков — нет, не отслеживает, поскольку отслеживание не начинается пока кто-нибудь не вызовет TrackViewState(), и ASP.NET делает это в фазе инициализации страницы. Этот прием позволяет с легкостью отличать значения, заданные декларативно, от динамически заданных значений. Если вы еще не понимаете, насколько это важно, пожалуйста, продолжайте читать.

3. Сереализация и десериализация


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

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

Небольшое отступление — прежде чем мы разберемся как ASP.NET создает эту закодированную строку, нам нужно понять иерархию элементов управления на странице. Многие опытные разработчики не знают, что страница состоит из дерева элементов управления, потому что все, с чем они работали, это ASPX страницы и объявленные на них элементы управления… но у элементов управления могут быть дочерние элементы, которые могу содержать свои дочерние элементы, и т.д. Так формируется дерево элементов управления, в корне которого лежит сама страница. Второй уровень — это элементы, объявленные на верхнем уровне ASPX страницы (как правило их ровно три — Literal, содержащий все до тэга <form>, HtmlForm, представляющий собой форму и ее дочерние элементы, и еще один Literal, содержащий все после тэга </form>). На третьем уровне лежат элементы управления, содержащиеся в упомянутых элементах, и так далее. Каждый из этих элементов управления имеет свой ViewState — свой собственный экземпляр класса StateBag. В System.Web.UI.Control объявлен защищенный метод SaveViewState. Он возвращает object. Реализация Control.SaveViewState — это просто вызов такого же метода у своего ViewState (в StateBag тоже есть метод SaveViewState()). Вызывая этот метод рекурсивно у каждого элемента управления во всем дереве, ASP.NET строит еще одно дерево с такой же структурой, но теперь это уже не дерево элементов управления, а дерево данных.

Данные на этом шаге еще не превратились ту строку в скрытом поле, это просто дерево объектов, которые надо сохранить. И вот тут-то все наконец-то сходится воедино… вы готовы? Когда StateBag должен сохранить и вернуть свое состояние (StateBag.SaveViewState()), он делает это только для записей, которые были помечены как грязные. Вот для чего StateBag'у его трекинг. Это единственная причина. Но что за причина! StateBag мог бы обработать все записи, в нем содержащиеся, но зачем сохранять данные, которые не изменялись по сравнению со своим естественным, декларированным состоянием? Нет совершенно никаких причин их обрабатывать — они так и так будут восстановлены, когда ASP.NET будет разбирать страницу, отвечая на следующий запрос (на самом деле парсинг страницы происходит только один раз, в процессе этого компилируется класс, с которым потом ASP.NET и работает). И несмотря на эту небольшую оптимизацию в ASP.NET, ненужные данные все равно сохраняются во ViewState из-за неправильного использования. Позже я покажу несколько примеров таких ошибок.

Задачка


Если вы дочитали до этого места — мои поздравления. В награду вот вам задачка. Допустим у нас есть две почти одинаковые ASPX формы, Page1.aspx и Page2.aspx. На каждой странице только форма и Label:
<form id="form1" runat="server">
  <asp:Label id="label1" runat="server" Text="" />
</form>

* This source code was highlighted with Source Code Highlighter.

Они абсолютно одинаковы, но есть небольшое различие. На Page1.aspx текст у Label простой — «abc»:
<asp:Label id="label1" runat="server" Text="abc" />

* This source code was highlighted with Source Code Highlighter.

А на Page2.aspx текст побольше, много больше (преамбула конституции США):
<asp:Label id="label1" runat="server" Text="We the people of the United States,
    in order to form a more perfect union, establish justice, insure
    domestic tranquility, provide for the common defense, promote the
    general welfare, and secure the blessings of liberty to ourselves and
    our posterity, do ordain and establish this Constitution for the United
    States of America."
/>

* This source code was highlighted with Source Code Highlighter.

Если вы откроете Page1.aspx, вы увидите только «abc». Но можно посмотреть HTML код страницы, и вы увидите печально известное скрытое поле _ViewState со строкой зашифрованных данных. Обратите внимание на размер этой строки. А теперь откройте страницу Page2.aspx, и вы увидите преамбулу. Теперь откройте исходный код страницы, и посмотрите какого размера поле _ViewState здесь. Внимание, вопрос: Размеры этих строк совпадают или нет? Перед тем, как озвучить ответ, давайте немного усложним задание. Давайте добавим на каждую страницу по кнопке:
<asp:Button id="button1" runat="server" Text="Postback" />

* This source code was highlighted with Source Code Highlighter.

На клик не повешено никакого обработчика, так что нажатие на кнопку только заставляет страницу «моргать». Теперь повторите эксперимент, но только перед тем, как смотреть исходный код страницы, кликните на кнопку. Вопрос прежний: Размеры этих строк совпадают или нет? Правильный ответ на первую часть задачи таков — ОНИ ОДИНАКОВЫ! Они одинаковы, поскольку ни в одном из этих ViewState'ов не хранится вообще ничего, связанного с Label'ами. Если вы понимаете, как работает ViewState, это должно быть очевидно. Свойству Text присваивается декларированное значение до того, как начинается трекинг ViewState'а. Это значит, что если бы вы проверили флаг Dirty записи Text в StateBag'е, он не был бы помечен. StateBag игнорирует непомеченные записи, когда вызывается SaveViewState(), поэтому свойство Text не сериализуется в скрытое поле. А поскольку Text не сериализуется, а во всем остальном формы абсолютно идентичны, размер ViewState'а остается прежним.

Правильный ответ на второй вопрос такой же — ОНИ ОДИНАКОВЫ! Для того, чтобы данные записались во ViewState, они должны быть помечены как грязные. Для того, чтобы данные были помечены как грязные, они должны быть изменены после того, как вызван TrackViewState(). Но даже после того, как мы выполнили postback, ASP.NET обрабатывает серверные элементы управления точно также. Свойство Text задается также декларативно, как и при первом запросе. Никаких других манипуляций с ним не проводилось, поэтому оно не помечается как грязное, даже на postback'е. Поэтому размер полей ViewState после postback'ов остается одинаковым.

Теперь мы понимаем, как ASP.NET определяет, какие данные нужно сериализовать. Но мы не знаем, как она их сериализует. Эта тема выходит за рамки данный статьи (are you missing an assembly reference?), но если вам интересно, почитайте про LosFormatter в ASP.NET 1.x или ObjectStateFormatter в ASP.NET 2.0.

И последнее здесь — десериализация. Ясное дело, что все эти хитрые трекинги и сериализации никуда бы не годились, если бы мы не смогли получить данные обратно. Эта тема тоже выходит за рамки данной статьи, достаточно сказать что весь процесс прямо противоположен рассмотренному выше. ASP.NET строит дерево объектов, вычитывая поле _ViewState, отправленное на сервер, и десериализует его с помощью LosFormatter'а (v1.x) или ObjectStateFormatter'а (v2.0).

4. Автоматически восстанавливает данные


Это последняя из функций ViewState'а. Велик соблазн увязать ее с только что упомянутым процессом десериализации, но это не часть этого процесса. ASP.NET десериализует данные ViewState'а, а уже ПОТОМ заполняет элементы управления этими данными. Многие статьи путают эти процессы.

В классе System.Web.UI.Control (еще раз, классе, от которого наследуются все серверные и клиентские элементы управления, включая страницы) определен метод LoadViewState(), который принимает параметр типа object. Это метод, противоположный методу SaveViewState(), который мы уже обсуждали. Как и SaveViewState(), LoadViewState() просто вызывает такой же метод у объекта StateBag. А StateBag просто заполняет свою коллекцию ключей/значений данными из полученного объекта. Если вам интересно, тип полученного объекта — System.Web.UI.Pair, очень простой класс с двумя полями, First и Second. First — это ArrayList ключей, а Second — ArrayList значений. StateBag просто проходит по этим спискам, и каждый раз выполняет this.Add(key, value). Здесь важно уловить, что данные, передаваемые в LoadViewState(), — это только те записи, которые были помечены как грязные при предыдущем запросе. Еще до загрузки записей из ViewState'а, StateBag уже может что-то в себе содержать, например данные, явно заданные разработчиком до вызова LoadViewState(). Если какое-то значение, переданное в LoadViewState(), по какой-то причине уже оказалось в StateBag, оно будет перезаписано.

Все это — чистой воды магия автоматического управления состоянием. Когда страница начинает загружаться во время postback'а (еще до инициализации), всем свойствам присваиваются их естественные значения по умолчанию. Потом происходит OnInit. В этой фазе ASP.NET вызывает TrackViewState() у всех StateBag'ов. Потом вызывается LoadViewState с десериализованными данными после предыдущего запроса. StateBag выполняет Add(key, value) для всех этих данных. Ну а поскольку механизм трекинга уже запущен, каждое значение помечается как грязное, и оно опять сохраняется для следующего postback'а. Блестяще! Фух. Теперь вы — эксперт по части работы ViewState'а.

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

Случаи неправильного использования


  1. Навязывание значения по умолчанию
  2. Сохранение статических данных
  3. Сохранение cheap (здесь и далее «дешевых» — прим. пер.) данных
  4. Программная инициализация дочерних элементов управления
  5. Программная инициализация динамически создаваемых элементов управления

1. Навязывание значения по умолчанию


Одна из самых распространенных ошибок, и исправить ее тоже проще всего. Правильный код к тому же обычно компактнее чем неправильный. Да-да, иногда, делая все правильно, вы можете обойтись меньшим количеством кода. Представляете? Обычно эта ошибка делается тогда, когда разработчик элемента управления хочет, чтобы у определенного свойства было определенное значение по умолчанию, но при этом либо не понимает механизма трекинга, либо этот механизм ему до лампочки. Например давайте представим, что свойство Text должно иметь определенное значение, которое хранится в сессии. Разработчик Джо пишет следующий код:
public class JoesControl : WebControl
{
  public string Text
  {
    get { return this.ViewState["Text"] as string; }
    set { this.ViewState["Text"] = value; }
  }

  protected override void OnLoad(EventArgs args)
  {
    if(!this.IsPostback)
    {
      this.Text = Session["SomeSessionKey"] as string;
    }

    base.OnLoad(e);
  }
}

* This source code was highlighted with Source Code Highlighter.

Программист совершил ViewState-преступление, кто-нибудь, вызовите полицию ViewState'а! С этим подходом связаны две серьезные проблемы. Прежде всего, поскольку Джо разрабатывает элемент управления, и он потратил время на написание свойства Text, Джо наверняка хочет, чтобы у других разработчиков была возможность установить какое-то другое значение этому свойству. Джейн, пишущая страницу, как раз это сделать и пытается:
<abc:JoesControl id="joe1" runat="server" Text="ViewState rocks!" />

* This source code was highlighted with Source Code Highlighter.

У Джейн будет очень плохой день. Чтобы она в этот атрибут Text не вписала, элемент управления Джо ее не послушается. Бедняжка Джейн. Она использует этот элемент управления так, как и любой другой элемент управления в ASP.NET, но этот-то работает иначе. Элемент управления Джо перезапишет значение Text, которое задала Джейн! Более того, поскольку Джо написал свой код в OnLoad, во ViewState'е это значение будет помечено как грязное. А значит Джейн навлекла на себя еще и увеличение сериализованного поля ViewState, просто-напросто положив элемент управления Джо на свою страницу! Похоже что Джо не очень-то хорошо относится к Джейн. Может быть он просто мстит ей за что-нибудь. Ну а поскольку все мы знаем, какой пол правит этим миром (буквально, «we all know which sex rules this world» — прим. пер.), можно предположить, что Джейн заставила-таки Джо исправить свой код. К радости Джейн, Джо в итоге написал вот это:
public class JoesControl : WebControl
{
  public string Text
  {
    get
    {
      return this.ViewState["Text"] == null ?
        Session["SomeSessionKey"] :
        this.ViewState["Text"] as string;
    }
    set { this.ViewState["Text"] = value; }
  }
}

* This source code was highlighted with Source Code Highlighter.

Посмотрите насколько уменьшился объем кода. Джо даже не приходится переопределять OnLoad. Поскольку StateBag возвращает null, если не содержит переданного ему ключа, Джо может проверить, не было ли уже присвоено значение его свойству, просто проверяя его на равенство null. Если равенство выполняется, он может спокойно вернуть свое ненаглядное значение по умолчанию. Если же равенство не выполняется, он с радостью возвращает присвоенное значение. Проще некуда. И теперь, когда Джейн будет использовать этот элемент управления, она не только получит свое значение атрибута Text, но и размер ViewState'а на ее странице не будет увеличиваться без причины. Работает — лучше. Производительность — лучше. Кода — меньше. Сплошная выгода!

2. Сохранение статических данных


Здесь под статическими я понимаю такие данные, которые никогда не меняются, или же не меняются в рамках жизненного цикла страницы, или даже пользовательской сессии. Предположим, что Джо, наш воображаемый горе-программист, получил задание — отобразить имя текущего пользователя на верху каждой страницы какого-то eCommerce приложения. Это неплохой способ сказать пользователю «Эгей, мы тебя знаем!» Это дает пользователям чувство индивидуальности и показывает, что сайт работает как надо. Предположим, что в этом eCommerce приложении есть API уровня бизнес-логики, которое позволяет Джо с легкостью получить имя текущего аутентифицированного пользователя: CurrentUser.Name. Вот как Джо выполняет это задание:
(ShoppingCart.aspx)
<asp:Label id="lblUserName" runat="server" />


(ShoppingCart.aspx.cs)
protected override void OnLoad(EventArgs args)
{
  this.lblUserName.Text = CurrentUser.Name;
  base.OnLoad(e);
}

* This source code was highlighted with Source Code Highlighter.

Конечно же имя текущего пользователя будет показано. «Пара пустяков,» — думает Джо. Но мы то знаем, какой проступок совершил парень. Использованный им элемент управления Label уже отслеживает состояние своего ViewState'а на момент присвоения его свойству Text имени пользователя. А значит имя пользователя будет не только отображено на странице, но и попадет в скрытое поле ViewState. Зачем же заставлять ASP.NET проделывать все эти сериализации и десериализации, если вы потом все равно это поле перезапишете? Да это просто неприлично! И даже если ему указать на проблему, Джо просто пожмет плечами: «Это всего лишь парочка байт.» Но ведь даже на это парочке байт можно легко сэкономить. Решение первое… вы просто отключаете ViewState для этого Label'а.
<asp:Label id="lblUserName" runat="server" EnableViewState="false" />

* This source code was highlighted with Source Code Highlighter.

Проблема решена. Но есть решение и получше. Label — один из наиболее часто используемых элементов управления, популярнее его наверное только Panel. Ноги этого явления растут из опыта программистов на Visual Basic. Чтобы показать текст на форме VB, вам нужен Label. Естественно полагать, что в ASP.NET WebForms Label выполняет аналогичную роль, и если вам нужно отобразить какой-нибудь текст, нужно использовать Label. Так вот, это не так. Label оборачивает свое содержимое в тэг . Спросите себя, действительно ли вам нужен этот тэг? Если только вы не применяете к этому тексту какие-либо стили, ответ скорее всего таков — НЕТ! Вам вполне хватит и этого:
<%= CurrentUser.Name %>

* This source code was highlighted with Source Code Highlighter.

Вы не только убрали со страницы лишний код (пусть даже и сгенерированный дизайнером), но и поступили в духе code-behind модели — отделили код от дизайна! Если в компании Джо есть специальный дизайнер, отвечающий за внешний вид этого eCommerce приложения, Джо просто передаст ему это задание со словами «Это работа для дизайнера», и будет прав. Есть еще одна причина, по которой вы можете ПОДУМАТЬ, что вам нужен Label, а именно если вам нужно что-то сделать с ним в code-behind'е. Но и тут задайте себе вопрос — вам нужен именно Label? Позвольте представить самый непопулярный элемент управления в ASP.NET: Literal!
<asp:Literal id="litUserName" runat="server" EnableViewState="false"/>

* This source code was highlighted with Source Code Highlighter.

И никаких span тэгов.

3. Сохранение «дешевых» данных


Этот пункт включает в себя предыдущий. Статическую информацию очень легко получить. Но не вся легкодоступная информация статична. Иногда у нас есть такие данные, которые могут изменяться в процессе работы приложения, но до них все равно дешево достучаться. Под «дешево» я понимаю незначительные затраты на поиск. Очень распространенная разновидность этой ошибки — заполнение выпадающего списка американских штатов. Если только вы не пишете приложение, которое собираетесь отправить в седьмое декабря 1787 года (вот сюда), список штатов не изменится в обозримом будущем. Однако, поскольку все программисты просто ненавидят hardcode, вы наверняка не хотите вбивать все эти штаты в свою страницу. Но в случае, если вдруг какой-нибудь штат подымет восстание (мечты, мечты), вы не хотите менять свой код. Наш старый знакомый Джо решил, что он будет заполнять этот выпадающий список из таблицы базы данных USSTATES. Сайт и так уже использует базу данных, так что все тривиально — добавить таблицу и написать к ней запрос:
<asp:DropdownList id="lstStates" runat="server" DataTextField="StateName" DataValueField="StateCode" />

* This source code was highlighted with Source Code Highlighter.

protected override void OnLoad(EventArgs args)
{
  if(!this.IsPostback)
  {
    this.lstStates.DataSource = QueryDatabase();
    this.lstStates.DataBind();
  }
  base.OnLoad(e);
}

* This source code was highlighted with Source Code Highlighter.

Как и любой элемент управления в ASP.NET, который работает с данными, Dropdown будет использовать ViewState для того, чтобы запомнить список своих записей. На данный момент существует ровно 50 штатов. Мало того, что для каждого штата в Dropdown'е хранится свой объект ListItem, так еще и каждый штат и его код будет сериализован и записан во ViewState. Целая куча данных будет постоянно забивать канал каждый раз, когда загружается страница, особенно если речь идет о dial-up соединении. Я часто думаю, как я буду объяснять своей бабушке что ее интернет работает так медленно, потому что ее компьютер перечисляет серверу все 50 штатов. Не думаю, что она поймет. Наверное она пустится в объяснения, что во времена ее молодости было только 46 штатов. Похоже что эти 4 лишних штата грузят полосу пропускания. Черт бы побрал эти припозднившееся штаты!

Как и в случае со статическими данными, эта проблему можно решить просто отключением ViewState'а элемента управления. К сожалению, это не всегда срабатывает. Это конечно зависит от того элемента управления, с которым вы работаете, и тех его функций, которые вы используете. В нашем примере, если Джо просто добавит EnableViewState=«false», и уберет условие if (!this.IsPostback), он с успехом избавится от лишних данных во ViewState, но столкнется с другим осложнением. Dropdown больше не будет запоминать выбранное значение после postback'а. СТОП! Это еще один миф о ViewState. Причина того, что Dropdown больше не может запомнить свое выбранное значение, кроется не в том, что вы отключили для него ViewState. Такие элементы управления, как Dropwdon или Textbox, могут запомнить свое текущее состояние даже при отключенном ViewState. У нас Dropdown забывает выбранное значение, потому что вы его каждый раз заполняете заново на OnLoad, уже после того как он восстановил свое текущее состояние. Первое, что он делает, когда получает новые данные, — отправляет старые на цифровую помойку. Это значит, что когда пользователь выберет Калифорнию, Dropdown будет упрямо возвращать ему значение по умолчанию (первый номер списка, если вы ничего не меняли). К счастью, у этой проблемы есть простое решение — перенесите загрузку данных в OnInit:
<asp:DropdownList id="lstStates" runat="server" DataTextField="StateName" DataValueField="StateCode" EnableViewState="false" />

* This source code was highlighted with Source Code Highlighter.

protected override void OnInit(EventArgs args)
{
  this.lstStates.DataSource = QueryDatabase();
  this.lstStates.DataBind();
  base.OnInit(e);
}

* This source code was highlighted with Source Code Highlighter.

Короткое объяснение того, почему это работает: вы записываете данные в список до того, как Dropdown пытается восстановить свое состояние. Теперь этот список будет вести себя точно так, как Джо и задумывал, но огромный список штатов не будет записываться во ViewState! Отлично! Важно отметить, что это правило применимо для любых данных, которые легко получить. Вы можете возразить, что бегать в базу данных на каждом запросе дороже, чем хранение данных во ViewState. Но я считаю, что в этом случае вы будете неправы. Современные базы данных (скажем, SQL Server) используют сложные механизмы кеширования, и могут быть очень эффективны, если их правильно сконфигурировать. Список штатов так и так должен обновляться при любом запросе, ничего не попишешь. Все, что мы изменили, это вместо того, чтобы отправлять его лишний раз (на каждый запрос) по медленному, ненадежному 56k-соединению за тысячи миль, мы отправляем его по в худшем случае 10-мегабитному LAN соединению от вашего БД-сервера к вашему интернет-серверу. И если уж вы действительно хотите заняться оптимизацией, можете закешировать результаты запроса к базе данных. Считайте, разбирайтесь!

4. Программная инициализация дочерних элементов управления


Суровая реальность такова — все декларативно не сделаешь. Иногда в дело вступает хитрая логика. Собственно, именно поэтому у нас у всех есть работа, так ведь? Проблема в том, что ASP.NET не предоставляет простого способа правильной программной инициализации дочерних элементов управления. Вы можете переопределить OnLoad и сделать это там — но тогда вы сохраняете данные, которые возможно во ViewState храниться не должны. Вы можете переопределить OnInit с той же целью, но получите ту же проблему. Помните, что ASP.NET вызывает TrackViewState() в фазе OnInit. Оно делает это рекурсивно для всего дерева элементов управления, но СНИЗУ ВВЕРХ! Другими словами, твой OnInit происходит уже ПОСЛЕ OnInit твоих потомков. Значит в момент, когда начинается твой OnInit, дочерние элементы управления уже запустили трекинг своих ViewState'ов! Пусть Джо хочет отобразить текущие дату и время в Label'е, объявленном на форме:
<asp:Label id="lblDate" runat="server" />

* This source code was highlighted with Source Code Highlighter.

protected override void OnInit(EventArgs args)
{
  this.lblDate.Text = DateTime.Now.ToString("MM/dd/yyyy HH:mm:ss");
  base.OnInit(e);
}

* This source code was highlighted with Source Code Highlighter.

И хотя Джо использует самое раннее событие, которое ему доступно, уже слишком поздно. ViewState Label'а уже отслеживает свои изменения, и текущие дата и время неизбежно будут записаны во ViewState. Этот пример можно отнести к описанному выше случаю «дешевых» данных. Джо может просто отключить ViewState у этого Label'а. Но здесь мы решаем другую проблему, чтобы проиллюстрировать важный принцип. Было бы замечательно, если бы Джо смог декларативно задать такой текст, какой ему нужен. Что-то вроде:
<asp:Label id="Label1" runat="server" Text="<%= DateTime.Now.ToString() %>" />

* This source code was highlighted with Source Code Highlighter.

Вы возможно уже это попробовали. Но ASP.NET бросит вам прямо в лицо тот факт, что синтаксис "<%= %>" не может быть использован для задания свойств серверных элементов управления. Джо мог бы использовать синтаксис "<%# %>", но этот подход не будет ничем отличаться от метода привязки к данным, который мы только что обсудили (отключение ViewState'а и заполнение данными на каждом запросе). Было бы здорово, если бы мы могли задавать значение свойства в коде, но при этом разрешить элементу управления продолжать работать в его обычном режиме. Наверное какой-то код будет работать с этим Label'ом, и мы бы хотели, чтобы все внесенные им изменения сохранялись во ViewState, как обычно. Например, возможно Джо хочет дать пользователям возможность отключить показ текущей даты на странице, и показывать вместо этого пустой шаблон:
private void cmdRemoveDate_Click(object sender, EventArgs args)
{
  this.lblDate.Text = "--/--/---- --:--:--";
}

* This source code was highlighted with Source Code Highlighter.

Если пользователь нажмет эту кнопку, текущие дата и время исчезнут. Но если мы уже решили нашу проблему отключением ViewState'а, дата и время магическим образом возникнут опять при следующем postback'е, потому что отключение ViewState'а означает, что новое состояние Label'а не будет сохранено. Нехорошо. И что же теперь делать?

Что нам действительно нужно, так это декларативно задавать значение, которое не статично, а получено в результате неких операций. Если оно будет задано декларативно, Label будет работать как прежде — начальное состояние не будет сохранено, поскольку оно задано до включения трекинга, и любые изменения будут записаны во ViewState. Как я уже сказал, ASP.NET не предоставляет простого способа выполнить эту задачу. К услугам разработчиков на ASP.NET 2.0 синтаксис типа $, который позволяет использовать конструкторы выражений (expression builder) для декларативного задания значений, которые приходят из динамических источников данных (например ресурсы, connection strings). Но не существует конструктора выражений типа «просто выполни этот код», так что это вам тоже не поможет (если только вы не используете мой CodeExpressionBuilder!). А еще у ASP.NET 2.0 разработчиков есть OnPreInit. Это отличное место для инициализации дочерних элементов управления, потому что это событие происходит до OnInit и поэтому ни один ViewState еще не отслеживает свои изменения, но все элементы управления уже созданы. Одна проблема — OnPreInit, в отличие от других событий, не рекурсивно. А значит оно доступно только на самой странице. Так что это вам тоже не поможет, если вы пишете свой элемент управления. Плохо, что OnPreInit не рекурсивно, как OnInit, OnLoad и OnPreRender, я ни вижу никаких причин для такой непоследовательности. Суть проблемы в том, что мы просто хотим задать значение свойства Label'а до того, как его ViewState начнет отслеживать свое состояние. Мы уже знаем, что OnInit страницы — это слишком поздно. А может мы можем как-нибудь вклиниться в OnInit самого Label? В коде мы повесить обработчик события не можем, потому что самое ранее где это возможно — OnInit, а это поздно. И в конструкторе этого сделать не получится, потому что еще не созданы дочерние элементы. Есть два выхода:

1. Декларативно подписаться на событие Init
<asp:Label id="Label2" runat="server" OnInit="lblDate_Init" />

* This source code was highlighted with Source Code Highlighter.

Это сработает, поскольку атрибут OnInit обрабатывается раньше, чем срабатывает событие Init у Label'а, что дает нам возможность провести все манипуляции до того, как будет включен трекинг. В нашем примере в обработчике будет просто задаваться свойство Text.

2. Написать свой элемент управления
public class DateTimeLabel : Label
{
  public DateTimeLabel()
  {
    this.Text = DateTime.Now.ToString("MM/dd/yyyy HH:mm:ss");
  }
}

* This source code was highlighted with Source Code Highlighter.

И потом использовать его вместо стандартного Label'а. Поскольку элемент управления сам инициализирует свое состояние, он может сделать это до включения трекинга.

5. Программная инициализация динамически создаваемых элементов управления


Здесь та же проблема, что и в предыдущем пункте, но поскольку вы лучше контролируете ситуацию, решить ее гораздо проще. Предположим, что Джо написал свой элемент управления, который в какой-то момент динамически создает Label:
public class JoesCustomControl : Control
{
  protected override void CreateChildControls()
  {
    Label l = new Label();

    this.Controls.Add(l);
    l.Text = "Joe's label!";
  }
}

* This source code was highlighted with Source Code Highlighter.

Хммм. А когда динамически созданные элементы управления запускают трекинг? Вы можете создавать и добавлять на страницу динамически элементы управления практически в любой момент, но ASP.NET включает трекинг ViewState'а на OnInit. Не пропустят ли наши элементы управления это событие? Нет, не пропустят. Дело в том, что Controls.Add() — это не просто добавление элемента в коллекцию. Это нечто большее. Как только динамически созданный элемент управления добавляется в дерево элементов, корнем которого является страница, ASP.NET запускает все пропущенные события для этого элемента и его потомков. Скажем вы добавили элементу управления на событии PreRender (хотя существует множество причин, по которым этого лучше не делать). В этот момент уже произошли события OnInit, LoadViewState, LoadPostBackData и OnLoad. Сразу же после того, как элемент управления попадет в коллекцию элементов управления страницы, для него срабатывают все эти события.

Это означает, друзья мои, что трекинг для элемента управления будет включен сразу же после того, как вы добавите его на страницу. Помимо конструктора, вы можете изменять свойства элементов управления на OnInit, а здесь дочерние элементы уже отслеживают изменения ViewState'а. Джо добавляет все в методе CreateChildControls(), который ASP.NET вызывает тогда, когда ей необходимо убедиться, что дочерние элементы управления существуют (момент вызова этого метода варьируется в зависимости от того, реализуете ли вы INamingContainer, происходит ли сейчас обработка postback'а, не вызвал ли кто-нибудь EnsureChildControl()). Он может быть вызван даже на OnPreRender. Но когда бы этот метод не был вызван, это случится уже после или во время OnInit, и Джо опять испачкает ViewState. Решение здесь элементарно, но его легко не заметить:
public class JoesCustomControl : Control
{
  protected override void CreateChildControls()
  {
    Label l = new Label();
    l.Text = "Joe's label!";

    this.Controls.Add(l);
  }
}

* This source code was highlighted with Source Code Highlighter.

Все просто — вместо того, что инициализировать свойство Text после добавления элемента управления в коллекцию, он делает это до. Это дает нам уверенность в том, что Label еще не включил трекинг ViewState'а на момент инициализации. Вообще-то вы можете использовать этот прием для чего-нибудь посложнее, нежели просто присваивание свойств. Вы можете наполнять элемент управления данными до того, как они становятся частью дерева элементов управления. Помните наш пример про список штатов? Если мы можем создавать Dropdown динамически, мы сможем решить проблему и без отключения ViewState'а:
public class JoesCustomControl : Control
{
  protected override void OnInit(EventArgs args)
  {
    DropDownList states = new DropDownList();
    states.DataSource = this.GetUSStatesFromDatabase();
    states.DataBind();

    this.Controls.Add(states);
  }
}

* This source code was highlighted with Source Code Highlighter.

Это работает просто отлично. Dropdown будет вести себя так, как будто штаты — просто встроенные элементы списка. Они не пишутся во ViewState, хотя ViewState и включен для этого элемента управления, а это значит что вы можете пользоваться всеми его функциями, зависящими от ViewState'а, например событием OnSelectedIndexChanged. Вы можете поступать так даже с Grid'ами, хотя тут все зависит от того, как вы их используете (у вас могут возникнуть проблемы с сортировкой, разбиением на страницы, и т.д.)

Будьте аккуратны при работе с ViewState'ом


Теперь, когда вы знаете все о магии ViewState'а и о том, как он взаимодействует с жизненным циклом страницы в ASP.NET, вам будет очень просто аккуратно использовать ViewState! Оптимизация ViewState'а — это очень просто, когда вы понимаете что происходит, а зачастую вам пригодятся эти знания и для того, чтобы сократить количество написанного вами кода.
Tags:
Hubs:
+68
Comments 37
Comments Comments 37

Articles