Как преобразовать структуру неограниченной вложенности в Дерево значений и вывести его на управляемую форму в 1С?

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

В процессе разработки на платформе 1С:Предприятие 8.3 программисты часто сталкиваются с задачей отображения сложных иерархических структур данных на управляемой форме. Это могут быть результаты разбора файлов XML, десериализации строк JSON, полученных из внешних API, или просто вложенные соответствия и структуры. Основная сложность заключается в том, что состав полей и уровень вложенности заранее неизвестны. Следовательно, мы не можем заранее жестко задать структуру колонок для реквизита формы типа ДеревоЗначений.

Если мы попытаемся использовать стандартный метод ЗначениеВРеквизитФормы() для дерева, чья структура колонок отличается от структуры реквизита на форме, платформа выдаст ошибку. В этой статье мы подробно разберем, как решить данную задачу «под ключ»: сначала преобразуем произвольную структуру во временный объект ДеревоЗначений, а затем динамически перестроим реквизиты и элементы управляемой формы для его корректного вывода.


Шаг 1. Рекурсивное преобразование вложенных структур в ДеревоЗначений

Рассмотрим ситуацию, когда мы получаем на вход объект типа Структура или Соответствие, внутри которого могут находиться как простые значения (строки, числа, даты), так и вложенные коллекции (другие структуры или массивы). Нам необходимо обойти всю эту глубину рекурсивно и построить универсальное дерево значений с колонками Имя и Значение.

Давайте проанализируем и напишем универсальный код для выполнения этой операции на сервере:


// Главная функция, инициализирующая Дерево значений и запускающая рекурсию
Функция ПреобразоватьСтруктуруВДерево(Знач ИсходныеДанные) Экспорт

    // Создаем новый объект ДеревоЗначений на сервере
    Дерево = Новый ДеревоЗначений;
    Дерево.Колонки.Добавить("Имя", Новый ОписаниеТипов("Строка"));
    Дерево.Колонки.Добавить("Значение"); // Используем произвольный тип данных для значений

    // Запускаем рекурсивный обход. В качестве коллекции строк передаем корень дерева.
    ЗаполнитьВеткуРекурсивно(ИсходныеДанные, Дерево.Строки);

    Возврат Дерево;

КонецФункции

// Вспомогательная рекурсивная процедура
Процедура ЗаполнитьВеткуРекурсивно(Знач Данные, КоллекцияСтрок)

    ТипДанных = ТипЗнч(Данные);

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

КонецПроцедуры

Этот алгоритм позволяет превратить любую вложенную структуру в читаемый древовидный вид, где родительские узлы наглядно показывают тип контейнера (например, [Структура] или [Массив]), а конечные ветви хранят реальные данные.


Шаг 2. Альтернативный плоский вариант построения дерева без рекурсии

Рассмотрим ситуацию, когда ваши исходные данные приходят не в виде вложенных объектов, а в виде плоской таблицы (например, полученной из СУБД или внешнего источника), но имеют ссылки на родителей через поля ИД и РодительИД. Чтобы избежать накладных расходов на рекурсию и защитить систему от переполнения стека при экстремальных объемах данных, мы можем применить линейный алгоритм $O(N)$ с использованием объекта Соответствие:


Функция ПостроитьДеревоИзТаблицыБезРекурсии(ТаблицаДанных) Экспорт

    Дерево = Новый ДеревоЗначений;
    Дерево.Колонки.Добавить("ИД");
    Дерево.Колонки.Добавить("Наименование");
    Дерево.Колонки.Добавить("Значение");

    // Соответствие для быстрого поиска строк дерева по уникальному идентификатору
    СоответствиеСтрок = Новый Соответствие;

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

    Возврат Дерево;

КонецФункции

Шаг 3. Динамическое изменение управляемой формы под структуру нового дерева

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

Давайте разберем по шагам логику серверной процедуры, которая выполняет перестроение формы:

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

Реализуем этот механизм в виде готовой процедуры:


&НаСервере
Процедура ОтобразитьДеревоНаФорме(Знач ИсходноеДеревоОбъект, Знач ИмяРеквизитаДерева = "ДеревоФормы")

    // Инициализируем массивы для изменения структуры реквизитов формы
    РеквизитыДляУдаления = Новый Массив;
    РеквизитыДляДобавления = Новый Массив;

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

    // 2. Добавляем новые реквизиты на основе колонок переданного дерева значений
    Для Каждого Колонка Из ИсходноеДеревоОбъект.Колонки Цикл
        
        // Системные колонки "Имя" и "Значение" уже созданы на форме в конфигураторе, их пропускаем
        Если Колонка.Имя = "Имя" Или Колонка.Имя = "Значение" Тогда
            Продолжить;
        КонецЕсли;
        
        // Создаем описание нового реквизита формы
        НовыйРеквизит = Новый РеквизитФормы(
            Колонка.Имя, 
            Колонка.ТипЗначения, 
            ИмяРеквизитаДерева, 
            ?(ЗначениеЗаполнено(Колонка.Заголовок), Колонка.Заголовок, Колонка.Имя)
        );
        РеквизитыДляДобавления.Добавить(НовыйРеквизит);
        
    КонецЦикла;

    // Применяем изменения структуры данных формы за один вызов
    ИзменитьРеквизиты(РеквизитыДляДобавления, РеквизитыДляУдаления);

    // 3. Загружаем данные из объекта ДеревоЗначений в реквизит формы
    ЗначениеВРеквизитФормы(ИсходноеДеревоОбъект, ИмяРеквизитаДерева);

    // 4. Отрисовываем новые колонки на форме в табличной части элемента управления
    ТаблицаФормы = Элементы[ИмяРеквизитаДерева];
    
    Для Каждого Колонка Из ИсходноеДеревоОбъект.Колонки Цикл
        
        ИмяЭлементаКолонки = ИмяРеквизитаДерева + "_" + Колонка.Имя;
        
        // Проверяем, существует ли уже элемент (например, базовые колонки "Имя" и "Значение")
        Если Элементы.Найти(ИмяЭлементаКолонки) = Неопределено Тогда
            
            // Программное добавление элемента управления типа "ПолеФормы" в таблицу формы
            НовоеПоле = Элементы.Добавить(ИмяЭлементаКолонки, Тип("ПолеФормы"), ТаблицаФормы);
            НовоеПоле.Вид = ВидПоляФормы.ПолеВвода;
            
            // Связываем поле формы с добавленным ранее реквизитом
            НовоеПоле.ПутьКДанным = ИмяРеквизитаДерева + "." + Колонка.Имя;
            НовоеПоле.Заголовок = ?(ЗначениеЗаполнено(Колонка.Заголовок), Колонка.Заголовок, Колонка.Имя);
            
        КонецЕсли;
        
    КонецЦикла;

КонецПроцедуры

Проанализируем важный нюанс: при программном создании полей ввода с помощью Элементы.Добавить() мы обязательно связываем их через свойство ПутьКДанным, которое формируется как строковое выражение вида "ИмяРеквизитаДерева.ИмяКолонки". Обратите внимание на имя элемента: рекомендуется формировать его уникальным, используя префикс, например "ДеревоФормы_" + Колонка.Имя, во избежание конфликтов имен с другими элементами на форме.

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

Используя предложенные алгоритмы, вы сможете без труда реализовать гибкий вывод любых иерархических структур данных на форму вашего решения в 1С:Предприятии 8.3.

← На главную