Почему ПередЗаписью в расширении 1С срабатывает дважды, сбрасывает Отказ и очищает ДополнительныеСвойства?

Программист 1С v8.3 (Управляемые формы) 1C:Бухгалтерия IT и автоматизация бизнеса
← На главную

При разработке расширений для конфигураций на платформе 1С:Предприятие 8.3 разработчики часто сталкиваются с нестандартным поведением стандартных механизмов. Типичный пример такой проблемы — использование перехватчика &Перед("ПередЗаписью") в модуле объекта для запрета ручного создания элементов (например, контрагентов). Разработчик ожидает однократного выполнения проверки и гарантированной отмены транзакции при установке параметра Отказ = Истина. Однако на практике процедура может вызываться дважды, значение параметра Отказ необъяснимым образом сбрасывается в Ложь, а коллекция ДополнительныеСвойства, призванная хранить временные флаги между вызовами, оказывается абсолютно пустой.

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

Выясняем причину: почему ПередЗаписью вызывается дважды?

Двойной вызов события ПередЗаписью в типовых конфигурациях (таких как "Бухгалтерия предприятия", "ERP Управление предприятием" или "Управление торговлей") — это не баг платформы 1С, а заложенная логика прикладных механизмов. Рассмотрим основные сценарии, при которых происходит повторная инициация процесса записи:

  1. Интерактивная запись через механизмы БСП. Когда пользователь нажимает кнопку "Записать и закрыть" или "Записать" в управляемой форме, управление передается не напрямую платформенному методу, а обработчикам БСП. Выполняется процедура вида Подключаемый_ОбработатьЗаписьОбъекта или ОбработатьЗаписьОбъектаВФорме. В ходе выполнения этих процедур система может сначала выполнить технологическую запись (например, для проверки заполнения или предварительной регистрации в планах обмена), а затем выполнить финальную запись объекта.
  2. Автоматическое создание связанных объектов. Справочник Контрагенты тесно связан с другими справочниками и регистрами сведений (например, договоры по умолчанию, контактная информация, банковские счета или партнеры). При создании нового контрагента в транзакции его записи стандартные механизмы могут программно создавать сопутствующие элементы. Программная запись подчиненного или связанного объекта может принудительно инициировать повторный вызов записи владельца для актуализации ссылочной целостности или обновления реквизитов (например, заполнения основного договора).
  3. Генерация кода или номера (Автонумерация). Иногда повторный вызов провоцируется самой платформой 1С. Это происходит, когда в процессе первой попытки записи транзакция приостанавливается для окончательного выделения и присвоения нового уникального кода/номера из пула автонумерации, после чего объект перезаписывается с уже присвоенным номером.

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

Почему во второй раз параметр Отказ приходит как Ложь?

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

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

Во-вторых, некорректная работа с параметром Отказ в коде конфигурации. Существует строгое правило стандартов разработки фирмы "1С": нельзя напрямую присваивать параметру Отказ булево значение без учета его текущего состояния. Например, конструкция Отказ = Ложь; или Отказ = ПроверитьЗаполнение(); является грубой ошибкой. Корректное присвоение должно выглядеть исключительно так:

Отказ = Отказ Или МояПроверка();

Если в основной конфигурации или в другом расширении кто-то пренебрег этим правилом и жестко переопределил параметр, то ваше значение Истина, установленное на ранних этапах выполнения цепочки перехватчиков, будет безвозвратно затерто.

Почему очищаются ДополнительныеСвойства объекта?

Попытка обойти проблему двойного вызова через использование коллекции ДополнительныеСвойства часто приводит к разочарованию: на первой итерации свойства заполнены, а на второй — коллекция оказывается пустой. Почему так происходит?

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

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

Разбираем эффективные варианты решения задачи

Если перед нами стоит задача запретить исключительно ручное (интерактивное) создание элементов, то попытка перехватить событие в модуле объекта через &Перед("ПередЗаписью") — архитектурно не самое лучшее решение. Модуль объекта отвечает за бизнес-логику уровня базы данных. Запрещая запись там, мы заблокируем не только ручной ввод, но и программный (например, загрузку контрагентов через XML, JSON, веб-сервисы, распределенные базы данных или фоновые задания).

Рассмотрим по шагам несколько надежных альтернативных вариантов реализации данной задачи.

Вариант А. Перенос проверки в модуль управляемой формы (Рекомендуемый способ)

Самый правильный способ запретить интерактивное создание — сделать это непосредственно в интерфейсном слое, то есть в форме элемента или форме списка справочника. Для этого мы создадим расширение для формы элемента справочника Контрагенты.

Проанализируем пошаговый алгоритм:

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

Пример реализации кода в расширении формы:


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

Преимущества данного подхода:

Вариант Б. Использование аннотации &После("ПередЗаписью") в модуле объекта

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

Разберем, почему это важно. Когда мы используем &Перед, наш код выполняется самым первым в цепочке обработчиков. После выполнения нашей процедуры управление передается в типовой код, который может сбросить или проигнорировать установленные нами флаги. Используя &После, мы гарантируем, что наша проверка отработает в самом конце, и установленный нами Отказ = Истина не будет переопределен стандартными механизмами.

Посмотрим на пример кода в модуле объекта расширения:


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

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

Вариант В. Использование параметров сеанса для передачи состояния

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

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

Рассмотрим пример использования кэша в общем модуле для отслеживания повторных вызовов:


// В служебном общем модуле "Расш1_СлужебныйСервер" (Вызов сервера = Истина)
Перем ПереданныеОбъекты Экспорт;

Процедура ЗарегистрироватьПроверку(УникальныйИдентификатор) Экспорт
    Если ПереданныеОбъекты = Неопределено Тогда
        ПереданныеОбъекты = Новый Соответствие;
    КонецЕсли;
    ПереданныеОбъекты.Вставить(УникальныйИдентификатор, Истина);
КонецПроцедуры

Функция ПроверкаУжеВыполнялась(УникальныйИдентификатор) Экспорт
    Если ПереданныеОбъекты = Неопределено Тогда
        Возврат Ложь;
    КонецЕсли;
    Возврат ПереданныеОбъекты.Получить(УникальныйИдентификатор) = Истина;
КонецФункции

В модуле объекта расширения мы можем обращаться к этому кэшу, чтобы избежать повторного выполнения тяжелых проверок или повторной отправки уведомлений:


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

Подведем итоги

Разработка качественных расширений в среде 1С:Предприятие требует глубокого понимания логики взаимодействия платформы с прикладными решениями БСП. Двойное срабатывание событий записи, очистка ДополнительныхСвойств и сброс параметров — это естественные последствия оптимизации транзакций и разделения логики на клиент-серверные уровни.

Для создания надежного и отказоустойчивого кода всегда следуйте трем золотым правилам:

← На главную