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