Как связать две табличные части на управляемой форме 1С 8.3 и организовать автозаполнение подчиненных строк

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При проектировании сложных интерфейсов в «1С:Предприятие 8.3» разработчики часто сталкиваются с необходимостью связать две табличные части (ТЧ) в рамках одного документа. Классический пример — автосервис: в первой таблице пользователь выбирает Услуги (например, «Замена масла»), а во второй таблице отображаются и редактируются Запчасти, необходимые именно для этой конкретной услуги (масляный фильтр, моторное масло).

В этой статье мы подробно разберем, как правильно реализовать такое связывание по принципу «главная — подчиненная» на управляемой форме. Выясним, почему стандартная привязка по ссылке на номенклатуру-услугу является ошибочной, и реализуем надежный механизм с использованием Ключа связи, настроим динамический отбор строк на клиенте без лишних серверных вызовов и организуем автоматическое заполнение зависимой таблицы.

Архитектурная проблема связи по реквизиту «Услуга» и ее решение

На первый взгляд кажется логичным добавить в ТЧ «Запчасти» реквизит Услуга (с типом ссылки на справочник номенклатуры) и фильтровать запчасти по нему. Однако этот подход скрывает критическую уязвимость: что делать, если в документе одна и та же услуга добавлена в разные строки?

Представим ситуацию: клиент заказывает шиномонтаж отдельно для передней оси автомобиля и отдельно для задней (например, у них разные параметры или разные исполнители). В ТЧ «Услуги» появляются две идентичные строки с услугой «Шиномонтаж». Если мы будем связывать запчасти просто по значению услуги, система не сможет определить, к какой именно строке услуг (первой или второй) относятся добавленные расходные материалы. Данные перепутаются.

Правильное архитектурное решение: использование служебного реквизита КлючСвязи.

  1. В табличную часть Услуги добавляем реквизит КлючСвязи с типом Число (или Строка, или УникальныйИдентификатор).
  2. В табличную часть Запчасти добавляем точно такой же реквизит КлючСвязи.
  3. При создании или копировании строки в ТЧ «Услуги» мы будем программно генерировать уникальный идентификатор и записывать его в обе таблицы. Это гарантирует жесткую связь между конкретными строками, даже если сами услуги полностью совпадают.

Шаг 1. Настройка метаданных в конфигураторе

Прежде чем переходить к написанию кода, настроим структуру нашего документа в конфигураторе. Проверим реквизиты табличных частей:

После добавления реквизитов выносим обе табличные части на управляемую форму. Чтобы интерфейс выглядел эргономично, рекомендуем разместить их в одной группе (например, горизонтальной или вертикальной со сплиттером), чтобы пользователь сразу видел обе таблицы.

Шаг 2. Организация интерактивного интерфейса (Отбор строк)

Теперь сделаем так, чтобы при выделении строки в таблице услуг в таблице запчастей отображались только те позиции, которые привязаны к этой конкретной строке услуг.

Для этого воспользуемся свойством ОтборСтрок таблицы формы. Нам понадобится обработать событие ПриАктивизацииСтроки для таблицы «Услуги» на клиенте. Разберем код этого обработчика:


&НаКлиенте
Процедура УслугиПриАктивизацииСтроки(Элемент)
    
    // Получаем текущие данные выделенной строки услуг
    ТекущиеДанные = Элементы.Услуги.ТекущиеДанные;
    
    Если ТекущиеДанные = Неопределено Тогда
        // Если строки отсутствуют, сбрасываем отбор во второй таблице
        Элементы.Запчасти.ОтборСтрок = Неопределено;
        Возврат;
    КонецЕсли;
    
    // Если ключ связи пустой (например, новая пустая строка), отбор не сработает корректно.
    // В этом случае отфильтруем по заведомо несуществующему значению или пустой строке
    Если ПустаяСтрока(ТекущиеДанные.КлючСвязи) Тогда
        Элементы.Запчасти.ОтборСтрок = Новый ФиксированнаяСтруктура("КлючСвязи", "ПустойКлюч");
    Иначе
        // Устанавливаем быстрый клиентский отбор по ключу связи
        Отбор = Новый ФиксированнаяСтруктура("КлючСвязи", ТекущиеДанные.КлючСвязи);
        Элементы.Запчасти.ОтборСтрок = Отбор;
    КонецЕсли;
    
КонецПроцедуры

