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