Привет, Хабр! Меня зовут Данил Кравченко, я разработчик 1С в IBS. Через руки моего отдела проходит много строительных проектов. Так уж повелось, что в стройке интерфейсы для табличных частей часто создаются на основе деревьев, что вызывает определенные сложности у разработчиков. Я сам совсем недавно собрал полный комплект шишек на эту тему и хотел бы поделиться способами решения типовых проблем. А в конце вы найдете ряд рекомендаций, соблюдение которых позволит свести эти проблемы к минимуму.

ДеревоЗначений и ДанныеФормыДерево

Начнем с рассмотрения объекта метаданных ДеревоЗначений и его аналога на форме — ДанныеФормыДерево.

Представить ДеревоЗначений просто. Это таблица значений, в которой, кроме колонок, которые мы описываем самостоятельно, есть два дополнительных атрибута:

  • свойство «Строки», которое ссылается на строки, подчиненные текущей,

  • свойство «Родитель», которое ссылается на родительскую строку.

При этом у строки самого верхнего уровня родитель будет равен Неопределено.

ДанныеФормыДерево — аналог ДанныеФормыКоллекция, но для дерева значений, а не для таблицы. Родительские и подчиненные строки здесь получаются несколько иначе — через методы ПолучитьЭлементы и ПолучитьРодителя. 

ДеревоЗначений можно преобразовывать в ДанныеФормыДерево и обратно. Для этого нам требуется контекст формы:

  • чтобы ДеревоЗначений преобразовать в ДанныеФормыДерево, мы используем ЗначениеВРеквизитФормы;

  • для обратного преобразования используем РеквизитФормыВЗначение.

ДанныеФормыДерево доступно и на клиенте, и на сервере. ДеревоЗначений доступно только на сервере.

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

Задачи, решаемые построением интерфейса на дереве

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

Отрисовать эти данные можно двумя способами.

Способ 1. Дерево по фиксированной иерархии

Понять, что это такое, можно на примере укрупненной расценки заказа на строительные работы.

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

Иерархию для переноса данных в дерево можно получить одним из способов:

  • ИТОГИ ПО ИмяПоля1, ИмяПоля2 … 

  • отдельные подзапросы для строк каждого уровня (на уровне ТЧ нужно обеспечить связь родительских строк с подчиненными)

Отрисовывать дерево по фиксированной иерархии удобно по нескольким причинам. Работая с деревом в объектной модели, мы полностью контролируем строки (и попадающие туда данные), потому что алгоритм заполнения приходится писать вручную. Если нам что-то нужно заполнить по-особенному, не надо повторно обходить строки дерева.

Главный минус такого способа — «лестница» циклов. Сколько в дереве будет уровней, столько же будет вложенных друг в друга циклов в коде. Зачастую не получится сократить количество кода, обойдясь рекурсией или выносом кода в отдельные методы, потому что может понадобиться обращение к данным вышестоящих или нижестоящих строк.

Отсюда вытекает и следующий минус. Как только на проект приходит новый разработчик, ему приходится тратить намного больше времени, чтобы разобраться с деревом (что там вообще происходит в коде заполнения).

Третий минус — запрос в консоли, возможно, покажет совсем не то, что увидит пользователь на форме, потому что уровни могут ветвиться по-разному, а какие-то данные могут подменяться.

В противовес этому подходу есть другой.

Способ 2. Дерево по иерархии ключевого поля

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

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

Плюс в том, что для заполнения нужна пара строчек кода, не считая текст запроса:

  • РеквизитФормыВЗначение: для переноса данных из запроса в дерево,

  • получить и удалить строки, у которых нет подчиненных: запрос развернет их на один лишний уровень вниз.

Нам не надо ломать голову над структурой дерева — платформа построит ее сама по иерархии справочника. Код заполнения при этом очень легко читается. Да и в консоли запросов при отладке мы видим практически то же самое, что и пользователь на форме.

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

Какой способ лучше?

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

Проблемы при построении интерфейсов на дереве

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

ОтборСтрок для дерева

Это свойство ТаблицыФормы позволяет скрыть ненужные пользователю строки по какому-то отбору. В элементе формы, связанным с деревом, такого свойства нет. И условное оформление нам тут не поможет. 

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

Другой путь — перейти с дерева на таблицу.

