Элементы управления ASP.NET AJAX

194

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

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

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

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

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

UpdatePanel

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

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

  1. Пользователь щелкает на кнопке внутри UpdatePanel.

  2. Определенный клиентский код JavaScript (который был сгенерирован ASP.NET AJAX) перехватывает клиентское событие щелчка и выполняет асинхронный обратный вызов к серверу.

  3. На сервере запускается нормальный жизненный цикл страницы, со всеми обычными событиями. И, наконец, страница визуализируется в виде HTML-кода и возвращается браузеру.

  4. Клиентский код JavaScript принимает полный HTML-код и обновляет каждый элемент управления UpdatePanel на странице, заменяя его текущий HTML-код новым содержимым. (Если изменение было внесено в содержимое, которое расположено вне элемента управления UpdatePanel, оно игнорируется.)

Элемент управления UpdatePanel в AJAX ASP.NET служит той же цели, что и специальный элемент управления DynamicPanel, который был разработан в статье «Использование Ajax с клиентскими обратными вызовами» с применением средства клиентского обратного вызова ASP.NET. Оба элемента управления используют асинхронный вызов для загрузки нового содержимого и обновляют часть страницы, не прибегая к полностраничной обратной отправке. Однако созданный ранее элемент управления DynamicPanel более ограничен, поскольку он должен применяться совместно с DynamicPanelRefreshLink, чтобы инициировать асинхронное обновление.

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

Элемент управления UpdatePanel работает совместно с элементом управления ScriptManager. При использовании UpdatePanel следует убедиться, что свойство ScriptManager.EnablePartialRendering установлено в true (значение по умолчанию). Затем, вслед за ScriptManager, к странице можно добавить одни или более элементов управления UpdatePanel.

По мере перетаскивания элементов управления в UpdatePanel содержимое появляется в разделе <ContentTemplate>. Вот пример элемента управления UpdatePanel, который содержит метку и кнопку:

<asp:UpdatePanel ID="UpdatePanel1" runat="server">
    <ContentTemplate>
        <asp:Label ID="Label1" runat="server" />
        <br />
        <asp:Button ID="Button1" runat="server" Text="Обновить" />
    </ContentTemplate>
</asp:UpdatePanel>

UpdatePanel является элементом управления, основанным на шаблоне. При своей визуализации он копирует содержимое из своего шаблона ContentTemplate в страницу. Это значит, что динамическое добавление элементов управления в UpdatePanel через коллекцию UpdatePanels.Controls невозможно. Однако элементы управления можно вставлять динамически, используя коллекцию UpdatePanels.ContentTemplateContainer.Controls.

Класс UpdatePanel унаследован не от Panel, а непосредственно от Control. Причина в том, что UpdatePanel играет только одну роль - он служит контейнером для содержимого, которое требуется обновлять асинхронно. В отличие от стандартного элемента управления Panel из ASP.NET, UpdatePanel не имеет никакого визуального представления и не поддерживает настройки стиля. Если требуется отобразить границу вокруг элемента UpdatePanel или изменить цвет фона, в него придется поместить обычный элемент управления Panel (или хотя бы статический дескриптор <div>).

На странице элемент управления UpdatePanel визуализируется как дескриптор <div>. Тем не менее, изменив значение свойства RenderMode элемента управления UpdatePanel с Block на Inline, его можно сконфигурировать так, чтобы он визуализировался в виде встроенного элемента. Например, этот шаг можно было бы предпринять, когда требуется создать UpdatePanel, который охватывает текст внутри абзаца или иной блочный элемент.

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

protected void Page_Load(object sender, EventArgs e)
{
    Label1.Text = Label2.Text = Label3.Text = DateTime.Now.ToLongTimeString();
}
Использование элемента управления UpdatePanel для предотвращения полностраничных обратных отправок

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

Обработка ошибок

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

Вместе с тем, проблемы могут возникать при выполнении асинхронной обратной отправки, точно так же, как при синхронной обратной отправке. Это поведение можно протестировать, добавив к обработчику серверного события Page.Load следующий код:

protected void Page_Load(object sender, EventArgs e)
{
        Label1.Text = Label2.Text = Label3.Text = DateTime.Now.ToLongTimeString();
        if (IsPostBack) 
            throw new ApplicationException("Какая-то ошибка");
}

