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