Разработчики, работающие с платформой 1С:Предприятие 7.7, часто сталкиваются со спецификой работы предопределенных процедур в модулях форм. Одна из наиболее известных архитектурных проблем — невозможность получить ссылку на только что созданный элемент справочника непосредственно в процедуре ПриЗаписи().
В этой статье мы подробно разберем, почему возникает эта проблема, проанализируем различные варианты её решения (от стандартных до продвинутых) и рассмотрим готовые примеры кода для реализации надежного механизма регистрации новых элементов.
Выясним причину такого поведения платформы. В отличие от 1С 8.x, где существуют раздельные события "ПередЗаписью" и "ПриЗаписи", в "семерке" процедура ПриЗаписи() фактически отрабатывает до того, как транзакция записи физически фиксируется в базе данных.
Для существующего (изменяемого) элемента это не вызывает проблем: у него уже есть уникальный внутренний идентификатор (ID), и метод ТекущийЭлемент() возвращает корректную ссылку. Но если мы записываем новый элемент, его уникальный идентификатор в СУБД еще не сгенерирован. Метод ТекущийЭлемент() в этот момент вернет пустое значение. Это делает невозможным прямую передачу ссылки на новый объект в другие процедуры или внешние системы прямо из тела ПриЗаписи().
Рассмотрим по шагам несколько способов решения этой задачи.
---
Если перед вами стоит задача решить проблему исключительно штатными средствами платформы 1С 7.7 (без использования внешних компонент), то наилучшим решением будет использование асинхронного вызова через обработку ожидания в связке с анализом закрытия формы.
Идея метода заключается в следующем: в момент записи нового элемента мы запоминаем его код (или другой уникальный реквизит, например, наименование) в локальную переменную модуля формы и запускаем односекундный таймер. Когда таймер срабатывает, элемент уже гарантированно записан в базу, и мы можем легко найти его по коду.
Однако здесь есть нюанс: если пользователь нажимает кнопку "ОК" (которая одновременно записывает элемент и закрывает форму), обработчик ожидания может не успеть сработать, так как форма закроется раньше. Чтобы избежать потери события, мы дублируем вызов регистрации в процедуре ПриЗакрытии().
Посмотрим на пример практической реализации этого подхода в модуле формы справочника:
Перем КодОбъекта; // Локальная переменная модуля для хранения кода нового элемента
Процедура ПриЗаписи()
// Проверяем, что запись разрешена
Если СтатусВозврата() = 1 Тогда
// Если ТекущийЭлемент() пустой - это новый элемент
Если ПустоеЗначение(ТекущийЭлемент()) = 1 Тогда
КодОбъекта = Код;
// Подключаем обработчик ожидания на 1 секунду
Форма.ОбработкаОжидания("ЗарегистрироватьИзменениеОбъектаПослеЗаписи", 1);
Иначе
// Для старых элементов регистрируем изменение сразу
КодОбъекта = "";
глЗарегистрироватьИзменениеОбъекта(ТекущийЭлемент(), "Справочник", Вид());
КонецЕсли;
КонецЕсли;
КонецПроцедуры
Процедура ЗарегистрироватьИзменениеОбъектаПослеЗаписи()
// Сразу отключаем обработчик ожидания, чтобы он не зацикливался
Форма.ОбработкаОжидания("", 0);
СпрОбъекта = СоздатьОбъект("Справочник." + Вид());
// Учитываем подчинение справочника, если оно есть
Если ПустоеЗначение(Владелец) = 0 Тогда
СпрОбъекта.ИспользоватьВладельца(Владелец);
КонецЕсли;
Если ПустоеЗначение(Родитель) = 0 Тогда
СпрОбъекта.ИспользоватьРодителя(Родитель);
КонецЕсли;
// Ищем элемент по сохраненному коду
Если СпрОбъекта.НайтиПоКоду(КодОбъекта, 1) = 1 Тогда
глЗарегистрироватьИзменениеОбъекта(СпрОбъекта.ТекущийЭлемент(), "Справочник", Вид());
// Сбрасываем переменную, чтобы не сдублировать в ПриЗакрытии()
КодОбъекта = "";
КонецЕсли;
КонецПроцедуры
Процедура ПриЗакрытии()
// Если форма закрывается, а обработчик ожидания еще не успел отработать
Если ПустоеЗначение(КодОбъекта) = 0 Then
// Элемент уже точно записан, так как мы закрываем форму после успешной записи
глЗарегистрироватьИзменениеОбъекта(ТекущийЭлемент(), "Справочник", Вид());
КонецЕсли;
КонецПроцедуры
---
Некоторые разработчики пытаются решить проблему "в лоб" — принудительно вызывать метод Записать() прямо внутри предопределенной процедуры ПриЗаписи(). Рассмотрим подробнее, почему этот метод крайне опасен и его не рекомендуется использовать.
На первый взгляд схема выглядит просто:
Процедура ПриЗаписи()
// Принудительно пишем объект в базу данных
Записать();
// Теперь ТекущийЭлемент() уже не пустой!
глЗарегистрироватьИзменениеОбъекта(ТекущийЭлемент(), "Справочник", Вид());
КонецПроцедуры
В чем заключается критическая проблема?
Представим стандартный сценарий работы пользователя. Он открывает форму создания нового элемента, вносит изменения, а затем решает закрыть форму нажатием на крестик или кнопку "Закрыть" (на которой висит стандартная команда #Закрыть).
Система 1С выдает штатный интерактивный вопрос: "Сохранить измененные данные?" с вариантами ответа "Да", "Нет" и "Отмена".
Если пользователь нажимает "Нет", то стандартный механизм ожидает, что транзакция записи будет отменена. Однако, если у вас в коде прописан жесткий вызов Записать(), элемент физически запишется в базу данных ДО того, как система обработает отказ пользователя. В итоге элемент будет сохранен вопреки желанию пользователя, что приведет к засорению базы данных некорректной информацией.
---
Если в вашей конфигурации используется популярная внешняя компонента FormEx, вы можете организовать максимально красивую и надежную асинхронную схему без использования ненадежных таймеров ОбработкаОжидания.
Для этого используется механизм генерации внешнего события системы через объект Сервис. Поскольку внешние события обрабатываются платформой асинхронно (только после того, как текущий поток выполнения полностью завершится), процедура обработки этого события выполнится строго после физической записи элемента в БД.
Разберем пример реализации:
В модуле формы справочника в процедуре ПриЗаписи() генерируем событие:
Процедура ПриЗаписи()
Если СтатусВозврата() = 1 Тогда
Если ПустоеЗначение(ТекущийЭлемент()) = 1 Тогда
// Передаем контекст формы во внешнее событие
Сервис = СоздатьОбъект("Сервис");
Сервис.ВнешнееСобытие("РегистрацияНового", "Справочник." + Вид(), Контекст);
КонецЕсли;
КонецЕсли;
КонецПроцедуры
В Глобальном модуле (в процедуре ОбработкаВнешнегоСобытия) ловим это событие:
Процедура ОбработкаВнешнегоСобытия(Источник, Событие, Данные)
Если Источник = "РегистрацияНового" Тогда
// В параметре "Данные" к нам пришел контекст формы записанного элемента
КонтекстФормы = Данные;
// Теперь элемент гарантированно записан, берем его готовую ссылку
НовыйЭлемент = КонтекстФормы.ТекущийЭлемент();
глЗарегистрироватьИзменениеОбъекта(НовыйЭлемент, "Справочник", КонтекстФормы.Вид());
КонецЕсли;
КонецПроцедуры
---
Еще один профессиональный инструмент расширения возможностей платформы 7.7 — внешняя компонента 1С++. Начиная с поздних версий, данная библиотека позволяет использовать полноценное событие ПослеЗаписи(), которое срабатывает сразу после того, как транзакция записи успешно завершилась и объект зафиксирован в базе данных.
При использовании этого механизма вам больше не нужно придумывать обходные пути с поиском по коду или асинхронными вызовами. Достаточно объявить в модуле формы процедуру:
Процедура ПослеЗаписи()
// Это событие вызывается платформой через перехватчик 1С++
// Здесь метод ТекущийЭлемент() уже гарантированно возвращает ссылку на новый объект
глЗарегистрироватьИзменениеОбъекта(ТекущийЭлемент(), "Справочник", Вид());
КонецПроцедуры
Этот метод является наиболее надежным и чистым с точки зрения архитектуры кода, так как исключает любые коллизии при параллельной работе пользователей и не требует дублирования кода в обработчиках закрытия формы.
---
Если ваша информационная база работает под управлением Microsoft SQL Server, а перед вами стоит глобальная задача регистрации абсолютно всех новых и измененных объектов (например, для организации репликации или обмена данными), то перенос этой логики на уровень СУБД будет самым производительным решением.
Вы можете повесить триггер на вставку (INSERT) в таблицу соответствующего справочника. Преимущество триггеров состоит в том, что они срабатывают всегда — независимо от того, как именно записывается элемент (интерактивно пользователем, программно кодом внутри 1С или через внешнее ADO/OLE соединение).
Пример простейшей структуры SQL-триггера на добавление записи:
CREATE TRIGGER tr_СправочникКонтрагенты_Insert
ON [dbo].[SC123] -- Имя таблицы справочника в вашей БД
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
-- Записываем ID нового элемента в специальную таблицу регистрации изменений
INSERT INTO [dbo].[РегистрацияИзменений] (ИдентификаторОбъекта, ТипОбъекта)
SELECT ID, 'Справочник.Контрагенты'
FROM Inserted; -- Системная таблица SQL Server, содержащая новые строки
END;
Применение SQL-триггеров полностью освобождает код 1С от лишней вычислительной нагрузки и гарантирует стопроцентную регистрацию изменений при любых обстоятельствах.
---
Мы проанализировали различные варианты решения проблемы получения ссылки нового элемента в 1С 7.7. Подведем итоги: