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