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