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

53

В ASP.NET веб-элементы управления используются для создания объектно-ориентированного уровня абстракции над низкоуровневыми деталями HTML и HTTP. Двумя краеугольными камнями этой абстракции являются состояние представления (механизм, позволяющий сохранять информацию между запросами) и обратная отправка (прием отправки страницей информации по тому же URL-адресу с коллекцией данных формы). Чтобы создавать реалистичные серверные элементы управления, необходимо знать, как создавать классы, которые подключаются к обеим этим частям инфраструктуры веб-страниц.

Состояние представления в элементах управления

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

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

Например, рассмотрим представленный ранее элемент LinkControl. В настоящий момент элемент управления не использует состояния представления, а это значит, что если изменить его свойства Text и HyperLink в коде, эти изменения будут утеряны в последующих обратных отправках. (Это не касается свойств стиля, таких как Font, ForeColor и BackColor, которые сохраняются в состоянии представления автоматически.) Чтобы изменить LinkControl для сохранения состояния представления в свойствах Text и HyperLink, понадобится удалить поля текста и гиперссылки из класса LinkControl и переписать свойства Text и HyperLink, как показано ниже:

public string Text
{
        get
        {
            return (string)ViewState["Text"];
        }

        set
        {
            ViewState["Text"] = value;
        }
}

public string HyperLink
{
        get
        {
            return (string)ViewState["HyperLink"];
        }
        set
        {
            if (value.IndexOf("http://") == -1)
            {
                throw new ApplicationException("Некорректный URL сайта");
            }
            else
            {
                ViewState["HyperLink"] = value;
            }
        }
}

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

protected override void OnInit(EventArgs e)
{
        base.OnInit(e);
        Page.RegisterRequiresViewStateEncryption();
}

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

Хотя состоянием представления легко пользоваться в элементе управления, необходимо принимать во внимание ряд моментов. Первый из них - не следует сохранять большие объекты, поскольку они увеличат время передачи страницы. Например, элементы управления ASP.NET, поддерживающие привязку данных, не сохраняют свойство DataSource в состоянии представления. Они просто хранят его в памяти до тех пор, пока не будет вызван метод DataBind(). Это несколько усложняет программирование, например, заставляет заново привязывать элементы управления данными после каждой обратной отправки, однако предотвращает чрезмерное разрастание страниц.

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

Даже если свойство EnableViewState установлено в false, коллекция ViewState все равно будет доступна в коде. Единственное отличие в том, что информация, помещаемая в эту коллекцию, будет отброшена, как только элемент управления завершит обработку, а страница визуализируется.

Наконец, имейте в виду, что нельзя делать предположения относительно того, какие данные хранятся в коллекции ViewState. Если вы попытаетесь извлечь несуществующий элемент данных, то получите исключение NullReferenceException. Чтобы предотвратить эту проблему, необходимо проверять значения на равенство null либо устанавливать информацию состояния представления по умолчанию в методе OnInit() или в специальном конструкторе. Например, LinkControl не получит null-ссылок, поскольку в его методе OnInit() устанавливаются начальные значения состояния представления.

Хотя класс WebControl предоставляет свойство ViewState, в нем не предусмотрено таких свойств, как Cache, Session и Application. Тем не менее, если эти объекты нужны для сохранения и извлечения данных, к ним можно добраться через статическое свойство HttpContext.Current.

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

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

Состояние элемента управления

Платформа ASP.NET поддерживает средство под названием состояние элемента управления, предназначенное для хранения данных, которые элемент использует в текущий момент. Формально состояние элемента управления работает точно так же, как состояние представления - хранит сериализируемую информацию, которая вставляется в скрытое поле при визуализации страницы. В действительности ASP.NET помещает информацию состояния представления и информацию состояния элемента управления в одно и то же скрытое поле. Отличие в том, что на состояние элемента управления свойство EnableViewState не влияет. Даже если это свойство установлено в false, элемент управления все равно сохраняет и извлекает информацию из состояния элемента управления.

