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

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

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

Понимание архитектуры клиент-серверного взаимодействия в 1С:Предприятии

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

  1. Клиентский контекст. Здесь выполняется код, который непосредственно взаимодействует с пользовательским интерфейсом. Модуль формы, помеченный директивой &НаКлиенте, создается и существует исключительно на клиенте. Все элементы управления формы (кнопки, поля ввода, таблицы), их свойства и методы доступны только в клиентском контексте. Если мы хотим изменить внешний вид формы, отобразить сообщение пользователю или обработать его ввод, мы делаем это в клиентском коде.
  2. Серверный контекст. Здесь происходит основная бизнес-логика, работа с данными информационной базы, сложные вычисления и операции, требующие доступа к ресурсам сервера. Модуль менеджера объекта (например, Документы.Документ1.МодульМенеджера) и общие модули с директивами &НаСервере или &НаСервереБезКонтекста выполняются исключительно на сервере. Серверный код не имеет прямого доступа к клиентскому интерфейсу и не "видит" форм, открытых у пользователя.

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

Почему экспортная процедура модуля формы не поможет?

Часто возникает мысль: "А что, если процедура в модуле формы будет экспортной? Разве это не позволит ее вызвать откуда угодно?" К сожалению, для процедур модуля формы это не так.

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

Рекомендации по размещению логики

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

  1. Перенос логики на сервер.

    Если процедура "Расчет" в модуле формы Документа2 выполняет сложные вычисления или изменяет данные документа (а не только их отображение), то ее место — в серверном модуле. Мы можем разместить эту логику в модуле объекта Документ2, в модуле менеджера Документ2 или в отдельном серверном общем модуле (с директивами &НаСервере или &НаСервереБезКонтекста). Эти серверные процедуры могут быть вызваны как из модуля менеджера Документа1, так и из формы Документа2 (через серверный вызов).

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

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

    
    // Пример: Разделение логики расчета и обновления формы
    // В МодулеОбъекта Документа2 (или в общем модуле &НаСервере)
    Функция ВыполнитьРасчетСервер() Экспорт
        // Здесь выполняются все сложные расчеты.
        // Мы можем получать данные из текущего объекта (ЭтотОбъект).
        // Например:
        // РезультатРасчета = ЭтотОбъект.ТабличнаяЧасть.Итог("Сумма");
        // Или:
        // МассивРезультатов = Новый Массив();
        // Для Каждого СтрокаТЧ Из ЭтотОбъект.ТабличнаяЧасть Цикл
        //     МассивРезультатов.Добавить(Новый Структура("Сотрудник, Сумма", СтрокаТЧ.Сотрудник, СтрокаТЧ.Сумма * 1.1));
        // КонецЦикла;
        // Возвращаем данные, которые нужны на клиенте для обновления формы.
        Возврат "Расчет завершен успешно на сервере!";
    КонецФункции
    
    // В МодулеФормы Документа2 (клиентская процедура, вызывающая серверную)
    &НаКлиенте
    Процедура РасчетНаКлиентеИОбновлениеФормы()
        // Вызываем серверную функцию объекта для выполнения расчета.
        // Результат будет возвращен на клиент.
        РезультатСерверногоРасчета = Объект.ВыполнитьРасчетСервер();
    
        // Теперь мы на клиенте и можем использовать РезультатСерверногоРасчета
        // для обновления элементов формы:
        Сообщить("Результат серверного расчета: " + РезультатСерверногоРасчета);
        // Например, если РезультатСерверногоРасчета - это структура с новыми значениями:
        // Элементы.ПолеИтог.Значение = РезультатСерверногоРасчета.Итог;
        // Или обновить табличную часть формы:
        // Элементы.ТабличнаяЧастьФормы.ОбновитьСтроки();
        // Возможно, потребуется перечитать данные с сервера, если они были изменены.
        ОбновитьОтображениеДанных(); // Стандартный метод формы для обновления
    КонецПроцедуры
    

Альтернативные подходы при работе с формами программно

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

  1. Программное создание и запись документа без открытия формы.

    Если нам не нужно именно "открывать форму" и работать с ее интерактивными элементами (то есть, не нужна визуализация или клиентские обработчики формы), а достаточно просто создать документ, заполнить его и провести необходимые расчеты, то мы можем:

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

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

  2. Программное получение формы (только на клиенте!).

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

    Мы можем использовать метод ПолучитьФорму(). Ключевой момент: этот метод доступен только на клиенте!

    
    // Пример в модуле формы Документа1 (клиентская процедура)
    &НаКлиенте
    Процедура СоздатьИРассчитатьДокумент2НаКлиенте()
    
        // 1. Создаем новый объект документа Документ2
        НовыйОбъектДокумент2 = Документы.Документ2.СоздатьДокумент();
                
        // 2. Заполняем реквизиты нового объекта (это делается на клиенте, но объект еще не записан)
        НовыйОбъектДокумент2.Наименование = "Новый документ для расчета " + Формат(ТекущаяДата(), "ДФ=dd.MM.yyyy HH:mm:ss");
        НовыйОбъектДокумент2.Дата = ТекущаяДата();
        // ... заполнение табличных частей и других реквизитов ...
        // Пример заполнения табличной части "Начисления"
        НоваяСтрока = НовыйОбъектДокумент2.Начисления.Добавить();
        НоваяСтрока.Сотрудник = Справочники.Сотрудники.НайтиПоНаименованию("Иванов И.И."); // Предположим, такой сотрудник есть
        НоваяСтрока.Сумма = 1000;
    
        // 3. Получаем форму Документа2, передавая созданный объект
        // ПараметрыФормы - это структура, которая может содержать "Ключ" (для существующего объекта)
        // или "ЗначенияЗаполнения" (для нового объекта).
        // В нашем случае, мы передаем объект, который хотим, чтобы форма отобразила.
        ПараметрыФормы = Новый Структура("Ключ", НовыйОбъектДокумент2);
                
        // ЭтаФорма - владелец, если форма должна быть привязана к текущей.
        ФормаДокумента2 = ПолучитьФорму("Документ.Документ2.Форма.ФормаДокумента", ПараметрыФормы, ЭтаФорма);
    
        // 4. Теперь у нас есть объект формы Документа2.
        // Мы можем получить доступ к его реквизитам и методам.
        // Важно: методы формы, которые мы хотим вызвать, должны быть экспортными и &НаКлиенте,
        // или они должны вызывать серверные методы формы/объекта для работы с данными.
    
        Сообщить("Получена форма документа Документ2: " + ФормаДокумента2.Заголовок);
    
        // Пример вызова экспортной клиентской процедуры формы,
        // аналогичной той, что упомянута в Сообщении 10:
        // Предположим, в модуле формы Документа2 есть процедура:
        // &НаКлиенте
        // Процедура ПересчитатьСотрудника(ИмяТаблицы, ИдентификаторыСтрок, ВедущееПоле, ТипВедущегоПоля) Экспорт
        //    // Логика расчета, которая может вызывать серверные функции для работы с данными
        // КонецПроцедуры
    
        // Мы имитируем вызов, как в Сообщении 10:
        ИмяТаблицы = "Начисления";
        ВедущееПоле = "Сотрудник";
        ТипВедущегоПоля = Тип("СправочникСсылка.Сотрудники");
    
        // ИдентификаторыСтрок в примере из сообщения 10 берутся из элементов формы.
        // Это очень тесная связь с интерфейсом. Лучше, чтобы расчетная процедура
        // принимала более абстрактные данные (например, массив ссылок на сотрудников).
        // В данном случае мы можем создать пустую коллекцию или передать массив строк
        // если процедура ПересчитатьСотрудника может их обработать.
        // Для примера из сообщения 10 это означало бы, что процедура работает с выделенными строками
        // элемента табличной части. Это сложный момент для эмуляции без открытия формы.
        // Допустим, мы просто передаем информацию, которая будет использоваться в процедуре.
        ИдентификаторыСтрок = Новый Массив(); // Например, пустой массив или список номеров строк
    
        // Важно: Процедура "ПересчитатьСотрудника" должна быть экспортной и &НаКлиенте в модуле формы Документа2.
        // Если она модифицирует данные объекта, она должна делать это через вызов серверных функций.
        // ФормаДокумента2.ПересчитатьСотрудника(ИмяТаблицы, ИдентификаторыСтрок, ВедущееПоле, ТипВедущегоПоля);
        // Закомментировано, так как реальная процедура ПересчитатьСотрудника может быть очень специфичной.
    
        // 5. После всех манипуляций с объектом формы (ФормаДокумента2.Объект)
        // необходимо сохранить изменения.
        // Для этого можно вызвать метод Записать() у объекта формы:
        ФормаДокумента2.Записать(); // Эквивалентно нажатию кнопки "Записать" на форме
        Сообщить("Документ Документ2 был программно обработан и записан через форму.");
    КонецПроцедуры
    

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

