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

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

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

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

Давайте разберем эту ситуацию по шагам, предложив наиболее эффективные и корректные подходы.

Основные подходы к заполнению ДереваЗначений на управляемой форме

В платформе 1С:Предприятие 8.3 существует несколько способов заполнения ДереваЗначений, особенно если речь идет об управляемых формах и данных из запроса. Мы рассмотрим два основных:

  1. Автоматическое заполнение из результата запроса с группировками и иерархией. Этот метод является предпочтительным для большинства сценариев, поскольку он наиболее эффективен и прост в реализации.
  2. Программное (рекурсивное) заполнение. Этот метод дает больше гибкости, но требует более тщательного написания кода и может быть менее производительным на больших объемах данных.

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

Подготовительные шаги: Реквизит формы и запрос

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

  1. Создание реквизита формы:

    На управляемой форме необходимо создать реквизит с типом "ДеревоЗначений". Например, назовем его ДеревоМебели. Затем в свойствах этого реквизита добавьте колонки, которые будут отображать данные. В нашем случае это могут быть колонки ПредметМебели (тип "СправочникСсылка.Мебель" или "Строка", если это наименование) и СоставныеЧасти (тип "Строка"). Колонки должны быть созданы для реквизита формы, а не для самого элемента формы ДеревоЗначений.

    Например, в модуле формы, в обработчике ПриСозданииНаСервере или отдельной серверной процедуре, мы можем определить структуру дерева:

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

    Запрос должен извлекать данные о мебели и её составных частях. Важно, чтобы запрос был структурирован таким образом, чтобы его результат можно было легко преобразовать в иерархию. Для этого часто используются левые соединения (ЛЕВОЕ СОЕДИНЕНИЕ) и группировки.

    Предположим, у нас есть справочник "Мебель" с наименованием и регистр сведений "СоставМебели" с измерениями "Мебель" (ссылка на справочник "Мебель") и "НаименованиеЧасти" (строка).

    
    &НаСервере
    Функция ПолучитьДанныеДляДерева()
    
        Запрос = Новый Запрос;
        Запрос.Текст = 
        "ВЫБРАТЬ
        |    Мебель.Наименование КАК ПредметМебели,
        |    СоставМебели.НаименованиеЧасти КАК СоставныеЧасти,
        |    Мебель.Ссылка КАК СсылкаМебели
        |ИЗ
        |    Справочник.Мебель КАК Мебель
        |        ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СоставМебели КАК СоставМебели
        |        ПО Мебель.Ссылка = СоставМебели.Мебель
        |УПОРЯДОЧИТЬ ПО
        |    ПредметМебели,
        |    СоставныеЧасти"; // Важно для правильной группировки
    
        РезультатЗапроса = Запрос.Выполнить();
        Возврат РезультатЗапроса;
    
    КонецФункции
    

    В этом запросе мы соединяем справочник "Мебель" с регистром сведений. Обратите внимание на УПОРЯДОЧИТЬ ПО – это критически важно для автоматического формирования иерархии методом Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией) и для корректного программного обхода.

Метод 1: Автоматическое заполнение из запроса с группировками

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

  1. Добавляем реквизит формы с типом ДеревоЗначений, например, ДанныеДереваМебели.
  2. Определяем колонки для этого реквизита, например: ПредметМебели (Строка), СоставныеЧасти (Строка).
  3. Реализуем процедуру заполнения:

