Как программно добавить обработчик события ПриЗакрытииПосле для расширения и корректно выводить информационные сообщения при закрытии формы в 1С?

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

Разберем распространенную проблему, с которой сталкиваются разработчики 1С при работе с расширениями: как добавить дополнительную проверку с информационными сообщениями, которые должны быть видны пользователю даже после того, как форма документа была проведена и закрыта. Часто стандартные методы вывода сообщений не позволяют достичь желаемого результата, особенно при использовании кнопки "Провести и закрыть". Мы также выясним, можно ли программно "добавить" обработчик события к существующему, а не просто "переопределить" его.

Ограничения программного добавления обработчиков событий формы в расширении

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

Мы должны понимать, что команда Форма.УстановитьДействие("Событие", "ИмяПроцедуры"), которую мы можем использовать на клиенте или при создании формы на сервере, предназначена для переназначения, а не для добавления нового обработчика. Это означает, что если вы используете УстановитьДействие(), то штатный обработчик, который был назначен для этого события ранее, перестанет работать. Он будет полностью замещен вашей новой процедурой, если вы не предусмотрите его явный вызов внутри своей процедуры.

С точки зрения архитектуры 1С, программное добавление новых обработчиков событий формы в расширении во время выполнения приложения, без перезапуска и компиляции, в принципе невозможно. Это связано с тем, что конфигурация событий формы и их привязка к процедурам определяется на этапе конфигурации (компиляции формы) в конфигураторе. Во время работы программы эти настройки уже установлены и не могут быть изменены динамически, подобно тому, как мы не можем программно изменять структуру метаданных информационной базы в режиме "Предприятие".

Для расширения функциональности типовых процедур и событий в модулях (модулях объектов, общих модулях) платформа предоставляет более мощный и безопасный механизм — аннотации расширений: &Перед, &После, &Вместо. Эти аннотации позволяют встраивать ваш код до, после или вместо вызова типовой процедуры, сохраняя при этом исходную логику (или заменяя ее контролируемым образом). Если мы создаем обработчик события формы в расширении через конфигуратор, мы выбираем тип перехватчика ("перед", "после" или "вместо") именно там, а не программно. Если нужно вызвать исходную процедуру из перехваченного метода, особенно с аннотацией &Вместо, мы используем метод ПродолжитьВызов(). Этот метод позволяет продолжить выполнение расширяемого метода после выполнения вашего кода.

Если несколько расширений перехватывают одно и то же событие, порядок их выполнения зависит от их назначения (Дополнение, Адаптация, Исправление) и порядка регистрации в информационной базе. Самые верхние расширяющие методы (по порядку применения) вызываются первыми, и выполнение кода опускается вниз по цепочке, затем после отработки расширяемого метода, если есть расширяющие методы &После, они выполняются снизу вверх.

Корректный вывод информационных сообщений при закрытии формы

Теперь давайте сосредоточимся на второй, не менее важной части проблемы: как обеспечить видимость информационных сообщений пользователю, когда форма документа закрывается сразу после проведения (например, при нажатии "Провести и закрыть").

Почему сообщения могут не отображаться?

Основная причина заключается в том, что информационные сообщения, выводимые через Сообщить() или СообщениеПользователю() в серверных процедурах или в клиентских обработчиках, связанных с закрытием формы, часто "теряются". Окно сообщений, принадлежащее форме, закрывается вместе с ней, и пользователь просто не успевает их увидеть. Это особенно актуально для информационных сообщений, которые не должны блокировать проведение документа, а лишь информировать пользователя.

Ограничения на серверные вызовы при закрытии формы

Еще один критически важный аспект, который мы должны учитывать: в клиентских обработчиках событий формы, связанных с ее закрытием (таких как ПередЗакрытием(), ПриЗакрытии()), запрещены контекстные серверные вызовы. Это ограничение введено платформой для обеспечения стабильности и корректной работы, особенно в веб-клиенте, так как контекст формы на сервере может быть уже разрушен или находиться в нестабильном состоянии в процессе закрытия. Следовательно, мы не можем просто вызвать контекстную серверную процедуру из ПриЗакрытии формы для получения сообщений.

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

