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