Как реализовать альтернативу событию «При получении данных» в динамическом списке 1С?

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

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

Типичный пример такой задачи — вывод агрегированных или связанных данных в списке. Например, для каждой номенклатуры в списке нужно показать объединенную строку доступных характеристик («CD / DVD») или для каждого заказа отобразить в одной ячейке список всех связанных документов поставки («Поставка 1, Поставка 2»). В этой статье мы подробно разберем, как решить эту задачу в управляемом приложении, проанализируем различные подходы, их плюсы, минусы и влияние на производительность системы.


Способ 1. Использование обработчика «ПриПолученииДанныхНаСервере»

Начиная с версии платформы 8.3.10, у динамического списка появился штатный аналог старого события — событие таблицы ПриПолученииДанныхНаСервере. Этот обработчик предназначен именно для дополнительной обработки строк перед их выводом пользователю.

Важнейшая особенность данного метода заключается в том, что обработчик выполняется строго на сервере без контекста формы (директива &НаСервереБезКонтекста). Это сделано для обеспечения максимальной производительности, но накладывает ограничение: мы не можем напрямую обратиться к реквизитам формы или элементам управления для получения настроек или кэша.

Лайфхак по передаче контекста: Чтобы передать промежуточные данные из контекста формы во внеконтекстный обработчик, мы можем использовать свойство ДополнительныеСвойства компоновщика настроек динамического списка. Запишем туда кэш-таблицу при создании формы, а внутри события прочитаем её.

Рассмотрим по шагам, как это реализовать в коде:


&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
    
    // Формируем кэш необходимых соответствий (например, Номенклатура -> Строка характеристик)
    КэшЗначений = ПолучитьКэшХарактеристикНоменклатуры();
    
    // Передаем кэш в настройки динамического списка
    Список.КомпоновщикНастроек.Настройки.ДополнительныеСвойства.Вставить("КэшХарактеристик", КэшЗначений);
    
КонецПроцедуры

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

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


Способ 2. Декларативное «Выражение представления» (платформа 8.3.20+)

Если ваша задача сводится к простому склеиванию нескольких реквизитов одного и того же объекта (например, нужно вывести в одну колонку «Код», «Артикул» и «Наименование» или объединить реквизиты шапки), начиная с версии 8.3.20, можно использовать встроенное свойство поля динамического списка — ВыражениеПредставления.

Этот метод не требует написания сложного серверного кода. Вы можете настроить формулу прямо в конфигураторе на вкладке «Данные» вашего динамического списка либо программно:


// Пример программной настройки выражения представления для поля списка
ПолеСписка = Список.КомпоновщикНастроек.Настройки.ДоступныеПоляВыбора.НайтиПоле(Новый ПолеКомпоновкиДанных("Номенклатура"));
Если ПолеСписка <> Неопределено Тогда
    ПолеСписка.ВыражениеПредставления = "[Артикул] + "" | "" + [Наименование]";
КонецЕсли;

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


Способ 3. Лучшая практика для больших баз — Денормализация данных

В случае, когда в базе данных содержится более 100 000 строк номенклатуры (как описывал автор темы на форуме), любые попытки динамического расчета связанных строк «на лету» в запросах или коде сервера при скроллинге списка неизбежно приведут к существенным задержкам (фризам) интерфейса.

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

Разберем этот паттерн по шагам:

  1. Создадим подчиненный некомандный Регистр сведений (например, СводныеДанныеЗаказов) с измерениями Заказ и ресурсом СписокПоставок (тип: Строка).
  2. При проведении или записи документов поставки будем программно собирать актуальный список документов, объединять их в строку через запятую и записывать в этот регистр.
  3. В динамическом списке мы выполним простейшее и максимально быстрое ЛЕВОЕ СОЕДИНЕНИЕ с этим регистром сведений по ключу заказа.

Посмотрим на пример кода, который вызывается в модуле объекта при проведении документов:


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