Проблема сохранения данных при программной работе с формой

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


// Пример из сообщения 10 с проблемой сохранения:
// ФормаДокумента = ПолучитьФорму("Документ.НачислениеЗарплаты.Форма.ФормаДокумента", Новый Структура("Ключ", ДокументФормы), ЭтаФорма);
// ИмяТаблицы = "Начисления";
// ВедущееПоле = "Сотрудник";
// ТипВедущегоПоля = Тип("СправочникСсылка.Сотрудники");
// Для Каждого Стр Из ФормаДокумента.Объект.Начисления Цикл
//     ФормаДокумента.Элементы.Начисления.ВыделенныеСтроки.Добавить(Стр.НомерСтроки - 1);
// КонецЦикла;
// ИдентификаторыСтрок = ФормаДокумента.Элементы[ИмяТаблицы].ВыделенныеСтроки;
// ФормаДокумента.ПересчитатьСотрудника(ИмяТаблицы, ИдентификаторыСтрок, ВедущееПоле, ТипВедущегоПоля);
// ФормаДокумента.Записать; // Вот здесь возникают проблемы, данные не сохраняются.

Причин, по которым ФормаДокумента.Записать() может не сохранять данные, может быть несколько:

  1. Форма не помечена как модифицированная. Метод Записать() формы обычно проверяет свойство формы Модифицированность. Если мы программно изменили данные в ФормаДокумента.Объект (например, добавили строки в табличную часть), но не установили Модифицированность = Истина, форма может "не понять", что нужно что-то сохранять. Хотя обычно при прямом изменении реквизитов объекта формы, свойство Модифицированность устанавливается автоматически, в сложных случаях или при косвенных изменениях это может не произойти.
  2. Отсутствие прямого вызова метода объекта. Сама форма лишь предоставляет интерфейс и управляет представлением данных. Основное хранение и запись данных происходит через объект конфигурации. Если мы манипулировали данными внутри ФормаДокумента.Объект, то после этого необходимо явно вызвать метод объекта для записи: ФормаДокумента.Объект.Записать(РежимЗаписиДокумента.Запись);. Метод ФормаДокумента.Записать() сам по себе вызывает этот метод, но иногда прямой вызов может быть более надежным.
  3. Некорректная передача или инициализация. Убедитесь, что объект, переданный в ПолучитьФорму(), корректно инициализирован. Если ДокументФормы в примере является новым несохраненным объектом, то Записать() формы или объекта должна его сохранить. Если же это уже существующий объект, то Записать() обновит его.
  4. Ошибки в процедуре "ПересчитатьСотрудника". Возможно, сама процедура ПересчитатьСотрудника не вносит изменения в основной объект документа, а только в некие временные структуры, или ее изменения не возвращаются в основной реквизит формы Объект. Важно, чтобы все модификации данных, которые должны быть сохранены в базе, применялись к ФормаДокумента.Объект.
  5. Неправильное взаимодействие клиент-сервер внутри процедуры формы. Если процедура ПересчитатьСотрудника является клиентской и изменяет данные, эти изменения должны быть переданы на сервер для их сохранения. Это делается путем вызова серверных функций или процедур из клиентской процедуры.
    
    // Пример: Корректное взаимодействие клиент-сервер при изменении данных объекта формы
    // В модуле формы Документа2
    &НаКлиенте
    Процедура ПересчитатьСотрудника(ИмяТаблицы, ИдентификаторыСтрок, ВедущееПоле, ТипВедущегоПоля) Экспорт
        // 1. Получаем объект из реквизита формы.
        // Это важно, так как объект формы - это не просто ссылка, а временная копия данных.
        ОбъектДокумента = РеквизитФормыВЗначение("Объект");
                
        // 2. Вызываем серверную процедуру/функцию, которая изменяет этот объект.
        // Передаем объект как параметр.
        ОбъектДокумента = ПересчитатьСотрудникаНаСервере(ОбъектДокумента, ИмяТаблицы, ИдентификаторыСтрок, ВедущееПоле, ТипВедущегоПоля);
                
        // 3. Возвращаем измененный объект обратно в реквизит формы.
        ЗначениеВРеквизитФормы(ОбъектДокумента, "Объект");
                
        // 4. Обязательно устанавливаем модифицированность формы, чтобы Записать() сработало.
        Модифицированность = Истина;
                
        // 5. Обновляем отображение на форме, если это необходимо.
        ОбновитьОтображениеДанных();
    КонецПроцедуры
    
    // В этом же модуле формы или в общем модуле (&НаСервере)
    &НаСервере
    Функция ПересчитатьСотрудникаНаСервере(ОбъектДокумента, ИмяТаблицы, ИдентификаторыСтрок, ВедущееПоле, ТипВедущегоПоля)
        // Здесь выполняем все расчеты и изменения объектаДокумента.
        // Например:
        // Для Каждого СтрокаТЧ Из ОбъектДокумента[ИмяТаблицы] Цикл
        //     // ... модифицируем строки табличной части ...
        // КонецЦикла;
        // ОбъектДокумента.Сумма = ...;
                
        Сообщить("Расчет ПересчитатьСотрудникаНаСервере для " + ОбъектДокумента.ПредставлениеДокумента);
        ОбъектДокумента.Комментарий = ОбъектДокумента.Комментарий + " (Обработано в ПересчитатьСотрудникаНаСервере)";
    
        // После всех изменений возвращаем измененный объект.
        Возврат ОбъектДокумента;
    КонецФункции
    

    Без этого механизма "передачи" объекта на сервер, его изменения и возврата обратно, клиентские манипуляции с ФормаДокумента.Объект останутся только на клиенте и не будут сохранены в базе данных.

