При разработке интерактивных обработок в среде «1С:Предприятие 8.3» программисты часто сталкиваются с задачами обеспечения плавности интерфейса при выполнении длительных операций. Типичный сценарий: пользователю необходимо показать красивый кастомный индикатор загрузки (прогресс-бар) во вспомогательной форме или быстро закрыть диалоговое окно выбора пути сохранения файла, не дожидаясь окончания долгой физической записи данных на диск.
В этой статье мы подробно разберем, как правильно спроектировать вызов второй формы для отображения прогресса, почему стандартные синхронные методы заставляют интерфейс «зависать», и как решить проблему блокирующего диалога сохранения файлов с помощью современных асинхронных механизмов платформы.
Рассмотрим ситуацию: у нас есть основная форма обработки, и мы хотим при нажатии кнопки открывать вторую форму, содержащую индикатор (шкалу или GIF-анимацию), которая бы динамически обновлялась по мере выполнения алгоритма.
Если мы попытаемся сделать это «в лоб» — открыть форму методом ОткрытьФорму() и запустить тяжелый цикл обработки данных в том же сеансе — мы столкнемся с проблемой: форма откроется, но внутри нее ничего не будет прорисовываться (вместо индикатора пользователь увидит белое окно или зависший интерфейс). Прорисованная форма отобразится только тогда, когда весь алгоритм полностью завершит свою работу. Это связано с тем, что в 1С клиентский поток однопоточный, и пока выполняется код, интерфейс не может обрабатывать системные сообщения перерисовки экрана.
Выясним, как обойти это ограничение. Для реализации плавной кастомной шкалы прогресса нам потребуется связать три компонента: клиентскую логику, фоновое задание на сервере и периодический опрос состояния (таймер).
Шаг 1. Создание фонового задания
Весь тяжелый код (чтение файлов, обработка строк, запись в базу данных) мы обязаны перенести во внесистемный поток — фоновое задание. Это позволит разгрузить клиентское приложение. Код фонового задания должен находиться в общем модуле с установленными свойствами «Сервер» и «Вызов сервера».
Для передачи информации о прогрессе обратно на клиент мы будем периодически записывать текущее состояние во временное хранилище. Посмотрим на пример серверной процедуры:
Процедура ВыполнитьДлительнуюЗагрузкуВФоне(АдресРезультата, ПараметрыЗагрузки) Экспорт
// Предположим, нам нужно обработать массив данных
ВсегоШагов = ПараметрыЗагрузки.Количество();
Для Индекс = 0 По ВсегоШагов - 1 Цикл
// Выполняем полезную работу
ОбработатьСтрокуДанных(ПараметрыЗагрузки[Индекс]);
// Каждые N шагов или на каждом шаге обновляем прогресс
Процент = Округление((Индекс + 1) * 100 / ВсегоШагов);
СостояниеЗагрузки = Новый Структура;
СостояниеЗагрузки.Вставить("Процент", Процент);
СостояниеЗагрузки.Вставить("Статус", "Обработано " + (Индекс + 1) + " из " + ВсегоШагов);
СостояниеЗагрузки.Вставить("Завершено", Ложь);
ПоместитьВоВременноеХранилище(СостояниеЗагрузки, АдресРезультата);
КонецЦикла;
// Фиксируем финальное завершение
СостояниеЗагрузки = Новый Структура;
СостояниеЗагрузки.Вставить("Процент", 100);
СостояниеЗагрузки.Вставить("Статус", "Загрузка успешно завершена!");
СостояниеЗагрузки.Вставить("Завершено", Истина);
ПоместитьВоВременноеХранилище(СостояниеЗагрузки, АдресРезультата);
КонецПроцедуры
Шаг 2. Разработка формы прогресс-бара
Создадим новую вспомогательную форму в нашей обработке и назовем ее ФормаИндикатора. На этой форме разместим:
ИндикаторПрогресса (тип: Число, длина 3, отображается на форме в виде поля индикатора).НадписьСтатуса для текстового вывода текущего действия.ИдентификаторЗадания (тип: УникальныйИдентификатор) и АдресХранилища (тип: Строка).В модуле этой формы настроим обработчик ожидания, который будет регулярно опрашивать временное хранилище:
&НаКлиенте
Процедура ПриОткрытии(Отказ)
// Запускаем периодический опрос сервера раз в 1 секунду
ПодключитьОбработчикОжидания("ПроверитьСостояниеВыполнения", 1);
КонецПроцедуры
&НаКлиенте
Процедура ПроверитьСостояниеВыполнения()
ДанныеПрогресса = ПолучитьДанныеССервера(АдресХранилища);
Если ДанныеПрогресса <> Неопределено Тогда
ЭтаФорма.ИндикаторПрогресса = ДанныеПрогресса.Процент;
Элементы.НадписьСтатуса.Заголовок = ДанныеПрогресса.Статус;
Если ДанныеПрогресса.Завершено Тогда
ОтключитьОбработчикОжидания("ПроверитьСостояниеВыполнения");
ПоказатьПредупреждение(, "Процесс успешно завершен!");
Закрыть();
КонецЕсли;
КонецЕсли;
КонецПроцедуры
&НаСервереБезКонтекста
Функция ПолучитьДанныеССервера(Знач АдресХранилища)
Возврат ПолучитьИзВременногоХранилища(АдресХранилища);
КонецФункции
Шаг 3. Запуск процесса из основной формы
Теперь свяжем всё воедино. На основной форме обработки при нажатии на кнопку запуска мы подготавливаем уникальный адрес во временном хранилище, программно инициализируем фоновое задание и сразу открываем форму индикатора, передавая ей параметры:
&НаКлиенте
Процедура ЗапуститьПроцесс(Команда)
// Подготовка хранилища для обмена информацией
АдресРезультата = ПоместитьДанныеВоВременноеХранилищеНаСервере();
// Запуск фонового задания на сервере
ПараметрыЗапуска = Новый Массив; // Сюда складываем нужные для алгоритма данные
ИдентификаторЗадания = ЗапуститьФоновоеЗаданиеНаСервере(АдресРезультата, ПараметрыЗапуска);
// Открываем форму индикатора и передаем ей параметры связи
ПараметрыФормы = Новый Структура;
ПараметрыФормы.Вставить("АдресХранилища", АдресРезультата);
ПараметрыФормы.Вставить("ИдентификаторЗадания", ИдентификаторЗадания);
ОткрытьФорму("Обработка.МояОбработка.Форма.ФормаИндикатора", ПараметрыФормы, ЭтаФорма);
КонецПроцедуры
&НаСервере
Функция ПоместитьДанныеВоВременноеХранилищеНаСервере()
Возврат ПоместитьВоВременноеХранилище(Неопределено, ЭтаФорма.УникальныйИдентификатор);
КонецФункции
&НаСервереБезКонтекста
Функция ЗапуститьФоновоеЗаданиеНаСервере(Знач АдресРезультата, Знач ПараметрыЗапуска)
ИмяМетода = "ОбщийМодульФоновыеЗадачи.ВыполнитьДлительнуюЗагрузкуВФоне";
ПараметрыПроцедуры = Новый Массив;
ПараметрыПроцедуры.Добавить(АдресРезультата);
ПараметрыПроцедуры.Добавить(ПараметрыЗапуска);
Задание = ФоновыеЗадания.Выполнить(ИмяМетода, ПараметрыПроцедуры, , "Загрузка данных обработки");
Возврат Задание.УникальныйИдентификатор;
КонецФункции
Проанализировав данную схему, мы увидим, что интерфейс остается отзывчивым: вспомогательная форма плавно перерисовывается, пользователь видит бегущие проценты, а основное приложение не зависает.
Совет по Библиотеке Стандартных Подсистем (БСП): Если ваша конфигурация работает на БСП, вам не обязательно вручную писать обработчики ожидания и создавать кастомную форму. Рекомендуется использовать встроенную подсистему «Длительные операции» и метод ДлительныеОперацииКлиент.ОжидатьЗавершение(), который берет на себя все заботы по выводу анимированного окна ожидания.
Перейдем ко второму вопросу, поднятому на форуме: почему при сохранении файла окно выбора пути закрывается только после того, как физический процесс записи на диск полностью завершится?
Причина кроется в синхронном выполнении кода в обработчике выбора файла. Когда пользователь нажимает кнопку «Сохранить» в стандартном диалоговом окне, управление немедленно возвращается в ту же процедуру, где сразу же начинается ресурсоемкое формирование и сохранение файла. Пока этот поток занят записью байтов на локальный диск или в сетевую папку, операционная система не успевает обработать визуальное событие закрытия окна диалога выбора файлов. Как результат — окно выбора «висит» открытым и разблокируется только тогда, когда файл уже записан.
Выясним, как сделать так, чтобы диалог закрывался мгновенно, а долгий процесс записи выполнялся в фоновом режиме.
Для реализации этой логики мы применим современный паттерн асинхронного программирования 1С Асинх / Ждать (доступный в платформе начиная с версии 8.3.18) в сочетании с фоновой сборкой файла на сервере.
Пошаговый алгоритм решения проблемы:
Ждать, мы приостанавливаем выполнение нашей процедуры, отдавая приоритет платформе на закрытие окна. Как только путь выбран, окно закрывается мгновенно, а код продолжает работу.Рассмотрим детальный пример реализации асинхронного сохранения файлов:
&НаКлиенте
Асинх Процедура СохранитьВыгрузкуАсинхронно(Команда)
// 1. Инициализация диалога выбора файла
ДиалогВыбора = Новый ДиалогВыбораФайла(РежимДиалогаВыбораФайла.Сохранение);
ДиалогВыбора.Фильтр = "Файлы XML (*.xml)|*.xml|Все файлы (*.*)|*.*";
ДиалогВыбора.Заголовок = "Выберите путь для сохранения файла выгрузки";
// Асинхронный вызов диалога выбора
ВыбранныеФайлы = Ждать ДиалогВыбора.ВыбратьАсинх();
Если ВыбранныеФайлы = Неопределено Тогда
Возврат; // Пользователь отказался от выбора
КонецЕсли;
// В этот момент диалог выбора файла ПОЛНОСТЬЮ закрывается и исчезает с экрана
ИмяФайлаНаДиске = ВыбранныеФайлы[0];
// 2. Подготовка и запуск фонового формирования файла на сервере
АдресРезультата = ПоместитьДанныеВоВременноеХранилищеНаСервере();
// Открываем форму индикатора прогресса (как в Части 1)
ПараметрыИндикатора = Новый Структура;
ПараметрыИндикатора.Вставить("АдресХранилища", АдресРезультата);
ФормаПрогресса = ОткрытьФорму("Обработка.МояОбработка.Форма.ФормаИндикатора", ПараметрыИндикатора, ЭтаФорма);
// Запускаем серверный процесс формирования файла
Ждать ВыполнитьФормированиеФайлаВФоне(АдресРезультата);
// После завершения фонового задания закрываем индикатор
Если ФормаПрогресса.Открыта() Тогда
ФормаПрогресса.Закрыть();
КонецЕсли;
// 3. Получаем сформированный файл из временного хранилища на клиент
// Используем неблокирующий асинхронный метод получения файла
Попытка
Ждать ПолучитьФайлССервераАсинх(АдресРезультата, ИмяФайлаНаДиске);
ПоказатьОповещениеПользователя("Успех",, "Файл успешно сохранен: " + ИмяФайлаНаДиске);
Исключение
ВызватьИсключение "Ошибка при записи файла на диск компьютера: " + ПодробноеПредставлениеОшибки(ИнформацияОбОшибке());
КонецПопытки;
КонецПроцедуры
На стороне сервера в общем модуле или модуле менеджера обработки мы реализуем процедуру сборки файла, которая вызывается фоновым заданием:
Процедура СформироватьФайлНаСервере(АдресРезультата) Экспорт
// Долгая операция генерации контента (например, выгрузка данных)
ДеревоДанных = ПолучитьДанныеДляВыгрузки();
// Создаем временный файл на сервере
ИмяВременногоФайла = ПолучитьИмяВременногоФайла("xml");
ЗаписьXML = Новый ЗаписьXML;
ЗаписьXML.ОткрытьФайл(ИмяВременногоФайла, "UTF-8");
// Пишем данные...
ЗаписьXML.ЗаписатьОбъявлениеXML();
// Имитируем долгую запись в цикле с передачей прогресса во временное хранилище
// ...
ЗаписьXML.Закрыть();
// Преобразуем готовый файл в ДвоичныеДанные
ДанныеФайла = Новый ДвоичныеДанные(ИмяВременногоФайла);
// Помещаем результат работы в хранилище, откуда его заберет клиент
ПоместитьВоВременноеХранилище(ДанныеФайла, АдресРезультата);
// Удаляем временный файл с сервера
УдалитьФайлы(ИмяВременногоФайла);
КонецПроцедуры
Использование связки асинхронного диалога ВыбратьАсинх(), сборки данных в фоновом задании на сервере и последующего неблокирующего скачивания через ПолучитьФайлССервераАсинх() позволяет полностью избавиться от «зависания» интерфейса. Диалоговое окно закрывается сразу после нажатия кнопки сохранения, а весь длительный процесс записи плавно визуализируется перед пользователем на кастомной форме индикатора прогресса.