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