&НаСервере
Процедура ЗаполнитьДеревоМебелиНаСервере()

    // 1. Получаем результат запроса
    РезультатЗапроса = ПолучитьДанныеДляДерева();

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

    // 3. Выгружаем результат запроса в ДеревоЗначений с иерархией
    // Используем обход по группировкам, указывая поля для группировки
    // В нашем случае, 'ПредметМебели' будет верхним уровнем, 'СоставныеЧасти' - подчиненным
    // Для этого в запросе нужно обеспечить сортировку, как было показано ранее.
    // Если нам нужна жесткая группировка, то запрос должен быть таким:
    /*
    Запрос.Текст =
    "ВЫБРАТЬ
    |    Мебель.Наименование КАК ПредметМебели,
    |    СоставМебели.НаименованиеЧасти КАК СоставныеЧасти
    |ИЗ
    |    Справочник.Мебель КАК Мебель
    |        ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СоставМебели КАК СоставМебели
    |        ПО Мебель.Ссылка = СоставМебели.Мебель
    |
    |УПОРЯДОЧИТЬ ПО
    |    ПредметМебели,
    |    СоставныеЧасти
    |
    |ИТОГИ ПО
    |    ПредметМебели"; // Ключевое отличие для автоматической иерархии
    */

    // Если запрос содержит ИТОГИ ПО, то можем выгрузить так:
    ВыборкаПоГруппировкам = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией);
    
    // В данном сценарии, с учетом нашего запроса БЕЗ ИТОГОВ, но с УПОРЯДОЧИТЬ ПО,
    // мы можем использовать другой подход для автоматической иерархии, 
    // либо сделать ручное заполнение.
    // Если мы хотим автоматическую иерархию, запрос должен быть с ИТОГАМИ ПО.
    // Пример выгрузки с ИТОГАМИ ПО:
    // НовоеДерево.Загрузить(ВыборкаПоГруппировкам);
    // Однако, текущий запрос без ИТОГОВ ПО.
    // Поэтому рассмотрим ручной обход, как наиболее приближенный к попыткам автора.
    // Но для начала - идеальный случай с автоматической загрузкой (если запрос с ИТОГАМИ).

    // Если бы запрос был с ИТОГАМИ ПО:
    // Например, запрос с ИТОГАМИ ПО ПредметМебели и СоставныеЧасти
    // НовоеДерево.Загрузить(РезультатЗапроса.Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией));
    // Это автоматически сформировало бы дерево.
    // В нашем случае, поскольку запрос простой, без ИТОГОВ, нам придется делать обход.
    // Но сначала давайте представим, как бы выглядел запрос с ИТОГАМИ:

    ЗапросДляАвтоматическойИерархии = Новый Запрос;
    ЗапросДляАвтоматическойИерархии.Текст =
    "ВЫБРАТЬ
    |    Мебель.Наименование КАК ПредметМебели,
    |    СоставМебели.НаименованиеЧасти КАК СоставныеЧасти
    |ИЗ
    |    Справочник.Мебель КАК Мебель
    |        ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СоставМебели КАК СоставМебели
    |        ПО Мебель.Ссылка = СоставМебели.Мебель
    |
    |ИТОГИ ПО
    |    ПредметМебели ИЕРАРХИЯ,
    |    СоставныеЧасти"; // Ключевое слово ИЕРАРХИЯ для ДереваЗначений

    РезультатЗапросаИерархия = ЗапросДляАвтоматическойИерархии.Выполнить();

    // Теперь, с таким запросом, мы можем выгрузить результат в ДеревоЗначений очень просто:
    НовоеДерево = РезультатЗапросаИерархия.Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией);
    // Здесь платформа сама создаст колонки, если их нет, но лучше определить их заранее.
    // Мы уже определили их, так что платформа будет использовать существующие.
    // Важно: имена колонок в ДеревеЗначений должны совпадать с именами полей запроса.
    
    // После заполнения ДереваЗначений на сервере, передаем его на форму
    ЗначениеВРеквизитФормы(НовоеДерево, "ДеревоМебели");

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

Важные моменты для автоматической иерархии:

Метод 2: Программное (рекурсивное) заполнение ДереваЗначений

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

Для программного заполнения мы будем использовать результат запроса, который мы уже написали (без ИТОГОВ ПО, но с УПОРЯДОЧИТЬ ПО ПредметМебели, СоставныеЧасти).

  1. Получаем выборку из запроса.
  2. Обходим выборку, создавая родительские элементы (мебель).
  3. Внутри каждого родительского элемента, добавляем дочерние элементы (составные части).
  4. Используем переменные для отслеживания текущего родительского элемента, чтобы избежать повторений.

Пример корректного программного заполнения:


&НаСервере
Процедура ЗаполнитьДеревоМебелиРекурсивноНаСервере()
    
    // 1. Получаем результат запроса
    РезультатЗапроса = ПолучитьДанныеДляДерева(); // Используем запрос без ИТОГОВ ПО, но с УПОРЯДОЧИТЬ ПО
    Выборка = РезультатЗапроса.Выбрать();

    // 2. Создаем новое ДеревоЗначений
    НовоеДерево = Новый ДеревоЗначений;
    НовоеДерево.Колонки.Добавить("ПредметМебели", Новый ОписаниеТипов("Строка"));
    НовоеДерево.Колонки.Добавить("СоставныеЧасти", Новый ОписаниеТипов("Строка"));

    // Передаем выборку и текущий уровень дерева в рекурсивную функцию
    // Создадим вспомогательную функцию, чтобы не передавать НовоеДерево напрямую в рекурсию,
    // а работать со строками текущего уровня
    ЗаполнитьУровеньДерева(НовоеДерево.Строки, Выборка);

    // Передаем заполненное дерево на форму
    ЗначениеВРеквизитФормы(НовоеДерево, "ДеревоМебели");

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

