Разработка интерактивных интерфейсов в управляемом приложении «1С:Предприятие 8.3» часто требует вывода различной служебной информации. Одной из наиболее частых задач является отображение общего количества строк в таблице формы, которая связана с динамическим списком. Пользователю крайне важно видеть, сколько документов или элементов справочника попали под условия текущего поиска, группировки или настроенного отбора.
В этой статье мы подробно разберем, почему стандартные подходы новичков (например, использование временного хранилища внутри обработчиков динамического считывания) не работают, проанализируем решения разработчиков с форума и создадим максимально быстрое и безопасное решение для боевых баз данных.
Рассмотрим типичную ошибку, с которой часто начинают программисты. Они пытаются использовать событие динамического списка СписокПриПолученииДанныхНаСервере, рассчитывая получить размер таблицы через параметр Строки. Давайте разберем, почему этот метод не подходит для получения общего количества записей.
Суть проблемы: Динамический список на платформе 1С работает по принципу порционного (постраничного) считывания информации. Платформа запрашивает у СУБД ровно столько строк, сколько в данный момент физически помещается на экране у пользователя (обычно от 20 до 50 записей). Обработчик ПриПолученииДанныхНаСервере вызывается именно для этой конкретной порции строк.
Если мы попытаемся использовать в данном событии конструкцию Строки.Количество(), мы получим не общее число записей в базе данных, удовлетворяющих отбору, а всего лишь количество строк, отображаемых на мониторе в данный момент. Именно поэтому любые попытки записать это число во временное хранилище и передать на клиент бесполезны для решения нашей задачи.
Тем не менее, если в других задачах вам действительно потребуется связать контекст формы и внеконтекстную процедуру &НаСервереБезКонтекста (которой и является обработчик получения данных), нужно использовать правильный механизм платформы. Напрямую обратиться к реквизитам формы из внеконтекстного метода нельзя, но мы можем использовать дополнительные свойства компоновщика настроек динамического списка.
Проанализируем пример передачи адреса временного хранилища:
// В контекстном методе формы (например, при создании) подготавливаем хранилище
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
// Создаем уникальный адрес во временном хранилище, привязанный к текущей форме
АдресХранилища = ПоместитьВоВременноеХранилище(Неопределено, УникальныйИдентификатор);
// Записываем этот адрес в свойства компоновщика настроек списка
Список.КомпоновщикНастроек.Настройки.ДополнительныеСвойства.Вставить("АдресДанныхФормы", -АдресХранилища);
КонецПроцедуры
Теперь в процедуре, выполняемой без контекста, мы сможем легко прочитать этот адрес и поместить туда любые промежуточные данные вычислений:
&НаСервереБезКонтекста
Процедура СписокПриПолученииДанныхНаСервере(ИмяЭлемента, Настройки, Строки)
// Извлекаем переданный адрес из настроек компоновщика
Если Настройки.ДополнительныеСвойства.Свойство("АдресДанныхФормы") Тогда
АдресВХранилище = Настройки.ДополнительныеСвойства.АдресДанныхФормы;
// Для примера запишем туда какое-то вычисленное значение
ПоместитьВоВременноеХранилище(Строки.Количество(), АдресВХранилище);
КонецЕсли;
КонецПроцедуры
Этот прием позволяет эффективно обмениваться информацией между быстрыми внеконтекстными вызовами и формой, сохраняя высокую скорость работы интерфейса.
Вернемся к нашей главной задаче — получению общего числа строк списка с учетом всех фильтров пользователя. На форуме было предложено использовать Систему компоновки данных (СКД), которая управляет выборкой динамического списка за кулисами платформы.
Посмотрим на код, к которому пришли участники обсуждения:
&НаСервере
Процедура ПосчитатьКоличествоВСпискеНаСервере()
// Получаем исполняемую схему и настройки динамического списка
Схема = Элементы.Список.ПолучитьИсполняемуюСхемуКомпоновкиДанных();
Настройки = Элементы.Список.ПолучитьИсполняемыеНастройкиКомпоновкиДанных();
// Создаем макет компоновки для выгрузки в коллекцию значений
КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
МакетКомпоновки = КомпоновщикМакета.Выполнить(Схема, Настройки, , , Тип("ГенераторМакетаКомпоновкиДанныхДляКоллекцииЗначений"));
// Инициализируем процессор компоновки
ПроцессорКомпоновкиДанных = Новый ПроцессорКомпоновкиДанных;
ПроцессорКомпоновкиДанных.Инициализировать(МакетКомпоновки);
// Выводим результат компоновки в Таблицу Значений
Результат = Новый ТаблицаЗначений;
ПроцессорВывода = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВКоллекциюЗначений;
ПроцессорВывода.УстановитьОбъект(Результат);
ПроцессорВывода.Вывести(ПроцессорКомпоновкиДанных);
// Сообщаем количество строк в полученной таблице
Сообщить(Результат.Количество());
КонецПроцедуры
В чем критическая опасность этого кода? Этот алгоритм действительно решает задачу «в лоб» — он учитывает все пользовательские настройки, отборы и параметры. Однако использовать его на реальных («живых») базах данных категорически запрещено!
При таком подходе платформа выполняет полный запрос к СУДБ со всеми отборами и физически выгружает абсолютно все записи в оперативную память сервера приложений 1С в виде структуры ТаблицаЗначений. Если пользователь установит легкий отбор, под который подойдет, например, 100 000 строк документов, сервер попытается прокачать весь этот объем данных в память только ради того, чтобы в конце вызвать метод Количество(). Это приведет к моментальной перегрузке сервера, утечке памяти и сильным зависаниям клиентского сеанса.
Чтобы избежать проблем с производительностью, нам нужно заставить СУБД сделать легкую операцию — подсчитать только количество строк, не возвращая сами записи на сервер 1С. Для этого мы программно перехватим итоговый запрос, скомпилированный СКД динамического списка, и преобразуем его во вложенный запрос с агрегатной функцией КОЛИЧЕСТВО(*).
Рассмотрим детально этот оптимизированный алгоритм:
&НаСервере
Функция ПолучитьКоличествоСтрокДинамическогоСписка()
// 1. Извлекаем схему и настройки компоновки динамического списка
Схема = Элементы.Список.ПолучитьИсполняемуюСхемуКомпоновкиДанных();
Настройки = Элементы.Список.ПолучитьИсполняемыеНастройкиКомпоновкиДанных();
// 2. Строим макет компоновки данных
КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
МакетКомпоновки = КомпоновщикМакета.Выполнить(Схема, Настройки, , , Тип("ГенераторМакетаКомпоновкиДанныхДляКоллекцииЗначений"));
// 3. Достаем скомпилированный текст запроса, где уже применены все отборы и параметры
ТекстЗапросаИтоговый = МакетКомпоновки.НаборыДанных.НаборДанныхДинамическогоСписка.Запрос;
// 4. Оборачиваем исходный тяжелый запрос в легкую обертку для подсчета количества
ЗапросКоличества = Новый Запрос;
ЗапросКоличества.Текст = "ВЫБРАТЬ КОЛИЧЕСТВО(*) КАК Всего ИЗ (" + ТекстЗапросаИтоговый + ") КАК Подзапрос";
// 5. Обязательно переносим параметры из макета компоновки в наш новый запрос
Для Каждого Параметр Из МакетКомпоновки.ЗначенияПараметров Цикл
ЗапросКоличества.УстановитьПараметр(Параметр.Имя, Параметр.Значение);
КонецЦикла;
// 6. Выполняем запрос к СУБД
Выборка = ЗапросКоличества.Выполнить().Выбрать();
Если Выборка.Следующий() Тогда
Возврат Выборка.Всего;
КонецЕсли;
Возврат 0;
КонецФункции
Этот способ работает невероятно быстро. СУБД выполнит оптимизированный подсчет по индексам таблиц и вернет на сервер 1С ровно одно число. Никакие лишние мегабайты данных не будут передаваться по сети и перегружать оперативную память.
Если мы будем вызывать процедуру подсчета строк при каждом клике по списку или при каждом вводе символа в строку поиска, интерфейс программы начнет раздражающе «подлагивать». Чтобы этого избежать, применим паттерн отложенного вызова (Debouncing) с помощью обработчика ожидания.
Рассмотрим, как настроить плавное обновление количества на клиенте:
&НаКлиенте
Процедура СписокПриАктивизацииСтроки(Элемент)
// Сбрасываем таймер предыдущего вызова, если пользователь продолжает быстро листать список
ОтключитьОбработчикОжидания("Подключаемый_ОбновитьКоличествоЗаписей");
// Запускаем расчет только после того, как пользователь остановился на строке более чем на 0.7 секунды
ПодключитьОбработчикОжидания("Подключаемый_ОбновитьКоличествоЗаписей", 0.7, Истина);
КонецПроцедуры
&НаКлиенте
Процедура Подключаемый_ОбновитьКоличествоЗаписей()
// Вызываем серверную процедуру для обновления значения на форме
ОбновитьКоличествоНаСервере();
КонецПроцедуры
&НаСервере
Процедура ОбновитьКоличествоНаСервере()
// Получаем быстрое количество и записываем его в реквизит формы
КоличествоЗаписей = ПолучитьКоличествоСтрокДинамическогоСписка();
// Например, выводим результат в заголовок группы или декорацию на форме
Элементы.ГруппаСписок.Заголовок = "Список документов (" + КоличествоЗаписей + ")";
КонецПроцедуры
Применение такой схемы гарантирует, что пользователь не почувствует никаких задержек при активной работе со списком, поиске или фильтрации, а нагрузка на сервер СУБД снизится до минимума.
Мы проанализировали процесс подсчета записей в динамических списках и пришли к следующим выводам:
ПриПолученииДанныхНаСервере для подсчета общего количества бессмысленно, так как оно работает только с видимым фрагментом таблицы.ДополнительныеСвойства компоновщика настроек.ТаблицуЗначений на сервере работает, но опасна из-за риска перегрузки оперативной памяти при больших объемах данных.КОЛИЧЕСТВО(*).