Как правильно реализовать диалоговое окно с ожиданием ответа пользователя в 1С:Предприятие 8.3?

Программист 1С v8.3 (Управляемые формы)
← На главную

При разработке в 1С:Предприятие 8.3 часто возникает необходимость вывести пользователю диалоговое окно с вопросом и, в зависимости от его ответа, продолжить или прервать выполнение процедуры. Однако, метод ПоказатьВопрос, в отличие от своего устаревшего аналога Вопрос, является немодальным и не приостанавливает исполнение кода. Это может привести к тому, что код, предназначенный для выполнения после ответа пользователя, сработает до того, как диалог вообще появится. Рассмотрим подробнее эту проблему и выясним, как ее корректно решить.

В чем заключается проблема немодального диалога ПоказатьВопрос?

Автор столкнулся с ситуацией, когда требовалось вывести диалоговое окно перед сохранением карточки партнера, если пользователь не обладает определенными правами. Цель — прервать сохранение, если пользователь откажется. При этом исполнение кода продолжалось, и сохранение происходило до появления диалога.

Рассмотрим исходный код, который демонстрирует эту проблему:


Отказ = Ложь; // Моя переменная для дальнейшего прерывания исполнения кода, если пользователь ответил на диалог "Нет"
ДействияПослеОтветаНаВопрос = Новый ОписаниеОповещения("ПослеОтветаНаДиалог", ЭтотОбъект, Отказ); // Инициализация описания оповещения

Если Не ПользовательЯвляетсяОКЗИлиАдмином Тогда // Проверка ролей пользователя
    ПоказатьВопрос(ДействияПослеОтветаНаВопрос, "После сохранения участника корректировка карточки участника закупочной процедуры будет невозможна. " + Символы.ПС + "Подтвердите необходимость завершения процесса заведения карточки участника для дальнейшего её сохранения.", РежимДиалогаВопрос.ДаНет, , , "ВНИМАНИЕ!");
КонецЕсли;

// Здесь значение "Отказ" всегда ложь, из-за того, что диалог всплывает по итогу сохранения карточки партнёра
Если Отказ = Истина Тогда
    Возврат;
КонецЕсли;

СписокСозданных = Новый СписокЗначений;
ЗаписатьПартнераИСвязанныеДанные(СписокСозданных); // Эта процедура сохраняет нового партнёра

Как мы видим, после вызова ПоказатьВопрос сразу же идет проверка переменной Отказ, а затем вызов процедуры ЗаписатьПартнераИСвязанныеДанные. Проблема заключается в том, что метод ПоказатьВопрос является немодальным. Это означает, что он не блокирует выполнение кода. Диалоговое окно отображается на экране, но основная процедура продолжает работать, не дожидаясь ответа пользователя. В результате, Отказ остается Ложь, и сохранение происходит немедленно.

Платформа 1С:Предприятие, начиная с версии 8.3, активно перешла на использование немодальных диалоговых окон, таких как ПоказатьВопрос, взамен устаревших модальных методов (например, Вопрос). Основная причина этого перехода — развитие клиент-серверной архитектуры, появление управляемых форм, веб-клиента и мобильных приложений. Модальные окна, которые блокировали весь интерфейс программы или даже браузер, создавали множество неудобств и проблем, особенно в веб-клиенте, где они могли быть заблокированы браузером или нарушать пользовательский опыт. В режиме использования модальности, установленном как "Не использовать", вызов старых модальных функций, таких как Вопрос, может привести к ошибке "Использование модальных окон в данном режиме запрещено!".

Корректная обработка ПоказатьВопрос: асинхронный подход

Ключевое отличие работы с ПоказатьВопрос заключается в асинхронной модели. Это требует "перелома мышления" при написании кода. Процедура, которая вызывает ПоказатьВопрос, должна завершиться после его вызова. Дальнейшая логика, зависящая от ответа пользователя, должна быть вынесена в отдельную процедуру-обработчик, которая будет вызвана платформой после того, как пользователь ответит на вопрос.

Рассмотрим шаги для правильной реализации:

  1. Вызов ПоказатьВопрос и прерывание текущей процедуры: В основной процедуре, где требуется задать вопрос, мы вызываем ПоказатьВопрос и сразу же после него добавляем оператор Возврат;. Это гарантирует, что текущая процедура завершится, и код после вопроса не будет выполнен до получения ответа.
  2. Определение процедуры-обработчика оповещения: Создаем отдельную процедуру (например, ПослеОтветаНаДиалог), которая будет вызываться платформой после того, как пользователь нажмет кнопку в диалоге. Эта процедура получает результат ответа пользователя.
  3. Перенос логики в обработчик: Весь код, который должен выполняться после ответа на вопрос и зависит от этого ответа, переносится в процедуру-обработчик оповещения.
  4. Условное выполнение логики: Если условие для вывода вопроса не выполняется, мы напрямую вызываем ту же логику, которая находится в обработчике.

Разберем это на примере кода:

Предположим, наша основная процедура называется ПередЗаписьюНаСервере (или любая другая процедура, где происходит логика сохранения).


// Основная процедура, например, ПередЗаписьюНаСервере
Процедура ПередЗаписьюНаСервере(Отказ, ПараметрыЗаписи)
    // Переменная для хранения ответа пользователя
    Перем РезультатОтветаНаВопрос;

    // Если условие для вопроса не выполняется, сразу переходим к сохранению
    Если ПользовательЯвляетсяОКЗИлиАдмином Тогда
        // Вызываем процедуру сохранения напрямую
        СохранитьПартнераПослеПроверки(Отказ, ПараметрыЗаписи);
        Возврат;
    КонецЕсли;

    // Инициализация описания оповещения.
    // Передаем текущую процедуру в качестве контекста, чтобы можно было использовать ее параметры
    ДействияПослеОтветаНаВопрос = Новый ОписаниеОповещения("ПослеОтветаНаДиалог", ЭтотОбъект, Новый Структура("Отказ", Отказ)); // Передаем Отказ в структуру

    // Выводим диалог
    ПоказатьВопрос(ДействияПослеОтветаНаВопрос, "После сохранения участника корректировка карточки участника закупочной процедуры будет невозможна. " + Символы.ПС + "Подтвердите необходимость завершения процесса заведения карточки участника для дальнейшего её сохранения.", РежимДиалогаВопрос.ДаНет, , , "ВНИМАНИЕ!");

    // !!! ВАЖНО !!!
    // После вызова ПоказатьВопрос, текущая процедура должна быть прервана.
    // Это ключевой момент для асинхронной работы.
    // Если мы в обработчике ПередЗаписью, то Отказ=Истина, чтобы предотвратить запись.
    // Если это просто процедура без Отказа, то Возврат.
    Отказ = Истина; // Прерываем текущую запись, ответ будет обработан позже
    Возврат;
КонецПроцедуры