Я пробовал оба подхода, каждый из них по-своему приятен. Второй более предпочтителен, но не всегда применим.

Сообщение с привязкой к конкретной ячейке дерева

Следующая проблема — платформа не умеет привязывать сообщения к какой-то конкретной ячейке дерева.

В табличной части мы можем указать объект, имя табличной части, индекс строки и имя поля. Но в дереве самого понятия индекса конкретной строки (в рамках всего дерева) не существует. Есть идентификатор строки в ДанныеФормыДерево, но это разные вещи.

Первый способ обойти это банальный — писать сообщение пользователю так, чтобы он сам нашел строку с ошибкой. Мы всегда его используем. Например, в этой укрупненной расценке мы знаем, куда смотреть:

Второй способ более лихой — использовать самописную панель сообщений, но мы пока не успели его попробовать. Энтузиаст выложил ее на Инфостарт. Можете попробовать: https://infostart.ru/1c/tools/1155087.

Минус этого подхода в том, что сообщения в ней отвязаны от типовой панели сообщений (туда платформа выводит все, что мы говорим пользователю через Сообщить).

Соответственно, метод ПолучитьСообщенияПользователю не выдаст нам сообщений из самописной панели. Нужно будет иметь это в виду.

Загрузка данных в дерево из результата запроса

Представим дерево с колонками: «Колонка», «Кран», «Вентиль» (автоматизируем работу сантехника). 

Источником данных для дерева является таблица с точно такими же колонками, но одной не хватает.

По аналогии с ДанныеФормыКоллекция можно попытаться однострочно загрузить данные. Используем ЗначениеВРеквизитФормы, в запросе выбираем все данные.

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

При использовании метода ЗначенияВРеквизитФормы источник должен иметь точно такой же состав колонок, как и приемник. Платформа в данном случае не умеет заполнять значениями по умолчанию колонки, которые мы не описали в запросе. Также она не умеет «отсекать» лишние колонки. Если нам нечего записать в колонки, мы должны писать значение по умолчанию: 

И в таком случае все прекрасно переносится. 

Bus factor при работе с деревом

Когда я пришел на проект, ожидал рай для разработчика:

  • ТЗ по шаблонам, которые адаптированы в том числе под деревья,

  • описаны внутренние стандарты по работе с деревьями,

  • разработчики и аналитики одинаково хорошо понимают, как дерево устроено.

Но жестокая реальность такова: bus factor в данном случае определяется не одним человеком, а тандемом из аналитика и разработчика, которые поняли, как работает их дерево: аналитик со своей стороны, разработчик со своей. И если кто-то один из этого тандема либо весь тандем уходит с проекта, новый человек будет разбираться очень долго и кропотливо. 

Почему так происходит:

  • много нюансов работы с деревом, нет структурированного опыта;

  • у каждого аналитика — свое понимание того, как должно работать дерево;

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

Методы решения:

  • собрать базу знаний по деревьям: выделить типовые задачи, наработать оптимальные решения;

  • дополнить систему внутренних стандартов разработки и шаблоны СНР в части работы с деревьями;

  • создать/найти терминологический аппарат, понятный и программистам,
    и аналитикам. Своей статьей я отчасти делаю шаг в этом направлении.

Типовые задачи по работе с ДанныеФормыДерево

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

Желательно обеспечить пользователю возможность открытия значения, но запретить редактирование. Это очень важное замечание, поскольку пользователь в принципе не должен быть ответственен за соблюдение структуры дерева. Эта ответственность должна лежать на разработчике. То есть добавление и удаление строк нужно либо запрещать, либо переопределять.

Как реализовать возможность открыть группирующее значение

Шаг 1. Добавляем в дерево два служебных реквизита: 

  • ПредставлениеСтроки с типом Строка.

  • ЗначениеПредставления произвольного типа.

Шаг 2. На форму выводим только ПредставлениеСтроки. Ставим ему РедактированиеТекста = Ложь. КнопкаОткрытия = Да. Все остальные пункты, отвечающие за отображение кнопок (выбор, очистка и т.п.) = Нет. В итоге пользователь может войти в режим редактирования двойным нажатием, но само редактирование будет запрещено. Кнопка открытия будет активна.

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

Шаг 4. Для этого поля обязательно обрабатываем событие «Открытие». Ставим СтандартнаяОбработка = Ложь. Ищем текущую строку дерева, затем вызываем метод ПоказатьЗначение для значения представления.

