Как скопировать ветку дерева значений в другое дерево при активизации строки в управляемой форме 1С?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При разработке интерфейсов на управляемых формах часто возникает задача организации связанных списков или деревьев. Типичный сценарий: на форме расположены два реквизита типа ДанныеФормыДерево (на форме отображаются как элементы управления Таблица с иерархией). В первом дереве отображается общая структура, а во втором дереве требуется динамически показывать только ту ветку (включая всех ее потомков), которая в данный момент выбрана пользователем в первом дереве.

Разберем, как правильно реализовать такое копирование, почему для этой задачи лучше использовать клиентские методы без лишних серверных вызовов и как оптимизировать перенос значений колонок.

Почему не стоит использовать РеквизитФормыВЗначение на сервере

Частая ошибка начинающих разработчиков — попытка отправить контекст формы на сервер, преобразовать коллекцию формы в объект ДеревоЗначений через РеквизитФормыВЗначение, выполнить копирование и вернуть результат обратно через ЗначениеВРеквизитФормы.

Проанализируем, почему этого делать не следует:

  1. Избыточные серверные вызовы. Событие ПриАктивизацииСтроки срабатывает при каждом перемещении курсора по строкам таблицы (в том числе при зажатых стрелках на клавиатуре). Вызов сервера на каждое движение приведет к ощутимым зависаниям интерфейса.
  2. Накладные расходы на сериализацию. Преобразование ДанныеФормыДерево в ДеревоЗначений и обратно выполняет полную сериализацию всех строк обоих деревьев, что неэффективно по памяти и быстродействию.
  3. Ограничения стандартных методов. Метод Скопировать() объекта ДеревоЗначений предназначен для дублирования строк внутри того же объекта, но не умеет копировать произвольную ветку в другое независимое дерево без обхода.

Коллекция ДанныеФормыДерево и ее элементы ДанныеФормыЭлементДерева полностью доступны на клиенте. Поэтому оптимальное решение — выполнить рекурсивный обход и заполнение второго дерева непосредственно в тонком клиенте.

Рекурсивный алгоритм копирования ветки на клиенте

Рассмотрим пошаговую реализацию алгоритма. Создадим обработчик события ПриАктивизацииСтроки для первого дерева, а также вспомогательные процедуры для рекурсивного добавления строк.

Для автоматического переноса всех совпадающих колонок между строками используем метод ЗаполнитьЗначенияСвойств, который отлично работает на клиенте с элементами данных формы.


&НаКлиенте
Процедура Дерево1ПриАктивизацииСтроки(Элемент)
    
    ТекущаяСтрока = Элементы.Дерево1.ТекущиеДанные;
    
    // Очищаем коллекцию второго дерева
    Дерево2.ПолучитьЭлементы().Очистить();
    
    Если ТекущаяСтрока = Неопределено Тогда
        Возврат;
    КонецЕсли;
    
    // Добавляем корень выбранной ветки в Дерево2
    НовыйКорень = ДобавитьСтрокуВДерево2(Неопределено, ТекущаяСтрока);
    
    // Запускаем рекурсивное заполнение дочерних строк
    СкопироватьДочерниеЭлементы(НовыйКорень, ТекущаяСтрока.ПолучитьЭлементы());
    
    // Разворачиваем добавленный узел для наглядности
    Элементы.Дерево2.Развернуть(НовыйКорень.ПолучитьИдентификатор(), Истина);
    
КонецПроцедуры

&НаКлиенте
Процедура СкопироватьДочерниеЭлементы(РодительПриемник, КоллекцияИсточника)
    
    Для Каждого СтрокаИсточник Из КоллекцияИсточника Цикл
        
        // Создаем подчиненную строку в дереве-приемнике
        НоваяСтрока = ДобавитьСтрокуВДерево2(РодительПриемник, СтрокаИсточник);
        
        // Рекурсивно спускаемся по иерархии
        СкопироватьДочерниеЭлементы(НоваяСтрока, СтрокаИсточник.ПолучитьЭлементы());
        
    КонецЦикла;
    
КонецПроцедуры

&НаКлиенте
Функция ДобавитьСтрокуВДерево2(РодительПриемник, СтрокаИсточник)
    
    Если РодительПриемник = Неопределено Тогда
        НоваяСтрока = Дерево2.ПолучитьЭлементы().Добавить();
    Иначе
        НоваяСтрока = РодительПриемник.ПолучитьЭлементы().Добавить();
    КонецЕсли;
    
    // Копируем все совпадающие по именам колонки
    ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаИсточник);
    
    Возврат НоваяСтрока;
    
КонецФункции

Как работает данный код

Разберем ключевые этапы выполнения алгоритма:

  1. В обработчике Дерево1ПриАктивизацииСтроки мы получаем текущую выделенную строку через Элементы.Дерево1.ТекущиеДанные. Если таблица пуста, выполнение прерывается.
  2. Методом Дерево2.ПолучитьЭлементы().Очистить() мы полностью сбрасываем предыдущее состояние подчиненного дерева.
  3. Функция ДобавитьСтрокуВДерево2 создает новую строку в корне дерева (если родитель не указан) или в подчиненной коллекции переданного родителя, после чего переносит значения колонок с помощью метода ЗаполнитьЗначенияСвойств.
  4. Процедура СкопироватьДочерниеЭлементы принимает коллекцию дочерних строк источника (СтрокаИсточник.ПолучитьЭлементы()) и для каждого узла рекурсивно повторяет создание строки и обход его вложенных уровней.
  5. Для того чтобы скопированная ветка отображалась сразу в развернутом виде, мы используем метод Элементы.Дерево2.Развернуть(), передавая числовой идентификатор созданного корневого узла через ПолучитьИдентификатор().

Рекомендации по оптимизации при работе с большими деревьями

Если дерево содержит тысячи строк с глубокой вложенностью, частое срабатывание события ПриАктивизацииСтроки при быстром перемещении по стрелкам клавиатуры может вызывать микрозадержки. Чтобы интерфейс оставался отзывчивым, можно применить отложенное обновление через обработчик ожидания:


&НаКлиенте
Перем ТекущийИдентификаторДляОбновления;

&НаКлиенте
Процедура Дерево1ПриАктивизацииСтроки(Элемент)
    
    // Подключаем однократный обработчик ожидания с задержкой 0.1-0.2 сек
    ОтключитьОбработчикОжидания("ВыполнитьОтложенноеОбновлениеДерева2");
    ПодключитьОбработчикОжидания("ВыполнитьОтложенноеОбновлениеДерева2", 0.1, Истина);
    
КонецПроцедуры

&НаКлиенте
Процедура ВыполнитьОтложенноеОбновлениеДерева2()
    
    ТекущаяСтрока = Элементы.Дерево1.ТекущиеДанные;
    Дерево2.ПолучитьЭлементы().Очистить();
    
    Если ТекущаяСтрока <> Неопределено Тогда
        НовыйКорень = ДобавитьСтрокуВДерево2(Неопределено, ТекущаяСтрока);
        СкопироватьДочерниеЭлементы(НовыйКорень, ТекущаяСтрока.ПолучитьЭлементы());
    КонецЕсли;
    
КонецПроцедуры

Такой подход гарантирует, что перезаполнение второго дерева начнется только тогда, когда пользователь остановит выбор на конкретном узле, исключая промежуточные лишние перерисовки.

← На главную