Преимущества архитектурного решения: Чтение из регистра сведений по левому соединению происходит мгновенно. СУБД отдает уже готовую строку. По этой колонке без проблем будет работать стандартный полнотекстовый поиск 1С и любые отборы пользователей.


Способ 4. Эмуляция склеивания строк через левые соединения в запросе

В языке запросов 1С отсутствуют стандартные SQL-функции агрегации строк типа GROUP_CONCAT или STRING_AGG. Однако, если количество подчиненных элементов заведомо ограничено и невелико (например, не более 3-5 документов поставки на один заказ), мы можем обойти это ограничение непосредственно в самом тексте запроса динамического списка.

Для этого мы последовательно соединяем таблицу документов саму с собой по номеру строки или по уникальному идентификатору:


ВЫБРАТЬ
    Заказы.Ссылка КАК Заказ,
    ЕСТЬNULL(Док1.Ссылка.Номер, "") 
    + ЕСТЬNULL(ВЫБОР КОГДА Док2.Ссылка.Номер ЕСТЬ НЕ NULL ТОГДА ", " + Док2.Ссылка.Номер ИНАЧЕ "" КОНЕЦ)
    + ЕСТЬNULL(ВЫБОР КОГДА Док3.Ссылка.Номер ЕСТЬ НЕ NULL ТОГДА ", " + Док3.Ссылка.Номер ИНАЧЕ "" КОНЕЦ) КАК СтрокаПоставок
ИЗ
    Документ.ЗаказПокупателя КАК Заказы
    ЛЕВОЕ СОЕДИНЕНИЕ Документ.Поставка КАК Док1
        ПО Док1.Заказ = Заказы.Ссылка И Док1.НомерСтроки = 1
    ЛЕВОЕ СОЕДИНЕНИЕ Документ.Поставка КАК Док2
        ПО Док2.Заказ = Заказы.Ссылка И Док2.НомерСтроки = 2
    ЛЕВОЕ СОЕДИНЕНИЕ Документ.Поставка КАК Док3
        ПО Док3.Заказ = Заказы.Ссылка И Док3.НомерСтроки = 3

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


Способ 5. Использование агрегатной функции СКД «СоединитьСтроки()»

Один из участников исходной дискуссии упомянул, что в итоге заменил динамический список полноценным отчетом. Это очень грамотное решение, если пользователю требуется сложная аналитическая таблица. Система компоновки данных (СКД), на базе которой строятся все современные отчеты в 1С, обладает мощным встроенным языком выражений.

В СКД на вкладке «Ресурсы» мы можем использовать специализированную функцию СоединитьСтроки(Выражение, Разделитель, ВыражениеУпорядочивания).

Например, в качестве выражения ресурса мы можем написать:

СоединитьСтроки(Поставка.Номер, ", ", Поставка.Дата)

Система автоматически сгруппирует все подчиненные строки в одну красивую строку с указанным разделителем на уровне вывода готового отчета, не требуя от разработчика написания громоздких SQL-запросов.


Способ 6. Постраничный вывод в Таблице значений

Если бизнес-логика вашего решения требует обязательного использования коллекции формы типа ТаблицаЗначений (поскольку в ней гораздо проще программно интерактивно менять строки и ячейки), но при этом объем данных в 100 000 строк вызывает зависание клиента, следует применить паттерн постраничного вывода (пейджинга).

Рассмотрим алгоритм реализации:

  1. Добавим на форму управляющие элементы: кнопки «Вперед», «Назад», поле ввода «Номер страницы» и «Размер страницы» (например, по 50 строк).
  2. При получении данных с сервера будем использовать конструкцию ВЫБРАТЬ ПЕРВЫЕ... с ограничением по количеству строк.
  3. Для пропуска предыдущих страниц будем использовать условие «Больше текущего ключа» (по уникальному идентификатору или упорядоченному полю).

Посмотрим на пример реализации запроса для выборки очередной страницы данных:


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

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

← На главную