Как развернуть или свернуть строки дерева на форме

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

А вот сворачивания нет. Мы его писали сами.

Как удалить строку дерева, включая родительскую (если это единственная в подчинении)

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

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

Шаг 1. Переопределяем событие ПередУдалением. Отказ сразу ставим равным Истина.

Шаг 2. Перебираем все строки дерева, которые подчинены текущей. Если для них существуют строки в табличной части — удаляем их оттуда.

Шаг 3. Идем от текущей строки вверх, ищем ту, что будет корневой для данной «лестницы». Для этого проверяем, является ли текущая строка единственной у родителя. Если родительские строки связаны со строками табличной части — удаляем их. Запоминаем корневую строку иерархии.

Шаг 4. Удаляем корневую строку иерархии. Готово!

Как получить строки дерева определенного уровня

Следующая типовая задача — получение строк дерева на определенном уровне. Задача делится на два подвида:

  • получение детальных записей (строк без подчиненных в моей терминологии). Во многих случаях на уровне табличных частей хранятся именно такие строки; 

  • получение строк определенного уровня (уровень — это или уровень вложенности строки, или служебный признак).

Методов решения два, но и идеального среди них нет.

  • первый способ — это кэшировать строки нужных уровней в реквизитах формы (список значений, адрес во временном хранилище);

  • второй способ — получать строки на лету. Обращаясь к дереву, мы перебираем строки и возвращаем те, что лежат на нужном уровне.

Я сравнил два способа по нескольким параметрам:

Первый параметр — временная сложность. Кэширование будет иметь временную сложность О(1), потому что все строки уже готовы и лежат в реквизитах формы. Осталось только обратиться к ним и делать все что хотим. А вот у второго способа временная сложность будет О(N), где N — количество строк в дереве.

Второй параметр: нужно ли нам актуализировать данные по строкам нужных уровней при их изменении? Если храним их в реквизитах формы — нужно при добавлении или удалении строк. При использовании второго подхода за этим следить не нужно.

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

Как перенести данные из дерева в табличную часть

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

  • грязный, но легкий: переносить данные полностью в событие формы ПередЗаписью на клиенте;

  • чистый, но посложнее: переносить данные точечно при изменении строки дерева.

Сравним способы:

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

Однако дьявол кроется в деталях. 

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

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

Как добавить строку в дерево

Я уже делал акцент на том, что пользователю надо запретить добавлять (по кнопке) и перетаскивать строки. Но так или иначе добавлять строки приходится. Для этого нужно создать свои команды, которые будут выполнять некоторые дополнительные действия, например открывать форму выбора из справочника. Реализовать это можно одним из двух способов:

  • грубый, но простой: добавить строку в табличную часть, отрисовать дерево заново;

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

Здесь тоже есть нюансы:

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

А еще есть некоторое пользовательское неудобство. При переотрисовке дерева оно сворачивается в состояние по умолчанию. Таких состояния три: 

  • строки полностью свернуты, показываются только строки первого уровня;

  • строки первого уровня развернуты;

  • дерево полностью развернуто.

Если пользователь будет выполнять какую-то команду, добавляющую строки в дерево, оно будет постоянно сворачиваться-разворачиваться, и это выглядит очень неприятно.

Рекомендации

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

  • ПредставлениеСтроки и ЗначениеПредставления — нужны для упрощения жизни пользователя, обеспечивают отображение и возможность открыть то значение, по которому группируются данные в дереве. Заполняются один раз: при добавлении строки дерева.

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

  • КоличествоПодчиненных — также помогает с условным оформлением. Пригождается в алгоритмах работы с деревом: зависит от предметной области. Заполняется как при формировании дерева, так и при добавлении/удалении его строк. Не обязательно хранить именно КоличествоПодчиненных, можно хранить количество подчиненных с каким-то признаком — здесь все ограничено только фантазией разработчика.

Напоследок — секрет от шефа. Проанализируйте вашу задачу и подумайте хорошенько, нужно ли вам визуализировать данные с деревом. Если есть возможность, не используйте дерево!  Очень с ним много мороки.

Если все же пришлось столкнуться — крепитесь. Мои наработки в помощь: https://disk.yandex.ru/d/I9uFSfKd1K76Ng

Комментарии (0)