Правильное сохранение изменений, сделанных через ПолучитьФорму()

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

  1. Изменяем объект формы. Все изменения данных, которые должны быть сохранены, должны быть применены к свойству Объект полученной формы (например, ФормаДокумента2.Объект.Реквизит = Значение; или модификация строк табличных частей через ФормаДокумента2.Объект.ТабличнаяЧасть.Добавить();).
  2. Помечаем форму как модифицированную. Если изменения вносятся не через интерактивное взаимодействие, а напрямую в свойство Объект, то может потребоваться явно установить ФормаДокумента2.Модифицированность = Истина;. Это гарантирует, что система воспримет форму как измененную и попытается сохранить ее.
  3. Вызываем метод Записать(). После всех изменений, вызываем ФормаДокумента2.Записать();. Этот метод формы вызовет серверную процедуру записи, которая сохранит данные объекта. Если документ новый, он будет создан в базе; если существующий — его данные будут обновлены. Также можно напрямую вызвать метод объекта: ФормаДокумента2.Объект.Записать(РежимЗаписиДокумента.Запись);.
  4. Проверяем обработчики модуля объекта. Убедитесь, что в модуле объекта документа нет логики (например, в обработчиках ПередЗаписью или ПриЗаписи), которая блокирует запись, отменяет изменения или возвращает Отказ = Истина. Такая логика может препятствовать сохранению данных даже при корректном программном вызове.

Заключение и лучшие практики

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

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

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

← На главную