Alex Belozerov Blog

IT, бизнес, саморазвитие

UpdatePanel: что происходит за кулисами?

Данный пост является переводом статьи The UpdatePanel opened: what happens behind the scenes? с сайта dotnetslackers.com.

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

Введение

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

Возьмем простейший пример. Я создал UpdatePanel с двумя контролами внутри: Button и Label.


Пусть при нажатии на кнопку в label записывается текущее время:

using System;
public partial class _Default : System.Web.UI.Page
{
protected void Button1_Click(object sender, EventArgs e)
{
Label1.Text = DateTime.Now.ToString();
}
}

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

Перед запуском событий PageRequestManger’а, происходит множество более низкоуровневых внутренних событий, которые запускают PageRequestManager. Эти  низкоуровневые обработчики выполняют такие функции как оборачивание браузерного event object в Sys.UI.DomEvent. Но не будем вдаваться в детали в этом месте.

Обработчик PageRequestManager _onFormElementClick

Этот обработчик вызывается когда вы нажимаете кнопку внутри UpdatePanel, и делает следующие вещи:

Во-первых, устанавливает свойство _postBackSettings PageRequestManager’а:

this._postBackSettings = this._getPostBackSettings(element, element.name);

postBackSettings содержит 3 элемента:
  • Булевую переменную обозначающую должен ли запрос быть асинхронным
  • ID UpdatePanel’и и элемента, вызвавшего postback
  • Ссылку на элемент вызвавшего postback

Метод _getPostBackSettings проходит по дереву контролов вверх начиная с элемента, вызвавшего событие (в данном случае button) в поисках UpdatePanel. Если он найдет UpdatePanel, что в нашем случае обязательно произойдет, то установит свойство postBackSettings таким образом, чтобы произошел асинхронный запрос:

var indexOfPanel = Array.indexOf(this._updatePanelClientIDs, element.id);
if (indexOfPanel !== -1)
{
if (this._updatePanelHasChildrenAsTriggers[indexOfPanel])
{
return this._createPostBackSettings(true,
this._updatePanelIDs[indexOfPanel] + '|' +
elementUniqueID, originalElement);
}

Вторая вещь, которую делает обработчик onFormElementClick - записывает в свойство additionalInput имя (name) и значение (value) нажатой кнопки. Данная процедура выполняется потому, что процесс, который собирает данные с формы когда форма готовится к submit’у, игнорирует кнопки, а value нажатой кнопки должно быть отправлено на сервер.

else if ((element.tagName === 'BUTTON') && 
(element.name.length !== 0) &&
(element.type === 'submit'))
{
this._additionalInput = element.name + '=' +
encodeURIComponent(element.value);
}

Обработчик PageRequestManager _onFormSubmit

Этот метод вызывается после того как обработчик _onFormElementClick заканчивает свою работу, и выполняет упаковку данных формы и отправку асинхронного запроса на сервер. Но он делает это только если требуется асинхронный запрос, что определяется значением соответствующего булевого флага в свойстве _postBackSettings, которое было установлено в методе onFormElementClick.

В нашем случае требуется асинхронный запрос. Метод проходит по детям формы, упаковывая их значения в переменную formBody типа StringBuilder, если они являются элементами типа INPUT (text, password, hidden, checkbox and radio types), SELECT или TEXTAREA.

Отходя от темы, небольшая оптимизация может быть выполнена для обработки SELECT’ов, текущая реализация просматривает каждый элемент OPTION для определения выбран он или нет, тогда как если свойство multiple SELECT’а равно false, можно использовать свойство selectedIndex. Если у вас 1000 опций, необязательно просматривать все 1000 если SELECT не multiple:

else if (tagName === 'SELECT') 
{
var optionCount = element.options.length;
for (var j = 0; j < optionCount; j++)
{
var option = element.options[j];
if (option.selected)
{
formBody.append(name);
formBody.append('=');
formBody.append(encodeURIComponent(option.value));
formBody.append('&');
}
}
}

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

Далее для того чтобы отправить запрос метод создает объект Sys.Net.WebRequest и устанавливает в его поле URL значение form action URL, устанавливает значение заголовка x-microsoft-ajax в delta=true, что сигнализирует о том что это запрос для получения дельт/delta (изменений) документа. Также он отключает кэширование и устанавливает timeout в значение, определенное в ScriptManager серверной части.

var request = new Sys.Net.WebRequest();
request.set_url(form.action);
request.get_headers()['X-MicrosoftAjax'] = 'Delta=true';
request.get_headers()['Cache-Control'] = 'no-cache';
request.set_timeout(this._asyncPostBackTimeout);

Очевидно что PageRequestManager должен быть оповещен о пришествии ответа с сервера, и это осуществляется за счет создания делегата на его метод _onFormSubmitCompleted и добавления этого делегата к списку делегатов которые вызываются после завершения запроса:

request.add_completed(
Function.createDelegate(this,
this._onFormSubmitCompleted));

Можно подумать, что PageRequestManager  уже готов отправить запрос, однако есть еще несколько вещей, которые он должен сделать.

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

Итак, перед тем как запрос будет отправлен, PageRequestManager  запускает событие InitializeRequest, для каждого, кто подписан на него, на этой стадии отправка формы может быть отменена подписчиками на событие путем установки флага cancel аргумента события:

var handler = this._get_eventHandlerList().getHandler("initializeRequest");
if (handler) {
var eventArgs = new Sys.WebForms.InitializeRequestEventArgs(
request, this._postBackSettings.sourceElement);
handler(this, eventArgs);
continueSubmit = !eventArgs.get_cancel();
}

Пример того, как код пользователя может подписаться на это событие:

var prm = Sys.WebForms.PageRequestManager.getInstance();
prm.add_initializeRequest(InitializeRequestHandler);

function InitializeRequestHandler(sender, initializeRequestEventArgs) {
var cancel = !confirm('Initialize request occurring.');
initializeRequestEventArgs.set_cancel(cancel);
}

После того, как прошло событие InitializeRequest, и если отправка не была отменена, PageRequestManager  запускает событие BeginRequest для каждого, кто на него подписан. Разница в том, что на этой стадии отправка уже не может быть отменена.

В конце, записав текущую scroll position документа в свойство _scrollPosition, он просит Sys.Net.WebRequest отправить запрос, и отменяет обработку события кнопки по умолчанию, чтобы ничего более не произошло до того как вернется WebRequest:

request.invoke();

if (evt) {
evt.preventDefault();
}

Обработчик PageRequestManager _onFormSubmitCompleted

Этот метод вызывается после того как получен ответ с сервера, либо что-то пошло не так

Первая вещь которую он делает - проверяет всё, что могло пойти не так, такие вещи как таймаут, был ли запрос прерван, или пришел ответ на какой-то другой (левый) запрос, произошла какая-то ошибка и т.д., ну вы поняли. В большинстве случаев он вызывает внутренний метод _endPostBack, который запускает событие EndRequest для каждого кто на него подписан.

После того как выполнены проверки, метод пробегает по response buffer, полученному с сервера, извлекая delta nodes, каждая из которых содержит type, id и content.

После получения массива deltas, метод пробегает по каждому из них. Существует около 20 различных типов delta nodes, некоторые из которых обрабатываются немедленно, например document title:

for (var i = 0; i < delta.length; i++) {
var deltaNode = delta[i];
switch (deltaNode.type) {
...
case "pageTitle":
document.title = deltaNode.content;
break;

Остальные сохраняются для дальнейшей обработки:

case "updatePanel":
Array.add(updatePanelNodes, deltaNode);
break;
case "hiddenField":
Array.add(hiddenFieldNodes, deltaNode);
break;

После группировки delta nodes по соответствующим типам, происходит обработка каждого типа delta node, но не перел тем как запустится событие PageLoadingEvent для каждого кто на него подписан.

Обработка updatePanelNodes заключается в уничтожении существующего содержимого панели и заменой его содерджимым поля content delta node.

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

Вы можете быть склонны думать, что всё это предельно просто и понятно, и для представленного элементарного случая это действительно так. Но если вы задумаетесь о сложности того, что можно сделать на сервере, тогда вы поймете что тут еще много всего. К примеру что если вы зарегистрируете новые script blocks на сервере, или установите фокус на другой контрол? Именно поэтому существует около 20 типов delta node, каждый для обработки своего специфического сценария.

Заключение

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

Что дала мне эта статья (от переводчика):

Теперь понятно почему перед отправкой асинхронного запроса на сервер невозможно понять какие именно UpdatePanelи будут обновляться, клиент такую информацию вообще не передает, сервер по желанию может в response прислать обновление для каждой UpdatePanel на странице. С одной стороны, удобно, с другой, не получится красиво реализовать Progress bar, закрывающий одну единственную панель при ее обновлении.

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

Archived comments

Даша Шулеко
Спасибо большое! Очень пригодилось.

Comments