// Процедура-обработчик оповещения, которая будет вызвана после ответа пользователя
Процедура ПослеОтветаНаДиалог(Результат, ДополнительныеПараметры) Экспорт
    // ДополнительныеПараметры теперь содержат переданный нами параметр Отказ
    Перем Отказ;
    ДополнительныеПараметры.Получить("Отказ", Отказ);

    // Если пользователь ответил "Да", продолжаем процесс
    Если Результат = КодВозвратаДиалога.Да Тогда
        // Здесь мы должны инициировать процесс сохранения заново,
        // но уже без повторного вопроса.
        // Это можно сделать, например, вызвав ту же процедуру, но с флагом.
        // Или, если это форма, имитировать нажатие кнопки "Записать".

        // Если это обработчик формы ПередЗаписью, то нам нужно
        // повторно инициировать запись объекта, но уже без вопросов.
        // Для этого часто используют временную переменную на форме или другой механизм.
        // Простейший вариант - повторный вызов записи, но с флагом,
        // который говорит, что вопрос уже задан и отвечен.
        // Например:
        // ЭтаФорма.ЗаписыватьБезВопроса = Истина;
        // ЭтаФорма.Записать();
        // ЭтаФорма.ЗаписыватьБезВопроса = Ложь;

        // В данном случае, чтобы не усложнять, предположим, что мы можем вызвать
        // процедуру сохранения напрямую, как будто вопроса не было.
        // Отказ = Ложь;
        // Мы НЕ МОЖЕМ просто изменить Отказ здесь и ожидать, что запись продолжится,
        // потому что текущий контекст ПередЗаписью уже завершен.
        // Нужно инициировать запись заново.

        // Самый правильный подход для ПередЗаписью:
        // 1. В обработчике ПередЗаписью мы выставляем Отказ = Истина и задаем вопрос.
        // 2. В ПослеОтветаНаДиалог, если ответ "Да", мы устанавливаем специальный флаг
        //    (например, реквизит формы "ЗаписатьБезВопроса" типа Булево) в Истина.
        // 3. Вызываем метод формы "Записать()" или "ЗаписатьОбъект()".
        // 4. В следующем вызове ПередЗаписью, если флаг "ЗаписатьБезВопроса" = Истина,
        //    мы пропускаем логику вопроса и устанавливаем Отказ = Ложь.

        // Пример для формы:
        ЭтаФорма.ЗаписатьБезВопроса = Истина;
        ЭтаФорма.Записать(); // Повторно вызываем запись, но уже без вопроса
        ЭтаФорма.ЗаписатьБезВопроса = Ложь; // Сбрасываем флаг

        // Если это не ПередЗаписью, а обычная процедура, то можно вызывать логику сохранения
        // СохранитьПартнераПослеПроверки(Отказ, ДополнительныеПараметры); // Пример
    Иначе
        // Если пользователь ответил "Нет" или закрыл диалог, то ничего не делаем.
        // Запись уже была прервана в основной процедуре (Отказ = Истина)
        // Если бы это была обычная процедура, здесь можно было бы выставить Отказ = Истина
        // или просто ничего не делать, чтобы не продолжать логику.
        Сообщить("Запись партнера отменена пользователем.");
    КонецЕсли;
КонецПроцедуры

// Вспомогательная процедура, содержащая логику сохранения
Процедура СохранитьПартнераПослеПроверки(Отказ, ПараметрыЗаписи)
    // Здесь размещаем весь код, который отвечает за сохранение партнера.
    // Если Отказ = Истина, то выход не нужен, т.к. этот Отказ уже обрабатывается на уровне обработчика события.
    СписокСозданных = Новый СписокЗначений;
    ЗаписатьПартнераИСвязанныеДанные(СписокСозданных);
    Сообщить("Партнер успешно сохранен.");
КонецПроцедуры

// Дополнительный реквизит формы, необходимый для работы в событии ПередЗаписью
// Должен быть создан на форме или в модуле формы.
// РеквизитФормы = Новый РеквизитФормы("ЗаписатьБезВопроса", Новый ОписаниеТипов("Булево"), "ЭтотОбъект");
// ДобавитьРеквизит(РеквизитФормы);

Для того чтобы решить проблему с `Отказ` в событии `ПередЗаписью`, нам нужно немного модифицировать код. Поскольку `Отказ` в `ПередЗаписью` должен быть установлен до выхода из обработчика, мы устанавливаем его в `Истина` при первом проходе, когда задаем вопрос. Затем, в процедуре-обработчике оповещения, если пользователь согласится, мы должны повторно инициировать запись объекта, но уже с флагом, который говорит, что вопрос был задан и ответ получен.

Модифицированный подход для события ПередЗаписью:

  1. Создаем на форме дополнительный реквизит типа Булево, например, ЗаписатьБезВопроса. По умолчанию он Ложь.
  2. В обработчике ПередЗаписью:
    • Если ЗаписатьБезВопроса = Истина, то устанавливаем Отказ = Ложь и возвращаемся, позволяя записи пройти.
    • Иначе (т.е. вопрос еще не задавался или ответ не получен):
      • Если условие для вопроса соблюдено:
        • Устанавливаем Отказ = Истина (прерываем текущую попытку записи).
        • Вызываем ПоказатьВопрос, передавая в ОписаниеОповещения контекст формы и, возможно, ссылку на объект записи.
        • Добавляем Возврат;.
      • Если условие для вопроса не соблюдено (пользователь является админом):
        • Устанавливаем Отказ = Ложь.
        • Вызываем основную логику сохранения или позволяем процедуре завершиться.
  3. В процедуре-обработчике оповещения (ПослеОтветаНаДиалог):
    • Если пользователь ответил "Да":
      • Устанавливаем ЭтаФорма.ЗаписатьБезВопроса = Истина;.
      • Вызываем метод формы Записать(); или ЗаписатьОбъект();. Это инициирует новую попытку записи.
      • Сразу же сбрасываем ЭтаФорма.ЗаписатьБезВопроса = Ложь;, чтобы при следующих записях вопрос задавался снова.
    • Если пользователь ответил "Нет" или закрыл диалог, просто ничего не делаем, так как запись уже была отменена в ПередЗаписью первым проходом.

