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