Как обойти дерево значений на клиенте и сохранить развернутые строки при пересчете?

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

При разработке интерфейсов на управляемых формах 1С часто возникает задача: отобразить иерархические данные в виде дерева, пересчитать по ним итоги на сервере и вернуть результат пользователю. Однако стандартный вызов метода ЗначениеВРеквизитФормы приводит к тому, что все узлы дерева непроизвольно сворачиваются, а пользователю приходится раскрывать нужные ветви заново. Разберем, почему это происходит, как обойти строки дерева на клиенте и как сохранить или восстановить состояние иерархии после пересчета.

Выясним причину сброса визуального состояния

Проанализируем механику работы управляемой формы. На сервере мы оперируем объектом ДеревоЗначений, а на клиенте работаем с реквизитом формы, имеющим тип ДанныеФормыДерево. Когда для обновления данных вызывается метод ЗначениеВРеквизитФормы, платформа полностью пересоздает коллекцию строк в памяти формы:

  1. Таблица формы сбрасывает свое визуальное состояние к начальным настройкам, из-за чего узлы сворачиваются.
  2. Всем строкам коллекции заново генерируются целочисленные идентификаторы.
  3. Порядок и нумерация идентификаторов строк полностью обновляются, поэтому сопоставление по старым числовым идентификаторам перестает работать корректно.

Обход дерева и полное раскрытие узлов на клиенте

Посмотрим на базовую задачу: как обойти дерево на клиенте и раскрыть его элементы. На клиенте нам доступны коллекции ДанныеФормыДерево, ДанныеФормыКоллекцияЭлементовДерева и строки ДанныеФормыЭлементДерева. Непосредственное управление отображением узлов выполняется методами таблицы формы: Развернуть(), Свернуть() и Развернут(). Обратите внимание: эти методы принадлежат элементу управления формы и доступны исключительно в контексте &НаКлиенте.

Если требуется полностью раскрыть все уровни дерева без сохранения предыдущих настроек, можно использовать второй параметр метода Развернуть(). При установке значения Истина платформа разворачивает указанный узел вместе со всеми подчиненными элементами любой глубины вложенности.

Рассмотрим пример кода обхода элементов верхнего уровня:


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

Кроме того, если дерево должно быть полностью развернуто всегда при открытии или заполнении, можно настроить свойство элемента управления таблицы НачальноеОтображениеДерева, установив его в значение РаскрыватьВсеУровни.

Рекурсивный обход для запоминания и восстановления развернутых узлов

Рассмотрим более сложную задачу: восстановить именно те узлы, которые пользователь раскрыл вручную до запуска расчета. Если мы перезаполняем дерево через ЗначениеВРеквизитФормы, старые идентификаторы строк меняются. Чтобы сопоставить строки до и после пересчета, необходимо привязаться к уникальным ключевым полям данных либо добавить в структуру скрытую колонку с уникальным идентификатором строки (например, GUID в строковом формате).

Разберем по шагам алгоритм с запоминанием ключей развернутых строк:

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

Посмотрим на практическую реализацию рекурсивного сохранения и восстановления:


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

&НаКлиенте
Процедура ВосстановитьРазвернутыеУзлы(КоллекцияСтрок, СписокРазвернутых)
    Для Каждого СтрокаДерева Из КоллекцияСтрок Цикл
        КлючСтроки = СтрокаДерева.УникальныйКлючСтроки;
        Идентификатор = СтрокаДерева.ПолучитьИдентификатор();
        
        Если СписокРазвернутых[КлючСтроки] = Истина Тогда
            // Разворачиваем узел без подчиненных, чтобы не раскрыть лишние вложенные ветви
            Элементы.ДеревоЗначений.Развернуть(Идентификатор, Ложь);
        КонецЕсли;
        
        ПодчиненныеСтроки = СтрокаДерева.ПолучитьЭлементы();
        Если ПодчиненныеСтроки.Количество() > 0 Тогда
            ВосстановитьРазвернутыеУзлы(ПодчиненныеСтроки, СписокРазвернутых);
        КонецЕсли;
    КонецЦикла;
КонецПроцедуры

Важный момент: при программном разворачивании обход необходимо выполнять строго сверху вниз — от родительских узлов к дочерним. Если вызвать метод Развернуть() для вложенной строки при свернутом родителе, визуально строка останется скрытой.

Альтернативное решение: пересчет итогов без пересоздания коллекции

Проанализируем архитектурную сторону вопроса. Зачастую разработчики используют вызовы РеквизитФормыВЗначение и ЗначениеВРеквизитФормы исключительно для того, чтобы посчитать суммы по ветвям дерева. Однако в этом нет строгой необходимости. Реквизит формы ДанныеФормыДерево доступен для прямого чтения и изменения в серверных процедурах формы.

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

Рассмотрим пример серверного расчета итогов прямо в данных формы:


&НаСервере
Функция ПересчитатьИтогиВДанныхФормы(КоллекцияСтрок)
    СуммаПоУзлу = 0;
    
    Для Каждого СтрокаДерева Из КоллекцияСтрок Цикл
        Подчиненные = СтрокаДерева.ПолучитьЭлементы();
        Если Подчиненные.Количество() > 0 Тогда
            // Рекурсивно вычисляем итоги для нижележащих узлов
            СтрокаДерева.Сумма = ПересчитатьИтогиВДанныхФормы(Подчиненные);
        КонецЕсли;
        
        СуммаПоУзлу = СуммаПоУзлу + СтрокаДерева.Сумма;
    КонецЦикла;
    
    Возврат СуммаПоУзлу;
КонецФункции

&НаСервере
Процедура ПересчитатьНаСервере()
    ПересчитатьИтогиВДанныхФормы(ДеревоЗначений.ПолучитьЭлементы());
КонецПроцедуры

При таком подходе отпадает необходимость в громоздких процедурах запоминания состояния дерева на клиенте, снижается нагрузка на трафик между клиентом и сервером, а интерфейс работает быстро и предсказуемо для пользователя.

← На главную