При работе с данными в системе 1С:Предприятие 8.3 часто возникает задача представить плоский список элементов в виде иерархической структуры. Это необходимо для более наглядного и удобного взаимодействия пользователя с информацией, особенно когда данные имеют несколько уровней вложенности. Например, мы хотим увидеть даты запрета изменения в разрезе организаций, пользователей и разделов.
Рассмотрим, как мы можем решить эту задачу, а также проанализируем смежные вопросы: как сохранить изменения, внесенные пользователем в такое дерево, обратно в базу данных и как настроить его отображение для лучшей читаемости.
Предположим, у нас есть данные, полученные запросом из регистра сведений, например, РегистрСведений.ДатыЗапретаИзменения. Эти данные имеют следующий вид:
Организация | Пользователь | Раздел | Дата
Наша цель — представить их в виде иерархии:
Организация1
Пользователь1
Раздел1 Дата
Пользователь2
Раздел1 Дата
Раздел2 Дата
Организация2
Пользователь1
Раздел2 Дата
Пользователь2
Раздел1 Дата
Раздел2 Дата
Рассмотрим два основных подхода к формированию такой структуры: итеративный метод с использованием методов таблицы значений и более оптимизированный подход с использованием запроса.
Этот метод подразумевает, что мы сначала получаем все необходимые данные в плоскую таблицу значений, а затем в циклах проходим по ней, формируя структуру дерева. Этот подход хорошо подходит для небольших и средних объемов данных.
Давайте разберем его по шагам:
ДеревоЗначений и определим для него колонки, соответствующие полям из регистра.Скопировать() и Свернуть().Посмотрим на пример кода, который реализует этот подход:
&НаСервере
Процедура ЗаполнитьДеревоНаСервере()
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ
| ДатыЗапретаИзменения.Объект КАК Организация,
| ДатыЗапретаИзменения.Пользователь КАК Пользователь,
| ДатыЗапретаИзменения.Раздел КАК РазделДатыЗапрета,
| ДатыЗапретаИзменения.ДатаЗапрета КАК ДатаЗапрета
|ИЗ
| РегистрСведений.ДатыЗапретаИзменения КАК ДатыЗапретаИзменения";
// 1. Получаем исходные данные в плоскую таблицу значений
ТЗ = Запрос.Выполнить().Выгрузить();
// 2. Подготавливаем структуру дерева значений
Дерево = Новый ДеревоЗначений;
// Определяем типы колонок для дерева, чтобы избежать ошибок при передаче данных
ОписаниеОрганизации = Новый ОписаниеТипов("СправочникСсылка.Организации");
ОписаниеПользователя = Новый ОписаниеТипов("СправочникСсылка.Пользователи");
ОписаниеРаздела = Новый ОписаниеТипов("ПланВидовХарактеристикСсылка.РазделыДатЗапретаИзменения");
ОписаниеДаты = Новый ОписаниеТипов("Дата");
Дерево.Колонки.Добавить("Организация", ОписаниеОрганизации);
Дерево.Колонки.Добавить("Пользователь", ОписаниеПользователя);
Дерево.Колонки.Добавить("РазделДатыЗапрета", ОписаниеРаздела);
Дерево.Колонки.Добавить("ДатаЗапрета", ОписаниеДаты);
// 3. Итеративное заполнение дерева
// Сначала получаем уникальные организации
ТЗ_Организации = ТЗ.Скопировать(, "Организация");
ТЗ_Организации.Свернуть("Организация");
Для Каждого СтрОрг Из ТЗ_Организации Цикл
// Создаем узел для Организации
УзелОрганизация = Дерево.Строки.Добавить();
УзелОрганизация.Организация = СтрОрг.Организация;
// Для текущей Организации получаем строки, относящиеся к ней
СтрокиОрганизации = ТЗ.Скопировать(Новый Структура("Организация", СтрОрг.Организация));
// Из этих строк получаем уникальных Пользователей
ТЗ_Пользователи = СтрокиОрганизации.Скопировать(, "Пользователь");
ТЗ_Пользователи.Свернуть("Пользователь");
Для Каждого СтрП Из ТЗ_Пользователи Цикл
// Создаем узел для Пользователя под узлом Организации
УзелПользователь = УзелОрганизация.Строки.Добавить();
УзелПользователь.Организация = СтрОрг.Организация; // Заполняем для полноты данных, даже если не отображается
УзелПользователь.Пользователь = СтрП.Пользователь;
// Для текущего Пользователя получаем строки, относящиеся к нему в рамках Организации
СтрокиПользователя = СтрокиОрганизации.Скопировать(Новый Структура("Пользователь", СтрП.Пользователь));
Для Каждого Стр Из СтрокиПользователя Цикл
// Создаем конечный узел для Раздела и Даты под узлом Пользователя
НоваяСтрока = УзелПользователь.Строки.Добавить();
// Используем ЗаполнитьЗначенияСвойств для быстрого переноса данных
ЗаполнитьЗначенияСвойств(НоваяСтрока, Стр);
КонецЦикла;
КонецЦикла;
КонецЦикла;
// Привязываем дерево к реквизиту формы для отображения
ЗначениеВРеквизитФормы(Дерево, "ДеревоЗначенийДатыЗапрета");
КонецПроцедуры
В этом коде мы последовательно фильтруем и сворачиваем таблицу значений, чтобы на каждом уровне иерархии получить уникальные значения и создать соответствующие узлы в дереве. Метод Скопировать() с параметром Структура позволяет эффективно отфильтровать строки, а Свернуть() — получить уникальные значения по указанным колонкам.
Для больших объемов данных или когда требуется максимальная производительность, рекомендуется переложить работу по группировке на СУБД. Это можно сделать, используя оператор ИТОГИ ПО в запросе. Результат такого запроса можно напрямую выгрузить в дерево значений.
Рассмотрим, как изменить запрос:
ВЫБРАТЬ
ДатыЗапретаИзменения.Объект КАК Организация,
ДатыЗапретаИзменения.Пользователь КАК Пользователь,
ДатыЗапретаИзменения.Раздел КАК РазделДатыЗапрета,
ДатыЗапретаИзменения.ДатаЗапрета КАК ДатаЗапрета
ИЗ
РегистрСведений.ДатыЗапретаИзменения КАК ДатыЗапретаИзменения
ИТОГИ ПО
Организация,
Пользователь,
РазделДатыЗапрета
Когда мы выполняем такой запрос, он возвращает не просто плоскую таблицу, а структурированный набор данных, где каждая строка может быть как детальной записью, так и итоговой строкой для определенного уровня группировки. Чтобы выгрузить это в ДеревоЗначений, мы используем специальный параметр метода Выгрузить().
Посмотрим на пример кода:
&НаСервере
Процедура ЗаполнитьДеревоНаСервереОптимизировано()
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ
| ДатыЗапретаИзменения.Объект КАК Организация,
| ДатыЗапретаИзменения.Пользователь КАК Пользователь,
| ДатыЗапретаИзменения.Раздел КАК РазделДатыЗапрета,
| ДатыЗапретаИзменения.ДатаЗапрета КАК ДатаЗапрета
|ИЗ
| РегистрСведений.ДатыЗапретаИзменения КАК ДатыЗапретаИзменения
|ИТОГИ ПО
| Организация,
| Пользователь,
| РазделДатыЗапрета"; // Указываем поля, по которым нужна иерархия
РезультатЗапроса = Запрос.Выполнить();
// Создаем новое дерево значений
Дерево = Новый ДеревоЗначений;
// Колонки дерева должны соответствовать полям выборки запроса
// Если используются итоговые поля, их также нужно добавить в колонки.
// Если в запросе использовались АГРЕГАТНЫЕ функции в ИТОГАХ, то
// эти колонки также нужно будет добавить в дерево.
Дерево.Колонки.Добавить("Организация");
Дерево.Колонки.Добавить("Пользователь");
Дерево.Колонки.Добавить("РазделДатыЗапрета");
Дерево.Колонки.Добавить("ДатаЗапрета");
// Выгружаем результат запроса в дерево значений, используя обход по группировкам.
// Этот метод автоматически строит иерархию.
РезультатЗапроса.Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам, Дерево);
ЗначениеВРеквизитФормы(Дерево, "ДеревоЗначенийДатыЗапрета");
КонецПроцедуры
Метод Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам, Дерево) является крайне мощным инструментом, который позволяет платформе самостоятельно построить иерархическое дерево на основе итогов запроса. Это значительно сокращает количество кода и повышает производительность, особенно при работе с большими объемами данных.
После того как мы успешно отобразили данные в виде дерева, возникает следующая важная задача: как позволить пользователю изменять эти данные и сохранять их обратно в регистр сведений. Прямое динамическое сохранение каждого изменения может быть неудобным и неэффективным. Лучшим подходом является использование кнопки "Сохранить" и отслеживание изменений.
Рассмотрим по шагам, как организовать надежный механизм сохранения:
ДеревоЗначений новую колонку булевского типа, например, Изменено. Эта колонка не будет отображаться пользователю, но будет служить индикатором того, что строка была изменена.ПриИзменении поля установить флаг Изменено = Истина для этой строки. Это событие вызывается на клиенте, поэтому нам достаточно установить флаг в клиентском контексте.Изменено = Истина, это означает, что данные в ней были изменены и требуют записи в регистр сведений.Посмотрим на пример концептуального кода:
// Добавление колонки в ДеревоЗначений (например, в процедуре инициализации или в ЗаполнитьДеревоНаСервере)
// Дерево.Колонки.Добавить("Изменено", Новый ОписаниеТипов("Булево"));
// При создании строк дерева: НоваяСтрока.Изменено = Ложь;
// Пример обработчика события изменения поля на форме (Клиентский код)
// Предположим, поле ввода связано с колонкой дерева, например, "ДатаЗапрета"
&НаКлиенте
Процедура ДеревоЗначенийДатыЗапретаДатаЗапретаПриИзменении(Элемент)
// Получаем текущую строку дерева, в которой произошло изменение
ТекущаяСтрока = Элементы.ДеревоЗначенийДатыЗапрета.ТекущиеДанные;
Если ТекущаяСтрока Тогда
// Устанавливаем флаг изменения для этой строки
ТекущаяСтрока.Изменено = Истина;
// Можно обновить условное оформление для визуального выделения измененной строки
ОбновитьОтображениеЭлементов(); // Пример вызова для обновления формы
КонецЕсли;
КонецПроцедуры
// Кнопка на форме "Сохранить"
&НаКлиенте
Процедура СохранитьИзменения(Команда)
ЗаписатьИзмененияНаСервере();
КонецПроцедуры
// Серверная процедура для записи изменений
&НаСервере
Процедура ЗаписатьИзмененияНаСервере()
// Получаем текущее дерево значений из реквизита формы
Дерево = РеквизитФормыВЗначение("ДеревоЗначенийДатыЗапрета");
Для Каждого СтрокаОрганизации Из Дерево.Строки Цикл
ОбходДереваДляСохранения(СтрокаОрганизации);
КонецЦикла;
// После сохранения, если нужно, можно сбросить флаги "Изменено"
// или перезаполнить дерево, чтобы обновить данные и убрать подсветку
КонецПроцедуры
&НаСервере
Процедура ОбходДереваДляСохранения(УзелДерева)
Если УзелДерева.Свойство("Изменено") И УзелДерева.Изменено Тогда
// Эта строка была изменена, записываем ее в регистр
// Определяем, к какому уровню иерархии относится строка
// и соответствующим образом записываем/обновляем данные в РегистреСведений.ДатыЗапретаИзменения
// Пример для конечной строки "РазделДатыЗапрета"
Если УзелДерева.РазделДатыЗапрета <> Неопределено Тогда
НаборЗаписей = РегистрыСведений.ДатыЗапретаИзменения.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Объект.Установить(УзелДерева.Организация);
НаборЗаписей.Отбор.Пользователь.Установить(УзелДерева.Пользователь);
НаборЗаписей.Отбор.Раздел.Установить(УзелДерева.РазделДатыЗапрета);
// Пробуем прочитать текущую запись
НаборЗаписей.Прочитать();
// Если записи нет, добавляем новую
Если НаборЗаписей.Количество() = 0 Тогда
НоваяЗапись = НаборЗаписей.Добавить();
НоваяЗапись.Объект = УзелДерева.Организация;
НоваяЗапись.Пользователь = УзелДерева.Пользователь;
НоваяЗапись.Раздел = УзелДерева.РазделДатыЗапрета;
Иначе
// Обновляем существующую запись
НоваяЗапись = НаборЗаписей[0];
КонецЕсли;
НоваяЗапись.ДатаЗапрета = УзелДерева.ДатаЗапрета;
НаборЗаписей.Записать();
// После успешной записи сбрасываем флаг
УзелДерева.Изменено = Ложь;
КонецЕсли;
// Для родительских строк (Организация, Пользователь) обычно не требуется прямая запись
// их изменение может сигнализировать об изменении их дочерних элементов
КонецЕсли;
// Рекурсивный обход дочерних строк
Для Каждого ДочернийУзел Из УзелДерева.Строки Цикл
ОбходДереваДляСохранения(ДочернийУзел);
КонецЦикла;
КонецПроцедуры
Важным аспектом является то, что мы можем использовать условное оформление элементов формы для визуального выделения измененных, но еще не сохраненных строк. Это значительно улучшает пользовательский опыт, так как пользователь всегда видит, какие данные требуют сохранения.
После построения дерева мы можем столкнуться с проблемой визуального дублирования информации. Например, при отображении дерева в обычном режиме мы можем увидеть:
-Организация1
-Организация1 Пользователь1
-Организация1 Пользователь1 Раздел 1
Такое представление может быть избыточным и не очень удобным для восприятия. Мы бы хотели видеть более лаконичную структуру, где каждый уровень отображает только свою специфичную информацию:
-Организация1
-Пользователь1
-Раздел 1 Дата
-Пользователь2
-Раздел 1 Дата
Платформа 1С не предоставляет стандартных средств для автоматического скрытия значений родительских колонок в дочерних строках при использовании нескольких колонок. Однако мы можем достичь желаемого визуального эффекта, используя одну колонку для отображения иерархической информации.
Рассмотрим следующий подход:
ДеревеЗначений создадим только одну видимую колонку, например, с именем Представление. Эта колонка будет содержать текстовое описание для каждой строки в дереве.Представление в зависимости от уровня иерархии.
Представление запишем название организации.Представление запишем имя пользователя.Представление запишем название раздела и дату.Организация, Пользователь, РазделДатыЗапрета, ДатаЗапрета, будут добавлены в дерево, но их свойство Видимость на элементе формы "Дерево" будет установлено в Ложь. Таким образом, они будут хранить фактические данные, но не будут отображаться пользователю.Давайте посмотрим, как можно адаптировать код для такого отображения, основываясь на итеративном подходе (похожий принцип будет и для метода с ИТОГИ ПО):
&НаСервере
Процедура ЗаполнитьДеревоНаСервереСЕдинойКолонкой()
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ
| ДатыЗапретаИзменения.Объект КАК Организация,
| ДатыЗапретаИзменения.Пользователь КАК Пользователь,
| ДатыЗапретаИзменения.Раздел КАК РазделДатыЗапрета,
| ДатыЗапретаИзменения.ДатаЗапрета КАК ДатаЗапрета
|ИЗ
| РегистрСведений.ДатыЗапретаИзменения КАК ДатыЗапретаИзменения";
ТЗ = Запрос.Выполнить().Выгрузить();
Дерево = Новый ДеревоЗначений;
// Создаем одну видимую колонку для представления
Дерево.Колонки.Добавить("Представление", Новый ОписаниеТипов("Строка"));
// Добавляем служебные колонки для хранения фактических данных
Дерево.Колонки.Добавить("Организация", Новый ОписаниеТипов("СправочникСсылка.Организации"));
Дерево.Колонки.Добавить("Пользователь", Новый ОписаниеТипов("СправочникСсылка.Пользователи"));
Дерево.Колонки.Добавить("РазделДатыЗапрета", Новый ОписаниеТипов("ПланВидовХарактеристикСсылка.РазделыДатЗапретаИзменения"));
Дерево.Колонки.Добавить("ДатаЗапрета", Новый ОписаниеТипов("Дата"));
// Если используем механизм сохранения, добавляем флаг изменения
Дерево.Колонки.Добавить("Изменено", Новый ОписаниеТипов("Булево"));
ТЗ_Организации = ТЗ.Скопировать(, "Организация");
ТЗ_Организации.Свернуть("Организация");
Для Каждого СтрОрг Из ТЗ_Организации Цикл
УзелОрганизация = Дерево.Строки.Добавить();
УзелОрганизация.Организация = СтрОрг.Организация;
УзелОрганизация.Представление = СтрОрг.Организация.Наименование; // Заполняем представление только именем организации
УзелОрганизация.Изменено = Ложь;
СтрокиОрганизации = ТЗ.Скопировать(Новый Структура("Организация", СтрОрг.Организация));
ТЗ_Пользователи = СтрокиОрганизации.Скопировать(, "Пользователь");
ТЗ_Пользователи.Свернуть("Пользователь");
Для Каждого СтрП Из ТЗ_Пользователи Цикл
УзелПользователь = УзелОрганизация.Строки.Добавить();
УзелПользователь.Организация = СтрОрг.Организация;
УзелПользователь.Пользователь = СтрП.Пользователь;
УзелПользователь.Представление = СтрП.Пользователь.Наименование; // Заполняем представление только именем пользователя
УзелПользователь.Изменено = Ложь;
СтрокиПользователя = СтрокиОрганизации.Скопировать(Новый Структура("Пользователь", СтрП.Пользователь));
Для Каждого Стр Из СтрокиПользователя Цикл
НоваяСтрока = УзелПользователь.Строки.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, Стр); // Заполняем служебные колонки
НоваяСтрока.Представление = Стр.РазделДатыЗапрета.Наименование + " " + Формат(Стр.ДатаЗапрета, "ДФ=dd.MM.yyyy"); // Формируем представление для конечного уровня
НоваяСтрока.Изменено = Ложь;
КонецЦикла;
КонецЦикла;
КонецЦикла;
ЗначениеВРеквизитФормы(Дерево, "ДеревоЗначенийДатыЗапрета");
// На клиенте (в процедуре ПриСозданииНаСервере) можно настроить видимость колонок элемента формы:
// Элементы.ДеревоЗначенийДатыЗапрета.Колонки.Организация.Видимость = Ложь;
// Элементы.ДеревоЗначенийДатыЗапрета.Колонки.Пользователь.Видимость = Ложь;
// ... и т.д., оставляя видимой только колонку "Представление"
КонецПроцедуры
Таким образом, мы полностью контролируем то, что видит пользователь на каждом уровне иерархии, избегая дублирования информации. Важно помнить, что если мы хотим редактировать данные в этом дереве, то поля для редактирования должны быть связаны со скрытыми колонками, а не с колонкой Представление, так как она является исключительно отображающей.
Мы рассмотрели комплексный подход к работе с иерархическими данными в 1С:Предприятие 8.3, начиная от формирования дерева из плоской таблицы, продолжая механизмом сохранения изменений и заканчивая тонкой настройкой его визуального представления. Используя эти методы, вы сможете создавать удобные и функциональные интерфейсы для работы с данными любой сложности.