Этот двухпроходный механизм позволяет корректно обработать асинхронный ответ пользователя в контексте модального по своей природе события ПередЗаписью.


// В модуле формы или объекта
// Реквизит формы: ЗаписатьБезВопроса (Тип: Булево, Использование: Для всех)

Процедура ПередЗаписью(Отказ, ПараметрыЗаписи)
    // Проверяем флаг, если запись инициирована после ответа на вопрос
    Если ЭтаФорма.ЗаписатьБезВопроса Тогда
        Отказ = Ложь; // Разрешаем запись
        Возврат;
    КонецЕсли;

    // Если условие для вопроса не выполняется (пользователь имеет права)
    Если ПользовательЯвляетсяОКЗИлиАдмином Тогда
        Отказ = Ложь; // Разрешаем запись
        Возврат;
    КонецЕсли;

    // Условие для вопроса соблюдено, пользователь не имеет прав
    // Прерываем текущую запись, чтобы дождаться ответа пользователя
    Отказ = Истина;

    // Инициализация описания оповещения
    // Передаем ссылку на текущую форму, чтобы в обработчике можно было управлять ее реквизитами
    ДействияПослеОтветаНаВопрос = Новый ОписаниеОповещения("ПослеОтветаНаДиалог", ЭтотОбъект, ЭтотОбъект);

    // Выводим диалог
    ПоказатьВопрос(ДействияПослеОтветаНаВопрос, "После сохранения участника корректировка карточки участника закупочной процедуры будет невозможна. " + Символы.ПС + "Подтвердите необходимость завершения процесса заведения карточки участника для дальнейшего её сохранения.", РежимДиалогаВопрос.ДаНет, , , "ВНИМАНИЕ!");

    // Текущая процедура завершается, Отказ=Истина уже установлен
КонецПроцедуры

// Процедура-обработчик оповещения
Процедура ПослеОтветаНаДиалог(Результат, ДополнительныеПараметры) Экспорт
    // ДополнительныеПараметры здесь - это ЭтотОбъект, то есть форма
    Перем ФормаОбъект;
    ФормаОбъект = ДополнительныеПараметры;

    // Если пользователь ответил "Да"
    Если Результат = КодВозвратаДиалога.Да Тогда
        // Устанавливаем флаг, чтобы при повторной записи вопрос не задавался
        ФормаОбъект.ЗаписатьБезВопроса = Истина;
        // Инициируем повторную запись объекта формы
        ФормаОбъект.Записать();
        // Сбрасываем флаг сразу после инициирования записи
        ФормаОбъект.ЗаписатьБезВопроса = Ложь;
    Иначе
        // Пользователь ответил "Нет" или закрыл диалог.
        // Запись уже была отменена в ПередЗаписью.
        Сообщить("Сохранение партнера отменено пользователем.");
    КонецЕсли;
КонецПроцедуры

// Функция-проверка ролей (пример)
Функция ПользовательЯвляетсяОКЗИлиАдмином()
    Возврат РольДоступна("ПолныеПрава") ИЛИ РольДоступна("АдминистраторСистемы"); // Пример
КонецФункции

// Процедура сохранения данных (будет вызвана при Записать() формы)
Процедура ЗаписатьПартнераИСвязанныеДанные(СписокСозданных)
    // Ваша логика сохранения партнера
    Сообщить("Данные партнера записаны в базу.");
КонецПроцедуры

Альтернатива для более новых версий платформы (8.3.18+)

Если ваша конфигурация поддерживает версию платформы 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+) доступен упрощенный метод ВопросАсинх, который позволяет избежать ручного разделения кода.

← На главную