Как вызвать клиентскую процедуру из Web-сервиса или Внешнего соединения в 1С:Предприятие?

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

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

Процедура или функция с указанным именем не определена (Проверка: Сервер)

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

Анализ исходной проблемы: почему возникает ошибка компиляции?

Рассмотрим код, который попытался написать автор темы:


Процедура ОтправитьСообщениеИзWS(ПараметрыСообщения,Состояние) Экспорт
    ОтправитьСообщениеИзWSНаКлиент(ПараметрыСообщения,Состояние);
КонецПроцедуры

&НаКлиенте
Процедура ОтправитьСообщениеИзWSНаКлиент(ПараметрыСообщения,Состояние) Экспорт
    ОбщийМодульКлиент.ОтправитьСообщение(ПараметрыСообщения,Состояние)
КонецПроцедуры

Данный код расположен в общем модуле со свойствами «Клиент (управляемое приложение)», «Сервер» и «Внешнее соединение». Здесь допущены сразу три серьезные ошибки:

  1. Использование директив компиляции в общих модулях: Использование директивы &НаКлиенте (или &НаСервере) в общем модуле является грубейшей синтаксической ошибкой. Согласно стандартам 1С, директивы компиляции применяются исключительно в модулях форм, команд или в модуле управляемого приложения. Контекст процедур общего модуля определяется исключительно свойствами самого общего модуля (галочками в конфигураторе).
  2. Смешение контекстов при компиляции: Поскольку общий модуль имеет флаг «Сервер», платформа пытается скомпилировать весь его код для сервера. На сервере общего модуля ОбщийМодульКлиент не существует (так как он чисто клиентский), равно как и процедуры, предназначенной только для клиента. Из-за этого серверный компилятор сообщает об ошибке: процедура не определена.
  3. Неверная попытка обхода через препроцессор: Попытка использовать условие препроцессора вида #Если Не Сервер Или Не ВнешнееСоединение Тогда содержит логическую ошибку. На сервере условие «Не ВнешнееСоединение» вернет истину, и компилятор сервера все равно попытается скомпилировать клиентский вызов, что приведет к ошибке. Правильное условие должно использовать логическое «И» (#Если Не Сервер И Не ВнешнееСоединение Тогда) или просто #Если Клиент Тогда.

Если применить правильную директиву препроцессора, код компилируется без ошибок:


Процедура ОтправитьСообщениеИзWS(ПараметрыСообщения,Состояние) Экспорт
    #Если Клиент Тогда
        ОтправитьСообщениеИзWSНаКлиент(ПараметрыСообщения,Состояние);
    #КонецЕсли
КонецПроцедуры

Процедура ОтправитьСообщениеИзWSНаКлиент(ПараметрыСообщения,Состояние) Экспорт
    // Данный вызов безопасен, так как компилируется только на клиенте
    #Если Клиент Тогда
        ОбщийМодульКлиент.ОтправитьСообщение(ПараметрыСообщения,Состояние);
    #КонецЕсли
КонецПроцедуры

Однако при вызове этого метода из Web-сервиса или COM-соединения разработчик сталкивается со следующим поведением: отладчик просто проскакивает этот блок кода, не заходя внутрь клиентской процедуры. Выясним, почему так происходит.

Главное архитектурное ограничение фоновых сеансов

Давайте разберем физический смысл выполнения кода при обращении к Web-сервису или через COM-соединение:

Таким образом, вызвать клиентский код напрямую из Web-сервиса невозможно «от слова совсем». Но как быть, если бизнес-логика требует передать информацию из Web-сервиса конкретному пользователю, работающему в данный момент в программе?

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

Вариант А. Механизм «Уведомления клиента» (для платформы 8.3.26 и выше)

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

Проанализируем шаги реализации:

  1. На стороне клиента (например, в модуле управляемого приложения при старте системы) мы регистрируем обработчик получения уведомлений:

Процедура ПриНачалеРаботыСистемы()
    УведомленияКлиента.ПодключитьОбработчик("WS_Уведомления", "ОбработкаСобытийWS");
КонецПроцедуры

Процедура ОбработкаСобытийWS(ИмяСобытия, ДанныеУведомления) Экспорт
    // Этот код сработает на клиенте при получении сигнала с сервера
    ПоказатьОповещениеПользователя("Получено сообщение из WS",, ДанныеУведомления.Текст);
