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