При разработке в 1С:Предприятие 8.3 часто возникает необходимость вывести пользователю диалоговое окно с вопросом и, в зависимости от его ответа, продолжить или прервать выполнение процедуры. Однако, метод ПоказатьВопрос, в отличие от своего устаревшего аналога Вопрос, является немодальным и не приостанавливает исполнение кода. Это может привести к тому, что код, предназначенный для выполнения после ответа пользователя, сработает до того, как диалог вообще появится. Рассмотрим подробнее эту проблему и выясним, как ее корректно решить.
Автор столкнулся с ситуацией, когда требовалось вывести диалоговое окно перед сохранением карточки партнера, если пользователь не обладает определенными правами. Цель — прервать сохранение, если пользователь откажется. При этом исполнение кода продолжалось, и сохранение происходило до появления диалога.
Рассмотрим исходный код, который демонстрирует эту проблему:
Отказ = Ложь; // Моя переменная для дальнейшего прерывания исполнения кода, если пользователь ответил на диалог "Нет"
ДействияПослеОтветаНаВопрос = Новый ОписаниеОповещения("ПослеОтветаНаДиалог", ЭтотОбъект, Отказ); // Инициализация описания оповещения
Если Не ПользовательЯвляетсяОКЗИлиАдмином Тогда // Проверка ролей пользователя
ПоказатьВопрос(ДействияПослеОтветаНаВопрос, "После сохранения участника корректировка карточки участника закупочной процедуры будет невозможна. " + Символы.ПС + "Подтвердите необходимость завершения процесса заведения карточки участника для дальнейшего её сохранения.", РежимДиалогаВопрос.ДаНет, , , "ВНИМАНИЕ!");
КонецЕсли;
// Здесь значение "Отказ" всегда ложь, из-за того, что диалог всплывает по итогу сохранения карточки партнёра
Если Отказ = Истина Тогда
Возврат;
КонецЕсли;
СписокСозданных = Новый СписокЗначений;
ЗаписатьПартнераИСвязанныеДанные(СписокСозданных); // Эта процедура сохраняет нового партнёра
Как мы видим, после вызова ПоказатьВопрос сразу же идет проверка переменной Отказ, а затем вызов процедуры ЗаписатьПартнераИСвязанныеДанные. Проблема заключается в том, что метод ПоказатьВопрос является немодальным. Это означает, что он не блокирует выполнение кода. Диалоговое окно отображается на экране, но основная процедура продолжает работать, не дожидаясь ответа пользователя. В результате, Отказ остается Ложь, и сохранение происходит немедленно.
Платформа 1С:Предприятие, начиная с версии 8.3, активно перешла на использование немодальных диалоговых окон, таких как ПоказатьВопрос, взамен устаревших модальных методов (например, Вопрос). Основная причина этого перехода — развитие клиент-серверной архитектуры, появление управляемых форм, веб-клиента и мобильных приложений. Модальные окна, которые блокировали весь интерфейс программы или даже браузер, создавали множество неудобств и проблем, особенно в веб-клиенте, где они могли быть заблокированы браузером или нарушать пользовательский опыт. В режиме использования модальности, установленном как "Не использовать", вызов старых модальных функций, таких как Вопрос, может привести к ошибке "Использование модальных окон в данном режиме запрещено!".
Ключевое отличие работы с ПоказатьВопрос заключается в асинхронной модели. Это требует "перелома мышления" при написании кода. Процедура, которая вызывает ПоказатьВопрос, должна завершиться после его вызова. Дальнейшая логика, зависящая от ответа пользователя, должна быть вынесена в отдельную процедуру-обработчик, которая будет вызвана платформой после того, как пользователь ответит на вопрос.
Рассмотрим шаги для правильной реализации:
ПоказатьВопрос и прерывание текущей процедуры: В основной процедуре, где требуется задать вопрос, мы вызываем ПоказатьВопрос и сразу же после него добавляем оператор Возврат;. Это гарантирует, что текущая процедура завершится, и код после вопроса не будет выполнен до получения ответа.ПослеОтветаНаДиалог), которая будет вызываться платформой после того, как пользователь нажмет кнопку в диалоге. Эта процедура получает результат ответа пользователя.Разберем это на примере кода:
Предположим, наша основная процедура называется ПередЗаписьюНаСервере (или любая другая процедура, где происходит логика сохранения).
// Основная процедура, например, ПередЗаписьюНаСервере
Процедура ПередЗаписьюНаСервере(Отказ, ПараметрыЗаписи)
// Переменная для хранения ответа пользователя
Перем РезультатОтветаНаВопрос;
// Если условие для вопроса не выполняется, сразу переходим к сохранению
Если ПользовательЯвляетсяОКЗИлиАдмином Тогда
// Вызываем процедуру сохранения напрямую
СохранитьПартнераПослеПроверки(Отказ, ПараметрыЗаписи);
Возврат;
КонецЕсли;
// Инициализация описания оповещения.
// Передаем текущую процедуру в качестве контекста, чтобы можно было использовать ее параметры
ДействияПослеОтветаНаВопрос = Новый ОписаниеОповещения("ПослеОтветаНаДиалог", ЭтотОбъект, Новый Структура("Отказ", Отказ)); // Передаем Отказ в структуру
// Выводим диалог
ПоказатьВопрос(ДействияПослеОтветаНаВопрос, "После сохранения участника корректировка карточки участника закупочной процедуры будет невозможна. " + Символы.ПС + "Подтвердите необходимость завершения процесса заведения карточки участника для дальнейшего её сохранения.", РежимДиалогаВопрос.ДаНет, , , "ВНИМАНИЕ!");
// !!! ВАЖНО !!!
// После вызова ПоказатьВопрос, текущая процедура должна быть прервана.
// Это ключевой момент для асинхронной работы.
// Если мы в обработчике ПередЗаписью, то Отказ=Истина, чтобы предотвратить запись.
// Если это просто процедура без Отказа, то Возврат.
Отказ = Истина; // Прерываем текущую запись, ответ будет обработан позже
Возврат;
КонецПроцедуры
// Процедура-обработчик оповещения, которая будет вызвана после ответа пользователя
Процедура ПослеОтветаНаДиалог(Результат, ДополнительныеПараметры) Экспорт
// ДополнительныеПараметры теперь содержат переданный нами параметр Отказ
Перем Отказ;
ДополнительныеПараметры.Получить("Отказ", Отказ);
// Если пользователь ответил "Да", продолжаем процесс
Если Результат = КодВозвратаДиалога.Да Тогда
// Здесь мы должны инициировать процесс сохранения заново,
// но уже без повторного вопроса.
// Это можно сделать, например, вызвав ту же процедуру, но с флагом.
// Или, если это форма, имитировать нажатие кнопки "Записать".
// Если это обработчик формы ПередЗаписью, то нам нужно
// повторно инициировать запись объекта, но уже без вопросов.
// Для этого часто используют временную переменную на форме или другой механизм.
// Простейший вариант - повторный вызов записи, но с флагом,
// который говорит, что вопрос уже задан и отвечен.
// Например:
// ЭтаФорма.ЗаписыватьБезВопроса = Истина;
// ЭтаФорма.Записать();
// ЭтаФорма.ЗаписыватьБезВопроса = Ложь;
// В данном случае, чтобы не усложнять, предположим, что мы можем вызвать
// процедуру сохранения напрямую, как будто вопроса не было.
// Отказ = Ложь;
// Мы НЕ МОЖЕМ просто изменить Отказ здесь и ожидать, что запись продолжится,
// потому что текущий контекст ПередЗаписью уже завершен.
// Нужно инициировать запись заново.
// Самый правильный подход для ПередЗаписью:
// 1. В обработчике ПередЗаписью мы выставляем Отказ = Истина и задаем вопрос.
// 2. В ПослеОтветаНаДиалог, если ответ "Да", мы устанавливаем специальный флаг
// (например, реквизит формы "ЗаписатьБезВопроса" типа Булево) в Истина.
// 3. Вызываем метод формы "Записать()" или "ЗаписатьОбъект()".
// 4. В следующем вызове ПередЗаписью, если флаг "ЗаписатьБезВопроса" = Истина,
// мы пропускаем логику вопроса и устанавливаем Отказ = Ложь.
// Пример для формы:
ЭтаФорма.ЗаписатьБезВопроса = Истина;
ЭтаФорма.Записать(); // Повторно вызываем запись, но уже без вопроса
ЭтаФорма.ЗаписатьБезВопроса = Ложь; // Сбрасываем флаг
// Если это не ПередЗаписью, а обычная процедура, то можно вызывать логику сохранения
// СохранитьПартнераПослеПроверки(Отказ, ДополнительныеПараметры); // Пример
Иначе
// Если пользователь ответил "Нет" или закрыл диалог, то ничего не делаем.
// Запись уже была прервана в основной процедуре (Отказ = Истина)
// Если бы это была обычная процедура, здесь можно было бы выставить Отказ = Истина
// или просто ничего не делать, чтобы не продолжать логику.
Сообщить("Запись партнера отменена пользователем.");
КонецЕсли;
КонецПроцедуры
// Вспомогательная процедура, содержащая логику сохранения
Процедура СохранитьПартнераПослеПроверки(Отказ, ПараметрыЗаписи)
// Здесь размещаем весь код, который отвечает за сохранение партнера.
// Если Отказ = Истина, то выход не нужен, т.к. этот Отказ уже обрабатывается на уровне обработчика события.
СписокСозданных = Новый СписокЗначений;
ЗаписатьПартнераИСвязанныеДанные(СписокСозданных);
Сообщить("Партнер успешно сохранен.");
КонецПроцедуры
// Дополнительный реквизит формы, необходимый для работы в событии ПередЗаписью
// Должен быть создан на форме или в модуле формы.
// РеквизитФормы = Новый РеквизитФормы("ЗаписатьБезВопроса", Новый ОписаниеТипов("Булево"), "ЭтотОбъект");
// ДобавитьРеквизит(РеквизитФормы);
Для того чтобы решить проблему с `Отказ` в событии `ПередЗаписью`, нам нужно немного модифицировать код. Поскольку `Отказ` в `ПередЗаписью` должен быть установлен до выхода из обработчика, мы устанавливаем его в `Истина` при первом проходе, когда задаем вопрос. Затем, в процедуре-обработчике оповещения, если пользователь согласится, мы должны повторно инициировать запись объекта, но уже с флагом, который говорит, что вопрос был задан и ответ получен.
Модифицированный подход для события ПередЗаписью:
ЗаписатьБезВопроса. По умолчанию он Ложь.ПередЗаписью:
ЗаписатьБезВопроса = Истина, то устанавливаем Отказ = Ложь и возвращаемся, позволяя записи пройти.Отказ = Истина (прерываем текущую попытку записи).ПоказатьВопрос, передавая в ОписаниеОповещения контекст формы и, возможно, ссылку на объект записи.Возврат;.Отказ = Ложь.ПослеОтветаНаДиалог):
ЭтаФорма.ЗаписатьБезВопроса = Истина;.Записать(); или ЗаписатьОбъект();. Это инициирует новую попытку записи.ЭтаФорма.ЗаписатьБезВопроса = Ложь;, чтобы при следующих записях вопрос задавался снова.ПередЗаписью первым проходом.Этот двухпроходный механизм позволяет корректно обработать асинхронный ответ пользователя в контексте модального по своей природе события ПередЗаписью.
// В модуле формы или объекта
// Реквизит формы: ЗаписатьБезВопроса (Тип: Булево, Использование: Для всех)
Процедура ПередЗаписью(Отказ, ПараметрыЗаписи)
// Проверяем флаг, если запись инициирована после ответа на вопрос
Если ЭтаФорма.ЗаписатьБезВопроса Тогда
Отказ = Ложь; // Разрешаем запись
Возврат;
КонецЕсли;
// Если условие для вопроса не выполняется (пользователь имеет права)
Если ПользовательЯвляетсяОКЗИлиАдмином Тогда
Отказ = Ложь; // Разрешаем запись
Возврат;
КонецЕсли;
// Условие для вопроса соблюдено, пользователь не имеет прав
// Прерываем текущую запись, чтобы дождаться ответа пользователя
Отказ = Истина;
// Инициализация описания оповещения
// Передаем ссылку на текущую форму, чтобы в обработчике можно было управлять ее реквизитами
ДействияПослеОтветаНаВопрос = Новый ОписаниеОповещения("ПослеОтветаНаДиалог", ЭтотОбъект, ЭтотОбъект);
// Выводим диалог
ПоказатьВопрос(ДействияПослеОтветаНаВопрос, "После сохранения участника корректировка карточки участника закупочной процедуры будет невозможна. " + Символы.ПС + "Подтвердите необходимость завершения процесса заведения карточки участника для дальнейшего её сохранения.", РежимДиалогаВопрос.ДаНет, , , "ВНИМАНИЕ!");
// Текущая процедура завершается, Отказ=Истина уже установлен
КонецПроцедуры
// Процедура-обработчик оповещения
Процедура ПослеОтветаНаДиалог(Результат, ДополнительныеПараметры) Экспорт
// ДополнительныеПараметры здесь - это ЭтотОбъект, то есть форма
Перем ФормаОбъект;
ФормаОбъект = ДополнительныеПараметры;
// Если пользователь ответил "Да"
Если Результат = КодВозвратаДиалога.Да Тогда
// Устанавливаем флаг, чтобы при повторной записи вопрос не задавался
ФормаОбъект.ЗаписатьБезВопроса = Истина;
// Инициируем повторную запись объекта формы
ФормаОбъект.Записать();
// Сбрасываем флаг сразу после инициирования записи
ФормаОбъект.ЗаписатьБезВопроса = Ложь;
Иначе
// Пользователь ответил "Нет" или закрыл диалог.
// Запись уже была отменена в ПередЗаписью.
Сообщить("Сохранение партнера отменено пользователем.");
КонецЕсли;
КонецПроцедуры
// Функция-проверка ролей (пример)
Функция ПользовательЯвляетсяОКЗИлиАдмином()
Возврат РольДоступна("ПолныеПрава") ИЛИ РольДоступна("АдминистраторСистемы"); // Пример
КонецФункции
// Процедура сохранения данных (будет вызвана при Записать() формы)
Процедура ЗаписатьПартнераИСвязанныеДанные(СписокСозданных)
// Ваша логика сохранения партнера
Сообщить("Данные партнера записаны в базу.");
КонецПроцедуры
Если ваша конфигурация поддерживает версию платформы 8.3.18 или выше, вы можете использовать метод ВопросАсинх (DoQueryBoxAsync). Этот метод позволяет значительно упростить код, так как он сам реализует асинхронное ожидание ответа пользователя, и вам не придется вручную разбивать логику на несколько процедур и использовать флаги. Примерно это выглядело бы так:
#Если ВерсияПлатформы("8.3.18") ИЛИ ВерсияПлатформы("8.3.19") ИЛИ ВерсияПлатформы("8.3.20") ИЛИ ВерсияПлатформы("8.3.21") ИЛИ ВерсияПлатформы("8.3.22") ИЛИ ВерсияПлатформы("8.3.23") ИЛИ ВерсияПлатформы("8.3.24") ИЛИ ВерсияПлатформы("8.3.25") ИЛИ ВерсияПлатформы("8.3.26") Тогда // Проверка версии платформы
Процедура ПередЗаписью(Отказ, ПараметрыЗаписи)
Если Не ПользовательЯвляетсяОКЗИлиАдмином Тогда
// ВопросАсинх позволяет ожидать ответ без прерывания всей логики
Результат = ВопросАсинх("После сохранения участника корректировка карточки участника закупочной процедуры будет невозможна. " + Символы.ПС + "Подтвердите необходимость завершения процесса заведения карточки участника для дальнейшего её сохранения.", РежимДиалогаВопрос.ДаНет, , , "ВНИМАНИЕ!");
Если Результат <> КодВозвратаДиалога.Да Тогда
Отказ = Истина; // Если пользователь не подтвердил, отменяем запись
Возврат;
КонецЕсли;
КонецЕсли;
// Если вопрос не задавался или пользователь ответил "Да", продолжаем запись
Отказ = Ложь;
СписокСозданных = Новый СписокЗначений;
ЗаписатьПартнераИСвязанныеДанные(СписокСозданных);
КонецПроцедуры
#КонецЕсли
К сожалению, в рассматриваемом случае версия платформы 8.3.11, поэтому метод ВопросАсинх недоступен, и приходится использовать подход с ОписаниеОповещения.
Для облегчения перехода от модальных вызовов Вопрос к немодальным ПоказатьВопрос, в конфигураторе 1С предусмотрены инструменты рефакторинга. Вы можете кликнуть правой кнопкой мыши на методе Вопрос в коде и выбрать "Рефакторинг / Нерекомендуемые синхронные вызовы / Преобразовать вызов". Платформа автоматически разделит код на ПоказатьВопрос и соответствующую процедуру обработки оповещения. Это может значительно ускорить процесс адаптации существующего кода.
Понимание асинхронной природы метода ПоказатьВопрос является критически важным при разработке в 1С:Предприятие 8.3. Вместо ожидания блокировки выполнения кода, мы должны использовать механизм обратных вызовов через ОписаниеОповещения. Это требует перестройки логики, вынесения зависимого кода в отдельные процедуры и использования флагов или повторного инициирования действий при необходимости. Хотя это может показаться более сложным, такой подход соответствует современным принципам разработки и обеспечивает корректную работу приложения в различных клиентских режимах, включая веб и мобильные клиенты. Для более новых версий платформы (8.3.18+) доступен упрощенный метод ВопросАсинх, который позволяет избежать ручного разделения кода.