КонецПроцедуры
  1. На стороне сервера (непосредственно внутри метода вашего Web-сервиса) мы отправляем уведомление конкретному пользователю или во все активные сеансы:

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

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

Вариант Б. Использование Системы взаимодействия (Collaboration System)

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

Разберем по шагам логику работы:

  1. При старте клиентского приложения мы подписываемся на новые сообщения в специальном служебном обсуждении.
  2. На сервере в Web-сервисе мы программно отправляем сообщение в это обсуждение.

Пример клиентского кода для подключения обработчика:


Процедура ПриНачалеРаботыСистемы()
    ПодключитьОбработчикСлужебныхСообщений();
КонецПроцедуры

Процедура ПодключитьОбработчикСлужебныхСообщений() Экспорт
    // Поиск служебного обсуждения (например, по уникальному ключу)
    КлючОбсуждения = "СлужебныеУведомленияWS";
    Обсуждение = СистемаВзаимодействия.ПолучитьОбсуждениеПоКлючу(КлючОбсуждения);
    
    Если Обсуждение <> Неопределено Тогда
        СистемаВзаимодействия.НачатьПодключениеОбработчикаНовыхСообщений(Обсуждение, Новый ОписаниеОповещения("ПриПолученииСообщенияWS", ЭтотОбъект));
    КонецЕсли;
КонецПроцедуры

Процедура ПриПолученииСообщенияWS(Сообщение, ДополнительныеПараметры) Экспорт
    // Сюда управление придет мгновенно при записи сообщения на сервере
    ПоказатьОповещениеПользователя("Новое событие!",, Сообщение.Текст);
КонецПроцедуры

Пример серверного кода внутри метода Web-сервиса:


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

Вариант В. Классический подход через очередь сообщений (Регистр сведений + Обработчик ожидания)

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

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

Разберем реализацию этого алгоритма:

  1. Создание регистра сведений: Создадим непериодический независимый регистр сведений ОчередьСообщенийКлиенту.
    • Измерения: Пользователь (СправочникСсылка.Пользователи), ИдентификаторЗаписи (УникальныйИдентификатор).
    • Ресурсы: ТекстСообщения (Строка), ДатаСоздания (Дата).
  2. Запись сообщения на сервере: В коде Web-сервиса мы просто делаем запись в этот регистр сведений, указывая целевого пользователя:

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

Процедура ПриНачалеРаботыСистемы()
    // Подключаем проверку раз в 15 секунд
    ПодключитьОбработчикОжидания("ПроверитьОчередьСлужебныхСообщений", 15);
КонецПроцедуры

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

Функция ПолучитьИОчиститьНовыеСообщения() Экспорт
    МассивСообщений = Новый Массив;
    ТекущийПользователь = Пользователи.ТекущийПользователь(); // Получаем текущего сеансового пользователя
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |	ОчередьСообщенийКлиенту.ИдентификаторЗаписи КАК ИдентификаторЗаписи,
        |	ОчередьСообщенийКлиенту.ТекстСообщения КАК ТекстСообщения
        |ИЗ
        |	РегистрСведений.ОчередьСообщенийКлиенту КАК ОчередьСообщенийКлиенту
        |ГДЕ
        |	ОчередьСообщенийКлиенту.Пользователь = &ТекущийПользователь";
    
    Запрос.УстановитьПараметр("ТекущийПользователь", ТекущийПользователь);
    РезультатЗапроса = Запрос.Выполнить();
    
    Если НЕ РезультатЗапроса.Пустой() Тогда
        Выборка = РезультатЗапроса.Выбрать();
        Пока Выборка.Следующий() Цикл
            МассивСообщений.Добавить(Выборка.ТекстСообщения);
            
            // Сразу удаляем прочитанную запись, чтобы не обрабатывать повторно
            Набор = РегистрыСведений.ОчередьСообщенийКлиенту.СоздатьНаборЗаписей();
            Набор.Отбор.Пользователь.Установить(ТекущийПользователь);
            Набор.Отбор.ИдентификаторЗаписи.Установить(Выборка.ИдентификаторЗаписи);
            Набор.Записать(Истина); // Запись пустого набора удалит строку из БД
        КонецЦикла;
    КонецЕсли;
    
    Возврат МассивСообщений;
КонецФункции

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

Резюме

Мы проанализировали проблему вызова клиентских процедур из серверного контекста Web-сервиса и пришли к следующим выводам:

← На главную