При доработке типовых конфигураций в 1С:Предприятии 8.3 с использованием механизмов расширений разработчики часто сталкиваются с вопросами, касающимися возможностей и ограничений расширений. Два из наиболее распространенных — это отсутствие аннотаций &Перед и &После для функций, а также сложности с добавлением новых реквизитов на формы без заимствования всей формы целиком, что может привести к проблемам при последующих обновлениях. В этой статье мы подробно рассмотрим эти аспекты, выясним причины таких ограничений и предложим эффективные решения, которые помогут вам создавать устойчивые и легко поддерживаемые доработки.
Многие разработчики, привыкшие к гибкости аннотаций &Перед, &После и &Вместо для процедур, с удивлением обнаруживают, что для функций в расширениях доступны только варианты &Вместо или &Вместо с контролем. Мы проанализируем, почему это происходит и как можно обойти данное ограничение, чтобы получить желаемый результат.
Почему для функций недоступны аннотации &Перед и &После?
Основная причина кроется в самой природе функций: они всегда возвращают какое-либо значение. Аннотации &Перед и &После предназначены для выполнения дополнительного кода до или после выполнения исходной процедуры или функции, но они не позволяют изменить логику возврата значения самой функции. Если бы для функции можно было использовать &Перед, то не было бы однозначного способа определить, какое из значений — возвращаемое исходной функцией или значение, присвоенное в расширении — должно быть принято системой. Такая неопределенность могла бы привести к непредсказуемому поведению системы и сложностям в отладке.
Чтобы избежать таких коллизий и обеспечить детерминированное поведение, платформа 1С ограничила возможности расширения функций только аннотацией &Вместо. Эта аннотация полностью заменяет тело исходной функции, предоставляя разработчику полный контроль над её выполнением и, что особенно важно, над возвращаемым значением. Таким образом, мы получаем явный механизм для изменения логики функции.
Как использовать &Вместо для имитации поведения &Перед и &После для функций?
Хотя напрямую использовать &Перед и &После для функций нельзя, мы можем добиться аналогичного эффекта, применяя аннотацию &Вместо в сочетании с методом глобального контекста ПродолжитьВызов(). Этот метод является ключевым для работы с расширениями и позволяет нам вызвать оригинальную функцию из тела расширяющей функции. Используя его, мы можем выполнить наш код до вызова оригинальной функции, затем вызвать её, получить результат, а затем выполнить наш код после её выполнения.
Давайте рассмотрим, как это работает по шагам:
&Вместо, которая полностью замещает исходную функцию конфигурации. Имя расширяющей функции обычно начинается с префикса расширения, но аннотация связывает её с оригинальной функцией.ПродолжитьВызов(). Крайне важно передать в этот метод все параметры, которые были переданы в расширяющую функцию, чтобы оригинальная функция получила корректные входные данные. Метод ПродолжитьВызов() вернет результат выполнения оригинальной функции.Посмотрим на пример, который наглядно демонстрирует этот механизм:
// Предположим, в конфигурации есть такая функция:
// Функция ПолучитьЗначение(Парам1, Парам2) Экспорт
// Сообщить("Выполняется оригинальная функция ПолучитьЗначение");
// Возврат Парам1 + Парам2;
// КонецФункции
// В расширении мы создаем расширяющую функцию:
&Вместо("ПолучитьЗначение")
Функция Расш_ПолучитьЗначение(Парам1, Парам2)
// --- Код, который выполняется "ПЕРЕД" оригинальной функцией ---
Сообщить("Расширение: Выполняемся перед вызовом оригинальной функции ПолучитьЗначение.");
Сообщить("Расширение: Исходные параметры: Парам1 = " + Парам1 + ", Парам2 = " + Парам2);
// Модифицируем параметры перед вызовом оригинальной функции
// Это аналог логики, которую можно было бы реализовать в &Перед
Перем МодифицированныйПарам1 = Парам1 * 2;
Перем МодифицированныйПарам2 = Парам2 + 10;
// Вызываем оригинальную функцию и получаем её результат
Перем РезультатОригинальнойФункции;
Попытка
РезультатОригинальнойФункции = ПродолжитьВызов(МодифицированныйПарам1, МодифицированныйПарам2);
Сообщить("Расширение: Оригинальная функция вернула: " + РезультатОригинальнойФункции);
Исключение
Сообщить("Расширение: Произошла ошибка при вызове оригинальной функции: " + ОписаниеОшибки());
// В случае ошибки можем вернуть свое значение или пробросить исключение
Возврат Неопределено;
КонецПопытки;
// --- Код, который выполняется "ПОСЛЕ" оригинальной функции ---
// Это аналог логики, которую можно было бы реализовать в &После
Сообщить("Расширение: Выполняемся после вызова оригинальной функции ПолучитьЗначение.");
// Модифицируем результат, если это необходимо
// Например, добавляем к нему еще что-то
РезультатОригинальнойФункции = РезультатОригинальнойФункции + 100;
Сообщить("Расширение: Окончательный результат после модификации: " + РезультатОригинальнойФункции);
// Возвращаем окончательный результат из расширяющей функции
Возврат РезультатОригинальнойФункции;
КонецФункции
В этом примере мы видим, как с помощью &Вместо и ПродолжитьВызов() мы не только вызываем исходную функцию, но и выполняем код до и после её выполнения, а также имеем возможность модифицировать её параметры и возвращаемое значение. Это мощный инструмент, который позволяет нам гибко дорабатывать функциональность функций, даже несмотря на отсутствие прямых аннотаций &Перед и &После.
Аннотации для процедур:
Для процедур ситуация иная, и здесь мы имеем больше возможностей. Мы можем использовать все три аннотации: &Перед, &После и &Вместо. Однако существуют важные ограничения на их одновременное использование, которые мы должны учитывать:
&Перед или &После (даже если эти методы находятся в разных расширениях, примененных к одной и той же процедуре), то мы не можем использовать аннотации &Вместо или &ИзменениеИКонтроль для той же процедуры. Платформа не позволит применить такое расширение.&Вместо, то мы не можем добавить к ней расширения с &Перед или &После.Эти правила введены для предотвращения конфликтов и обеспечения предсказуемого порядка выполнения кода. Если метод полностью заменяется (&Вместо), то выполнение кода до или после него с помощью других аннотаций теряет смысл, поскольку оригинальный метод может быть вообще не вызван. Система гарантирует, что для одной процедуры будет либо набор &Перед/&После, либо одно &Вместо.
Особенности использования &Вместо:
Аннотация &Вместо считается потенциально опасной, и мы должны использовать её с осторожностью. Когда мы используем &Вместо, мы полностью заменяем собой весь исходный метод, и платформа 1С не отслеживает автоматически изменения в тексте оригинального метода в базовой конфигурации. Это означает, что если в новом релизе типовой конфигурации разработчики изменят внутреннюю логику или алгоритм работы исходного метода, наша расширяющая функция &Вместо продолжит работать по старому алгоритму, который мы определили. Это может привести к неактуальной или некорректной работе функциональности, расхождениям с типовой логикой, появлению ошибок, которые отсутствуют в обновленной конфигурации.
Поэтому рекомендуется использовать &Вместо преимущественно для расширений с назначением "исправление", когда нужно внести небольшие, точечные изменения в существующую логику, которая, как мы ожидаем, не будет часто меняться. Для более масштабных доработок или там, где важна устойчивость к обновлениям, лучше рассмотреть другие механизмы, если они применимы.
Важно также понимать, как работает цепочка вызовов, если несколько расширений используют &Вместо для одного метода:
&Вместо, которое зарегистрировано выше в списке примененных расширений.ПродолжитьВызов(), то оригинальный метод конфигурации не будет вызван. Более того, никакие другие расширения с &Вместо, которые находятся ниже в списке, также не будут вызваны. Цепочка вызовов прерывается.&Вместо вызывает ПродолжитьВызов(), то управление передается следующему расширению с &Вместо (если такое есть), или непосредственно оригинальному методу, если больше нет расширений с &Вместо. Таким образом, формируется цепочка, где каждое &Вместо может либо вызвать следующее звено, либо прервать выполнение.Чтобы полностью прервать цепочку вызовов и предотвратить выполнение как оригинального метода, так и других расширений с &Вместо, необходимо удостовериться, что в нашем расширяющем коде не вызывается ПродолжитьВызов(). В большинстве случаев, если мы хотим полностью заменить логику, мы просто не вызываем этот метод.
Второй частый вопрос, с которым сталкиваются разработчики, касается добавления новых реквизитов на формы объектов конфигурации (например, справочников или документов) через расширения. Возникает дилемма: как это сделать, чтобы минимизировать проблемы при обновлениях типовой конфигурации? Мы рассмотрим два основных подхода: заимствование формы и программное добавление реквизитов, подробно анализируя преимущества и недостатки каждого из них.
Самый простой и интуитивно понятный на первый взгляд способ — это заимствовать нужную форму в расширение. Для этого в конфигураторе мы выполняем следующие действия:
После заимствования формы она появляется в нашем расширении, и мы можем открыть её в редакторе форм. Здесь мы можем добавить в неё новые реквизиты (если они были добавлены в заимствованный объект или являются реквизитами только формы), а затем разместить их на форме, используя стандартные механизмы перетаскивания и настройки элементов формы. Этот подход интуитивно понятен и быстро реализуем, что делает его привлекательным для разработчиков, особенно начинающих.
Недостатки заимствования формы:
Однако этот подход имеет существенные недостатки, особенно в контексте поддержки и обновлений типовой конфигурации, и мы настоятельно рекомендуем тщательно взвешивать все "за" и "против" перед его использованием:
В связи с этим, заимствование формы в расширение рекомендуется использовать только в крайних случаях, когда нет другого способа реализовать требуемую доработку, или когда изменения формы в типовой конфигурации происходят крайне редко и их легко отслеживать. Во всех остальных случаях мы настоятельно рекомендуем применять программный подход.
Наиболее предпочтительным, гибким и устойчивым к обновлениям способом добавления реквизитов на форму через расширение является программное изменение формы. Этот подход позволяет нам добавлять реквизиты и элементы управления на форму, не забирая саму форму в расширение. Это значительно упрощает процесс обновления типовой конфигурации, поскольку изменения внешнего вида и логики формы в типовой конфигурации не будут "ломать" наше расширение, за исключением очень редких случаев кардинальных изменений структуры формы, затрагивающих места вставки наших элементов.
Мы будем работать с формой на сервере, используя метод ИзменитьРеквизиты() для добавления реквизитов и методы коллекции Элементы для добавления элементов управления.
Давайте рассмотрим пошаговый алгоритм:
Если новый реквизит должен хранить данные, то его необходимо добавить либо непосредственно в типовой объект конфигурации (например, в справочник "Номенклатура" через конфигуратор, если это допустимо в рамках поддержки), либо в новый объект расширения, созданный специально для хранения данных (например, новый справочник или регистр сведений в расширении). Следует отметить, что добавление новых реквизитов непосредственно в конфигурацию противоречит общей идеологии расширений, которая предполагает отсутствие изменений в типовой конфигурации.
Однако, при удалении расширения данные, хранящиеся в его объектах, могут быть потеряны. Поэтому часто практикуется гибридный подход: новые объекты для хранения данных создаются в основной конфигурации (если это допустимо и согласовано с вендором), а вся логика работы с ними (добавление на формы, обработка, отображение) реализуется в расширении. Если же реквизит нужен только для временного использования на форме (например, для вывода информации или временного ввода данных, которые не сохраняются в базе), его можно добавить только на форму, не создавая соответствующий реквизит в объекте конфигурации.
Для этого мы используем метод ИзменитьРеквизиты() объекта формы. Этот метод позволяет динамически добавлять, изменять или удалять реквизиты формы. Он выполняется на сервере, поэтому наш код должен быть в серверном контексте (например, в обработчиках событий формы ПриСозданииНаСервере или ПриЧтенииНаСервере, которые мы расширяем).
Сначала мы получаем объект формы (ЭтаФорма), затем создаем новый объект РеквизитФормы() с нужными свойствами (имя, тип, путь к данным) и передаем его в метод ИзменитьРеквизиты(). Мы обязательно проверяем, что реквизит еще не добавлен, чтобы избежать повторного добавления при каждом открытии формы, если она сохраняет состояние.
// Пример кода в модуле формы расширения, который будет вызываться при открытии формы
// Например, расширяем обработчик события формы "ПриСозданииНаСервере"
&Вместо("ПриСозданииНаСервере") // Предполагаем, что это обработчик формы документа/справочника
Процедура Расш1_МояФорма_ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
// Вызываем оригинальный обработчик формы
ПродолжитьВызов(Отказ, СтандартнаяОбработка);
// Проверяем, существует ли уже наш реквизит, чтобы не добавлять его повторно
Если Не ЭтаФорма.НайтиРеквизит("Расш1_НовыйРеквизитФормы") Тогда
// Создаем описание нового реквизита формы
// Имя реквизита должно быть уникальным, лучше с префиксом расширения
// Тип указываем с помощью ОписаниеТипов, например, для Строки длиной 100 символов
// ПутьКДанным можно оставить пустым, если реквизит не связан с объектом
Перем НовыйРеквизит = Новый РеквизитФормы(
"Расш1_НовыйРеквизитФормы",
Новый ОписаниеТипов("Строка", , , Новый КвалификаторыСтроки(100)),
"Объект.Ссылка", // Если реквизит связан с полем объекта, указываем путь к нему
"Мой новый реквизит для формы" // Заголовок, который будет отображаться в отладчике
);
// Добавляем реквизит на форму
ЭтаФорма.ИзменитьРеквизиты(Новый Массив([НовыйРеквизит]));
// Устанавливаем значение по умолчанию для нового реквизита, если это необходимо
// Обращаемся к реквизиту по его имени
ЭтаФорма.Расш1_НовыйРеквизитФормы = "Значение по умолчанию";
Сообщить("Расширение: Добавлен новый реквизит формы 'Расш1_НовыйРеквизитФормы'.");
КонецЕсли;
КонецПроцедуры
В данном примере мы добавляем строковый реквизит с именем "Расш1_НовыйРеквизитФормы" и длиной 100 символов. Если реквизит формы должен быть связан с реквизитом объекта, то в третьем параметре конструктора РеквизитФормы указывается путь к данным объекта, например, "Объект.МойРеквизитОбъекта".
После того как реквизит формы добавлен, нам нужно отобразить его на форме, то есть создать для него соответствующий элемент управления (например, поле ввода, надпись или кнопку). Это также делается программно, путем создания нового элемента формы и привязки его к только что созданному реквизиту.
Для этого мы используем метод Элементы.Добавить() объекта формы и устанавливаем необходимые свойства нового элемента, такие как Вид (например, ВидПоляФормы.ПолеВвода), ПутьКДанным (здесь мы указываем имя нашего программно созданного реквизита формы), Заголовок и Родитель (для размещения в нужной группе элементов формы).
// Продолжение предыдущего примера, в том же обработчике "ПриСозданииНаСервере"
// Проверяем, существует ли уже наш элемент формы
Если Не ЭтаФорма.Элементы.Найти("Расш1_ПолеНовогоРеквизита") Тогда
// Находим группу, в которую хотим поместить наш новый элемент
// Например, "ГруппаВажныхДанных" - это имя существующей группы на форме
Перем ГруппаРодитель = ЭтаФорма.Элементы.Найти("ГруппаВажныхДанных");
Если ГруппаРодитель = Неопределено Тогда
// Если группы нет, добавляем в корень формы или в другую подходящую группу
ГруппаРодитель = ЭтаФорма.Элементы;
КонецЕсли;
// Создаем новый элемент формы типа "ПолеФормы"
Перем НовыйЭлемент = ЭтаФорма.Элементы.Добавить("Расш1_ПолеНовогоРеквизита", Тип("ПолеФормы"), ГруппаРодитель);
// Настраиваем свойства элемента
НовыйЭлемент.Вид = ВидПоляФормы.ПолеВвода;
НовыйЭлемент.ПутьКДанным = "Расш1_НовыйРеквизитФормы"; // Привязываем к созданному реквизиту формы
НовыйЭлемент.Заголовок = "Мой программный реквизит";
НовыйЭлемент.Видимость = Истина;
НовыйЭлемент.Доступность = Истина;
НовыйЭлемент.ПоложениеЗаголовка = ПоложениеЗаголовкаЭлементаФормы.Авто;
// ... другие свойства по необходимости ...
Сообщить("Расширение: Добавлен новый элемент формы 'Расш1_ПолеНовогоРеквизита'.");
КонецЕсли;
Здесь ГруппаВажныхДанных — это пример имени существующего элемента формы (группы), в которую мы хотим поместить наш новый элемент. Если мы хотим разместить его в корне формы, мы можем указать ЭтаФорма как родителя для Элементы.Добавить(), хотя для лучшей организации интерфейса предпочтительнее использовать существующие группы.
Если наш программно добавленный реквизит требует какой-либо обработки при изменении (например, вызов серверной процедуры для валидации или расчета), мы можем создать обработчики событий в модуле расширения и привязать их к соответствующим событиям элемента формы. Это обеспечивает полноценную интерактивность с новым элементом.
// Продолжение предыдущего примера
// В том же обработчике "ПриСозданииНаСервере" или "ПриЧтенииНаСервере" после создания элемента
Если ЭтаФорма.Элементы.Найти("Расш1_ПолеНовогоРеквизита") Тогда
Перем МойЭлемент = ЭтаФорма.Элементы.Расш1_ПолеНовогоРеквизита;
// Устанавливаем действие (процедуру-обработчик) для события "ПриИзменении"
// Процедура "Расш1_НовыйЭлементНаФормеПриИзменении" должна быть определена
// в модуле формы расширения или в общем модуле, доступном для формы.
МойЭлемент.УстановитьДействие("ПриИзменении", "Расш1_НовыйЭлементНаФормеПриИзменении");
Сообщить("Расширение: Установлен обработчик 'ПриИзменении' для элемента 'Расш1_ПолеНовогоРеквизита'.");
КонецЕсли;
// В модуле формы расширения объявляем эту процедуру:
// (Она должна быть Экспортной, если она вызывается из другого модуля,
// но для событий формы достаточно быть процедурой модуля формы)
// Процедура Расш1_НовыйЭлементНаФормеПриИзменении(Элемент)
// Сообщить("Расширение: Значение нового реквизита изменено на: " + Элемент.Значение);
// // Здесь можно реализовать любую логику: валидацию, пересчеты и т.д.
// КонецПроцедуры
Имя процедуры-обработчика (например, Расш1_НовыйЭлементНаФормеПриИзменении) должно быть уникальным в контексте формы и зарегистрировано в модуле формы расширения. Этот подход позволяет нам интегрировать нашу доработку максимально бесшовно.
Преимущества программного подхода:
Рекомендации по организации кода в расширениях:
Для более удобной работы, поддержания порядка и обеспечения долгосрочной стабильности в расширении мы рекомендуем следовать следующим практикам:
Расш1_МоиФункцииСервер или Расш1_МоиФункцииКлиент.
Расш1_, _МоеРасширение, _Ext_). Это поможет избежать конфликтов имен как с типовыми объектами конфигурации, так и с объектами других расширений, которые могут быть применены к одной и той же базе. Это правило применимо не только к коду, но и к именам программно создаваемых реквизитов и элементов формы.&Вместо или сложную логику программного изменения форм. Указывайте, для чего нужна та или иная доработка, какие риски она несет и как она должна работать.Таким образом, хотя платформа 1С накладывает определенные ограничения на использование аннотаций для функций и требует внимательного подхода к доработке форм, предложенные методы с использованием &Вместо, ПродолжитьВызов() и программного изменения форм позволяют эффективно решать большинство задач по расширению функциональности. Применяя эти подходы, вы сможете создавать гибкие, устойчивые к обновлениям и легко поддерживаемые доработки в системе 1С:Предприятие.