При работе с большими объемами данных в управляемых формах 1С:Предприятие разработчики часто сталкиваются с неприятным эффектом: при выполнении длительных операций на клиенте интерфейс приложения "зависает", окно окрашивается в серый цвет, а стандартный индикатор процесса (анимированный спиннер или "кошак") замирает. Пользователю кажется, что программа сломалась, и он завершает процесс аварийно.
В этой статье мы подробно разберем, почему возникает эта проблема на уровне платформы 1С, проанализируем ошибки из обсуждения на форуме и рассмотрим 5 эффективных практических способов реализации плавного и отзывчивого интерфейса при работе с деревьями значений.
Для начала проанализируем ситуацию изнутри. Клиентское приложение 1С (тонкий, толстый или веб-клиент) работает в однопоточном режиме. Это означает, что один и тот же поток операционной системы отвечает и за выполнение программного кода на встроенном языке, и за отрисовку (перерисовку) элементов интерфейса.
Когда мы запускаем тяжелый цикл непосредственно на клиенте — например, обходим сотни строк дерева значений и для каждой вызываем метод Развернуть() — происходит следующее:
Состояние(), перестает обновлять кадры анимации и зависает.Интересное наблюдение автора темы о разнице работы в файловом и клиент-серверном режимах абсолютно логично. В файловом режиме локальный компьютер берет на себя еще и функции СУБД и сервера приложений, что максимально нагружает процессор и ускоряет "осерение" интерфейса. В клиент-серверном режиме клиент разгружен от вычислений базы данных, поэтому анимация может проскакивать стабильнее, но при росте дерева значений интерфейс все равно зависнет.
Давайте проанализируем решения, предложенные участниками форума, и выясним, почему некоторые из них не работают или работают неэффективно.
1. Ошибка при использовании Развернуть(0, Истина) (Сообщение 10)
В сообщении 10 предлагается отказаться от цикла и развернуть всё дерево одной строкой кода:
Элементы.ДеревоОстатков.Развернуть(0, Истина);
Этот код не будет работать в управляемом интерфейсе 1С для дерева значений. Метод Развернуть() первым параметром ожидает получить уникальный идентификатор строки (тип Число), который платформа генерирует динамически. Жестко прописанный ноль вызовет исключение (ошибку времени выполнения) вида "Отсутствует строка данных", так как строки с идентификатором 0 просто не существует в коллекции.
2. Неправильная логика работы с обработчиком ожидания
В исходном коде автора используется конструкция:
ПодключитьОбработчикОжидания("РазвернутьДерево", 1, Истина);
Здесь третий параметр равен Истина, что означает однократное выполнение обработчика. Тем не менее, в конце процедуры автор пишет ОтключитьОбработчикОжидания("РазвернутьДерево"). Это избыточное действие, так как однократный обработчик отключается платформой автоматически. Кроме того, задержка в 1 секунду заставляет пользователя неоправданно ждать перед началом реального действия. Как верно подсказали в Сообщении 1, интервал стоит уменьшить до 0.1 секунды.
Рассмотрим подробнее рабочие методики, которые позволят развернуть дерево значений быстро и сохранить отзывчивость интерфейса.
ОбновитьОтображениеДанных() в циклеЕсли дерево значений относительно небольшое (до нескольких сотен строк), мы можем принудительно заставить платформу перерисовать интерфейс прямо во время выполнения тяжелого цикла. Для этого используется метод ОбновитьОтображениеДанных().
Разберем пример реализации:
&НаКлиенте
Процедура РаскрытьВсеВетки(Команда)
Состояние("Разворачивание дерева значений...",,, БиблиотекаКартинок.ДлительнаяОперация48);
КоллекцияЭлементов = ДеревоОстатков.ПолучитьЭлементы();
Счетчик = 0;
Для Каждого ЭлементКоллекции Из КоллекцияЭлементов Цикл
Элементы.ДеревоОстатков.Развернуть(ЭлементКоллекции.ПолучитьИдентификатор(), Истина);
// Принудительно обновляем интерфейс каждую 10-ю итерацию, чтобы не замедлять работу избыточным рендерингом
Счетчик = Счетчик + 1;
Если Счетчик % 10 = 0 Тогда
ОбновитьОтображениеДанных();
КонецЕсли;
КонецЦикла;
Состояние(); // Скрываем окно состояния
КонецПроцедуры
Важное ограничение: этот метод очень ресурсоемок. Если вызывать ОбновитьОтображениеДанных() на каждой итерации, общее время развертывания дерева вырастет в несколько раз. Оптимально вызывать его порциями. Кроме того, в веб-клиенте этот метод сработает только после полного завершения работы всей процедуры, поэтому в браузере интерфейс все равно замрет.
Для действительно больших деревьев рекомендуется использовать разделение задачи на части (батчинг). Мы не выполняем весь цикл за один раз, а обрабатываем строки порциями, возвращая управление платформе. Интерфейс успевает обновиться, "кошак" крутится, а пользователь видит реальный прогресс.
Рассмотрим по шагам, как это реализовать. Для этого нам понадобятся переменные формы для хранения текущего состояния процесса.
&НаКлиенте
Перем МассивСтрокДляОбработки; // Переменная для хранения списка ID строк
&НаКлиенте
Процедура НачатьРазворачиваниеДереваПорциями(Команда)
// Шаг 1. Собираем идентификаторы всех строк верхнего уровня
МассивСтрокДляОбработки = Новый Массив;
КоллекцияЭлементов = ДеревоОстатков.ПолучитьЭлементы();
Для Каждого ЭлементКоллекции Из КоллекцияЭлементов Цикл
МассивСтрокДляОбработки.Добавить(ЭлементКоллекции.ПолучитьИдентификатор());
КонецЦикла;
// Шаг 2. Подключаем многократный обработчик ожидания с минимальным интервалом
Если МассивСтрокДляОбработки.Количество() > 0 Тогда
Состояние("Разворачивание структуры дерева...", 0);
ПодключитьОбработчикОжидания("ВыполнитьРазворотПорции", 0.1, Ложь);
КонецЕсли;
КонецПроцедуры
&НаКлиенте
Процедура ВыполнитьРазворотПорции()
Порция = 20; // Количество строк, обрабатываемых за один такт
ВсегоСтрок = МассивСтрокДляОбработки.Количество();
Если ВсегоСтрок = 0 Тогда
ОтключитьОбработчикОжидания("ВыполнитьРазворотПорции");
Состояние(); // Убираем индикатор
Возврат;
КонецЕсли;
// Определяем, сколько строк обработать в этой итерации
ИндексКонца = ?(ВсегоСтрок < Порция, ВсегоСтрок - 1, Порция - 1);
Для Счетчик = 0 По ИндексКонца Цикл
ИдентификаторСтроки = МассивСтрокДляОбработки[0];
Элементы.ДеревоОстатков.Развернуть(ИдентификаторСтроки, Истина);
МассивСтрокДляОбработки.Удалить(0); // Удаляем обработанный элемент
КонецЦикла;
// Обновляем текст состояния с указанием оставшегося количества
Состояние("Разворачивание структуры дерева. Осталось элементов: " + МассивСтрокДляОбработки.Количество(),
(ВсегоСтрок / (ВсегоСтрок + МассивСтрокДляОбработки.Количество())) * 100);
КонецПроцедуры
Этот подход гарантирует абсолютно плавную работу интерфейса на любых компьютерах и во всех типах клиентов (включая веб-клиент), так как платформа регулярно получает кванты времени на перерисовку экрана.
Если ваша конфигурация разработана на базе Библиотеки стандартных подсистем (БСП), вам не нужно самостоятельно писать циклы обхода. В БСП есть оптимизированная обертка, разработанная с учетом особенностей работы платформы:
СтандартныеПодсистемыКлиент.РазвернутьУзлыДерева(ЭтаФорма, "ДеревоОстатков", "*", Истина);
Проанализируем параметры этого метода:
ЭтаФорма — контекст текущей формы."ДеревоОстатков" — строковое имя элемента формы (таблицы), отображающей дерево."*" — специальный символ, указывающий, что нужно развернуть все корневые узлы верхнего уровня.Истина — признак рекурсивного разворачивания (будут раскрыты все подчиненные уровни дерева).Данный метод работает значительно быстрее ручного обхода на встроенном языке, так как использует внутренние оптимизированные алгоритмы платформы.
Часто программисты пытаются развернуть дерево программно сразу после его заполнения данными на сервере. Но существует гораздо более элегантное и быстрое решение, не требующее написания серверно-клиентских циклов вообще.
Достаточно на этапе инициализации формы или прямо в конфигураторе установить свойство элемента формы:
Элементы.ДеревоОстатков.НачальноеОтображениеДерева = НачальноеОтображениеДерева.РаскрыватьВсеУровни;
Когда данные будут переданы на клиент, платформа 1С автоматически сформирует и отобразит дерево в полностью развернутом виде. Пользователю не придется ждать выполнения клиентских скриптов, и интерфейс отрисуется мгновенно.
Если само формирование дерева значений на сервере занимает длительное время (более 2-3 секунд), правильным архитектурным подходом будет перенос расчета в фоновое задание:
ДлительныеОперацииКлиент.ОжидатьЗавершение().Резюмируя: для простых задач используйте свойство НачальноеОтображениеДерева или готовые функции БСП. Для сложных динамических операций, требующих визуального контроля со стороны пользователя, идеальным выбором станет порционный запуск задач через ПодключитьОбработчикОжидания.