LinkControl не требует состояния элемента управления. Если разработчик указывает для свойства EnableViewState значение true, то, скорее всего, потому, что собирается устанавливать свойства HyperLink и Text при каждой обратной отправке.

Поскольку состояние элемента управления не может быть отключено, необходимо тщательно ограничить объем хранимой информации. Обычно она должна быть ограничена только самыми необходимыми сведениями - такими как индекс текущей страницы или значение ключа данных. Чтобы использовать состояние элемента управления, следует начать с переопределения метода OnInit() и вызвать в нем Page.RegisterRequiresControlState() для уведомления о том, что элемент управления нуждается в доступе к своему состоянию:

protected override void OnInit(EventArgs e)
{
        base.OnInit(e);
        Page.RegisterRequiresControlState(this);
}

В отличие от состояния представления, обращаться напрямую к состоянию элемента управления через коллекцию нельзя. (Это ограничение, скорее всего, объясняется необходимостью снизить риск злоупотребления разработчиками состоянием элемента управления в тех случаях, когда лучше подходит состояние представления.) Вместо этого должны быть переопределены два метода: SaveControlState() и LoadControlState().

Эти методы используют слегка необычный шаблон. Базовая идея в том, чтобы взять любое состояние элемента управления, которое было сериализировано базовым классом, и скомбинировать его с объектом, содержащим новый сериализированный объект. Достигается это с помощью класса System.Web.Pair, как показано ниже:

string someData;
protected override object SaveControlState()
{
        // Получить состояние из базового класса
        object baseState = base.SaveControlState();

        // Скомбинировать его с объектом состояния, который требуется сохранить, 
        // затем вернуть окончательный объект
        return new Pair(baseState, someData);
}

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

string someData;
int intData;
protected override object SaveControlState()
{

        // Получить состояние из базового класса
        object baseState = base.SaveControlState();

        // Скомбинировать его с объектом состояния, который требуется сохранить, 
        // затем вернуть окончательный объект
        Pair pairl = new Pair(someData, intData);
        Pair pair2 = new Pair(baseState, pairl);
        return pair2;
}

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

protected override void LoadControlState(object state)
{
        Pair p = state as Pair;
        if (p != null)
        {
            // Предоставить базовому классу его состояние (из p.First)
            base.LoadControlState(p.First);

            // Теперь можно обработать сохраненное состояние (из p.Second)
            Pair pair1 = p.Second as Pair;
            someData = (string)pair1.First;
            intData = (int)pair1.Second;
        }
}

Обратная отправка данных и события изменений

Состояние представления и состояние элемента управления помогают отслеживать содержимое элемента управления, но для элементов управления вводом их недостаточно. Причина в том, что такие элементы обладают дополнительной возможностью - позволяют пользователям изменять их данные. Например, рассмотрим текстовое поле, представленное в форме дескриптором <input>. Когда выполняется обратная отправка страницы, данные из дескриптора <input> становятся частью информации в коллекции элементов управления. Элемент управления TextBox нуждается в извлечении этой информации и соответствующем обновлении своего состояния.

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

LoadPostData()

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

RaisePostDataChangeEvent()

После того, как все элементы управления вводом на странице инициализированы, ASP.NET предоставляет шанс инициировать событие изменения, если необходимо, вызывая метод RaisePostDataChangeEvent().

Наилучший способ понять работу этого метода - рассмотреть базовый пример. Следующий элемент управления эмулирует базовый элемент управления TextBox. Ниже показано определение элемента управления:

public class CustomTextBox : WebControl, IPostBackDataHandler
{
	// ...
}

Как видите, элемент управления унаследован от WebControl и реализует интерфейс IPostBackDataHandler. Элемент требует только одного свойства - Text. Свойство Text сохраняется в состоянии представления и инициализируется пустой строкой в конструкторе элемента управления. Конструктор также устанавливает базовый дескриптор в <input>:

public class CustomTextBox : WebControl, IPostBackDataHandler
{
    public CustomTextBox() : base(HtmlTextWriterTag.Input)
    {
        Text = "";
    }

