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