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