Когда веб-страница генерирует необработанное исключение, ошибка перехватывается элементом управления ScriptManager и передается обратно клиенту. Затем клиентские библиотеки ASP.NET AJAX генерируют в странице ошибку JavaScript.

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

Если отладка сценариев не используется, браузер может уведомить или не уведомить о возникновении проблемы. Обычно большинство браузеров конфигурируются так, чтобы молча подавлять ошибки JavaScript. В Google Chrome сообщение сообщение об ошибке можно увидеть в консоли JavaScript, как показано на рисунке ниже:

Отображение клиентского сообщения о серверной ошибке

Это поведение можно изменить, обрабатывая ошибку клиентским кодом JavaScript. Для этого нужно зарегистрировать обратный вызов для события endRequest класса System.Web.PageRequestManager. Элемент управления PageRequestManager - центральная часть модели приложения в ASP.NET AJAX. Он отвечает за процесс обновления для элементов управления UpdatePanel и инициирует клиентские события в ходе прохождения страницей этапов ее жизненного цикла.

Ниже приведен блок клиентского сценария, который выполняет именно это. Сначала он определяет функцию, которая автоматически инициируется при первоначальной загрузке страницы. В данном случае нет никакой необходимости использовать событие onload, поскольку ASP.NET AJAX автоматически вызывает функцию pageLoad(), если она существует. Аналогично, ASP.NET AJAX вызывает функцию pageUnload(), когда страница выгружается. Все другие события должны приниматься вручную - именно это и делает функция pageLoad(). Она принимает ссылку на текущий экземпляр PageRequestManager и присоединяет вторую функцию к событию endRequest:

function pageLoad() {
    var pageManager = Sys.WebForms.PageRequestManager.getInstance();
    pageManager.add_endRequest(endRequest);
}

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

function endRequest(sender, args) {
    // Обработка ошибки
    if (args.get_error() != null) {
        $get("lblError").innerHTML = args.get_error().message;

        // Подавить вывод ошибки в консоль
        args.set_errorHandled(true);
    }
}

Новый результат этого кода обработки ошибки показан на рисунке ниже:

Отображение информации об ошибке в странице

В ASP.NET есть два элемента управления, которые не могут использоваться в UpdatePanel: FileUpload и HtmlInputFile. Однако они могут применяться на странице, которая содержит элемент UpdatePanel, пока они действительно не располагаются в UpdatePanel.

Условные обновления

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

Для этого достаточно изменить значение свойства UpdateMode с Always на Conditional. Теперь этот элемент управления UpdatePanel будет обновляться при щелчке на кнопке внутри него, но не при щелчке на кнопке в другом элементе UpdatePanel. Если в примере, показанном в рисунке выше сделать все элементы управления UpdatePanel условными, они будут работать независимо один от другого. При щелчке на кнопке метка в этой панели обновится. Другие панели останутся без изменений.

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

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

Прерванные обновления

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

В большинстве случаев именно это и требуется. Если требуется воспрепятствовать тому, чтобы пользователь прерывал асинхронную обратную отправку, можно добавить код JavaScript, который отключает элементы управления на время выполнения асинхронной обратной отправки. Для этого понадобится присоединить обработчик к событию beginRequest, подобно тому, как был добавлен обработчик события endRequest в примере обработки ошибок. Другая возможность решения этой задачи - использование элемента управления UpdateProgress, который рассматривается позже.

Триггеры

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

Формально UpdatePanel всегда использует триггеры. Все элементы управления в UpdatePanel автоматически становятся триггерами для него. В текущем примере было показано, как это работает с вложенными кнопками - при возникновении события Button.Click выполняется асинхронная обратная отправка. Однако это работает также со стандартным событием любого веб-элемента управления (в соответствии с атрибутом DefaultEvent в коде этого элемента управления), при условии, что событие инициирует обратную отправку страницы.

Например, если поместить элемент TextBox внутрь UpdatePanel и установить свойство TextBox.AutoPostBack в true, событие TextBox.TextChanged инициирует асинхронную обратную отправку, и элемент управления UpdatePanel будет обновлен.

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

<asp:Button ID="Button4" runat="server" Text="Обновить все панели" />

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

<asp:UpdatePanel ID="UpdatePanel1" runat="server" UpdateMode="Conditional">
    <ContentTemplate>
        <asp:Label ID="Label1" runat="server" />
    </ContentTemplate>
    <Triggers>
        <asp:AsyncPostBackTrigger ControlID="Button4" EventName="Click" />
    </Triggers>
