При разработке внешних обработок заполнения объектов для Библиотеки Стандартных Подсистем (БСП) программисты часто сталкиваются с ситуацией, когда код, идеально работающий при ручном открытии («Файл -> Открыть»), начинает вести себя некорректно или полностью перестает работать при подключении обработки в информационную базу через БСП. В рамках этой статьи мы подробно разберем, почему пропадает управление в обработчике ОбработкаВыбора, проанализируем жизненный цикл управляемой формы и исправим критическую ошибку обновления данных в открытом документе.
Рассмотрим ситуацию на практическом примере. Разрабатывается внешняя обработка заполнения табличной части документа «Заказ клиента» (демо-база УТ 11.4). В обработке настроена команда с типом ВызовКлиентскогоМетода. Код модуля формы внешней обработки выглядит следующим образом:
&НаКлиенте
Перем ЗаказСсылка;
&НаКлиенте
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначения) Экспорт
Для каждого СсылкаИзМассива Из ОбъектыНазначения Цикл
ЗаказСсылка = СсылкаИзМассива;
Отбор = Новый Структура("Контрагент", ПолучитьКонтрагентаНаСервере(ЗаказСсылка));
ФормаСпискаРегСведенийПараметры = Новый Структура("Отбор,РежимВыбора,ТолькоПросмотр,МножественныйВыбор", Отбор, Истина, Истина, Ложь);
ОткрытьФорму("РегистрСведений.ШаблоныЗаказовКлиентов.ФормаСписка", ФормаСпискаРегСведенийПараметры, ЭтаФорма);
КонецЦикла;
КонецПроцедуры
&НаСервереБезКонтекста
Функция ПолучитьКонтрагентаНаСервере(ЗаказСсылка)
Возврат ЗаказСсылка.Контрагент;
КонецФункции
&НаКлиенте
Процедура ОбработкаВыбора(ВыбранноеЗначение, ИсточникВыбора)
ШаблонЗаказаСсылка = ПолучитьСсылкуНаЗаказКлиента(ВыбранноеЗначение);
ЗаполнитьЗаказНаСервере(ЗаказСсылка, ШаблонЗаказаСсылка);
КонецПроцедуры
&НаСервереБезКонтекста
Функция ПолучитьСсылкуНаЗаказКлиента(ВыбранноеЗначение)
Возврат ВыбранноеЗначение.ЗаказКлиента;
КонецФункции
&НаСервере
Процедура ЗаполнитьЗаказНаСервере(ЗаказСсылка, ШаблонЗаказаСсылка)
ЗаказОбъект = ЗаказСсылка.ПолучитьОбъект();
ЗаполнитьЗначенияСвойств(ЗаказОбъект, ШаблонЗаказаСсылка, , "Дата,Номер");
ЗаказОбъект.Товары.Загрузить(ШаблонЗаказаСсылка.Товары.Выгрузить());
ЗаказОбъект.Записать();
КонецПроцедуры
При интерактивном запуске через меню «Файл» все работает штатно: открывается форма списка регистра сведений, пользователь выбирает шаблон, управление передается в предопределенное событие ОбработкаВыбора, и документ перезаписывается. Однако при вызове этой же обработки через БСП из формы заказа клиента выбор в регистре сведений происходит, но процедура ОбработкаВыбора просто игнорируется системой. Выясним причину этого поведения.
Для того чтобы понять, почему ломается логика БСП, проанализируем поведение платформы «1С:Предприятие 8.3» на клиенте. Ключевая разница кроется в том, как именно создается и удерживается в памяти форма нашей внешней обработки в разных режимах работы.
Сценарий 1: Запуск через «Файл -> Открыть»
Пользователь открывает форму обработки явно. Она регистрируется в менеджере окон клиента. Когда мы вызываем метод ОткрытьФорму, передавая в качестве владельца ЭтаФорма, платформа знает, что вызывающая форма активна и должна оставаться в памяти. После закрытия формы выбора платформа находит форму-владельца и благополучно отправляет ей событие ОбработкаВыбора.
Сценарий 2: Запуск через БСП (ВызовКлиентскогоМетода)
Подсистема БСП работает иначе. Она не открывает форму обработки на экране. Вместо этого в общем модуле вызывается метод ПолучитьФорму, чтобы программно инициализировать объект формы и получить доступ к экспортной процедуре ВыполнитьКоманду. Далее происходит следующее:
ВыполнитьКоманду(...).ОткрытьФорму(...).ОткрытьФорму является асинхронным (неблокирующим). Платформа лишь дает команду открыть форму выбора регистра сведений, но выполнение кода процедуры ВыполнитьКоманду не приостанавливается — оно мгновенно завершается.ВыполнитьКоманду завершено, управление возвращается в БСП.ЭтаФорма). Но этого объекта в памяти больше не существует. Событие выбора просто аннулируется.Рассмотрим два варианта решения этой проблемы.
Если логика работы внешней обработки подразумевает интерактивное взаимодействие с пользователем (выбор параметров, подтверждение действий), то правильнее всего изменить тип команды в сведениях о внешней обработке на ОткрытиеФормы.
При таком подходе БСП сама корректно инициализирует, открывает на экране и удерживает в памяти форму нашей внешней обработки. Нам лишь потребуется передать объекты назначения (ссылку на текущий документ) в параметры открываемой формы.
Разберем по шагам реализацию этого механизма:
Шаг 1. В модуле объекта внешней обработки, в функции СведенияОВнешнейОбработке(), настраиваем команду следующим образом:
НоваяСтрока = ТаблицаКоманд.Добавить();
НоваяСтрока.Представление = "Заполнить по шаблону...";
НоваяСтрока.Идентификатор = "ЗаполнитьПоШаблону";
НоваяСтрока.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.ТипКомандыОткрытиеФормы();
НоваяСтрока.ПоказыватьОповещение = Истина;
Шаг 2. Перехватываем объекты назначения при открытии формы на сервере. Поскольку параметры БСП передает в форму при ее создании, сохраним их в реквизитах формы:
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
// БСП передает объекты назначения в параметре "ОбъектыНазначения"
Если Параметры.Свойство("ОбъектыНазначения") Тогда
// Сохраняем массив ссылок во временное хранилище или в реквизит формы (Массив)
АдресОбъектовНазначения = ПоместитьВоВременноеХранилище(Параметры.ОбъектыНазначения, УникальныйИдентификатор);
КонецЕсли;
КонецПроцедуры
Шаг 3. При открытии формы запускаем нашу основную логику:
&НаКлиенте
Процедура ПриОткрытии(Отказ)
ОбъектыНазначения = ПолучитьОбъектыНазначенияИзХранилища();
Если ОбъектыНазначения.Количество() = 0 Тогда
Отказ = Истина;
Возврат;
КонецЕсли;
// Открываем форму выбора регистра
ЗаказСсылка = ОбъектыНазначения[0]; // Работаем с текущим документом
Отбор = Новый Структура("Контрагент", ПолучитьКонтрагентаНаСервере(ЗаказСсылка));
ФормаСпискаПараметры = Новый Структура("Отбор,РежимВыбора,ТолькоПросмотр", Отбор, Истина, Истина);
// В качестве владельца передаем ЭтаФорма, которая теперь гарантированно жива!
ОткрытьФорму("РегистрСведений.ШаблоныЗаказовКлиентов.ФормаСписка", ФормаСпискаПараметры, ЭтаФорма);
КонецПроцедуры
В обработчике ОбработкаВыбора, после выполнения заполнения документа, мы просто вызываем метод ЭтаФорма.Закрыть(). Для конечного пользователя этот процесс будет выглядеть абсолютно бесшовно.
Если архитектура решения строго требует сохранения типа команды ВызовКлиентскогоМетода, то мы можем «обмануть» сборщик мусора платформы. Для этого достаточно принудительно открыть форму внешней обработки на клиенте перед вызовом асинхронных методов.
Посмотрим на пример изменения процедуры ВыполнитьКоманду:
&НаКлиенте
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначения) Экспорт
// Принудительно открываем форму обработки, чтобы зарегистрировать её в менеджере окон
ЭтаФорма.Открыть();
// Скрываем форму с экрана, чтобы не смущать пользователя "пустым" окном
ЭтаФорма.Видимость = Ложь;
// Дальнейшая логика остается прежней
ЗаказСсылка = ОбъектыНазначения[0];
Отбор = Новый Структура("Контрагент", ПолучитьКонтрагентаНаСервере(ЗаказСсылка));
ФормаСпискаРегСведенийПараметры = Новый Структура("Отбор,РежимВыбора,ТолькоПросмотр", Отбор, Истина, Истина);
ОткрытьФорму("РегистрСведений.ШаблоныЗаказовКлиентов.ФормаСписка", ФормаСпискаРегСведенийПараметры, ЭтаФорма);
КонецПроцедуры
В конце процедуры ОбработкаВыбора обязательно вызываем ЭтаФорма.Закрыть(), чтобы освободить оперативную память клиента.
В исходном коде автора (Сообщение 10) содержалась еще одна серьезная методическая ошибка: «После заполнения заказа и закрытия формы в документе не отображаются изменения, приходится делать пересчет руками».
Проанализируем причину. Код автора выполнял следующее:
ЗаказОбъект = ЗаказСсылка.ПолучитьОбъект();
...
ЗаказОбъект.Записать();
Почему это грубая ошибка?
У пользователя на экране в этот момент уже открыта форма этого же самого заказа клиента. Когда внешняя обработка получает объект напрямую из СУБД, изменяет его табличную часть и записывает обратно в базу данных, открытая экранная форма документа ничего об этом не знает. Она по-прежнему держит в своей оперативной памяти старые данные.
Более того, если пользователь решит нажать кнопку «Записать» в своей открытой форме документа, он затрет все изменения, внесенные внешней обработкой, либо получит критическую ошибку платформы: «Данные были изменены или удалены другим пользователем».
Как правильно изменять данные формы?
Любые внешние обработки заполнения, вызываемые из формы объекта, должны работать не с базой данных, а напрямую с контекстом открытой формы документа. Для этого используется свойство ВладелецФормы.
Рассмотрим правильный алгоритм обновления данных:
&НаКлиенте
Процедура ОбработкаВыбора(ВыбранноеЗначение, ИсточникВыбора)
ШаблонЗаказаСсылка = ПолучитьСсылкуНаЗаказКлиента(ВыбранноеЗначение);
// Получаем форму документа-владельца
ФормаДокумента = ЭтотОбъект.ВладелецФормы;
// Копируем данные формы в структуру для обработки на сервере
ДанныеФормыДокумента = ФормаДокумента.Объект;
// Выполняем заполнение без записи в базу данных!
ЗаполнитьЗаказНаСервере(ДанныеФормыДокумента, ШаблонЗаказаСсылка);
// Возвращаем измененные данные обратно в форму-владелец
КопироватьДанныеФормы(ДанныеФормыДокумента, ФормаДокумента.Объект);
// Сообщаем форме документа, что данные были модифицированы
ФормаДокумента.Модифицированность = Истина;
// При необходимости вызываем штатные серверные обработчики пересчета УТ
// ФормаДокумента.Подключаемый_ПриИзмененииРеквизитаОбъекта(Неопределено);
ЭтаФорма.Закрыть();
КонецПроцедуры
&НаСервереБезКонтекста
Процедура ЗаполнитьЗаказНаСервере(ДанныеФормыДокумента, ШаблонЗаказаСсылка)
// Заполняем реквизиты шапки (исключая системные "Дата" и "Номер")
ЗаполнитьЗначенияСвойств(ДанныеФормыДокумента, ШаблонЗаказаСсылка, , "Дата,Номер");
// Заполняем табличную часть прямо в контексте данных формы
ДанныеФормыДокумента.Товары.Очистить();
Для Каждого СтрокаШаблона Из ШаблонЗаказаСсылка.Товары Цикл
НоваяСтрока = ДанныеФормыДокумента.Товары.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаШаблона);
КонецЦикла;
КонецПроцедуры