Работа с иерархическими структурами данных, такими как ДеревоЗначений, часто вызывает трудности у начинающих разработчиков в 1С. Одной из наиболее распространенных задач является копирование строк или целых веток дерева из одного объекта в другой. При этом легко совершить логические ошибки, связанные с непониманием объектной модели платформы, работы ссылочных типов данных и коллекций.
В этой статье мы подробно разберем, почему стандартные попытки «простого присваивания» строк не работают, проанализируем типичные ошибки разработчиков и выработаем элегантные, универсальные механизмы для копирования данных как для обычных объектов на сервере, так и для управляемых форм на клиенте.
Рассмотрим ситуацию, с которой часто сталкиваются программисты. Нам необходимо вставить строку в определенную позицию дерева значений и заполнить ее данными из другой строки. Типичный ошибочный код выглядит следующим образом:
Прил5 = Контейнер.Лист02.Строки[0].Строки.Вставить(4);
Прил5 = ДеревоЗначенийЛиста2.Строки[0].Строки[4];
Выясним причину, почему этот код не выполняет поставленную задачу. Проанализируем ситуацию по шагам:
Вставить(4) создает новую абсолютно пустую строку в коллекции на четвертой позиции. Метод возвращает ссылку на эту только что созданную строку, и эта ссылка записывается в переменную Прил5.Прил5 значение другой строки из совершенно другого дерева значений (ДеревоЗначенийЛиста2).В этот момент происходит обычное переприсваивание значения переменной. Переменная Прил5 просто начинает указывать на строку во втором дереве значений. При этом созданная на первом шаге пустая строка в первом дереве так и остается пустой и никак не изменяется, а связь с ней утеряна. Это классическая ошибка работы со ссылочными типами данных.
Еще одна частая ошибка — попытка скопировать подчиненные строки путем прямого присваивания коллекции строк:
Прил5.Строки = ДеревоЗначенийЛиста2.Строки[0].Строки[4].Строки;
Этот код вызовет ошибку выполнения. Свойство Строки у строки дерева значений имеет тип КоллекцияСтрокДереваЗначений. Данное свойство доступно только для чтения. Платформа 1С запрещает прямую замену системных коллекций. Наполнять их можно только интерактивно или программно с помощью методов Добавить() или Вставить().
Иногда разработчики приходят к рабочему, но крайне неоптимальному решению, которое выглядит так:
Контейнер.Лист02.Строки[0].Строки[4].Строки.Очистить();
Прил5 = Контейнер.Лист02.Строки[0].Строки[4].Строки.Добавить();
Прил5.Данные = ДеревоЗначенийЛиста2.Строки[0].Строки[4].Строки[0].Данные;
Прил5.ДанныеМногострочныхЧастей = ДеревоЗначенийЛиста2.Строки[0].Строки[4].Строки[0].ДанныеМногострочныхЧастей;
Давайте разберем, почему писать такой код крайне не рекомендуется:
Для быстрого, безопасного и универсального копирования данных одной строки в другую без ручного перечисления колонок в платформе 1С предусмотрена замечательная процедура ЗаполнитьЗначенияСвойств(). Она автоматически сопоставляет имена колонок источника и приемника и переносит значения между ними.
Посмотрим на пример правильного копирования одиночной строки:
// Создаем новую пустую строку в нужном месте
НоваяСтрока = Контейнер.Лист02.Строки[0].Строки.Вставить(4);
// Получаем строку-источник, откуда нужно взять данные
СтрокаИсточник = ДеревоЗначенийЛиста2.Строки[0].Строки[4];
// Копируем все совпадающие колонки за один вызов
ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаИсточник);
Этот подход избавляет нас от необходимости знать точную структуру колонок дерева значений. Если в дереве появятся новые колонки с одинаковыми именами, платформа перенесет их значения автоматически.
Метод ЗаполнитьЗначенияСвойств() отлично решает задачу для плоской строки, но как быть, если у копируемой строки есть подчиненные ветки (дочерние строки) любой глубины вложенности? У объекта ДеревоЗначений есть метод Скопировать(), но он копирует дерево целиком, а не отдельную его ветку.
Для решения этой задачи мы можем написать простую и эффективную рекурсивную процедуру. Рассмотрим этот алгоритм подробнее:
// Рекурсивная процедура для копирования ветки дерева значений
Процедура СкопироватьВеткуДерева(СтрокаПриемник, СтрокаИсточник) Экспорт
// Копируем данные текущей строки
ЗаполнитьЗначенияСвойств(СтрокаПриемник, СтрокаИсточник);
// Обходим все подчиненные строки источника
Для Каждого ПодчиненнаяИсточник Из СтрокаИсточник.Строки Цикл
// Создаем новую подчиненную строку в приемнике
ПодчиненнаяПриемник = СтрокаПриемник.Строки.Добавить();
// Рекурсивно вызываем эту же процедуру для следующего уровня иерархии
СкопироватьВеткуДерева(ПодчиненнаяПриемник, ПодчиненнаяИсточник);
КонецЦикла;
КонецПроцедуры
Теперь мы можем применить эту универсальную процедуру в коде нашего примера:
НоваяСтрока = Контейнер.Лист02.Строки[0].Строки.Вставить(4);
СтрокаИсточник = ДеревоЗначенийЛиста2.Строки[0].Строки[4];
// Копируем саму строку и всю её структуру вниз по дереву
СкопироватьВеткуДерева(НоваяСтрока, СтрокаИсточник);
Такой подход гарантирует полное и точное воссоздание всей иерархической структуры данных в новом месте.
При работе в управляемом интерфейсе (на управляемых формах) необходимо помнить важное архитектурное ограничение платформы 1С: объект ДеревоЗначений не может существовать на клиенте. Он живет исключительно на сервере. На клиенте данные дерева представлены специальным типом ДанныеФормыДерево, а его элементы — типом ДанныеФормыЭлементДерева.
Если вам необходимо реализовать копирование строк дерева непосредственно в кодовой бизнес-логике формы на клиенте (например, при перетаскивании строк пользователем вручную — механизм Drag-and-Drop), то напрямую обратиться к свойству .Строки не получится. Вместо него у объектов данных формы используется метод ПолучитьЭлементы().
Проанализируем пример рекурсивной процедуры копирования ветки дерева, адаптированной специально для работы на клиенте управляемой формы:
&НаКлиенте
Процедура СкопироватьВеткуФормы(ЭлементПриемник, ЭлементИсточник)
// Переносим данные полей текущего элемента
ЗаполнитьЗначенияСвойств(ЭлементПриемник, ЭлементИсточник);
// Получаем коллекцию подчиненных элементов источника
ПодчиненныеИсточник = ЭлементИсточник.ПолучитьЭлементы();
Для Каждого ПодчиненныйИсточник Из ПодчиненныеИсточник Цикл
// Создаем элемент в коллекции приемника
ПодчиненныйПриемник = ЭлементПриемник.ПолучитьЭлементы().Добавить();
// Рекурсивно уходим на уровень ниже
СкопироватьВеткуФормы(ПодчиненныйПриемник, ПодчиненныйИсточник);
КонецЦикла;
КонецПроцедуры
Использование метода ПолучитьЭлементы() делает этот код полностью работоспособным в контексте тонкого или веб-клиента без лишних серверных вызовов, что значительно повышает отзывчивость интерфейса для конечного пользователя.
При разработке алгоритмов переноса или копирования элементов дерева значений «само в себя» (например, когда пользователь пытается скопировать или переместить родительскую строку внутрь одной из ее же дочерних строк) возникает серьезная опасность. Без предварительной проверки ваш рекурсивный алгоритм уйдет в бесконечный цикл. Это неминуемо вызовет ошибку "Переполнение стека встроенного языка" и приведет к аварийному завершению работы сеанса пользователя.
Чтобы избежать подобных критических сбоев, перед вызовом копирования обязательно внедряйте проверку на то, что целевой узел-приемник не является подчиненным по отношению к узлу-источнику. Для этого можно написать простую вспомогательную функцию:
&НаКлиентеНаСервереБезКонтекста
Функция ПриемникЯвляетсяПотомкомИсточника(УзелПриемник, УзелИсточник)
ТекущийРодитель = УзелПриемник.ПолучитьРодителя();
Пока ТекущийРодитель <> Неопределено Цикл
Если ТекущийРодитель = УзелИсточник Тогда
Возврат Истина; // Обнаружена попытка скопировать родителя внутрь самого себя
КонецЕсли;
ТекущийРодитель = ТекущийРодитель.ПолучитьРодителя();
КонецЦикла;
Возврат Ложь;
КонецФункции
Применение такой проверки гарантирует стабильность вашей программы при выполнении любых сложных операций с иерархическими структурами в 1С.