Как перебрать все строки дерева в управляемой форме 1С
Содержание
Чтобы обойти дерево на клиенте 1С, берите корневую коллекцию через Дерево.ПолучитьЭлементы() и для каждой строки снова вызывайте ПолучитьЭлементы(): рекурсивной процедурой или циклом со стеком на Массиве. Объекта ДеревоЗначений на клиенте нет, там живёт его «форменная» копия ДанныеФормыДерево, и работать надо с её методами.
Для деревьев до нескольких тысяч строк рекурсия на клиенте работает быстро. Медленно становится, когда в цикл попадает вызов сервера или когда дерево на десятки тысяч строк гоняют между клиентом и сервером целиком. Разберём оба способа, их ограничения и приёмы ускорения.
Чем дерево на клиенте отличается от серверного
Реквизит формы типа «Дерево значений» на сервере и на клиенте — это разные объекты. На сервере в модуле формы с ним можно работать как с данными формы или преобразовать его в настоящее ДеревоЗначений. На клиенте доступен только ДанныеФормыДерево.
Что из этого следует на практике:
- 🌳 У строк нет свойства
Строки. Подчинённые строки получают методомПолучитьЭлементы(). - ⬆️ Вместо свойства
Родитель— методПолучитьРодителя(). У строк верхнего уровня он возвращаетНеопределено. - 🔑 У каждой строки есть идентификатор:
ПолучитьИдентификатор(). По нему строку находят методомДерево.НайтиПоИдентификатору(Ид), и он же нужен дляЭлементы.Дерево.Развернуть(). - 🚫 Методов
Найти()иНайтиСтроки()с поиском по подчинённым на клиенте нет. Поиск по всему дереву на клиенте — это всегда обход. - 📦
РеквизитФормыВЗначение()иЗначениеВРеквизитФормы()работают только на сервере.
Код клиента выполняется на компьютере пользователя. В тонком клиенте это быстро, в веб-клиенте код исполняется в браузере и тот же обход идёт заметно медленнее. Если форма должна работать в веб-клиенте, закладывайте запас по скорости.
Если дерево выводится из динамического списка в режиме «Дерево», обойти его на клиенте не получится: на клиенте есть только видимая порция строк. Такие данные получают запросом на сервере.
Рекурсивный обход строк дерева: самый короткий код
Рекурсия — естественный способ: процедура обрабатывает коллекцию строк и вызывает сама себя для подчинённых. Типовая задача — проставить флажок всем подчинённым строкам, когда пользователь отметил родителя.
&НаКлиенте
Процедура ДеревоПометкаПриИзменении(Элемент)
ТекущаяСтрока = Элементы.Дерево.ТекущиеДанные;
Если ТекущаяСтрока = Неопределено Тогда
Возврат;
КонецЕсли;
УстановитьПометкуПодчиненным(ТекущаяСтрока, ТекущаяСтрока.Пометка);
КонецПроцедуры
&НаКлиенте
Процедура УстановитьПометкуПодчиненным(СтрокаДерева, Пометка)
Для Каждого Подчиненная Из СтрокаДерева.ПолучитьЭлементы() Цикл
Подчиненная.Пометка = Пометка;
УстановитьПометкуПодчиненным(Подчиненная, Пометка);
КонецЦикла;
КонецПроцедуры
Второй частый случай — пересчёт итогов снизу вверх. Функция возвращает сумму уровня, и родитель получает сумму своих детей:
&НаКлиенте
Функция ПересчитатьСуммы(КоллекцияСтрок)
Итог = 0;
Для Каждого Строка Из КоллекцияСтрок Цикл
Подчиненные = Строка.ПолучитьЭлементы();
Если Подчиненные.Количество() > 0 Тогда
Строка.Сумма = ПересчитатьСуммы(Подчиненные);
КонецЕсли;
Итог = Итог + Строка.Сумма;
КонецЦикла;
Возврат Итог;
КонецФункции
// Вызов: ВсегоПоДереву = ПересчитатьСуммы(Дерево.ПолучитьЭлементы());
Ограничения рекурсии:
- 📏 Глубина вызовов ограничена стеком платформы. Для деревьев номенклатуры или подразделений с глубиной 5–15 уровней это не проблема. Проблема возникает при «битых» данных: например, если дерево строят из иерархии, где элемент случайно стал родителем самого себя через цепочку, и построение уходит в бесконечность.
- 🧩 Прервать обход из глубины неудобно: нужен флаг-параметр, который проверяют на каждом уровне.
- 🔍 В отладчике длинную цепочку вложенных вызовов читать тяжело.
Обход дерева без рекурсии: стек и очередь на Массиве
Отдельного типа «Стек» во встроенном языке нет, его роль играет Массив. Кладём строку в конец через Добавить(), забираем последнюю по индексу ВГраница() и удаляем её. Это обход в глубину.
&НаКлиенте
Функция СобратьОтмеченные()
Результат = Новый Массив;
Стек = Новый Массив;
ПоложитьВСтек(Стек, Дерево.ПолучитьЭлементы());
Пока Стек.Количество() > 0 Цикл
Последний = Стек.ВГраница();
Текущая = Стек[Последний];
Стек.Удалить(Последний);
Если Текущая.Пометка Тогда
Результат.Добавить(Текущая.Номенклатура);
КонецЕсли;
ПоложитьВСтек(Стек, Текущая.ПолучитьЭлементы());
КонецЦикла;
Возврат Результат;
КонецФункции
&НаКлиенте
Процедура ПоложитьВСтек(Стек, КоллекцияСтрок)
// Кладём в обратном порядке, чтобы первая строка уровня вышла первой
Всего = КоллекцияСтрок.Количество();
Для Сч = 1 По Всего Цикл
Стек.Добавить(КоллекцияСтрок[Всего - Сч]);
КонецЦикла;
КонецПроцедуры
Если строки уровня положить в прямом порядке, обход тоже пройдёт по всем строкам, но братья будут перебираться снизу вверх. Для поиска это неважно, для вывода в отчёт или файл — важно.
Нужен обход по уровням (сначала все верхние строки, потом все вторые и так далее)? Используйте очередь. Удалять первый элемент массива через Удалить(0) не стоит: при каждом удалении сдвигается весь массив. Быстрее вести индекс-указатель:
&НаКлиенте
Функция ПосчитатьСтрокиПоУровням()
Очередь = Новый Массив;
Для Каждого Строка Из Дерево.ПолучитьЭлементы() Цикл
Очередь.Добавить(Строка);
КонецЦикла;
Указатель = 0;
Пока Указатель < Очередь.Количество() Цикл
Текущая = Очередь[Указатель];
Указатель = Указатель + 1;
Для Каждого Подчиненная Из Текущая.ПолучитьЭлементы() Цикл
Очередь.Добавить(Подчиненная);
КонецЦикла;
КонецЦикла;
Возврат Очередь.Количество();
КонецФункции
Сравнение способов:
| Способ | Когда подходит | Минусы |
|---|---|---|
| Рекурсия | Пометки, итоги снизу вверх, деревья обычной глубины | Сложно прервать, зависит от глубины стека |
Стек на Массиве | Поиск с досрочным выходом, глубокие деревья | Больше кода, порядок надо контролировать |
| Очередь с указателем | Обход по уровням, поиск ближайшей к корню строки | Массив растёт до размера всего дерева |
Досрочный выход в итеративном варианте — обычный Прервать или Возврат внутри Пока. Это главный аргумент за стек, когда вы ищете одну строку в большом дереве.
Когда обходить ДеревоЗначений на сервере
Если обработка не зависит от действий пользователя по ходу (массовый пересчёт, поиск по нескольким колонкам, сортировка), её выгоднее сделать на сервере одним вызовом. Там дерево превращается в ДеревоЗначений, у которого есть поиск сразу по всем уровням.
&НаСервере
Процедура СнятьПометкуСНулевыхНаСервере()
ДеревоЗн = РеквизитФормыВЗначение("Дерево");
Отбор = Новый Структура("Сумма", 0);
НайденныеСтроки = ДеревоЗн.Строки.НайтиСтроки(Отбор, Истина);
Для Каждого Строка Из НайденныеСтроки Цикл
Строка.Пометка = Ложь;
КонецЦикла;
ЗначениеВРеквизитФормы(ДеревоЗн, "Дерево");
КонецПроцедуры
Второй параметр НайтиСтроки() равный Истина включает поиск по подчинённым строкам. У строки ДеревоЗначений есть свойства Строки и Родитель и метод Уровень(), а у коллекции строк — Найти() для поиска первого совпадения.
После ЗначениеВРеквизитФормы() строки дерева на форме создаются заново. Идентификаторы, которые вы запомнили на клиенте до вызова, становятся недействительны, а развёрнутые узлы сворачиваются. Текущую строку и состояние развёрнутости придётся восстановить самим.
Когда что выбирать:
Как ускорить обход большого дерева на клиенте
На дереве в 500 строк любой способ отработает мгновенно. Заметные задержки начинаются с нескольких тысяч строк, особенно в веб-клиенте. Вот что реально помогает.
1. Ни одного вызова сервера внутри цикла
Каждый вызов процедуры &НаСервере из формы — это сетевой обмен, и с ним на сервер уходят данные формы, включая всё дерево. 2 000 строк × вызов сервера на каждую — это 2 000 обменов. Собирайте данные в массив на клиенте и вызывайте сервер один раз после цикла.
Если серверной процедуре не нужны реквизиты формы, объявляйте её &НаСервереБезКонтекста: тогда дерево вообще не передаётся.
2. Обходите только нужную ветку
Пометку подчинённых меняйте от текущей строки, а не перебором всего дерева с проверкой «а не потомок ли это». Для подъёма к корню используйте цепочку ПолучитьРодителя():
&НаКлиенте
Процедура ОбновитьПометкуРодителей(СтрокаДерева)
Родитель = СтрокаДерева.ПолучитьРодителя();
Пока Родитель <> Неопределено Цикл
ВсеОтмечены = Истина;
Для Каждого Подчиненная Из Родитель.ПолучитьЭлементы() Цикл
Если Не Подчиненная.Пометка Тогда
ВсеОтмечены = Ложь;
Прервать;
КонецЕсли;
КонецЦикла;
Родитель.Пометка = ВсеОтмечены;
Родитель = Родитель.ПолучитьРодителя();
КонецЦикла;
КонецПроцедуры
Подъём к корню стоит столько, сколько уровней в дереве, а не сколько в нём строк.
3. Запоминайте идентификаторы, а не ищите строку заново
Если к одной и той же строке приходится возвращаться (после диалога, в обработчике ожидания), сохраните ПолучитьИдентификатор() в реквизит формы. Потом строку берут через Дерево.НайтиПоИдентификатору(Ид) без обхода.
Для частого поиска по ключу один раз постройте Соответствие «ключ → идентификатор». Перестраивайте его после каждого заполнения дерева на сервере: идентификаторы при этом меняются.
4. Длинные операции — с индикацией и возможностью прервать
Для обхода, который идёт секунды, выводите прогресс через Состояние() и вставьте в цикл ОбработкаПрерыванияПользователя(), чтобы пользователь мог остановить процесс сочетанием Ctrl+Break. Выводите состояние раз в 100–500 строк, а не на каждой: перерисовка тоже стоит времени.
5. Разворачивайте дерево одной командой
Чтобы раскрыть дерево полностью, не нужно обходить все узлы. Достаточно перебрать верхний уровень и передать Истина во второй параметр:
Для Каждого Строка Из Дерево.ПолучитьЭлементы() Цикл
Элементы.Дерево.Развернуть(Строка.ПолучитьИдентификатор(), Истина);
КонецЦикла;
На очень больших деревьях полное раскрытие само по себе тяжёлое: форме приходится отрисовать все строки. Иногда лучше раскрыть только первый уровень.
6. Не держите на форме лишнее
Колонки, которые нужны только серверу (служебные ссылки, промежуточные суммы), увеличивают объём данных при каждом контекстном вызове. Если колонку не показывают и не читают на клиенте, подумайте, нужна ли она в реквизите формы.
Практический пример: сохранить и вернуть развёрнутые узлы
Типичная жалоба пользователей: нажали «Обновить», дерево перезаполнилось на сервере и свернулось. Решение — обход на клиенте до и после серверного вызова. До вызова собираем ключи развёрнутых строк, после — разворачиваем строки с этими ключами.
Идентификаторы для этого не годятся, они меняются. Нужна колонка с устойчивым ключом, например ссылкой на группу справочника.
&НаКлиенте
Процедура Обновить(Команда)
Развернутые = Новый Соответствие;
СобратьРазвернутые(Дерево.ПолучитьЭлементы(), Развернутые);
ОбновитьНаСервере();
ВосстановитьРазвернутые(Дерево.ПолучитьЭлементы(), Развернутые);
КонецПроцедуры
&НаКлиенте
Процедура СобратьРазвернутые(КоллекцияСтрок, Развернутые)
Для Каждого Строка Из КоллекцияСтрок Цикл
Если Элементы.Дерево.Развернут(Строка.ПолучитьИдентификатор()) Тогда
Развернутые.Вставить(Строка.Ключ, Истина);
СобратьРазвернутые(Строка.ПолучитьЭлементы(), Развернутые);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
&НаКлиенте
Процедура ВосстановитьРазвернутые(КоллекцияСтрок, Развернутые)
Для Каждого Строка Из КоллекцияСтрок Цикл
Если Развернутые[Строка.Ключ] = Истина Тогда
Элементы.Дерево.Развернуть(Строка.ПолучитьИдентификатор());
ВосстановитьРазвернутые(Строка.ПолучитьЭлементы(), Развернутые);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Обход идёт только по развёрнутым веткам: в свёрнутые узлы рекурсия не заходит. Поэтому даже на дереве в 20 000 строк, где пользователь раскрыл три группы, обрабатываются десятки строк, а не всё дерево.
Тот же приём подходит, чтобы вернуть текущую строку: запомните её ключ, а после обновления найдите строку обходом и присвойте её идентификатор свойству Элементы.Дерево.ТекущаяСтрока.
Частые ошибки при обходе дерева в 1С
- ❌ Удаление строк внутри
Для Каждого. Коллекция меняется под циклом, и часть строк пропускается. Удаляйте обратным циклом по индексу:Для Сч = 1 По Всего Цикл Строка = Коллекция[Всего - Сч], гдеВсегопосчитано до цикла. - ❌ Вызов
РеквизитФормыВЗначение()из клиентской процедуры. Метод серверный, код не скомпилируется на клиенте. Вынесите обработку в процедуру&НаСервере. - ❌ Обращение к
Строка.СтрокиилиСтрока.Родительна клиенте. Эти свойства есть только уДеревоЗначений. На клиенте —ПолучитьЭлементы()иПолучитьРодителя(). - ❌ Сохранённые идентификаторы после перезаполнения. После
ЗначениеВРеквизитФормы()НайтиПоИдентификатору()со старым идентификатором вернёт не ту строку илиНеопределено. Проверяйте результат наНеопределено. - ❌ Рекурсия при построении дерева из иерархии с ошибкой в данных. Если в справочнике есть зацикленная ссылка (не родитель, а, например, своё поле «Входит в»), рекурсивное построение не закончится. Храните уже обработанные ссылки в
Соответствиии пропускайте повторы. - ❌ Обработчик
ПриИзменениипометки, который вызывает сервер. На каждый щелчок флажка форма уходит на сервер вместе с деревом. Пометки считайте на клиенте.