Переход с обычных форм на управляемые в платформе «1С:Предприятие» принес разработчикам множество преимуществ, но также заставил пересмотреть привычные подходы к проектированию интерфейсов. Одной из наиболее частых проблем, с которой сталкиваются программисты, является отсутствие в управляемом интерфейсе привычного события формы ПриПолученииДанных. В обычных формах этот обработчик позволял «на лету» выполнять вычисления и заполнять колонки табличного поля данными, которых изначально не было в источнике.
Типичный пример такой задачи — вывод агрегированных или связанных данных в списке. Например, для каждой номенклатуры в списке нужно показать объединенную строку доступных характеристик («CD / DVD») или для каждого заказа отобразить в одной ячейке список всех связанных документов поставки («Поставка 1, Поставка 2»). В этой статье мы подробно разберем, как решить эту задачу в управляемом приложении, проанализируем различные подходы, их плюсы, минусы и влияние на производительность системы.
Начиная с версии платформы 8.3.10, у динамического списка появился штатный аналог старого события — событие таблицы ПриПолученииДанныхНаСервере. Этот обработчик предназначен именно для дополнительной обработки строк перед их выводом пользователю.
Важнейшая особенность данного метода заключается в том, что обработчик выполняется строго на сервере без контекста формы (директива &НаСервереБезКонтекста). Это сделано для обеспечения максимальной производительности, но накладывает ограничение: мы не можем напрямую обратиться к реквизитам формы или элементам управления для получения настроек или кэша.
Лайфхак по передаче контекста: Чтобы передать промежуточные данные из контекста формы во внеконтекстный обработчик, мы можем использовать свойство ДополнительныеСвойства компоновщика настроек динамического списка. Запишем туда кэш-таблицу при создании формы, а внутри события прочитаем её.
Рассмотрим по шагам, как это реализовать в коде:
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
// Формируем кэш необходимых соответствий (например, Номенклатура -> Строка характеристик)
КэшЗначений = ПолучитьКэшХарактеристикНоменклатуры();
// Передаем кэш в настройки динамического списка
Список.КомпоновщикНастроек.Настройки.ДополнительныеСвойства.Вставить("КэшХарактеристик", КэшЗначений);
КонецПроцедуры
&НаСервереБезКонтекста
Процедура СписокПриПолученииДанныхНаСервере(ИмяЭлемента, Настройки, Строки)
// Извлекаем переданный кэш из настроек компоновщика
КэшХарактеристик = Неопределено;
Настройки.ДополнительныеСвойства.Свойство("КэшХарактеристик", КэшХарактеристик);
Если КэшХарактеристик = Неопределено Тогда
Возврат;
КонецЕсли;
// Обходим только те строки, которые в данный момент видны на экране пользователя
Для Каждого СтрокаСписок Из Строки Цикл
СсылкаНаОбъект = СтрокаСписок.Ключ;
СтрокаКэша = КэшХарактеристик.Найти(СсылкаНаОбъект, "Номенклатура");
Если СтрокаКэша <> Неопределено Тогда
// Записываем данные в оформление строки динамического списка
СтрокаСписок.Данные["КолонкаХарактеристик"].Значение = СтрокаКэша.ТекстХарактеристик;
СтрокаСписок.Данные["КолонкаХарактеристик"].Оформление.Текст = СтрокаКэша.ТекстХарактеристик;
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Главный недостаток метода: Поля, заполняемые внутри этого события, являются виртуальными. Стандартный поиск платформы 1С, а также интерактивная фильтрация по этим колонкам работать не будут, так как СУБД ничего не знает о данных, сформированных в оперативной памяти сервера приложений.
Если ваша задача сводится к простому склеиванию нескольких реквизитов одного и того же объекта (например, нужно вывести в одну колонку «Код», «Артикул» и «Наименование» или объединить реквизиты шапки), начиная с версии 8.3.20, можно использовать встроенное свойство поля динамического списка — ВыражениеПредставления.
Этот метод не требует написания сложного серверного кода. Вы можете настроить формулу прямо в конфигураторе на вкладке «Данные» вашего динамического списка либо программно:
// Пример программной настройки выражения представления для поля списка
ПолеСписка = Список.КомпоновщикНастроек.Настройки.ДоступныеПоляВыбора.НайтиПоле(Новый ПолеКомпоновкиДанных("Номенклатура"));
Если ПолеСписка <> Неопределено Тогда
ПолеСписка.ВыражениеПредставления = "[Артикул] + "" | "" + [Наименование]";
КонецЕсли;
Такой подход работает очень быстро, не нагружает сервер приложений дополнительными выборками и поддерживается платформой на уровне встроенных механизмов компоновки.
В случае, когда в базе данных содержится более 100 000 строк номенклатуры (как описывал автор темы на форуме), любые попытки динамического расчета связанных строк «на лету» в запросах или коде сервера при скроллинге списка неизбежно приведут к существенным задержкам (фризам) интерфейса.
Наиболее надежным архитектурным решением в данном случае является денормализация данных. Вместо того чтобы собирать строку связанных документов или характеристик при каждом открытии формы, мы будем рассчитывать и хранить эту строку в базе данных.
Разберем этот паттерн по шагам:
СводныеДанныеЗаказов) с измерениями Заказ и ресурсом СписокПоставок (тип: Строка).ЛЕВОЕ СОЕДИНЕНИЕ с этим регистром сведений по ключу заказа.Посмотрим на пример кода, который вызывается в модуле объекта при проведении документов:
Процедура ОбновитьСводныеДанныеПоЗаказу(СсылкаНаЗаказ) Экспорт
// Собираем все проведенные поставки по заказу
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Поставка.Ссылка КАК Ссылка
|ИЗ
| Документ.Поставка КАК Поставка
|ГДЕ
| Поставка.Заказ = &Заказ
| И Поставка.Проведен";
Запрос.УстановитьПараметр("Заказ", СсылкаНаЗаказ);
Результат = Запрос.Выполнить();
Если Не Результат.Пустой() Тогда
Выборка = Результат.Выбрать();
СтрокаПоставок = "";
Пока Выборка.Следующий() Цикл
СтрокаПоставок = СтрокаПоставок + ?(СтрокаПоставок = "", "", ", ") + Строка(Выборка.Ссылка);
КонецЦикла;
// Записываем результат в регистр сведений
МенеджерЗаписи = РегистрыСведений.СводныеДанныеЗаказов.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Заказ = СсылкаНаЗаказ;
МенеджерЗаписи.СписокПоставок = СтрокаПоставок;
МенеджерЗаписи.Записать();
КонецЕсли;
КонецПроцедуры
Преимущества архитектурного решения: Чтение из регистра сведений по левому соединению происходит мгновенно. СУБД отдает уже готовую строку. По этой колонке без проблем будет работать стандартный полнотекстовый поиск 1С и любые отборы пользователей.
В языке запросов 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
Этот метод избавляет от необходимости создавать дополнительные структуры данных в базе, но применим только для задач с жестко ограниченным количеством связей.
Один из участников исходной дискуссии упомянул, что в итоге заменил динамический список полноценным отчетом. Это очень грамотное решение, если пользователю требуется сложная аналитическая таблица. Система компоновки данных (СКД), на базе которой строятся все современные отчеты в 1С, обладает мощным встроенным языком выражений.
В СКД на вкладке «Ресурсы» мы можем использовать специализированную функцию СоединитьСтроки(Выражение, Разделитель, ВыражениеУпорядочивания).
Например, в качестве выражения ресурса мы можем написать:
СоединитьСтроки(Поставка.Номер, ", ", Поставка.Дата)
Система автоматически сгруппирует все подчиненные строки в одну красивую строку с указанным разделителем на уровне вывода готового отчета, не требуя от разработчика написания громоздких SQL-запросов.
Если бизнес-логика вашего решения требует обязательного использования коллекции формы типа ТаблицаЗначений (поскольку в ней гораздо проще программно интерактивно менять строки и ячейки), но при этом объем данных в 100 000 строк вызывает зависание клиента, следует применить паттерн постраничного вывода (пейджинга).
Рассмотрим алгоритм реализации:
ВЫБРАТЬ ПЕРВЫЕ... с ограничением по количеству строк.Посмотрим на пример реализации запроса для выборки очередной страницы данных:
&НаСервере
Процедура ЗагрузитьСтраницуДанных(НомерСтраницы)
Запрос = Новый Запрос;
// Выбираем только порцию данных для текущей страницы
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 50
| Номенклатура.Ссылка КАК Ссылка,
| Номенклатура.Наименование КАК Наименование
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| Номенклатура.Ссылка > &ПоследняяСсылка
|УПОРЯДОЧИТЬ ПО
| Ссылка";
Запрос.УстановитьПараметр("ПоследняяСсылка", ПолучитьПоследнююСсылкуПредыдущейСтраницы(НомерСтраницы));
ТаблицаФормы = Запрос.Выполнить().Выгрузить();
// Быстро обрабатываем всего 50 строк в коде и выводим на форму
Для Каждого СтрокаТЗ Из ТаблицаФормы Цикл
СтрокаТЗ.ДополнительноеПоле = ПолучитьВычисляемоеЗначение(СтрокаТЗ.Ссылка);
КонецЦикла;
ЗначениеВРеквизитФормы(ТаблицаФормы, "ДеревоНоменклатуры");
КонецПроцедуры
Использование этого паттерна позволяет мгновенно открывать форму и производить любые сложные математические или строковые манипуляции с данными, так как серверу за один вызов приходится обрабатывать микроскопический объем информации.