    public string Text
    {
        get { return (string)ViewState["Text"]; }
        set { ViewState["Text"] = value; }
    }
    
    // ...
}

Поскольку базовый дескриптор уже установлен в <input>, требуется совсем немного дополнительной работы по визуализации. Можно обработать все, переопределив метод AddAttributesToRender() и добавив атрибут type, который указывает, что элемент управления <input> представляет текстовое поле, и атрибут value, содержащий текст, который необходимо отобразить в текстовом поле:

public class CustomTextBox : WebControl, IPostBackDataHandler
{
    // ...

    protected override void AddAttributesToRender(HtmlTextWriter output)
    {
        output.AddAttribute("name", this.UniqueID);
        output.AddAttribute(HtmlTextWriterAttribute.Type, "text");
        output.AddAttribute(HtmlTextWriterAttribute.Value, Text);
        base.AddAttributesToRender(output);
    }
}

С использованием атрибута name также должен быть добавлен идентификатор UniqueID элемента управления. Причина в том, что ASP.NET сопоставляет эту строку с отправленными данными. Если не добавить UniqueID, то метод LoadPostData() никогда не будет вызван, поэтому извлечь отправленные данные не удастся. В качестве альтернативы можно вызвать метод Page.RegisterRequiresPostback() внутри метода OnInit() специального элемента управления. В этом случае ASP.NET добавит уникальный идентификатор, если он не был явно визуализирован, гарантируя получение обратной отправки.

Останется только реализовать методы IPostBackDataHandler для обеспечения элементу управления возможности реагировать на изменения пользователя.

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

string postedValue = postData[postDataKey];

Метод LoadPostData() также должен сообщить ASP.NET о том, требуется ли событие изменения. Инициировать событие в этой точке нельзя, т.к. другие элементы управления могут не быть правильно обновлены отправленными данными. Однако можно сообщить ASP.NET, что изменения произошли, возвратив true. Если вернуть true, среда ASP.NET вызовет метод RaisePostDataChangeEvent() после инициализации всех элементов управления. В случае возврата false этот метод не вызывается.

Ниже приведен полный код метода LoadPostData() в CustomTextBox:

public bool LoadPostData(string postDataKey, NameValueCollection postData)
{
        // Получить отправленное значение и самое последнее 
        // значение состояния представления
        string postedValue = postData[postDataKey];
        string val = Text;

        // Если значение изменилось, сбросить значение свойства Text и вернуть true, 
        // чтобы было инициировано событие RaisePostDataChangedEvent
        if (val != postedValue)
        {
            Text = postedValue;
            return true;
        }
        else
        {
            return false;
        }
}

Метод RaisePostDataChangeEvent() решает относительно простую задачу по генерации события. Однако большинство элементов управления ASP.NET используют дополнительный уровень, в то время как RaisePostDataChangeEvent() вызывает метод On...(), а тот - действительно инициирует событие. Дополнительный уровень предоставляет другим разработчикам возможность наследовать новый элемент управления от вашего элемента, а также изменить его поведение, переопределяя метод On...().

Ниже приведен остальной код:

public void RaisePostDataChangedEvent()
{
    // Вызвать метод для генерации события изменения
    OnTextChanged(new EventArgs());
}

public event EventHandler TextChanged;
protected virtual void OnTextChanged(EventArgs e)
{
    // Проверить наличие хотя бы одного слушателя и затем инициировать событие
    if (TextChanged != null)
        TextChanged(this, e);
}

Ниже показан пример страницы, которая проверяет элемент управления CustomTextBox и реагирует на его событие:

<form id="form1" runat="server">
      <div>
            <professorweb:CustomTextBox ID="CustomTextBox1" OnTextChanged="CustomTextBox1_TextChanged" runat="server" />
            <asp:Button ID="Submit" runat="server" Text="Отправить" />
      </div>
</form>
protected void CustomTextBox1_TextChanged(object sender, EventArgs e)
{
        Response.Write("Текст изменился");
}
Извлечение отправленных данных в специальном элементе управления
Пройди тесты