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