Важное преимущество данного подхода: свойство ОтборСтрок работает исключительно на стороне клиента. Система не обращается к серверу для перезаполнения визуального отображения таблицы, что обеспечивает мгновенный отклик интерфейса даже на слабых компьютерах.

Шаг 3. Автоматическая генерация ключа связи

Для того чтобы связь работала, у каждой строки услуг должен быть свой уникальный ключ. Мы будем генерировать его в событии ПриНачалеРедактирования таблицы «Услуги». Также учтем сценарий, когда пользователь копирует строку услуг — в этом случае ключ связи должен обновиться, чтобы скопированная строка не ссылалась на запчасти исходной строки.

Рассмотрим реализацию генерации ключа связи:


&НаКлиенте
Процедура УслугиПриНачалеРедактирования(Элемент, НоваяСтрока, Копирование)
    
    ТекущиеДанные = Элементы.Услуги.ТекущиеДанные;
    Если ТекущиеДанные = Неопределено Тогда
        Возврат;
    КонецЕсли;
    
    Если НоваяСтрока И Не Копирование Тогда
        // Генерируем новый ключ для новой строки
        ТекущиеДанные.КлючСвязи = Строка(Новый УникальныйИдентификатор);
        
    ИначеЕсли Копирование Тогда
        // При копировании строки платформой копируется и старый КлючСвязи.
        // Нам необходимо сбросить его и сгенерировать новый уникальный ключ!
        СтарыйКлюч = ТекущиеДанные.КлючСвязи;
        НовыйКлюч = Строка(Новый УникальныйИдентификатор);
        ТекущиеДанные.КлючСвязи = НовыйКлюч;
        
        // Дополнительно: если нужно скопировать связанные запчасти для новой строки
        ПродублироватьСвязанныеЗапчасти(СтарыйКлюч, НовыйКлюч);
    КонецЕсли;
    
КонецПроцедуры

Для завершения логики копирования напишем вспомогательную процедуру ПродублироватьСвязанныеЗапчасти, которая на клиенте скопирует строки зависимой таблицы:


&НаКлиенте
Процедура ПродублироватьСвязанныеЗапчасти(СтарыйКлюч, НовыйКлюч)
    
    МассивНовыхСтрок = Новый Массив;
    
    // Сначала находим все запчасти со старым ключом связи
    Для Каждого СтрокаЗапчасти Из Объект.Запчасти Цикл
        Если СтрокаЗапчасти.КлючСвязи = СтарыйКлюч Тогда
            МассивНовыхСтрок.Добавить(СтрокаЗапчасти);
        КонецЕсли;
    КонецЦикла;
    
    // Копируем найденные строки и присваиваем им новый ключ связи
    Для Каждого ИсходнаяСтрока Из МассивНовыхСтрок Цикл
        НоваяСтрока = Объект.Запчасти.Добавить();
        ЗаполнитьЗначенияСвойств(НоваяСтрока, ИсходнаяСтрока);
        НоваяСтрока.КлючСвязи = НовыйКлюч;
    КонецЦикла;
    
КонецПроцедуры

Шаг 4. Автоматическое заполнение запчастей при изменении услуги

Переходим к ключевой задаче, описанной автором форума: при выборе конкретной работы в таблице услуг в таблицу запчастей должны автоматически подтянуться расходные материалы, привязанные к этой услуге в базе данных.

Так как данные о необходимых материалах хранятся в базе данных (например, в регистре сведений СпецификацииНоменклатуры), нам потребуется вызвать серверный контекст. Чтобы сделать это максимально эффективно, используем вызов &НаСервереБезКонтекста, передавая туда только ссылку на выбранную услугу. Это сэкономит трафик и ускорит работу формы.

Рассмотрим по шагам логику процедуры изменения услуги:


