При разработке в 1С часто возникает необходимость выполнять длительные операции, чтобы не блокировать интерфейс пользователя. Для этого идеально подходят фоновые задания. Однако возникает проблема: как сообщить пользователю о результате выполнения или о ходе процесса, если фоновое задание выполняется на сервере и не имеет прямого доступа к интерфейсу? Стандартный метод Сообщить() в этом случае не сработает. Разберем, почему так происходит и как правильно организовать взаимодействие между фоновым заданием и клиентом.
Чтобы понять причину, проанализируем архитектуру клиент-серверного взаимодействия в 1С. Когда пользователь запускает фоновое задание, на сервере 1С создается новый, обособленный сеанс. Этот сеанс не имеет никакой связи с сеансом или интерфейсом конкретного пользователя, который его инициировал. У него нет "контекста формы" или "контекста клиента".
Метод Сообщить() является процедурой общего модуля с установленным флагом "Клиент (управляемое приложение)". Он предназначен для вывода сообщений в окно сообщений именно на стороне клиента. Фоновое задание, работая на сервере в своем изолированном сеансе, просто не знает, какому из десятков или сотен активных пользователей нужно отправить это сообщение. Поэтому вызов Сообщить() из фонового задания просто игнорируется, не вызывая при этом ошибки.
Важный момент: любые другие методы, предназначенные для интерактивного взаимодействия с пользователем (например, Предупреждение(), Вопрос()), также не будут работать в серверном коде, выполняемом без контекста формы, и тем более в фоновых заданиях.
Классический и наиболее надежный способ решить эту задачу — использовать асинхронную модель взаимодействия. Суть ее в том, что клиент не ждет пассивно, а периодически опрашивает сервер о состоянии запущенной задачи. Для реализации этого нам понадобятся два ключевых механизма платформы:
ПоместитьВоВременноеХранилище() и ПолучитьИзВременногоХранилища().ПодключитьОбработчикОжидания() и ОтключитьОбработчикОжидания().Таким образом, общая схема работы выглядит так:
Сообщить().Рассмотрим весь процесс на конкретном примере. Предположим, у нас есть форма, на которой находится кнопка "Выполнить длительную операцию" (также можно использовать шаблон обработки для БСП).
Сначала подготовим команду на форме и связанные с ней переменные и процедуры. Нам нужно где-то хранить информацию о запущенном задании.
Добавим в реквизиты формы реквизит ИнформацияОЗадании с типом Структура.
Процедура, выполняемая по нажатию кнопки, будет выглядеть следующим образом:
&НаКлиенте
Процедура ВыполнитьДлительнуюОперацию(Команда)
// Подготовим структуру для хранения информации о задании
ИнформацияОЗадании = Новый Структура("АдресРезультата, ИдентификаторЗадания, Статус");
// Запускаем серверную процедуру, которая инициирует фоновое задание
СтатусЗапуска = ЗапуститьДлительнуюОперациюНаСервере();
Если СтатусЗапуска <> Неопределено Тогда
// Сохраняем идентификатор задания для последующей проверки
ИнформацияОЗадании.ИдентификаторЗадания = СтатусЗапуска.УникальныйИдентификатор;
// Подключаем обработчик ожидания, который будет проверять статус каждые 3 секунды
ПодключитьОбработчикОжидания("ОбработчикПроверкиЗадания", 3, Ложь);
Сообщить("Фоновое задание запущено. Ожидайте результата.");
Иначе
Сообщить("Не удалось запустить фоновое задание.");
КонецЕсли;
КонецПроцедуры
&НаСервереБезКонтекста
Функция ЗапуститьДлительнуюОперациюНаСервере()
Попытка
// Выполняем экспортную процедуру общего модуля в фоновом режиме
Задание = ФоновыеЗадания.Выполнить("МойОбщийМодуль.ВыполнитьДлительнуюРаботу",, "Длительная операция");
Возврат Задание;
Исключение
Возврат Неопределено;
КонецПопытки;
КонецФункции
Разберем код подробнее: В процедуре ВыполнитьДлительнуюОперацию мы сначала вызываем серверную функцию ЗапуститьДлительнуюОперациюНаСервере. Она, в свою очередь, использует метод ФоновыеЗадания.Выполнить(), который и запускает выполнение процедуры ВыполнитьДлительнуюРаботу из общего модуля в фоновом режиме. Если запуск успешен, мы получаем объект фонового задания, сохраняем его уникальный идентификатор и подключаем обработчик ожидания ОбработчикПроверкиЗадания, который будет срабатывать каждые 3 секунды.
Теперь создадим общий модуль (например, МойОбщийМодуль) с установленными флагами "Сервер" и "Вызов сервера". В нем разместим саму процедуру, которая будет выполняться фоново.
// Экспортная процедура в общем модуле
Процедура ВыполнитьДлительнуюРаботу() Экспорт
// Имитация долгой работы, например, цикл
Для сч = 1 По 10 Цикл
// Пауза 2 секунды, чтобы процесс не был мгновенным
РаботаВБезопасномРежиме.Пауза(2000);
КонецЦикла;
// Формируем результат работы
ТекстСообщения = "Длительная операция успешно завершена в " + ТекущаяДата();
// Помещаем результат во временное хранилище.
// Адрес этого хранилища автоматически станет результатом выполнения фонового задания.
ПоместитьВоВременноеХранилище(ТекстСообщения);
КонецПроцедуры
Ключевой момент здесь — вызов функции ПоместитьВоВременноеХранилище(). Мы передаем в нее данные, которые хотим вернуть на клиент (в нашем случае это строка). Платформа автоматически связывает адрес, возвращенный этой функцией, с объектом фонового задания после его успешного завершения. Этот адрес будет доступен в свойстве АдресРезультата.
Остался последний шаг — реализовать процедуру, которая будет проверять статус задания и забирать результат.
&НаКлиенте
Процедура ОбработчикПроверкиЗадания()
// Получаем актуальное состояние задания с сервера
Задание = ПолучитьСостояниеЗаданияНаСервере(ИнформацияОЗадании.ИдентификаторЗадания);
Если Задание = Неопределено Тогда
Сообщить("Не удалось найти фоновое задание. Проверка остановлена.");
ОтключитьОбработчикОжидания("ОбработчикПроверкиЗадания");
Возврат;
КонецЕсли;
// Проверяем состояние
Если Задание.Состояние = СостояниеФоновогоЗадания.Завершено Тогда
// Задание завершено, отключаем обработчик
ОтключитьОбработчикОжидания("ОбработчикПроверкиЗадания");
// Получаем результат из временного хранилища по адресу
Результат = ПолучитьИзВременногоХранилища(Задание.АдресРезультата);
// Выводим сообщение пользователю
Сообщить(Результат);
ИначеЕсли Задание.Состояние = СостояниеФоновогоЗадания.Ошибка Тогда
// Задание завершилось с ошибкой, отключаем обработчик
ОтключитьОбработчикОжидания("ОбработчикПроверкиЗадания");
// Выводим информацию об ошибке
Сообщить("Фоновое задание завершилось с ошибкой!");
Сообщить(Задание.ИнформацияОбОшибке.Описание);
КонецЕсли;
КонецПроцедуры
&НаСервереБезКонтекста
Функция ПолучитьСостояниеЗаданияНаСервере(ИдентификаторЗадания)
Возврат ФоновыеЗадания.НайтиПоУникальномуИдентификатору(ИдентификаторЗадания);
КонецФункции
Разберем код обработчика: процедура ОбработчикПроверкиЗадания вызывается каждые 3 секунды. Она обращается к серверу, чтобы получить актуальный объект фонового задания по его идентификатору. Далее она анализирует свойство Состояние. Если задание успешно завершено (СостояниеФоновогоЗадания.Завершено), мы отключаем обработчик, получаем результат из хранилища по адресу Задание.АдресРезультата и выводим его. Если произошла ошибка, мы также отключаем обработчик и сообщаем пользователю детали ошибки. Пока задание находится в состоянии Активно, ничего не происходит, и проверка повторится при следующем срабатывании таймера.
Описанный метод с обработчиком ожидания является универсальным и работает во всех версиях платформы 8.3. Однако он имеет недостаток — создается постоянный "шум" в виде периодических запросов от клиента к серверу. Для современных конфигураций существуют более технологичные альтернативы:
Несмотря на наличие новых технологий, метод с временным хранилищем и обработчиком ожидания остается актуальным, особенно для поддержания совместимости со старыми версиями платформы или когда нецелесообразно настраивать более сложные механизмы.