Использование Ajax с клиентскими обратными вызовами

183

Используя подход Ajax, можно создавать выразительные и быстро реагирующие веб-страницы. Однако написание клиентского сценария отнимает много времени. Visual Studio не может обеспечить то же удобство проектирования, которое предлагает при написании серверного кода, и не предоставляет инструменты отладки для облегчения поиска неизбежных ошибок, встречающихся в коде, который написан на слабо типизированном языке JavaScript. И даже при успешном решении своей задачи вам придется провести тестирование на широком множестве других браузеров, если только вы не изучили досконально мельчайшие нюансы поддержки JavaScript в различных браузерах.

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

Хотя и ASP.NET AJAX и jQuery являются достойным выбором, наиболее важную задачу Ajax - отправку асинхронного запроса серверу - можно решить с использованием более простого средства клиентского обратного вызова ASP.NET. Клиентские обратные вызовы предоставляют способ обновления части данных веб-страницы без необходимости в полной обратной отправке. Самое главное, что отпадает потребность в коде сценария, который использует объект XMLHttpRequest. Тем не менее, по-прежнему придется писать клиентский сценарий, который обрабатывает ответ сервера.

Создание клиентского обратного вызова

Чтобы создать клиентский обратный вызов в ASP.NET, сначала понадобится запланировать способ коммуникаций. Ниже описана общая модель:

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

  2. На этом этапе выполняется нормальный жизненный цикл страницы, что означает запуск всех обычных серверных событий, таких как Page.Load.

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

  4. Как только страница получает ответ от серверного метода, она использует код JavaScript, чтобы соответствующим образом изменить веб-страницу.

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

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

Заполнение списка с помощью обратного вызова

На рисунке ниже показана диаграмма этого процесса:

Стадии обратного вызова

Создание базовой страницы

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

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Основы ASP.NET</title>
    <script type="text/javascript">
        // Здесь находятся JavaScript-функции
    </script>
</head>
<body onload="CreateXMLHttpRequest();">
    <form id="form1" runat="server">
        <div style="font-family: Verdana; font-size: small">
            Выберите регион, а затем город:<br />
            <br />
            <asp:DropDownList ID="lstRegions" runat="server" Width="210px" DataSourceID="sourceRegions" 
                DataTextField="RegionDescription" DataValueField="RegionID" />
            <asp:DropDownList ID="lstTerritories" runat="server" Width="275px" />
            <br /><br /><br />
            <asp:Button ID="cmdOK" runat="server" Text="OK" Width="50px" OnClick="cmdOK_Click" />
            <br /><br />
            <asp:Label ID="lblInfo" runat="server"></asp:Label>
            <asp:SqlDataSource ID="sourceRegions" runat="server" ConnectionString="<%$ ConnectionStrings:Northwind %>" 
                SelectCommand="SELECT 0 As RegionID, '' AS RegionDescription UNION SELECT RegionID, RegionDescription FROM Region" />
        </div>
    </form>
</body>
</html>

Реализация обратного вызова

Чтобы можно было принимать обратный вызов, необходим класс, который реализует интерфейс ICallbackEventHandler. Если известно, что обратный вызов будет использоваться в нескольких страницах, имеет смысл создать выделенный класс (подобный специальному обработчику HTTP в примере применения Ajax из предыдущей статьи). Однако если требуется определить функциональность, которая предназначена для единственной страницы, ICallbackEventHandler можно реализовать непосредственно внутри веб-страницы.

Интерфейс ICallbackEventHandler определяет два метода. Метод RaiseCallbackEvent() получает данные события от браузера в виде строкового параметра. Он запускается первым. Метод GetCallbackResult() запускается следующим и возвращает результат странице.

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

В этом примере строковый параметр, переданный методу RaiseCallbackEvent(), содержит идентификатор RegionID для выбранного региона. Используя эту информацию, метод GetCallbackResult() подключается к базе данных и получает список всех территориальных записей в этом регионе. Эти результаты объединяются в длинную единую строку, разделенную символами |. Ниже приведен полный код:

using System;
using System.Web.UI;
using System.Configuration;
using System.Text;
using System.Web.Configuration;
using System.Data;
using System.Data.SqlClient;

public partial class _Default : System.Web.UI.Page, ICallbackEventHandler
{
    private string eventArgument;
    public void RaiseCallbackEvent(string eventArgument)
    {
        this.eventArgument = eventArgument;
    }

    public string GetCallbackResult()
    {
        // Создать объекты ADO.NET
        SqlConnection con = new SqlConnection(
            WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString);
        SqlCommand cmd = new SqlCommand(
            "SELECT * FROM Territories WHERE RegionID=@RegionID", con);
        cmd.Parameters.Add(new SqlParameter("@RegionID", SqlDbType.Int, 4));
        cmd.Parameters["@RegionID"].Value = Int32.Parse(eventArgument);

        // Создать объект StringBuilder, который содержит строку ответа
        StringBuilder results = new StringBuilder();
        try
        {
            con.Open();
            SqlDataReader reader = cmd.ExecuteReader();

            // Построить строку ответа
            while (reader.Read())
            {
                results.Append(reader["TerritoryDescription"]);
                results.Append("|");
                results.Append(reader["TerritoryID"]);
                results.Append("||");
            }
            reader.Close();
        }
        catch
        {
            // Скрыть ошибки подключения
        }
        finally
        {
            con.Close();
        }
        return results.ToString();
    }
    
    // ...
}

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

Поскольку результаты должны быть возвращены в виде единственной строки (и поскольку эта строка должна быть восстановлена в коде JavaScript), код выглядит несколько громоздким. Одиночный символ вертикальной черты (|) отделяет поле TerritoryDescription от поля TerritoryID. Два последовательных символа вертикальной черты (||) обозначают начало новой строки. Например, при запросе записи с Region ID, равным 1, полученный ответ может выглядеть следующим образом:

Columbia|80202||Atlanta|30346||Orlando|32859

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

Написание клиентского сценария

Клиентские сценарии подразумевают обмен данными между сервером и клиентом. Подобно тому, как сервер нуждается в методе для подготовки результатов, клиенту необходима функция, предназначенная для их приема и обработки. Функция JavaScript, которая обрабатывает ответ сервера, может иметь любое имя, но она должно принимать два параметра: result и context.

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

<script type="text/javascript">
        var xmlRequest;

        function CreateXMLHttpRequest() {
            try {
                // Этот код работает, если XMLHttpRequest является частью JavaScript
                xmlRequest = new XMLHttpRequest();
            }
            catch (err) {
                // В противном случае требуется объект ActiveX
                xmlRequest = new ActiveXObject("Microsoft.XMLHTTP");
            }
        }

        function ClientCallback(result, context) {
            // Найти элемент списка
            var lstTerritories = document.getElementById("lstTerritories");

            // Очистить содержимое списка
            lstTerritories.innerHTML = "";

            // Получить массив со списком территориальных записей
            var rows = result.split("||");

            for (var i = 0; i < rows.length - 1; ++i) {
                // Разбить каждую запись на два поля
                var fields = rows[i].split("|");
                var territoryDesc = fields[0];
                var territoryID = fields[1];

                // Создать элемент списка
                var option = document.createElement("option");

                // Сохранить идентификатор в атрибуте value
                option.value = territoryID;

                // Отобразить описание в тексте элемента списка
                option.innerHTML = territoryDesc;

                // Добавить элемент в конец списка
                lstTerritories.appendChild(option);
            }
        }
</script>

Однако здесь упущен один нюанс. Хотя обе стороны обмена сообщениями определены, они пока еще не связаны друг с другом. Понадобится клиентский триггер, который будет запускать обратный вызов. В данном случае необходимо реагировать на событие onchange списка регионов:

protected void Page_Load(object sender, EventArgs e)
{
        string callbackRef = Page.ClientScript.GetCallbackEventReference(
            this, "document.getElementById('lstRegions').value", "ClientCallback", "null", true);

        lstRegions.Attributes["onChange"] = callbackRef;
}

callbackRef - код JavaScript, который вызывает обратный вызов. Но как именно должна быть написана эта строка кода? К счастью, ASP.NET предоставляет удобный метод GetCallbackEventReference(), который может создать необходимую ссылку обратного вызова. Первый параметр является ссылкой на объект ICallbackEventHandler, который будет обрабатывать обратный вызов, в данном случае - содержащую страницу. Второй параметр представляет информацию, которую клиент передаст серверу. В этом примере требуется фрагмент кода JavaScript, который будет искать соответствующий элемент управления (lstRegions) и извлекать выбранное в настоящий момент значение.

Третий параметр - имя клиентской функции JavaScript, которая будет принимать результаты обратного вызова сервера. Четвертый параметр включает любую контекстную информацию, которую желательно передать клиентской функции. Это полезно при обработке нескольких обратных вызовов одной и той же функцией JavaScript, когда требуется различать, какой ответ какому вызову соответствует. Наконец, последний параметр указывает, нужно ли выполнять обратный вызов асинхронно. Этот параметр должен быть всегда установлен в true во избежание блокирования страницы в случае проблемы с сетью.

Обработчик щелчка по кнопке "OK" выглядит следующим образом:

protected void cmdOK_Click(object sender, EventArgs e)
{
        // Код, не защищенный от атак SQL-инъекциями
        lblInfo.Text = "Вы выбрали регион с ID #" + Request.Form["lstTerritories"];

        // Сбросить список
        lstRegions.SelectedIndex = 0;
}

Отключение проверки достоверности события

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

ASP.NET защищается от атак внедрением POST с помощью проверки достоверности событий. Проверка достоверности событий обеспечивает верификацию того, что все отправленные данные имеют смысл, перед тем как ASP.NET выполнит жизненный цикл страницы. К сожалению, проверка достоверности событий часто вызывает проблемы со страницами в стиле Ajax. В рассматриваемом примере элементы динамически добавляются в список территорий. Когда пользователь выберет территорию и отправит страницу обратно, ASP.NET сгенерирует ошибку "Invalid postback or callback argument" ("Недопустимый аргумент обратной отправки или обратного вызова"), поскольку выбранная территория не определена в серверном элементе управлении.

Средство проверки достоверности событий не является обязательной функцией всех элементов управления. Она реализована только для классов элементов управления, которые оснащены атрибутом SupportsEventValidation. В ASP.NET большинство элементов управления, которые полагаются на отправленные данные (ListBox, DropDownList, Checkbox, TreeView, Calendar и т.д.), используют этот атрибут. Исключение составляют элементы управления, которые не ограничивают разрешенные значения. Например, элемент управления TextBox не использует проверку достоверности событий, поскольку пользователь может вводить в нем любое значение.

Проблему проверки достоверности события можно обойти двумя способами. Самый безопасный подход - явное указание ASP.NET дополнительных значений, которые должны быть разрешены в элементе управления. (ASP.NET отслеживает все достоверные значения, используя скрытый дескриптор ввода по имени EVENTVALIDATION.) К сожалению, этот подход трудоемок и часто непрактичен.

Чтобы применить его, вызовите метод Page.ClientScript.RegisterForEventValidation() для каждого возможного значения. Это должно делаться на этапе визуализации путем переопределения метода Page.Render(), как показано ниже. Вот пример, который позволяет пользователю выбирать в элементе управления lstTerritories территорию с идентификатором TerritoryID, равным 10:

protected override void Render(HtmlTextWriter writer)
{
        ClientScript.RegisterForEventValidation(lstTerritories.UniqueID, "10"); 
        base.Render(writer);
}

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

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

Проверку достоверности событий можно отключить также для всего веб-сайта, установив атрибут enableEventValidation элемента pages в false:

<configuration>
  <system.web>
    <pages enableEventValidation="false" />
  </system.web>
</configuration>

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

lblInfo.Text = "Вы выбрали регион с ID #" + Request.Form["lstTerritories"];

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

Понятно, что клиентские обратные вызовы представляют мощное средство, которое позволяет создавать более гладкие и динамичные страницы. Но помните, что клиентские обратные вызовы полагаются на функциональность XMLHttpRequest, которая ограничивает их современными браузерами. Некоторые браузеры могут поддерживать JavaScript, но не клиентские обратные вызовы. Проверка, поддерживает ли браузер обратные вызовы Ajax, осуществляется с помощью свойства Request.Browser.SupportsCallback.

Внутреннее устройство клиентских обратных вызовов

Следует отметить, что во время выполнения обратного вызова целевая страница фактически начинает усеченный жизненный цикл. Большинство управляющих событий не будут выполняться, но обработчики событий Page.Load и Page.Init будут. Свойство Page.IsPostBack вернет true, но этот обратный вызов можно отличить от подлинной обратной отправки, проверяя свойство Page.IsCallback, которое также будет равно true. Процесс визуализации страницы полностью пропускается. Информацию о состоянии предоставления извлекается и становится доступной для метода обратного вызова, но любые вносимые изменения обратно странице не отправляются. События жизненного цикла показаны на рисунке ниже:

Сравнение обратных отправок и обратных вызовов

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

Клиентские обратные вызовы в специальных элементах управления

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

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

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

Процесс для специального элемента управления DynamicPanel показан на рисунке ниже:

Обновление части страницы с помощью обратного вызова

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

DynamicPanel

Сначала необходимо унаследовать класс от Panel и реализовать интерфейс ICallbackEventHandler. Будучи частью ICallbackEventHandler, объект DynamicPanel должен реализовать методы RaiseCallbackEvent() и GetCallbackResult(). Этот процесс состоит из двух шагов. Первым делом, панель DynamicPanel должна инициировать событие для уведомления страницы. Страница может обработать это событие и провести соответствующие модификации. Затем элемент DynamicPanel должен сгенерировать HTML-разметку для своего содержимого. После этого он может вернуть эту информацию (наряду с клиентским идентификатором) клиентской веб-странице:

public class DynamicPanel : Panel, ICallbackEventHandler, ICallbackContainer
{

        public event EventHandler Refreshing;

        public void RaiseCallbackEvent(string eventArgument)
        {
            // Инициировать событие для уведомления клиента о том, что затребовано обновление
            if (Refreshing != null)
            {
                Refreshing(this, EventArgs.Empty);
            }
        }

        public string GetCallbackResult()
        {
            // Подготовить текстовый ответ, который будет отправлен обратно странице
            EnsureChildControls();

            using (StringWriter sw = new StringWriter())
            {
                using (HtmlTextWriter writer = new HtmlTextWriter(sw))
                {
                    // Добавить идентификатор этой панели
                    writer.Write(this.ClientID + "_");

                    // Сгенерировать только часть страницы, находящуюся внутри панели
                    this.RenderContents(writer);
                }
                return sw.ToString();
            }
        }
=}

Вот код клиентского сценария, который находит панель на странице, а затем заменяет ее содержимое новым HTML-содержимым:

<script>
function RefreshPanel(result, context)
{
   if (result != '')
   {
     // Разделить строку снова на две части: 
     // идентификатор панели и HTML-содержимое
     var separator = result.indexOf('_'); 
     var elementName = result.substr(0, separator);
     
     // Найти панель
     var panel = document.getElementById(elementName);
     
     // Заменить ее содержимое
     panel.innerHTML = result.substr(separator+1);
   }
 }
</script>

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

public class DynamicPanel : Panel, ICallbackEventHandler, ICallbackContainer
{
     // ...
     
     protected override void OnInit(EventArgs e)
     {
        base.OnInit(e);

        string script = @"<script type='text/javascript'>
            function RefreshPanel(result, context)
            {
               if (result != '')
               {
                 var separator = result.indexOf('_'); 
                 var elementName = result.substr(0, separator);
                 var panel = document.getElementById(elementName);
                 panel.innerHTML = result.substr(separator+1);
               }
             }
          </script>";

        Page.ClientScript.RegisterClientScriptBlock(this.GetType(),
            "RefreshPanel", script);
     }
}

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

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

Первым действием в реализации этого решения является возврат к DynamicPanel и реализация интерфейса ICallbackContainer. Этот интерфейс позволяет DynamicPanel предоставлять ссылку обратного вызова, вместо того, чтобы вынуждать к перемещению по странице. Чтобы реализовать ICallbackContainer, необходимо предоставить метод GetCallbackScript(), который возвращает ссылку. При этом панель может полагаться на страницу, указывая себя в качестве цели обратного вызова, и на метод RefreshPanel() как клиентский сценарий, который будет обрабатывать ответ:

public class DynamicPanel : Panel, ICallbackEventHandler, ICallbackContainer
{
        // ...

        public string GetCallbackScript(IButtonControl buttonControl, string argument)
        {
            return Page.ClientScript.GetCallbackEventReference(
                this, "", "RefreshPanel", "null", true);
        }
}

Теперь все готово для реализации значительно более простой кнопки обновления. Этот элемент управления по имени DynamicPanelRefreshLink унаследован от LinkButton. Панель, с которой он должен работать, указывается в свойстве PanelID. Когда наступает время его визуализации, DynamicPanelRefreshLink с помощью метода FindControl() находит связанный с ним элемент управления DynamicPanel, а затем добавляет ссылку сценария обратного вызова в атрибут onclick:

public class DynamicPanelRefreshLink : LinkButton
{
        public DynamicPanelRefreshLink()
        {
            PanelID = "";
        }

        public string PanelID
        {
            get { return (string)ViewState["DynamicPanelID"]; }
            set { ViewState["DynamicPanelID"] = value; }
        }

        protected override void AddAttributesToRender(HtmlTextWriter writer)
        {
            if (!DesignMode)
            {
                DynamicPanel pnl = (DynamicPanel)Page.FindControl(PanelID);
                if (pnl != null)
                {
                    writer.AddAttribute("onclick", pnl.GetCallbackScript(this, ""));
                    writer.AddAttribute("href", "#");
                }
            }
        }
}

Клиентская страница

Чтобы завершить этот пример, создайте простую текстовую страницу и добавьте в нее панель DynamicPanel и элемент DynamicPanelRefreshLink. Для создания ссылки установите свойство DynamicPanelRefreshLink.PanelID. Поместите в панель какое-то содержимое и элементы управления. И, наконец, добавьте обработчик события DynamicPanel.Refresh и используйте его для изменения содержимого либо форматирования элементов управления в панели:

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>
<%@ Register Assembly="SimpleControls" Namespace="SimpleControls" TagPrefix="cc1" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Основы ASP.NET</title>
</head>
<body>
    <form id="form1" runat="server">
        <cc1:dynamicpanel runat="server" id="Panel1" onrefreshing="Panel1_Refreshing" 
            style="padding-right: 10px; padding-left: 10px; padding-bottom: 10px; padding-top: 10px" 
            bordercolor="Red" borderwidth="2px">
            <br />
            <asp:Label ID="Label1" runat="server" Font-Bold="False" Font-Names="Verdana" Font-Size="X-Large" />
            <br /><br />
        </cc1:dynamicpanel>
        <br />
        <cc1:dynamicpanelrefreshlink runat="server" id="link1" panelid="Panel1">
            Обновить данные в панели

        </cc1:dynamicpanelrefreshlink>
    </form>
</body>
</html>
protected void Panel1_Refreshing(object sender, EventArgs e)
{
        Label1.Text = "Произошло обновление страницы с использованием обратного вызова. " +
            "Серверное время: " + DateTime.Now.ToString();
}

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

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