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