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