Наиболее эффективным и правильным способом для вывода информационных сообщений в данной ситуации является комбинация накопления сообщений на сервере и их последующего отображения на клиенте. Разберем этот процесс по шагам:

  1. Накопление сообщений на сервере

    В процедуре, которая выполняется на сервере после записи документа (например, в подписке на событие ПослеЗаписиНаСервере или ПослеЗаписи модуля объекта документа, если она вызывается с сервера), мы можем собирать все необходимые информационные сообщения. Для этого удобно использовать стандартный механизм платформы — процедуру Сообщить(). Она добавляет текст сообщения в системный журнал сообщений пользователя, которые затем могут быть извлечены на клиенте.

    Важно: Сообщить() сохраняет сообщения для текущей сессии пользователя. Если мы хотим, чтобы они были доступны для извлечения, их нужно именно "сообщить".

    
    // Пример кода в модуле подписки на событие "ПослеЗаписиНаСервере"
    // Название: Расш_ПослеЗаписиДокументаНаСервере
    // Источник: ДокументСсылка.<ИмяДокумента>
    // Событие: ПослеЗаписиНаСервере
            
    Процедура Расш_ПослеЗаписиДокументаНаСервере(Источник, Отказ) Экспорт
        // Выполняем свои проверки для разных типов документов
        Если ТипЗнч(Источник) = Тип("ДокументСсылка.МойДокумент") Тогда
            ОбъектДокумента = Источник.ПолучитьОбъект();
            Если ОбъектДокумента.Сумма > 100000 Тогда
                Сообщить("Внимание! Сумма документа превышает 100 000. Требуется дополнительная проверка.", СтатусСообщения.Важное);
            КонецЕсли;
            Если ОбъектДокумента.Комментарий = "" Тогда
                Сообщить("Поле 'Комментарий' не заполнено.", СтатусСообщения.Информация);
            КонецЕсли;
        ИначеЕсли ТипЗнч(Источник) = Тип("ДокументСсылка.ДругойДокумент") Тогда
            ОбъектДокумента = Источник.ПолучитьОбъект();
            Если ОбъектДокумента.Ответственный.ЭтоГруппа Тогда
                Сообщить("В документе 'ДругойДокумент' ответственным указана группа. Желательно выбрать конкретного пользователя.", СтатусСообщения.Предупреждение);
            КонецЕсли;
        КонецЕсли;
        // Другие проверки для других типов документов...
    КонецПроцедуры
    
  2. Передача сообщений на клиент и их извлечение

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

    Хотя функция ПолучитьСообщенияПользователю() является серверной функцией в контексте общего модуля, ее вызов в клиентском коде не является контекстным серверным вызовом формы, который запрещен. Платформа предоставляет этот механизм для получения сообщений, накопленных для сессии пользователя, без необходимости обращаться к контексту конкретной формы на сервере.

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

    Для того чтобы сообщения гарантированно отобразились пользователю, даже если форма закрывается, мы используем клиентскую процедуру ПоказатьОповещениеПользователя(). Этот метод выводит всплывающее уведомление в углу экрана (обычно в правом нижнем), которое не блокирует работу пользователя, не требует от него немедленной реакции и остается видимым некоторое время (или пока пользователь не закроет его). Это позволяет вывести сообщения красиво и ненавязчиво, не прерывая процесс закрытия формы и не мешая пользователю.

    
    // Синтаксис метода ПоказатьОповещениеПользователя
    ПоказатьОповещениеПользователя(
        Заголовок,      // (Строка) Заголовок оповещения (например, "Информация о документе")
        Текст,          // (Строка) Основной текст оповещения
        Картинка,       // (Картинка) Иконка оповещения (необязательно, например, БиблиотекаКартинок.Информация)
        Оповещение,     // (ОписаниеОповещения) Обработчик оповещения, который будет вызван при клике (необязательно)
        Идентификатор   // (Строка) Уникальный идентификатор оповещения для управления им (необязательно)
    );
    

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