</asp:UpdatePanel>

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

Теперь щелчок на кнопке Button4 будет перехвачен на стороне клиента, и PageRequestManager выполнит асинхронную обратную отправку. Все элементы управления UpdatePanel, свойство UpdateMode которых установлено в Always, будут обновлены. Все элементы управления UpdatePanel, которые имеют свойство UpdateMode, установленное в Conditional, и AsyncPostBackTrigger установлено для Button4, также будут обновлены.

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

Триггеры можно использовать и другим способом. Вместо мониторинга большего числа элементов управления, с их помощью можно заставить UpdatePanel игнорировать определенные элементы управления. Например, предположим, что UpdatePanel содержит кнопку. Обычно щелчок на этой кнопке будет инициировать асинхронный запрос и частичное обновление. Если необходимо, чтобы вместо этого инициировалась полностраничная обратная отправка, понадобится просто добавить триггер PostBackTrigger (вместо AsyncPostBackTrigger).

Ниже приведен пример элемента управления UpdatePanel, который содержит вложенную кнопку, инициирующую полную обратную отправку, а не асинхронную обратную отправку:

<asp:UpdatePanel ID="UpdatePanel1" runat="server" UpdateMode="Conditional">
    <ContentTemplate>
        <asp:Label ID="Label1" runat="server" />
        <br />
        <asp:Button ID="Button1" runat="server" Text="Обновить синхронно" />
    </ContentTemplate>
    <Triggers>
        <asp:AsyncPostBackTrigger ControlID="Button4" EventName="Click" />
        <asp:PostBackTrigger ControlID="Button1" />
    </Triggers>
</asp:UpdatePanel>

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

Оптимизация элемента управления UpdatePanel

Иногда элемент управления UpdatePanel отрицательно влияет на пропускную способность. Это связано с тем, что почти всегда UpdatePanel передает больше информации, чем требуется.

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

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

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

И помните, что подход с применением UpdatePanel никогда не будет хуже подхода с использованием синхронной обратной отправки, поскольку последний всегда вызывает обратную отправку всей страницы. Подход с использованием UpdatePanel проигрывает только в сравнении с кодированием ASP.NET AJAX вручную.

Timer

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

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

Элемент управления Timer чрезвычайно прост. Его достаточно добавить к странице и установить значение его свойства Interval равным максимальному числу миллисекунд, которые должны истечь, прежде чем выполнится обновление. Например, если установить Interval в 60000, таймер вызовет обратную отправку по истечении одной минуты:

<asp:Timer ID="Timer1" runat="server" Interval="60000" />

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

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

Таймер генерирует серверное событие Tick, которое можно обработать для обновления страницы. Однако использование этого события не обязательно, т.к. при запуске таймера выполняется полностраничный жизненный цикл. Это означает, что можно реагировать на другие события страницы и элемента управления, такие как Page.Load.

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

Чтобы использовать таймер с частичной визуализацией, поместите обновляемые части страницы в элементы управления UpdatePanel со свойством UpdateMode, установленным в Conditional, и добавьте триггер, который вызывает обновление при каждом запуске таймера:

<asp:Timer ID="Timer1" runat="server" Interval="1000" />
<asp:UpdatePanel ID="UpdatePanel1" runat="server" UpdateMode="Conditional">
    <ContentTemplate>
        <asp:Label ID="Label1" runat="server" />
    </ContentTemplate>
    <Triggers>
        <asp:AsyncPostBackTrigger ControlID="Timer1" EventName="Tick" />
    </Triggers>
</asp:UpdatePanel>

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

Чтобы остановить таймер, нужно просто установить свойство Enabled в серверном коде в false. Например, вот как отключить таймер после десяти обновлений:

protected void Timer1_Tick(object sender, EventArgs e)
{
        // Обновить счетчик тактов и сохранить его в состоянии представления
        int tickCount = 0;
        if (ViewState["TickCount"] != null)
        {
            tickCount = (int)ViewState["TickCount"];
        }

        tickCount++;
        ViewState["TickCount"] = tickCount;

        // Принять решение об отключении таймера
        if (tickCount >= 10)
        {
            Timer1.Enabled = false;
        }
}

