К статье
Практический материал

Разработка и Конфигурация · 13 мин

Как обойти дерево значений на клиенте в 1С

Как обойти дерево значений на клиенте 1С без вызова сервера: рекурсия, стек, поиск строк по условию. Рабочие примеры кода, ограничения тонкого клиента и типичные ошибки.

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

На клиенте доступны три практических подхода: рекурсивный обход через процедуру формы, итеративный обход со стеком и обход через НайтиСтроки(), когда нужны не все узлы, а только подходящие под условие. Выбор зависит от глубины дерева, объёма данных и того, что нужно сделать с каждой строкой — прочитать, изменить или собрать в массив.

Ниже разобраны рабочие варианты кода для управляемых форм, ограничения тонкого клиента, способы избежать лишних серверных вызовов и типичные ошибки, из-за которых обход «теряет» ветки или падает с ошибкой переполнения стека.

Что доступно на клиенте при работе с деревом значений

На управляемой форме дерево обычно хранится в реквизите формы типа ДеревоЗначений и отображается элементом «Таблица» с видом «Дерево». На клиенте доступны все коллекции строк: у корня дерева — метод ПолучитьЭлементы(), у каждой строки — тот же метод для перехода к потомкам. Этого достаточно для полного обхода без единого серверного вызова.

Недоступно на клиенте: прямой запрос к базе, методы менеджеров объектов, обращение к серверным экспортным процедурам без явного вызова &НаСервере. Поэтому правильная стратегия — загрузить дерево на форму один раз при создании или по команде, а дальше обходить его локально. Каждый вызов сервера из цикла обхода — это сетевой round-trip, и на дереве из тысячи строк форма «зависнет» на секунды или минуты.

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

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

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

&НаКлиенте

Процедура ОбойтиДерево(КоллекцияСтрок)

Для Каждого Строка Из КоллекцияСтрок Цикл

// Действие со строкой: чтение или изменение

Строка.Пометка = Истина;

// Рекурсивный спуск к потомкам

ОбойтиДерево(Строка.ПолучитьЭлементы());

КонецЦикла;

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

&НаКлиенте

Процедура КомандаОбхода(Команда)

ОбойтиДерево(Дерево.ПолучитьЭлементы());

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

Обратите внимание на начальную точку: обход стартует с Дерево.ПолучитьЭлементы(), где Дерево — имя реквизита формы. Частая ошибка — передать сам реквизит вместо коллекции верхнего уровня, тогда цикл не выполнится или обратится не к тем данным.

Если внутри обхода нужно накапливать результат — например, собрать массив ссылок отмеченных строк — передавайте в процедуру второй параметр (массив или структуру). Массивы и структуры передаются по ссылке, поэтому изменения внутри рекурсии видны снаружи.

bsl
&НаКлиенте
Процедура СобратьОтмеченные(КоллекцияСтрок, Результат)
    Для Каждого Строка Из КоллекцияСтрок Цикл
        Если Строка.Пометка Тогда
            Результат.Добавить(Строка.Ссылка);
        КонецЕсли;
        СобратьОтмеченные(Строка.ПолучитьЭлементы(), Результат);
    КонецЦикла;
КонецПроцедуры

Итеративный обход со стеком для глубоких деревьев

Рекурсия в 1С ограничена глубиной стека вызовов. Для справочников с сильной вложенностью или деревьев, построенных из произвольных данных, глубина может достигать сотен уровней, и рекурсивный обход завершится ошибкой «Переполнение стека». Страховка — итеративный обход с явным стеком на основе массива.

&НаКлиенте

Процедура ОбойтиДеревоСтеком()

Стек = Новый Массив;

Для Каждого Строка Из Дерево.ПолучитьЭлементы() Цикл

Стек.Добавить(Строка);

КонецЦикла;

Пока Стек.Количество() > 0 Цикл

ТекущаяСтрока = Стек[Стек.Количество() - 1];

Стек.Удалить(Стек.Количество() - 1);

// Действие со строкой

ТекущаяСтрока.Обработано = Истина;

Для Каждого Потомок Из ТекущаяСтрока.ПолучитьЭлементы() Цикл

Стек.Добавить(Потомок);

КонецЦикла;

КонецЦикла;

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

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

Дополнительный плюс итеративного подхода — его проще прервать по условию и легче отлаживать, потому что весь контекст виден в одном цикле, а не размазан по цепочке вызовов.

Сравнение вариантов обхода

Рекурсия

Короткий и читаемый код. Подходит для деревьев глубиной до десятков уровней. Риск переполнения стека на очень глубоких иерархиях.

Стек на массиве

Выдерживает любую глубину, легко прервать и отладить. Код длиннее, порядок обхода веток требует внимания.

НайтиСтроки с Истина

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

Поиск строк по условию без полного обхода

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

&НаКлиенте

Процедура НайтиНужныеСтроки()

Отбор = Новый Структура("Пометка", Истина);

Найденные = Дерево.НайтиСтроки(Отбор, Истина);

Для Каждого Строка Из Найденные Цикл

// Работа с найденной строкой

КонецЦикла;

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

Второй параметр Истина — ключевой: без него поиск выполняется только среди строк верхнего уровня, и дочерние узлы в результат не попадут. Это одна из самых частых причин «неправильной работы» поиска по дереву у начинающих разработчиков.

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

Маршрут решения

Как выбрать способ обхода

Нужно выполнить действие с каждой строкой, дерево неглубокоеИспользуйте рекурсивную процедуру &НаКлиенте
Глубина дерева неизвестна или превышает десятки уровнейПрименяйте итеративный обход со стеком на массиве
Нужны только строки, равные заданным значениям колонокВызовите Дерево.НайтиСтроки(Отбор, Истина) без обхода
Дерево ещё не загружено на формуСначала заполните его на сервере, обход выполняйте на клиенте

Как не убить производительность: серверные вызовы и крупные деревья

Главный враг обхода на клиенте — серверные вызовы внутри цикла. Конструкция вида «для каждой строки вызвать серверную функцию» превращает обход в серию сетевых обращений. Если внутри обхода нужны данные, которых нет на клиенте (например, остатки или реквизиты ссылок), соберите сначала массив ссылок на клиенте, передайте его одним вызовом на сервер, получите соответствие с данными и используйте его в обходе.

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

Третий момент — размер дерева. Дерево значений на форме целиком передаётся между клиентом и сервером при каждом контекстном вызове. Дерево на десятки тысяч строк делает каждый серверный вызов тяжёлым. В таких случаях храните на форме только нужные колонки и рассмотрите вариант обхода и обработки полностью на сервере, а клиенту оставьте только отображение.

Работа с текущей строкой и разворачивание дерева

Частая сопутствующая задача — после обхода позиционироваться на найденной строке или развернуть дерево до нужного узла. Для этого у элемента формы (таблицы дерева) есть свойство ТекущаяСтрока, которому присваивается идентификатор строки, полученный методом ПолучитьИдентификатор():

&НаКлиенте

Процедура Спозиционироваться(СтрокаДерева)

Элементы.Дерево.ТекущаяСтрока = СтрокаДерева.ПолучитьИдентификатор();

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

Разворачивание выполняется методом Развернуть() элемента формы с тем же идентификатором строки. Чтобы развернуть всё дерево, придётся обойти его и вызвать Развернуть() для каждой строки, у которой есть потомки, — встроенной команды «развернуть всё» у элемента формы нет. Здесь как раз пригодится рекурсивный обход.

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

Порядок действий для типового обхода
  1. 1
    Убедитесь, что дерево заполнено: реквизит формы загружен на сервере при создании или по команде
  2. 2
    Напишите процедуру &НаКлиенте, принимающую коллекцию строк, с рекурсивным вызовом для ПолучитьЭлементы() каждой строки
  3. 3
    Запустите обход из обработчика команды, передав Дерево.ПолучитьЭлементы()
  4. 4
    Если нужны данные с сервера — соберите массив ссылок в обходе и получите данные одним серверным вызовом
  5. 5
    Проверьте результат на дереве с несколькими уровнями вложенности и пустыми ветками

Типичные ошибки при обходе дерева на клиенте

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