Техника "перехвата" и вызова штатного обработчика (для УстановитьДействие)

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

Если же вы вынуждены использовать УстановитьДействие(), вот как можно организовать вызов штатного обработчика:

  1. Сохранение имени штатного обработчика

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

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

    Обратите внимание, что сохранение в параметрах сеанса или в реквизите формы может потребовать определенных доработок и контроля жизненного цикла этих данных, чтобы избежать их потери или некорректного использования.

  2. Переопределение действия

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

    
    // Продолжение примера в процедуре &После("ПриСозданииНаСервере")
    // ...
    // После сохранения имени штатного обработчика:
    ЭтаФорма.УстановитьДействие("ПослеЗаписи", "Расш_МояНоваяПослеЗаписи");
    // ...
    
  3. Вызов штатного обработчика из вашей новой процедуры

    Теперь в вашей процедуре Расш_МояНоваяПослеЗаписи вы можете сначала выполнить свою дополнительную логику, а затем (или до нее, в зависимости от сценария) вызвать оригинальный штатный обработчик, имя которого вы сохранили. Для динамического вызова процедуры по имени можно использовать функцию Выполнить().

    
    // Пример в модуле формы документа (в расширении)
    // Процедура, которая будет вызываться после переопределения
    Процедура Расш_МояНоваяПослеЗаписи(ЗаписанныеОбъекты) Экспорт
        // Здесь мы выполняем нашу дополнительную логику,
        // например, выводим наши информационные сообщения
        // или выполняем другие проверки.
        Сообщить("Выполняется дополнительная логика из Расш_МояНоваяПослеЗаписи.");
    
        // Теперь вызываем оригинальный штатный обработчик, если он был сохранен
        Если ЭтаФорма.ДополнительныеПараметры.Свойство("ШтатныйОбработчикПослеЗаписи") Тогда
            ИмяШтатногоОбработчика = ЭтаФорма.ДополнительныеПараметры.Получить("ШтатныйОбработчикПослеЗаписи");
                    
            Если ИмяШтатногоОбработчика <> Неопределено И ЗначениеЗаполнено(ИмяШтатногоОбработчика) Тогда
                Попытка
                    // Динамический вызов штатного обработчика
                    Выполнить("ЭтаФорма." + ИмяШтатногоОбработчика + "(ЗаписанныеОбъекты)");
                    Сообщить("Штатный обработчик '" + ИмяШтатногоОбработчика + "' успешно вызван.");
                Исключение
                    Сообщить("Ошибка при вызове штатного обработчика '" + ИмяШтатногоОбработчика + "': " + ОписаниеОшибки(), СтатусСообщения.Важное);
                КонецПопытки;
            КонецЕсли;
        КонецЕсли;
    КонецПроцедуры
    

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

Заключение

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

Однако, для корректного отображения информационных сообщений после проведения и закрытия формы, особенно когда их может быть много и они не должны блокировать проведение, мы рекомендуем следующий алгоритм:

  1. Собирайте сообщения на сервере с помощью Сообщить() в серверных обработчиках записи (например, в подписке на событие ПослеЗаписиНаСервере или в процедуре ПослеЗаписиНаСервере модуля объекта).
  2. Извлекайте эти сообщения на клиенте в событии формы, предшествующем закрытию (ПриЗакрытии()), используя функцию ПолучитьСообщенияПользователю(Истина). Помните об ограничении на контекстные серверные вызовы в этих событиях.
  3. Отображайте их пользователю с помощью неблокирующего уведомления ПоказатьОповещениеПользователя(), которое гарантированно будет видно, даже если форма сразу же закрывается.

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

← На главную