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