UpdateProgress

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

Название элемента управлений UpdateProgress (ход выполнения обновления) несколько неточно. В действительности он не показывает ход выполнения; вместо этого он выводит сообщение ожидания, которое позволяет удостовериться, что страница все еще работает, и последний запрос обрабатывается. Одна из реализаций этого метода с помощью процессора страницы JavaScript была рассмотрена в статье «Взаимодействие с JavaScript».

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

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

Индикатор ожидания

Код разметки этой страницы определяет UpdatePanel, за которым следует UpdateProgress. Элемент управления UpdateProgress включает в себя кнопку отмены, которая будет рассмотрена в следующем разделе:

<form id="form1" runat="server">
        <div>
            <asp:ScriptManager ID="ScriptManager1" runat="server" />

            <asp:UpdatePanel ID="UpdatePanel1" runat="server" UpdateMode="Conditional">
                <ContentTemplate>
                    <div style="background-color: #FFFFE0; padding: 20px">
                        <asp:Label ID="Label1" runat="server" Font-Bold="True" />
                        <br />
                        <br />
                        <asp:Button ID="cmdRefreshTime" runat="server" 
                            Text="Запустить процесс обновления страницы"
                            OnClick="cmdRefreshTime_Click" />
                    </div>
                </ContentTemplate>
            </asp:UpdatePanel>

            <asp:UpdateProgress ID="UpdateProgress1" runat="server">
                <ProgressTemplate>
                    <br /><br />
                    <div style="font-size: x-small">
                        Соединение с сервером ...
                        <img src="ajax-loader.gif" alt="Загрузка" style="vertical-align:middle" />
                        <input id="Button1" onclick="AbortPostBack()" 
                            type="button" value="Отменить задачу" />
                    </div>
                </ProgressTemplate>
            </asp:UpdateProgress>
        </div>
</form>

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

protected void cmdRefreshTime_Click(object sender, EventArgs e)
{
        System.Threading.Thread.Sleep(10000);
        Label1.Text = DateTime.Now.ToLongTimeString();
}

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

Отмена

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

Добавление кнопки отмены осуществляется за два шага. Сначала нужно добавить код JavaScript, который выполняет отмену. Этот код (который должен быть помещен после элемента управления ScriptManager) имеет следующий вид:

<script>
    var prm = Sys.WebForms.PageRequestManager.getInstance();
    prm.add_initializeRequest(InitializeRequest);

    function InitializeRequest(sender, args) {
        if (prm.get_isInAsyncPostBack()) {
            args.set_cancel(true);
        }
    }

    function AbortPostBack() {
        if (prm.get_isInAsyncPostBack()) {
            prm.abortPostBack();
        }
    }
</script>

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

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

Управление хронологией просмотра страниц браузером

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

К счастью, ASP.NET 4 предлагает изящное решение. С помощью ScriptManager можно взять под свой контроль список хронологии браузера. По желанию можно добавлять элементы в список и реагировать соответствующим образом, когда пользователь щелкает на кнопке Back (Назад) или Forward (Вперед), чтобы обеспечить правильное восстановление состояния страницы.

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

Чтобы увидеть, как работает средство хронологии браузера, поддерживаемое ScriptManager, можно создать простой пример с элементом управления типа мастера (Wizard). Ниже приведен упрощенный пример, в котором трехшаговый мастер помещен внутрь элемента управления UpdatePanel:

<form id="form1" runat="server">
    <div>
        <asp:scriptmanager id="ScriptManager1" runat="server"
            enablehistory="True"
            onnavigate="ScriptManager1_Navigate" />

        <asp:updatepanel id="UpdatePanel1" runat="server">
                <ContentTemplate>
                    <asp:Wizard ID="Wizard1" runat="server" BackColor="#EFF3FB"
                        BorderColor="#B5C7DE" BorderWidth="1px" Font-Names="Verdana"
                        Font-Size="0.9em" ActiveStepIndex="0" CellPadding="10" Height="146px"
                        OnActiveStepChanged="Wizard1_ActiveStepChanged" Width="412px">
                        <StepStyle Font-Size="0.8em" ForeColor="#333333" Width="200px" />
                        <WizardSteps>
                            <asp:WizardStep ID="WizardStep1" runat="server" Title="Шаг 1">
                                Страница для шага 1.
                            </asp:WizardStep>
                            <asp:WizardStep ID="WizardStep2" runat="server" Title="Шаг 2">
                                Страница для шага 2.
                            </asp:WizardStep>
                            <asp:WizardStep ID="WizardStep3" runat="server" Title="Шаг 3">
                                Страница для шага 3.
                            </asp:WizardStep>
                        </WizardSteps>
                        <SideBarButtonStyle BackColor="#507CD1" Font-Names="Verdana"
                            ForeColor="White" />
                        <NavigationButtonStyle BackColor="White" BorderColor="#507CD1"
                            BorderStyle="Solid" BorderWidth="1px" Font-Names="Verdana" Font-Size="0.8em"
                            ForeColor="#284E98" />
                        <SideBarStyle BackColor="#507CD1" Font-Size="0.9em" VerticalAlign="Top" />
                        <HeaderStyle BackColor="#284E98" BorderColor="#EFF3FB" BorderStyle="Solid"
                            BorderWidth="2px" Font-Bold="True" Font-Size="0.9em" ForeColor="White"
                            HorizontalAlign="Center" />
                    </asp:Wizard>
                </ContentTemplate>
            </asp:updatepanel>
    </div>
</form>

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

Добавление точек предыдущего состояния

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

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

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

protected void Wizard1_ActiveStepChanged(object sender, EventArgs e)
{
        // Проверить, что это асинхронная обратная отправка, 
        // а не попытка выполнить навигацию
        if ((ScriptManager1.IsInAsyncPostBack) && (!ScriptManager1.IsNavigating))
        {
            string currentStep = Wizard1.ActiveStepIndex.ToString();
            ScriptManager1.AddHistoryPoint("Wizard1", Wizard1.ActiveStepIndex.ToString(),
                "Шаг " + (Wizard1.ActiveStepIndex + 1).ToString());
        }
}

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

Во-вторых, код проверяет свойство IsNavigating, которое имеет значение true, когда пользователь щелкает на кнопке Forward или Back, чтобы перейти к новой точке состояния страницы. Когда это происходит, сохранение состояния не требуется - вместо этого состояние нужно восстановить, обрабатывая событие ScriptManager.Navigate.

При добавлении точки хронологии код использует имя индекса Wizard1 (чтобы соответствовать имени элемента управления Wizard). Он сохраняет индекс текущего шага и устанавливает в качестве заголовка страницы описательную строку типа "Шаг 1". Обратите внимание, что заголовок страницы добавляет 1 к свойству ActiveStepIndex, поскольку оно начинается с нуля.

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

Пользовательские точки хронологии

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

При обработке события Navigate необходимо получить требуемое состояние с помощью коллекции HistoryEventArgs.State и ключа состояния, который применялся при первом добавлении точки хронологии. Если подходящее значение состояния найти не удается, пользователь, скорее всего, осуществил возврат к первой закладке страницы. Это означает, что мастер следует вернуть в исходное состояние.

Ниже приведен код, который в примере мастера используется для возврата к соответствующему шагу:

protected void ScriptManager1_Navigate(object sender, HistoryEventArgs e)
{
        if (e.State["Wizard1"] == null)
        {
            // Восстановить состояние страницы no умолчанию (например, для первой страницы)
            Wizard1.ActiveStepIndex = 0;
        }
        else
        {
            Wizard1.ActiveStepIndex = Int32.Parse(e.State["Wizard1"]);
        }
        Page.Title = "Step " + (Wizard1.ActiveStepIndex + 1).ToString();
}

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

Способ сохранения состояния в URL-адресе

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

URL-адрес в примере мастера будет выглядеть следующим образом:

http://localhost:8090/sites/#&&/fGR23TLj220...

Информация о состоянии размещается после маркера фрагмента URL-адреса (#). В результате информация о состоянии не будет мешать возможным аргументам строки запроса, которые будут располагаться перед маркером фрагмента.

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

В отличие от состояния представления, шифрование применить к значениям состояния нельзя. Однако кодирование и хеш-код можно удалить из значений просмотра, если они должны быть представлены в URL-адресе в более понятной форме. Достаточно установить свойство ScriptManager.EnableSecureHistoryState в false, и URL-адрес приобретет вид, подобный следующему:

http://localhost:8090/sites/#&&Wizard1=2

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

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

Пройди тесты
Лучший чат для C# программистов