&НаКлиенте
Процедура УслугиУслугаПриИзменении(Элемент)
    
    ТекущиеДанные = Элементы.Услуги.ТекущиеДанные;
    Если ТекущиеДанные = Неопределено Тогда
        Возврат;
    КонецЕсли;
    
    // Проверим наличие ключа связи (на случай, если строка редактируется особым образом)
    Если ПустаяСтрока(ТекущиеДанные.КлючСвязи) Тогда
        ТекущиеДанные.КлючСвязи = Строка(Новый УникальныйИдентификатор);
    КонецЕсли;
    
    // Перед заполнением новых запчастей обязательно очистим старые запчасти по этой услуге
    ОчиститьЗапчастиПоКлючу(ТекущиеДанные.КлючСвязи);
    
    // Если услуга была очищена пользователем, просто выходим
    Если Не ЗначениеЗаполнено(ТекущиеДанные.Услуга) Тогда
        Возврат;
    КонецЕсли;
    
    // Запрашиваем структуру запчастей с сервера
    ТаблицаЗапчастей = ПолучитьЗапчастиДляУслугиНаСервере(ТекущиеДанные.Услуга);
    
    // Заполняем ТЧ запчастей на клиенте
    Для Каждого СтрокаЗапчасти Из ТаблицаЗапчастей Цикл
        НоваяСтрока = Объект.Запчасти.Добавить();
        ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаЗапчасти);
        НоваяСтрока.КлючСвязи = ТекущиеДанные.КлючСвязи; // Привязываем к текущей строке услуги
    КонецЦикла;
    
КонецПроцедуры

Теперь напишем серверную функцию ПолучитьЗапчастиДляУслугиНаСервере, которая быстро извлечет список необходимых комплектующих с помощью запроса:


&НаСервереБезКонтекста
Функция ПолучитьЗапчастиДляУслугиНаСервере(Знач УслугаСсылка)
    
    РезультатФункции = Новый Массив;
    
    // Создаем запрос к регистру сведений спецификаций (или подчиненному справочнику)
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |	Спецификации.Материал КАК Номенклатура,
        |	Спецификации.Количество КАК Количество
        |ИЗ
        |	РегистрСведений.СпецификацииУслуг КАК Спецификации
        |ГДЕ
        |	Спецификации.Услуга = &Услуга";
        
    Запрос.УстановитьПараметр("Услуга", УслугаСсылка);
    
    Выборка = Запрос.Выполнить().Выбрать();
    Пока Выборка.Следующий() Цикл
        
        СтруктураСтроки = Новый Структура;
        СтруктураСтроки.Вставить("Номенклатура", Выборка.Номенклатура);
        СтруктураСтроки.Вставить("Количество", Выборка.Количество);
        
        РезультатФункции.Добавить(СтруктураСтроки);
        
    КонецЦикла;
    
    Возврат РезультатФункции;
    
КонецФункции

Также добавим вспомогательную клиентскую процедуру ОчиститьЗапчастиПоКлючу, которая удаляет строки из коллекции ТЧ Запчасти, обходя ее с конца во избежание смещения индексов строк при удалении:


&НаКлиенте
Процедура ОчиститьЗапчастиПоКлючу(КлючСвязи)
    
    ИндексСтроки = Объект.Запчасти.Количество() - 1;
    Пока ИндексСтроки >= 0 Цикл
        
        ТекущаяСтрока = Объект.Запчасти[ИндексСтроки];
        Если ТекущаяСтрока.КлючСвязи = КлючСвязи Тогда
            Объект.Запчасти.Удалить(ТекущаяСтрока);
        КонецЕсли;
        
        ИндексСтроки = ИндексСтроки - 1;
    КонецЦикла;
    
КонецПроцедуры

Шаг 5. Контроль целостности данных при удалении услуг

Важный аспект, о котором часто забывают начинающие разработчики, — контроль за «осиротевшими» строками. Если пользователь удаляет строку из таблицы услуг, все связанные с ней запчасти во второй таблице должны быть автоматически удалены. В противном случае в документе останется скрытый «мусор», который запишется в базу данных.

Для предотвращения этой проблемы настроим обработчик события ПередУдалением для таблицы «Услуги»:


&НаКлиенте
Процедура УслугиПередУдалением(Элемент, Отказ)
    
    ТекущиеДанные = Элементы.Услуги.ТекущиеДанные;
    Если ТекущиеДанные <> Неопределено И ЗначениеЗаполнено(ТекущиеДанные.КлючСвязи) Тогда
        // Очищаем связанные строки запчастей по ключу связи удаляемой строки услуг
        ОчиститьЗапчастиПоКлючу(ТекущиеДанные.КлючСвязи);
    КонецЕсли;
    
КонецПроцедуры

Подведем итоги

Использование концепции Ключа связи — это стандарт де-факто для реализации зависимых табличных частей в платформе «1С:Предприятие 8.3». Мы спроектировали надежное, масштабируемое и быстрое решение:

Данный паттерн проектирования можно применять для любых прикладных задач, связанных с иерархическим или связанным представлением табличных данных на управляемой форме. В качестве готового практического примера можно использовать шаблон связывания главной и подчиненной таблиц на управляемых формах.

← На главную