&НаСервере
Процедура ЗаполнитьУровеньДерева(СтрокиДерева, Выборка)

    ТекущийПредметМебели = "";
    НовСтрокаМебель = Неопределено;

    Пока Выборка.Следующий() Цикл

        Если Выборка.ПредметМебели <> ТекущийПредметМебели Тогда
            // Обнаружен новый предмет мебели, создаем новую родительскую строку
            НовСтрокаМебель = СтрокиДерева.Добавить();
            НовСтрокаМебель.ПредметМебели = Выборка.ПредметМебели;
            НовСтрокаМебель.СоставныеЧасти = ""; // Для родительского элемента эта колонка пуста
            ТекущийПредметМебели = Выборка.ПредметМебели;
        КонецЕсли;

        // Добавляем составную часть к текущему предмету мебели, если она есть
        Если ЗначениеЗаполнено(Выборка.СоставныеЧасти) Тогда
            Если НовСтрокаМебель <> Неопределено Тогда
                // Добавляем подчиненную строку для составной части
                НовСтрокаСоставнаяЧасть = НовСтрокаМебель.Строки.Добавить();
                НовСтрокаСоставнаяЧасть.ПредметМебели = ""; // Для подчиненной строки эта колонка пуста
                НовСтрокаСоставнаяЧасть.СоставныеЧасти = Выборка.СоставныеЧасти;
            КонецЕсли;
        КонецЕсли;

    КонецЦикла;

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

Проанализируем изменения и почему это работает:

  1. Мы обходим выборку только один раз с помощью одного Пока Выборка.Следующий() Цикл.
  2. Переменная ТекущийПредметМебели используется для отслеживания того, когда мы переходим от одного предмета мебели к другому. Это позволяет нам создавать новую родительскую строку только тогда, когда встречается новый предмет мебели, тем самым избегая повторений.
  3. Когда Выборка.ПредметМебели <> ТекущийПредметМебели, это означает, что мы начали обрабатывать новый предмет мебели, и для него создается новая родительская строка в СтрокиДерева.
  4. Далее, если есть СоставныеЧасти для текущего предмета мебели, они добавляются как подчиненные строки к только что созданной или уже существующей родительской строке НовСтрокаМебель.
  5. Для подчиненных строк (составных частей) мы специально оставляем поле ПредметМебели пустым, чтобы не было дублирования информации. Аналогично, для родительской строки ПредметМебели, поле СоставныеЧасти может быть пустым.

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

Передача ДереваЗначений на форму и обратно

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

Мы уже использовали ЗначениеВРеквизитФормы(НовоеДерево, "ДеревоМебели"); в примерах выше. Этот шаг является обязательным для того, чтобы заполненное дерево отобразилось пользователю.

Рекомендации по отображению на форме

После того как ДеревоЗначений заполнено, важно правильно настроить его отображение на форме:

  1. Элемент формы ДеревоЗначений: Перетащите реквизит формы ДеревоМебели на форму. Платформа автоматически создаст элемент формы с тем же именем.
  2. Настройка колонок элемента формы: Убедитесь, что в свойствах элемента формы ДеревоЗначений (на вкладке "Колонки") отображаются нужные вам колонки (ПредметМебели, СоставныеЧасти). Вы можете настроить их ширину, заголовок, порядок.
  3. Условное оформление (как вариант, предложенный в обсуждении): Хотя это не решает проблему структуры данных, как заметил автор, условное оформление может улучшить визуальное восприятие. Например, можно скрыть колонку "ПредметМебели" для подчиненных строк, где она пустая, или колонку "СоставныеЧасти" для родительских строк.
    
    // Пример условного оформления (настраивается в конфигураторе)
    // Условие:
    //  - Для строки дерева, если ПредметМебели пустой
    // Оформление:
    //  - Видимость колонки "ПредметМебели" = Ложь
    //  - Шрифт = Полужирный (для СоставныеЧасти)
    

    Это может помочь сделать иерархию более читаемой, если вы не хотите, чтобы в колонке "ПредметМебели" были пустые значения напротив составных частей.

Когда использовать ДинамическийСписок вместо ДереваЗначений?

В ходе обсуждения был задан вопрос: "А точно уверены что нужно дерево на форме, а не динамический список?". Это очень важный вопрос, который стоит рассмотреть:

В вашем случае, поскольку условие задачи явно указывает на "построить дерево руками", мы сосредоточились на ДеревеЗначений. Но всегда держите в уме ДинамическийСписок как мощную альтернативу.

Заключение

Мы подробно рассмотрели два основных подхода к заполнению ДереваЗначений на управляемых формах 1С:Предприятия 8.3 для отображения иерархических данных без повторений. Автоматический метод с использованием ИТОГИ ПО ИЕРАРХИЯ в запросе и Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией) является наиболее эффективным. Если же требуется полный контроль или специфическая логика, программное рекурсивное заполнение, как мы показали, требует аккуратности в обходе выборки для избежания повторений и корректного построения иерархии. Не забывайте о функциях ЗначениеВРеквизитФормы() и РеквизитФормыВЗначение() для корректной работы с реквизитами форм.

Применяя эти знания, вы сможете успешно решать задачи по отображению сложных иерархических структур данных в ваших 1С-решениях.

← На главную