Разработка интерфейса подбора номенклатуры или других элементов в табличную часть документа — одна из самых частых задач в практике программиста 1С. При реализации множественного выбора разработчики регулярно сталкиваются с двумя классическими проблемами: ошибкой доступа к реквизитам объекта на клиенте и проблемой низкой производительности из-за избыточных серверных вызовов в цикле (так называемая ошибка N+1 запроса).
В этой статье мы подробно разберем, почему возникают эти ошибки, проанализируем код и рассмотрим два профессиональных метода реализации подбора, которые обеспечат мгновенную работу интерфейса даже на слабых каналах связи.
Рассмотрим ситуацию, с которой столкнулся автор темы. В обработчике выбора на клиенте он пытается обойти массив выбранных элементов и напрямую обратиться через точку к их свойствам:
&НаКлиенте
Процедура НоменклатураОбработкаВыбора(Элемент, ВыбранноеЗначение, СтандартнаяОбработка)
Для Каждого ВыбранныйЭлемент Из ВыбранноеЗначение Цикл
НоваяСтрока = Объект.Номенклатура.Добавить;
НоваяСтрока.Наименование = ВыбранныйЭлемент;
НоваяСтрока.МестоХранения = ВыбранныйЭлемент.МестоХранения; // Ошибка здесь!
КонецЦикла;
КонецПроцедуры
При выполнении этого кода система выдает ошибку: Поле объекта не обнаружено (МестоХранения). Давайте разберем, почему это происходит.
Платформа «1С:Предприятие 8» строго разделяет контекст выполнения кода на клиентский и серверный. Переменная ВыбранныйЭлемент на клиенте является обычной ссылкой (например, типа СправочникСсылка.Номенклатура). На клиенте ссылка — это лишь уникальный идентификатор (GUID) объекта в базе данных. Она не содержит внутри себя реальных данных объекта.
Попытка обратиться через точку к реквизиту ссылки на клиенте (например, ВыбранныйЭлемент.МестоХранения) неявным образом заставляет платформу обратиться к серверу и базе данных для извлечения этого свойства. В управляемом приложении такое обращение заблокировано на уровне платформы для обеспечения предсказуемой производительности интерфейса. Именно поэтому система выдает ошибку отсутствия поля.
Пытаясь исправить ситуацию, разработчик выносит получение реквизитов на сервер, создавая неконтекстные функции:
&НаКлиенте
Процедура НоменклатураОбработкаВыбора(Элемент, ВыбранноеЗначение, СтандартнаяОбработка)
Для Каждого ВыбранныйЭлемент Из ВыбранноеЗначение Цикл
НоваяСтрока = Объект.Номенклатура.Добавить();
НоваяСтрока.Наименование = ВыбранныйЭлемент;
НоваяСтрока.МестоХранения = ПолучитьМестоХранения(ВыбранноеЗначение); // Ошибка передачи параметра!
КонецЦикла;
КонецПроцедуры
&НаСервереБезКонтекста
Функция ПолучитьМестоХранения(ВыбранноеЗначение)
Возврат ВыбранноеЗначение.МестоХранения;
КонецФункции
Здесь допущены сразу две критические ошибки:
ВыбранноеЗначение представляет собой массив (так как включен множественный выбор). В цикле нужно было передавать итератор — ВыбранныйЭлемент. Передавая весь массив на сервер и пытаясь получить свойство ВыбранноеЗначение.МестоХранения, мы гарантированно получаем ту же ошибку, так как у объекта типа Массив нет свойства МестоХранения.ВыбранныйЭлемент, вызов серверной функции внутри цикла Для Каждого порождает сетевой запрос на каждую выбранную строку. Если пользователь выберет 50 товаров, система сделает 100 запросов к серверу (по 2 на каждую итерацию). В реальных условиях работы (особенно через веб-клиент или тонкий клиент по VPN) это приведет к зависанию программы на несколько секунд.Перейдем к правильным архитектурным решениям этой задачи.
Самый надежный способ быстро получить реквизиты для множества выбранных элементов — передать весь массив на сервер за один раз, выполнить один запрос к базе данных, вернуть результат на клиент в виде массива структур и заполнить табличную часть.
Давайте пошагово разберем реализацию этого метода. В модуле формы вашего документа напишем следующий код:
&НаКлиенте
Процедура НоменклатураОбработкаВыбора(Элемент, ВыбранноеЗначение, СтандартнаяОбработка)
// 1. Проверяем, что пользователь действительно что-то выбрал
Если ТипЗнч(ВыбранноеЗначение) <> Тип("Массив") Или ВыбранноеЗначение.Количество() = 0 Тогда
Возврат;
КонецЕсли;
// 2. Получаем все необходимые данные за один серверный вызов
ТаблицаДополнительныхДанных = ПолучитьДанныеДляПодбораИзРегистра(ВыбранноеЗначение);
// 3. Быстро заполняем табличную часть документа на клиенте
Для Каждого СтрокаДанных Из ТаблицаДополнительныхДанных Цикл
НоваяСтрока = Объект.Номенклатура.Добавить();
НоваяСтрока.Наименование = СтрокаДанных.Номенклатура;
НоваяСтрока.МестоХранения = СтрокаДанных.МестоХранения;
НоваяСтрока.СтатьяФинансирования = СтрокаДанных.СтатьяФинансирования;
КонецЦикла;
КонецПроцедуры
&НаСервереБезКонтекста
Функция ПолучитьДанныеДляПодбораИзРегистра(МассивНоменклатуры)
// Используем запрос для пакетного извлечения данных из регистра сведений
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| РегистрСведений.Номенклатура КАК Номенклатура,
| РегистрСведений.МестоХранения КАК МестоХранения,
| РегистрСведений.СтатьяФинансирования КАК СтатьяФинансирования
|ИЗ
| РегистрСведений.МестаХраненияИФинансирование КАК РегистрСведений
|ГДЕ
| РегистрСведений.Номенклатура В(&МассивНоменклатуры)";
Запрос.УстановитьПараметр("МассивНоменклатуры", МассивНоменклатуры);
ТаблицаРезультата = Запрос.Выполнить().Выгрузить();
// Таблицу значений нельзя напрямую передать на клиент, преобразуем её в массив структур
МассивРезультатов = Новый Массив;
Для Каждого СтрокаТЗ Из ТаблицаРезультата Цикл
СтруктураСтроки = Новый Структура("Номенклатура, МестоХранения, СтатьяФинансирования");
ЗаполнитьЗначенияСвойств(СтруктураСтроки, СтрокаТЗ);
МассивРезультатов.Добавить(СтруктураСтроки);
КонецЦикла;
Возврат МассивРезультатов;
КонецФункции
Преимущества этого способа: мы делаем ровно один запрос к серверу независимо от того, выбрал пользователь 5 позиций или 500. База данных оптимизирует поиск по индексу операции В(&МассивНоменклатуры), что обеспечивает моментальный отклик интерфейса.
Если в форме выбора справочника Номенклатура («ФормаВыбораДляЗаявки») уже выведен динамический список, который содержит колонки «МестоХранения» и «СтатьяФинансирования» (например, полученные левым соединением в запросе динамического списка), то обращаться к серверу вообще нет необходимости! Данные уже прочитаны платформой и отображаются на экране пользователя.
Мы можем перехватить событие выбора в форме подбора, прочитать данные отображаемых строк прямо на клиенте и передать их в документ готовой структурой с помощью механизма оповещения.
Рассмотрим по шагам реализацию этого элегантного и самого быстрого метода.
Шаг 1. Настройка формы подбора («ФормаВыбораДляЗаявки»)
В свойствах таблицы динамического списка на форме выбора переопределим стандартное событие выбора (двойной клик или нажатие Enter), назначив обработчик СписокВыбор:
&НаКлиенте
Процедура СписокВыбор(Элемент, ВыбраннаяСтрока, Поле, СтандартнаяОбработка)
// Отменяем стандартное поведение платформы (которое вернуло бы только ссылку на Номенклатуру)
СтандартнаяОбработка = Ложь;
МассивВыбора = Новый Массив;
// Обходим все выделенные пользователем строки в динамическом списке
Для Каждого ИдентификаторСтроки Из Элементы.Список.ВыделенныеСтроки Цикл
// Метод ДанныеСтроки() считывает ВСЕ отображаемые данные строки на клиенте без обращения к серверу
ДанныеСтроки = Элементы.Список.ДанныеСтроки(ИдентификаторСтроки);
Если ДанныеСтроки <> Неопределено Тогда
СтруктураДанных = Новый Структура;
СтруктураДанных.Вставить("Номенклатура", ДанныеСтроки.Ссылка);
СтруктураДанных.Вставить("МестоХранения", ДанныеСтроки.МестоХранения);
СтруктураДанных.Вставить("СтатьяФинансирования", ДанныеСтроки.СтатьяФинансирования);
МассивВыбора.Добавить(СтруктураДанных);
КонецЕсли;
КонецЦикла;
// Оповещаем форму-владельца о сделанном выборе, передавая подготовленный массив структур
ОповеститьОВыборе(МассивВыбора);
КонецПроцедуры
Шаг 2. Прием данных в форме документа
В модуле формы документа мы просто принимаем готовый массив структур и мгновенно заполняем табличную часть:
&НаКлиенте
Процедура НоменклатураОбработкаВыбора(Элемент, ВыбранноеЗначение, СтандартнаяОбработка)
// Проверяем тип пришедших данных
Если ТипЗнч(ВыбранноеЗначение) <> Тип("Массив") Тогда
Возврат;
КонецЕсли;
Для Каждого ЭлементВыбора Из ВыбранноеЗначение Цикл
// Проверяем, что получили структуру с подготовленными реквизитами
Если ТипЗнч(ЭлементВыбора) = Тип("Структура") Тогда
НоваяСтрока = Объект.Номенклатура.Добавить();
НоваяСтрока.Наименование = ЭлементВыбора.Номенклатура;
НоваяСтрока.МестоХранения = ЭлементВыбора.МестоХранения;
НоваяСтрока.СтатьяФинансирования = ЭлементВыбора.СтатьяФинансирования;
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Преимущества второго способа: Этот метод является абсолютным лидером по производительности. Он не производит ни одного дополнительного обращения к базе данных или серверу приложений 1С. Все данные берутся из оперативной памяти клиентского сеанса, где они уже находятся для отрисовки интерфейса динамического списка.
Выбор оптимального решения зависит от архитектуры вашей конфигурации:
Избегайте обращений к серверу внутри циклов и никогда не пытайтесь получить реквизиты ссылочных типов на клиенте напрямую через точку. Соблюдение этих простых правил сделает ваши конфигурации быстрыми, надежными и приятными в использовании!