Минимизация трафика в ASP.NET Web Forms, кликабельный div и периодический опрос сервера
Технология ASP.NET Web Forms медленно, но верно уходит в прошлое. На смену ей приходят Web API с Angular 6 и похожие стеки. Но мне по наследству достался проект именно на Web Forms с огромным legacy. У меня есть несколько друзей, у которых плюс-минус похожая ситуация. Давно написанные приложения на старой технологии, которые надо развивать и поддерживать. У Web Forms есть возможность на PostBack не обновлять всю страницу, а только её часть. То что обёрнуто в UpdatePanel. Это добавляет интерактива, но всё равно работает довольно медленно и потребляет много трафика, т.к. рендеринг каждый раз происходит на сервере, а клиенту передаётся готовая разметка, которую нужно вставить вместо текущей внутрь div. К слову, UpdatePanel как раз рендерится в div, в котором потом разметка и заменяется.
Что тут можно сделать, чтобы минимизировать трафик?
- Написать WebMethod на странице и вызывать его с клиента средствами AJAX, при получении ответа изменять DOM через JS.
Минус этого решения в том, что нельзя определить WebMethod в контроле. Вовсе не хочется весь функционал писать на странице, особенно если он используется несколько раз на разных страницах.
- Написать asmx сервис, и вызывать его с клиента. Это уже лучше, но в этом случае нет явной связи между контролом и сервисом. Количество сервисов будет расти с увеличением количества контролов. Кроме того нам не будет доступен ViewState, а значит параметры будем передавать явным образом при обращении к сервису, значит будем делать серверную валидацию и проверять имеет ли пользователь право сделать то что он запросил.
- Использовать интерфейс ICallbackEventHandler. Это на мой взгляд довольно неплохой вариант.
Остановлюсь на нём подробнее.
Первое что нужно сделать это отнаследовать наш UserControl от ICallbackEventHandler и написать методы RaiseCallbackEvent и GetCallbackResult. Немного странно, что их 2. Первый отвечает за получение параметров от клиента, второй за возвращение результата.
Выглядеть это будет примерно так
public partial class SomeControl : UserControl, ICallbackEventHandler
{
#region Поля
///
/// Идентификатор некоего файла
///
private Guid _someFileId;
#endregion
#region Реализация ICallbackEventHandler
///
public void RaiseCallbackEvent(string eventArgument)
{
//сначала управление придёт сюда
try
{
dynamic args = JsonConvert.DeserializeObject(eventArgument);
_someFileId = (Guid) args.SomeFileId;
string type = (string) args.Type;
}
catch (Exception exc)
{
//логируем ошибку
throw;
}
}
///
public string GetCallbackResult()
{
//затем сюда
try
{
//делаем что-то полезное
return JsonConvert.SerializeObject(new
{
Action = actionName,
FileId = _someFileId,
});
}
catch (Exception exc)
{
//логируем ошибку
throw;
}
}
#endregion
}
Это была серверная часть. Теперь клиентская
var SomeControl = {
_successCallbackHandler: function (responseData) {
let data = JSON.parse(responseData);
switch (data.Action) {
case "continue":
//делаем что-то на клиенте
break;
case "success":
//или делаем что-то другое
break;
case "fail":
//или делаем это
break;
default:
//не рекомендую использовать alert, но для примера пойдёт
alert("Произошла ошибка при получении данных от сервера");
break;
}
},
_failCallbackHandler: function() {
alert("Произошла ошибка при получении данных от сервера");
},
}
Это ещё не всё. Нам ещё необходимо сгенерировать JS что-бы связать все наши функции
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
//Добавляем на страницу SomeControl.js, если его ещё нет
Page.ClientScript.RegisterClientScriptInclude(Page.GetType(), "SomeControl", "/Scripts/controls/SomeControl.js?v=2.24.0");
string callbackArgument = //задаём некий тип
//***Тут самое интересное.*** Регистрируем в JS в объекте SomeControl функцию CallServer. Никогда так не называйте объекты и функции, это только для примера
ScriptManager.RegisterStartupScript(Page, Page.GetType(), "SomeControl.Initialize",
$@"SomeControl.CallServer = function(someFileId) {{
let args = JSON.stringify({{ SomeFileId : someFileId, Type: '{callbackArgument}' }});
{Page.ClientScript.GetCallbackEventReference(this, "args", "SomeControl._successCallbackHandler", string.Empty, "SomeControl._failCallbackHandler", true)};
}};",
true);
//Регистрируем контрол как асинхронный
ScriptManager.GetCurrent(Page)?.RegisterAsyncPostBackControl(this);
}
Это очевидно code behind контрола.
Самое интересное — это генерация JS функции методом GetCallbackEventReference.
Передаём в него
- ссылку на наш контрол
- имя JS-переменной, значение которой будет передано на сервер в метод RaiseCallbackEvent через eventArgument (строкой выше сериализуем объект в JSON для передачи и собственно устанавливаем значение этой самой переменной args)
- имя JS-функции обратного вызова для случая успешного выполнения
- контекст выполнения (я им не пользуюсь)
- имя JS-функции обратного вызова для случая если что-то пошло не так
- будем валидировать средствами ASP.NET пришедший на сервер запрос
Как это всё будет работать в связке?
Из JS мы можем вызвать SomeControl.CallServer, эта функция создаст локальную переменную args и передаст управление функции, которая сделает запрос на сервер через AJAX.
Далее управление передаётся серверному методу RaiseCallbackEvent. Всё что было в клиентской переменной args теперь попало в серверный входной параметр eventArgument.
После выполнения RaiseCallbackEvent управление будет передано GetCallbackResult.
Строка, которую мы вернём через return будет отправлена на клиента и попадёт во входной параметр функции SomeControl._successCallbackHandler, то есть в responseData.
Если на каком-то этапе серверный код выдаст Exception, то управление будет передано клиентскому SomeControl._failCallbackHandler
Ещё необходимо сказать про ViewState. ViewState передаётся с клиента на сервер, и им можно пользоваться, но только в режиме ReadOnly, т.к. обратно на клиента ViewState не отправляется.
Конструкция на первый взгляд запутанная, но если разобраться, то получается довольно удобно, и трафик сэкономили.
Второй вопрос, который я хочу осветить в этой статье — это кликабельные div-ы или как можно вызвать обновление UpdatePanel со стороны клиента.
Зачем нужны кликабельные div-ы, можно же просто использовать
Мне нравиться, что div можно сверстать как мне хочется, я не ограничен рамками input type=«button»
Для реализации нужно отнаследоваться от интерфейса IPostBackEventHandler
У него всего 1 метод
public void RaisePostBackEvent(string eventArgument)
Теперь, как и в предыдущем случае, нам необходимо сгенерировать JS для вызова этого метода
Выглядит это так
Page.ClientScript.GetPostBackEventReference(this, callbackArgument)
callbackArgument задаётся на сервере и поменять его на клиенте не выйдет. Но всегда можно что-то положить в HiddenField. У нас же полноценный PostBack
Теперь результат выполнения GetPostBackEventReference можно повесить на onclick любого div или span или вообще чего угодно.
Или просто вызвать из JS по таймеру.
Обязательно регистрируем контрол как асинхронный (на OnLoad вызываем
ScriptManager.GetCurrent(Page)?.RegisterAsyncPostBackControl(this);
), иначе даже будучи внутри UpdatePanel будет вызван синхронный PostBack и обновится вся страница, а не только содержимое UpdatePanel
Используя 2 описанных выше метода я реализовывал, например, такой сценарий.
Пользователь нажал на кнопку, на сервер ушёл маленький запрос на длительную операцию (10–15 сек), пользователю пришёл короткий ответ, при разборе которого клиентский скрипт вызывает setTimeout. В setTimeout передаётся функция для callback на сервер для того, чтобы узнать о результатах запрошенной ранее операции. Если результат готов, то вызываем PostBack в UpdatePanel — происходит обновление заданной UpdatePanel. Если же результат ещё не готов, то опять вызываем setTimeout.
Всем кто всё ещё работает с Web Forms — удачи, надеюсь статья сделает ваши системы быстрее и красивее, и пользователи скажут вам слова благодарности.