Вторая ошибка — вызов НайтиСтроки() без второго параметра и вывод, что «поиск не работает». Третья — попытка использовать запрос или обращение к базе внутри клиентского обхода: код либо не компилируется, либо порождает серию серверных вызовов. Четвёртая — модификация коллекции во время её перебора: удаление строк внутри цикла Для Каждого приводит к пропуску элементов. Удалять нужно либо с конца по индексу, либо собирать строки на удаление в отдельный массив и удалять после обхода.

Типичные ошибки
01
Перебор только верхнего уровня без спуска к ПолучитьЭлементы() строки — дочерние узлы игнорируются
02
НайтиСтроки(Отбор) без второго параметра Истина — поиск идёт только по первому уровню
03
Серверный вызов внутри цикла обхода — форма зависает на больших деревьях
04
Удаление строк внутри цикла Для Каждого — часть строк пропускается
05
Рекурсия на дереве неизвестной глубины — ошибка переполнения стека

Как проверить, что обход отработал правильно

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

Для визуальной проверки удобно временно добавить в дерево служебную колонку, например «ПорядокОбхода», и записывать в неё номер итерации. Развернув дерево на форме, вы сразу увидите, в каком порядке обходились узлы и не пропущена ли какая-то ветка. После отладки колонку скрывают или удаляют.

Контрольные точки

Как проверить результат

Количество обработанных строк
Норма

Совпадает с общим числом строк дерева на всех уровнях

Тревожный признак

Меньше ожидаемого — обход не спускается в часть веток

Строки по условию отбора
Норма

Найдены строки со всех уровней иерархии

Тревожный признак

Найдены только строки верхнего уровня — не хватает параметра Истина

Время выполнения обхода
Норма

Форма отвечает мгновенно или за доли секунды

Тревожный признак

Форма подвисает — вероятны серверные вызовы в цикле

Частые вопросы

Можно ли обойти дерево значений на клиенте без рекурсии?

Да. Используйте массив как стек: положите туда строки верхнего уровня, затем в цикле извлекайте последнюю строку, обрабатывайте её и добавляйте потомков. Такой подход выдерживает любую глубину иерархии и не рискует переполнением стека вызовов.

Почему НайтиСтроки не находит строки на нижних уровнях дерева?

По умолчанию метод ищет только среди строк текущего уровня коллекции. Передайте второй параметр Истина: Дерево.НайтиСтроки(Отбор, Истина) — тогда поиск выполнится рекурсивно по всей иерархии.

Как удалить из дерева строки по условию на клиенте?

Не удаляйте строки внутри цикла перебора — это пропускает элементы. Соберите подлежащие удалению строки в массив во время обхода, а затем удаляйте каждую через коллекцию её родителя: Родитель.ПолучитьЭлементы().Удалить(Строка). Родителя строки возвращает метод ПолучитьРодителя().

Что быстрее: обход на клиенте или на сервере?

Для чтения и изменения уже загруженных данных обход на клиенте быстрее, потому что не тратит время на сетевые вызовы. На сервер обработку имеет смысл переносить, когда нужны данные из базы внутри цикла или когда дерево очень большое и сам его перенос между клиентом и сервером дорог.

Как развернуть всё дерево на форме программно?

Встроенной команды нет. Обойдите дерево рекурсивно и для каждой строки с потомками вызовите Элементы.Дерево.Развернуть(Строка.ПолучитьИдентификатор()). Разворачивайте начиная с верхних уровней, иначе дочерние узлы могут остаться свёрнутыми внутри ещё не раскрытых родителей.

Можно ли на клиенте создать новое дерево значений программно?

Нет, конструктор дерева значений доступен только на сервере. На клиенте можно работать с уже существующим реквизитом формы: добавлять и удалять строки в нём, менять значения колонок. Структуру колонок и первоначальное заполнение выполняйте на сервере.

Итог

Что важно запомнить

Обход дерева на клиенте выполняется через ПолучитьЭлементы() у строк — рекурсией для обычных деревьев или стеком для глубоких. Для поиска по условию используйте НайтиСтроки(Отбор, Истина). Избегайте серверных вызовов внутри цикла: собирайте ссылки и обращайтесь к серверу один раз. Не изменяйте коллекцию во время её перебора.