Прежде чем начать
Сегодня мы создадим простой, но функциональный виджет для 1С:Предприятия, который будет показывать количество активных пользователей в базе. Главной его особенностью будет асинхронное взаимодействие с базой, что позволит выполнять обновление виджета без блокировки основного интерфейса приложения.
Это информация из старого блога DevelPlatform.ru
Давным давно мной был создан блог DevelPlatform, в котором были статьи по разработке на платформе 1С, администрированию, онлайн-инструменты, а также немного о платформе .NET.
Больше года назад сайт был закрыт. Некоторые из его материалов будут реанимированы на Инфостарт.
Хоть материал и был создан в далеком 2015 году, но описанные подходы до сих пор применимы при разработке. Конечно, сейчас уже обновилась работа поля HTML-документа и появились некоторые другие крутые возможности платформы 1С, но это в основном частности. Все предложенные способы создания виджетов не единственные, в конце статьи Вы найдете ссылки на связанные публикации с другими работами коллег.
Виджет
В качестве примера подхода при работе с HTML-виджетами можно продемонстрировать конфигурацию 1С:Документооборот 2.x, где в обработке "Текущие дела" создана форма для отображения различных виджетов с данными о моих задачах, задачах отдела, созданных документах, редактируемых файлов и т.д. Замечательная реализация и в плане функционала, и в плане юзабилити интерфейса, но есть один минус. Обновление виджетов происходит, конечно же, через синхронную контекстную серверную процедуру, что означает передачу на сервер всей формы, получение там данных, перенос их в форму и затем возвращение ее на клиент.
Проще говоря, для обновления виджетов необходимо выполнять синхронный серверный вызов, на время которого выполняется блокировка пользовательского интерфейса. Кто знает, может в новых версиях 1С:Документооборот эта ситуация уже изменилась.
При эксплуатации системы была замечена серьезная проблема - если включить автообновление виджетов, то при вызове серверной процедуры обновления подвисал весь интерфейс приложения пока оно ожидало ответа от сервера. Пользователи жаловались на частые подвисания интерфейса в самый не подходящий момент.
Конечно, есть достаточно простой выход - не использовать автообновление или оптимизировать процедуру обновления данных виджетов. Но это не самый оптимальный путь, ведь обновление виджетов может быть для кого-то критичным, а оптимизация получения данных и заполнения виджетов не избавит нас от контекстного серверного вызова и периодической блокировки интерфейса.
Мы пойдем другим путем и решим задачу двумя способами:
- асинхронное обновление виджета с помощью фоновых заданий.
- асинхронное обновление виджета с помощью AJAX-запросов к HTTP-сервису из поля HTML-документа.
Оба способа имеют плюсы и минусы, которые мы рассмотрим. И так, поехали!
Подготовка
Не буду создавать интригу и сразу покажу результат, который мы добьемся проделав шаги, описанные далее.
Как Вы можете заметить, обновление виджета выполняется автоматически без блокировки пользовательского интерфейса.
На форме виджет добавлен в качестве поля HTML-документа, которое используется и для обновления через фоновые задания, и для обновления с помощью AJAX-запросов. Количество активных пользователей определяется по количеству активных сеансов с помощью следующей функции, расположенной в общем модуле "ВиджетыСервер" (серверный, вызов сервера):
Функция ПолучитьКоличествоАктивныхСеансов() Экспорт
// Получаем количество активных сеансов
КоличествоАктивныхСеансов = 0;
Попытка
ТекущиеСоединения = ПолучитьСеансыИнформационнойБазы();
КоличествоАктивныхСеансов = ТекущиеСоединения.Количество();
Исключение
КоличествоАктивныхСеансов = -1;
КонецПопытки;
Возврат КоличествоАктивныхСеансов;
КонецФункции
Кроме этого в конфигурацию добавлен общий макет "ГлавнаяСтраница" с типом HTML-документ, в котором содержится разметка для виджета, а также скрипты для обновления данных с помощью AJAX-запроса.
Разметка страницы в общем макете
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title></title>
<meta charset="utf-8">
<style>
.hit-the-floor {
color: #fff;
font-weight: bold;
font-family: Helvetica;
text-shadow: 0 1px 0 #ccc, 0 2px 0 #c9c9c9, 0 3px 0 #bbb, 0 4px 0 #b9b9b9, 0 5px 0 #aaa, 0 6px 1px rgba(0, 0, 0, .1), 0 0 5px rgba(0, 0, 0, .1), 0 1px 3px rgba(0, 0, 0, .3), 0 3px 5px rgba(0, 0, 0, .2), 0 5px 10px rgba(0, 0, 0, .25), 0 10px 10px rgba(0, 0, 0, .2), 0 20px 20px rgba(0, 0, 0, .15);
}
.hit-the-floor {
text-align: center;
}
body {
background-color: #f1f1f1;
}
.parent {
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
overflow: auto;
}
.block {
p: {
display: block;
border: none;
}
}
</style>
<script id="jquery-js" type="text/javascript" src="http://code.jquery.com/jquery-latest.min.js"></script>
<script>
$(document).on("ready", function () {
$.support.cors = true;
fontSize();
$.ajaxSetup({ cache: false });
//<!--Раздел скриптов - Начало-->
getActiveUsers();
var timerMulti = window.setInterval("getActiveUsers();", 1000);
//<!--Раздел скриптов - Конец-->
});
function fontSize() {
var width = 200; // ширина, от которой идет отсчет
var fontSize = 25; // минимальный размер шрифта
var bodyWidth = $('html').width();
var multiplier = bodyWidth / width;
if ($('html').width() >= width) fontSize = Math.floor(fontSize * multiplier);
$('body').css({ fontSize: fontSize + 'px' });
}
$(window).resize(function () { fontSize(); });
function getActiveUsers() {
$.ajax({
crossDomain: true,
type: "GET",
contentType: "application/json;charset=utf-8",
url: "http://localhost/Exp/hs/DevelPlatform/Users",
dataType: "json",
success: function (queryResult) {
$("#activeUsersValue").text(queryResult.ActiveUsers - 1);
},
error: function (xhr, ajaxOptions, thrownError) {
$("#activeUsersValue").text("---");
}
});
}
</script>
<base
href="/redirect.php?url=djhjb25maWc6Ly8yMzg3MjViZS1lNWM2LTQ4NTktOTZjZi1mM2U2MWE5NDE1MzYvbWRvYmplY3QvaWQyZjE1ZWEyZi0xNTk1LTRlOGUtOTQ5MS0yNmZkYWUyN2RkZTgvOGViNGZhZDEtMWZhNi00MDNlLTk3MGYtMmMxMmRiYjQzZTIz">
</head>
<body style="font-size: 162px;">
<div class="parent">
<div class="block">
<p id="activeUsersValue" class="hit-the-floor">---</p>
</div>
</div>
</body>
</html>
Также добавлена общая форма "ВиджетАктивныеСеансы" с помещенным на нее полем HTML-документа, в которое будет помещаться содержимое виджета. Эта форма добавлена в рабочую область начальной страницы, чтобы при запуске сеанса пользователя виджет сразу же открывался. Ничего особенного в ней нет, только полей HTML-документа (см. выше).
Теперь рассмотрим подробнее каждый из способов.
Фоновые задания
Механизм фоновых заданий предназначен для асинхронного выполнения каких-либо операций. Этот механизм используется повсеместно. В конфигурации "Библиотека стандартных подсистем"реализована подсистема "Длительные операции", предназначенная для запуска каких-либо операций в фоновых заданиях. В свою очередь БСП внедрена практически во все новые конфигурации от фирмы "1С", поэтому использовать ее можно без особых проблем. На Инфостарте можно посмотреть пример использования этой подсистемы.
Мы реализуем собственный функционал по выполнению асинхронных операций в фоновых заданиях, потому что внедрять для демонстрации примером БСП было бы не разумно =). Но для рабочих задач БСП конечно же правильный выбор.
Запуск и отслеживание
Для запуска и отслеживания запущенных фоновых заданий был реализован небольшой функционал. В конфигурацию добавлены четыре общих модуля:
#Если Клиент Тогда
// Функция - Вызвать асинхронно функцию
//
// Параметры:
// ИмяФункции - Строка - Обязательный. Имя запускаемой функции
// Варианты:
// - Функция модуля менеджера, <ВидОбъекта>.<ИмяОбъекта>.<ИмяФункции>
// - Функция неглобального серверного общего модуля, <ОбщийМодуль>.<ИмяФункции>
// - Функция глобального серверного общего модуля, <ИмяФункции>
// Параметры - Массив - Необязательный. Массив параметров передаваемый в запускаемую функцию
// Форма - Форма - Необязательный. Вызывающая форма, необходимо указывать если вы используете обработчик завершения
// или обработчик хода выполнения
// ИмяОбработчикаЗавершения - Строка - Необязательный. Имя экспортной процедуры модуля вызывающей формы
// Параметры:
// - АсинхронныйВызов (Структура) - структура содержащая описание асинхронного вызова
// - Состояние (Строка) - состояние асинхронного вызова (Завершен,Отменен,ЗавершенСОшибкой)
// - ВозвращенноеЗначение (Произвольный) - значение возвращенное запущенной функцией (Состояние=Завершен)
// - ОписаниеОшибки (Строка) - описание произошедшей ошибки (Состояние=ЗавершенСОшибкой)
// ИмяОбработчикаХодаВыполнения - Строка - Необязательный. Имя экспортной процедуры модуля вызывающей формы
// Параметры:
// - АсинхронныйВызов (Структура) - структура содержащая описание асинхронного вызова
// - ЗначениеХодаВыполнения (Произвольный) - новое значение хода выполнения
// - СообщенияХодаВыполнения (Массив) - новые сообщения хода выполнения
// Контекст - Произвольный - Необязательный. Произвольное значение сохраняемое в структуре асинхронного вызова в свойстве с именем Контекст
// АвтоотменаЧерез - Число - Необязательный. Количество секунд после которого асинхронное выполение функции будет автоматически отменено
// Возвращаемое значение:
// Структура - Структура содержащая описание асинхронного вызова
Функция ВызватьФункцию(ИмяФункции, Параметры = Неопределено, Форма = Неопределено, ИмяОбработчикаЗавершения = Неопределено) Экспорт
Возврат АсинхронныеВызовыКлиент.ВызватьФункциюИлиПроцедуру(ИмяФункции, Истина, Параметры, Форма, ИмяОбработчикаЗавершения);
КонецФункции
#КонецЕсли
Функция ВызватьФункциюИлиПроцедуру(ИмяФункцииИлиПроцедуры, ЭтоФункция, Параметры = Неопределено, Форма = Неопределено, ИмяОбработчикаЗавершения = Неопределено) Экспорт
Перем ПериодичностьПроверкиСостоянийАсинхронныхВызовов, ИдентификаторВызова, АсинхронныйВызов;
ПериодичностьПроверкиСостоянийАсинхронныхВызовов = 1;
Если Параметры = Неопределено Тогда
Параметры = Новый Массив;
КонецЕсли;
Если АктивныеАсинхронныеОперации = Неопределено Тогда
АктивныеАсинхронныеОперации = Новый Соответствие;
КонецЕсли;
ИдентификаторВызова = АсинхронныеВызовыСервер.ВызватьФункциюИлиПроцедуру(ИмяФункцииИлиПроцедуры, ЭтоФункция, Параметры);
АсинхронныйВызов = Новый Структура;
АсинхронныйВызов.Вставить("Идентификатор", ИдентификаторВызова);
АсинхронныйВызов.Вставить("ИмяФункцииИлиПроцедуры", ИмяФункцииИлиПроцедуры);
АсинхронныйВызов.Вставить("ЭтоФункция", ЭтоФункция);
АсинхронныйВызов.Вставить("Параметры", Параметры);
АсинхронныйВызов.Вставить("Форма", Форма);
АсинхронныйВызов.Вставить("ИмяОбработчикаЗавершения", ИмяОбработчикаЗавершения);
АсинхронныйВызов.Вставить("НачатВ", ТекущаяУниверсальнаяДатаВМиллисекундах());
АсинхронныйВызов.Вставить("Состояние", "Активен");
Если ЭтоФункция Тогда
АсинхронныйВызов.Вставить("ВозвращенноеЗначение", Неопределено);
КонецЕсли;
АсинхронныйВызов.Вставить("ЗначениеХодаВыполнения", Неопределено);
АсинхронныйВызов.Вставить("СообщенияХодаВыполнения", Новый Массив());
АсинхронныйВызов.Вставить("ОписаниеОшибки", Неопределено);
АктивныеАсинхронныеОперации.Вставить(ИдентификаторВызова, АсинхронныйВызов);
ПодключитьОбработчикОжидания("ОбработчикПроверкиСостоянийАсинхронныхВызовов", ПериодичностьПроверкиСостоянийАсинхронныхВызовов);
Возврат АсинхронныйВызов;
КонецФункции
Процедура ПроверитьСостоянияВызовов() Экспорт
Перем ИдентификаторыВызовов, АсинхронныйВызовКлючЗначение, СостоянияВызовов, ИдентификаторВызова, СВ, АВ, ДлительностьВызова;
ИдентификаторыВызовов = Новый Массив;
Для Каждого АсинхронныйВызовКлючЗначение Из АктивныеАсинхронныеОперации Цикл
ИдентификаторыВызовов.Добавить(АсинхронныйВызовКлючЗначение.Ключ);
КонецЦикла;
СостоянияВызовов = АсинхронныеВызовыСервер.ПолучитьСостоянияВызовов(ИдентификаторыВызовов);
Для Каждого СостояниеВызоваКлючЗначение Из СостоянияВызовов Цикл
ИдентификаторВызова = СостояниеВызоваКлючЗначение.Ключ;
СВ = СостояниеВызоваКлючЗначение.Значение; //СостояниеВызова
АВ = АктивныеАсинхронныеОперации.Получить(ИдентификаторВызова); //АсинхронныйВызов
//Обновляем состояние асинхронного вызова
АВ.Состояние = СВ.Состояние;
Если АВ.ЭтоФункция Тогда
АВ.ВозвращенноеЗначение = СВ.ВозвращенноеЗначение;
КонецЕсли;
АВ.ОписаниеОшибки = СВ.ОписаниеОшибки;
Если СВ.Состояние <> "Активен" Тогда
АктивныеАсинхронныеОперации.Удалить(ИдентификаторВызова);
Если АВ.Форма <> Неопределено И ЗначениеЗаполнено(АВ.ИмяОбработчикаЗавершения) Тогда
Выполнить("АВ.Форма." + АВ.ИмяОбработчикаЗавершения + "(АВ, АВ.Состояние," + ?(АВ.ЭтоФункция, "АВ.ВозвращенноеЗначение, ", " ") + "АВ.ОписаниеОшибки);");
КонецЕсли;
КонецЕсли;
КонецЦикла;
Если АктивныеАсинхронныеОперации.Количество() = 0 Тогда
ОтключитьОбработчикОжидания("ОбработчикПроверкиСостоянийАсинхронныхВызовов");
КонецЕсли;
КонецПроцедуры
АсинхронныеВызовыКлиентГлобальный
Процедура ОбработчикПроверкиСостоянийАсинхронныхВызовов() Экспорт
АсинхронныеВызовыКлиент.ПроверитьСостоянияВызовов();
КонецПроцедуры
Функция ВызватьФункциюИлиПроцедуру(ИмяФункцииИлиПроцедуры, ЭтоФункция, Параметры) Экспорт
Перем ПараметрыФЗ, НаименованиеФоновогоЗадания, ФЗ, ИдентификаторВызова;
ПараметрыФЗ = Новый Массив;
ПараметрыФЗ.Добавить(ИмяФункцииИлиПроцедуры);
ПараметрыФЗ.Добавить(ЭтоФункция);
ПараметрыФЗ.Добавить(Параметры);
НаименованиеФоновогоЗадания = "Асинхронный вызов " + ?(ЭтоФункция, "функции", "процедуры") + " " + ИмяФункцииИлиПроцедуры;
ФЗ = ФоновыеЗадания.Выполнить("АсинхронныеВызовыСервер.ВызватьФункциюИлиПроцедуруФЗ", ПараметрыФЗ, ,НаименованиеФоновогоЗадания);
ИдентификаторВызова = ФЗ.УникальныйИдентификатор;
Возврат ИдентификаторВызова;
КонецФункции
Процедура ВызватьФункциюИлиПроцедуруФЗ(ИмяФункцииИлиПроцедуры, ЭтоФункция, Параметры) Экспорт
Перем СтрокаНаВыполнение, ИндексПараметра, ВозвращенноеЗначение, СообщениеПользователю;
СтрокаНаВыполнение = "";
Если ЭтоФункция Тогда
СтрокаНаВыполнение = "ВозвращенноеЗначение = ";
КонецЕсли;
СтрокаНаВыполнение = СтрокаНаВыполнение + ИмяФункцииИлиПроцедуры + "(";
Для ИндексПараметра = 0 По Параметры.Количество() - 2 Цикл
СтрокаНаВыполнение = СтрокаНаВыполнение + "Параметры[" + ИндексПараметра + "], "
КонецЦикла;
Если Параметры.Количество() > 0 Тогда
СтрокаНаВыполнение = СтрокаНаВыполнение + "Параметры[" + (Параметры.Количество()-1) + "]"
КонецЕсли;
СтрокаНаВыполнение = СтрокаНаВыполнение + ");";
ВозвращенноеЗначение = Неопределено;
Выполнить(СтрокаНаВыполнение);
Если ЭтоФункция Тогда
СообщениеПользователю = Новый СообщениеПользователю;
СообщениеПользователю.Текст = ЗначениеВСтрокуВнутр(ВозвращенноеЗначение);
СообщениеПользователю.Поле = "АсинхронныйВызов.ВозвращенноеЗначение";
СообщениеПользователю.Сообщить();
КонецЕсли;
КонецПроцедуры
Функция ПолучитьСостоянияВызовов(ИдентификаторыВызовов) Экспорт
Перем СостоянияВызовов, ТекущийИдентификаторВызова, СостояниеВызова, ФоновоеЗадание, ТекущееСообщение, МассивСообщений;
СостоянияВызовов = Новый Соответствие;
Для Каждого ТекущийИдентификаторВызова Из ИдентификаторыВызовов Цикл
СостояниеВызова = Новый Структура("Состояние, ВозвращенноеЗначение, ОписаниеОшибки");
ФоновоеЗадание = ФоновыеЗадания.НайтиПоУникальномуИдентификатору(ТекущийИдентификаторВызова);
Если ФоновоеЗадание = Неопределено Тогда
СостояниеВызова.Состояние = Неопределено;
Иначе
Если ФоновоеЗадание.Состояние = СостояниеФоновогоЗадания.Активно Тогда
СостояниеВызова.Состояние = "Активен";
Иначе
СостояниеВызова.Состояние = "Завершен";
КонецЕсли;
МассивСообщений = ФоновоеЗадание.ПолучитьСообщенияПользователю(Истина);
ЗначениеХодаВыполненияСтрокой = Неопределено;
Если МассивСообщений <> Неопределено Тогда
Для Каждого ТекущееСообщение Из МассивСообщений Цикл
Если ТекущееСообщение.Поле = "АсинхронныйВызов.ВозвращенноеЗначение" Тогда
СостояниеВызова.ВозвращенноеЗначение = ЗначениеИзСтрокиВнутр(ТекущееСообщение.Текст);
КонецЕсли;
КонецЦикла;
КонецЕсли;
КонецЕсли;
СостоянияВызовов.Вставить(ТекущийИдентификаторВызова, СостояниеВызова);
КонецЦикла;
Возврат СостоянияВызовов;
КонецФункции
Функция ПолучитьКоличествоАктивныхСеансов() Экспорт
// Получаем количество активных сеансов
КоличествоАктивныхСеансов = 0;
Попытка
ТекущиеСоединения = ПолучитьСеансыИнформационнойБазы();
КоличествоАктивныхСеансов = ТекущиеСоединения.Количество();
Исключение
КоличествоАктивныхСеансов = -1;
КонецПопытки;
Возврат КоличествоАктивныхСеансов;
КонецФункции
В модуле "АсинхронныеВызовыСервер" находятся процедуры и функции для непосредственного запуска фоновых заданий и проверки их состояний. Все остальные модули реализуют взаимодействие с фоновыми заданиями с клиентской стороны: запуск, проверка состояния, запуск клиентского метода по завершению фонового задания.
Для отслеживания состояния запущенных фоновых заданий в модуль управляемого приложения была добавлена экспортная переменная:
Перем АктивныеАсинхронныеОперации Экспорт;
Переменная инициализируется как массив, куда при запуске операции добавляется объект "Фоновое задание".
При открытии формы виджета выполняется клиентская процедура:
&НаКлиенте
Процедура ОбновитьКоличествоАктивныхСеансовНачало() Экспорт
ТекущаяОперация = АсинхронныеВызовы.ВызватьФункцию(
// Функция, запускаемая в фоновом задании
"ВиджетыСервер.ПолучитьКоличествоАктивныхСеансов",
// Доп. параметры, у нас они не используются
,
// Текущая форма вызова
ЭтаФорма,
// Клиентская экспортная процедура, выполняемая
// после завершения фонового задания
"ОбновитьКоличествоАктивныхСеансовНачалоЗавершение");
КонецПроцедуры
После вызова асинхронной функции запускается глобальный обработчик ожидания, проверяющий текущее состояние всех запущенных фоновых заданий. Когда фоновое задание завершает свою работу, обработчик запускает ту клиентскую процедуру, имя которой мы указали в последнем параметре. Эта процедура должна располагаться в передаваемой форме. Листинг нашей завершающей процедуры следующий:
&НаКлиенте
Процедура ОбновитьКоличествоАктивныхСеансовНачалоЗавершение(АсинхронныйВызов,
Состояние, ВозвращенноеЗначение, ОписаниеОшибки) Экспорт
// Если состояние "Завершен", значит фоновое задание отработало без ошибок
// и вернуло корректное значение. В противном случае оставляем значение
// по умолчанию
КоличествоАктивныхСеансов = "---";
Если Состояние = "Завершен" Тогда
КоличествоАктивныхСеансов = ВозвращенноеЗначение;
КонецЕсли;
// Заменяем часть разметки страницы, подставляя туда
// полученное значение из фонового задания
НовоеЗначениеТекстаВиджета = ТекстШаблонаВиджета;
НачалоРазделаСкриптовСтрока = "<!--Раздел скриптов - Начало-->";
КонецРазделаСкриптовСтрока = "<!--Раздел скриптов - Конец-->";
НачалоРазделаЗначениеСтрока = "<!--Активные пользователи - Начало-->";
КонецРазделаЗначениеСтрока = "<!--Активные пользователи - Конец-->";
НачалоРазделаСкриптов = СтрНайти(НовоеЗначениеТекстаВиджета, НачалоРазделаСкриптовСтрока);
КонецРазделаСкриптов = СтрНайти(НовоеЗначениеТекстаВиджета, КонецРазделаСкриптовСтрока);
НовоеЗначениеТекстаВиджета = Сред(НовоеЗначениеТекстаВиджета, 1, НачалоРазделаСкриптов-1)
+ Сред(НовоеЗначениеТекстаВиджета, КонецРазделаСкриптов+СтрДлина(КонецРазделаСкриптовСтрока), СтрДлина(НовоеЗначениеТекстаВиджета)-КонецРазделаСкриптов+1);
НачалоРазделаЗначение = СтрНайти(НовоеЗначениеТекстаВиджета, НачалоРазделаЗначениеСтрока);
КонецРазделаЗначение = СтрНайти(НовоеЗначениеТекстаВиджета, КонецРазделаЗначениеСтрока);
НовоеЗначениеТекстаВиджета = Сред(НовоеЗначениеТекстаВиджета, 1, НачалоРазделаЗначение-1) +
Строка(КоличествоАктивныхСеансов)
+ Сред(НовоеЗначениеТекстаВиджета, КонецРазделаЗначение+СтрДлина(КонецРазделаЗначениеСтрока), СтрДлина(НовоеЗначениеТекстаВиджета)-КонецРазделаЗначение+1);
// Передаем сформированную HTML-разметку в поле HTML-документа на форме
АктивныеСеансыВиджет = НовоеЗначениеТекстаВиджета;
// Подключаем обработчик ожидания для повторного запуска
// асинхронной операции
ПодключитьОбработчикОжидания("ОбновитьКоличествоАктивныхСеансовНачало", 5, Истина);
КонецПроцедуры
Таким образом будет выполняться асинхронный запуск серверной процедуры "ПолучитьКоличествоАктивныхСеансов()", а ее по завершению операции полученное значение передано обратно на клиент.
Плюсы и минусы
За:
- Относительная простота реализации
- Работоспособность при любом режиме запуска (тонкий клиент, толстый клиент, веб-клиент, обычное и управляемое приложение).
Против:
- Иногда фоновые задания перестают стабильно работать. Зависит от многих факторов (версия платформы, настройки сервера и др.). Большинства проблем с зависанием можно избежать, но с этим вопросом нужно работать. То есть усложняется сопровождение.
- Будет появляться большое количество фоновых заданий. Нужно аккуратно писать код для них, чтобы избежать зависания сеансов (ставить таймауты на соединения, корректно обрабатывать исключения и др.).
AJAX
AJAX (Asynchronous Javascript and XML) - подход к построению интерактивных пользовательских интерфейсов веб-приложений, заключающийся в «фоновом» обмене данными браузера с веб-сервером. У нас, конечно, не веб-приложение, но частично применить этот подход все же возможно. Например, в одной из прошлых статей мы уже рассматривали пример создания и использования HTTP-сервиса и там тоже был AJAX.
HTTP-сервис
Основой для работы виджета является HTTP-сервис, возвращающий необходимые данные. Он достаточно простой, никаких сложных алгоритмов и настроек. Для его создания нужно выполнить следующие шаги:
- Добавляем HTTP-сервис и настраиваем корневой URL
- Создаем шаблон URL
- Добавляем GET-метод
- Публикуем базу
Листинг обработчика GET-метода приведен ниже:
Функция ActiveUsersget(Запрос)
УстановитьПривилегированныйРежим(Истина);
Ответ = Новый HTTPСервисОтвет(200);
КоличествоАктивныхПользователей = ВиджетыСервер.ПолучитьКоличествоАктивныхСеансов();
// Формируем ответ в формате JSON
ЗаписьJSON = Новый ЗаписьJSON;
ЗаписьJSON.ЗаписатьНачалоОбъекта();
ЗаписьJSON.ЗаписатьИмяСвойства("ActiveUsers");
ЗаписьJSON.ЗаписатьЗначение(КоличествоАктивныхПользователей);
ЗаписьJSON.ЗаписатьКонецОбъекта();
СтрокаJSON = ЗаписьJSON.Закрыть();
Ответ.УстановитьТелоИзСтроки(СтрокаJSON, "UTF-8");
Возврат Ответ;
КонецФункции
И все! Далее создаем сам виджет.
Поле HTML
Виджет будет представлять собой область, в центре которой число активных сеансов в информационной базе. Во время выполнения асинхронного запроса к HTTP-сервису будет показана дополнительная анимация. Все это реализовано с помощью HTML-разметки, которая будет помещена в поле HTML-документа в форме конфигурации.
Как и в случае с реализацией виджета при помощи фоновых заданий, HTML-разметка виджета хранится в том же общем макете и в момент создания формы помещается в поле HTML-документа. Только теперь получение количества активных пользователей выполняется не фоновым заданием, а при помощи AJAX-запроса самой страницы, направленного ранее созданному к HTTP-сервису.
Вот так выглядит синтаксис AJAX-запроса на странице:
$.ajax({
crossDomain: true,
type: "GET",
contentType: "application/json;charset=utf-8",
url: "http://localhost/Exp/hs/DevelPlatform/Users",
dataType: "json",
success: function (queryResult) {
$("#activeUsersValue").text(queryResult.ActiveUsers - 1);
},
error: function (xhr, ajaxOptions, thrownError) {
$("#activeUsersValue").text("---");
}
});
Для упрощения адрес указан явно и не настраивается. Если это задача для рабочего окружения, то адрес сервиса обязательно нужно задавать параметрами.
HTTP-сервис возвращает нам JSON-объект с единственным свойством "ActiveUsers". В событии "success", при успешном выполнении запроса, извлекается полученное значение и присваивается элементу <p> на веб-странице. При возникновении ошибок в качестве значения будет присвоена строка "---".
Именно эта реализация демонстрируется на анимации раздела "Подготовка" в самом начале статьи.
Плюсы и минусы
За:
- Никаких фоновых заданий и серверных вызовов платформы. Только AJAX-запросы к HTTP-сервису средствами JS, причем запросы непосредственно от клиента.
- Можно уменьшить время обновления значений на виджете до минимума, хоть 0.1 секунды. С фоновыми заданиями и обработчиками ожидания трудно добиться режима "онлайн" из-за особенностей реализации.
- Это не 1С! =)
Против:
- Поле HTML-документа это нечто особенное. Платформа 1С:Предприятие использует его в качестве обрезанной версии браузера, никакой полноценной поддержки HTML5, SVG, CANVAS и прочего. Но самое главное - мне так и не удалось заставить платформу отправлять AJAX-запросы в режиме тонкого или толстого клиента! =) Это просто невероятно и странно. Может кто уже сталкивался и знает ответ на вопрос как заставить платформу отправить AJAX-запрос из поля HTML-документа, обойдя ошибку "No transport"? Конечно, ситуация изменилась с появлением WebKit, но он доступен только на свежих версиях платформы. Но само это событие уже хорошо. Если у вам доступен WebKit, то этот пункт можно не учитывать.
- Это не 1С! =)
- Есть вероятность, что с выходом новой версии платформы поле HTML-документа будет работать по другому и все придется переделывать.
- В толстом и тонком клиентах не отображаются корректно CSS-стили, а также не всегда нормальным образом отрабатывают JS-скрипты. Можно потанцевать с бубном и добиться результата, но я не стал этим заниматься.
Выводы
Лучшим вариантом все же является использование фоновых заданий, ведь работа виджетов в тонком и толстом клиенте критична в большинстве случаев. С появлением WebKit использование AJAX в поле HTML-документа становится возможной, поэтому виджеты с его использованием будут очень эффективными и функциональными.
Предложенные варианты не единственные и не всегда могут подойти. Делитесь своим опытом, будет интересно узнать!
А как виджеты делайте Вы?
Другие ссылки