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