Разработка клиент-серверных приложений на платформе 1С:Предприятие часто преподносит сюрпризы, связанные с пониманием контекста выполнения кода. Типичная ситуация: программист пытается вызвать клиентскую процедуру (например, вывод оповещения пользователю или открытие формы) из кода, который инициирован внешним запросом через Web-сервис (WS) или Внешнее соединение (COM). При этом система выдает ошибку компиляции:
Процедура или функция с указанным именем не определена (Проверка: Сервер)
В этой статье мы подробно разберем, почему возникает эта ошибка, какие стандарты разработки 1С были нарушены, почему физически невозможно выполнить клиентский код напрямую из веб-сервиса и как правильно решить эту архитектурную задачу современными методами платформы 1С.
Рассмотрим код, который попытался написать автор темы:
Процедура ОтправитьСообщениеИзWS(ПараметрыСообщения,Состояние) Экспорт
ОтправитьСообщениеИзWSНаКлиент(ПараметрыСообщения,Состояние);
КонецПроцедуры
&НаКлиенте
Процедура ОтправитьСообщениеИзWSНаКлиент(ПараметрыСообщения,Состояние) Экспорт
ОбщийМодульКлиент.ОтправитьСообщение(ПараметрыСообщения,Состояние)
КонецПроцедуры
Данный код расположен в общем модуле со свойствами «Клиент (управляемое приложение)», «Сервер» и «Внешнее соединение». Здесь допущены сразу три серьезные ошибки:
&НаКлиенте (или &НаСервере) в общем модуле является грубейшей синтаксической ошибкой. Согласно стандартам 1С, директивы компиляции применяются исключительно в модулях форм, команд или в модуле управляемого приложения. Контекст процедур общего модуля определяется исключительно свойствами самого общего модуля (галочками в конфигураторе).ОбщийМодульКлиент не существует (так как он чисто клиентский), равно как и процедуры, предназначенной только для клиента. Из-за этого серверный компилятор сообщает об ошибке: процедура не определена.#Если Не Сервер Или Не ВнешнееСоединение Тогда содержит логическую ошибку. На сервере условие «Не ВнешнееСоединение» вернет истину, и компилятор сервера все равно попытается скомпилировать клиентский вызов, что приведет к ошибке. Правильное условие должно использовать логическое «И» (#Если Не Сервер И Не ВнешнееСоединение Тогда) или просто #Если Клиент Тогда.Если применить правильную директиву препроцессора, код компилируется без ошибок:
Процедура ОтправитьСообщениеИзWS(ПараметрыСообщения,Состояние) Экспорт
#Если Клиент Тогда
ОтправитьСообщениеИзWSНаКлиент(ПараметрыСообщения,Состояние);
#КонецЕсли
КонецПроцедуры
Процедура ОтправитьСообщениеИзWSНаКлиент(ПараметрыСообщения,Состояние) Экспорт
// Данный вызов безопасен, так как компилируется только на клиенте
#Если Клиент Тогда
ОбщийМодульКлиент.ОтправитьСообщение(ПараметрыСообщения,Состояние);
#КонецЕсли
КонецПроцедуры
Однако при вызове этого метода из Web-сервиса или COM-соединения разработчик сталкивается со следующим поведением: отладчик просто проскакивает этот блок кода, не заходя внутрь клиентской процедуры. Выясним, почему так происходит.
Давайте разберем физический смысл выполнения кода при обращении к Web-сервису или через COM-соединение:
#Если Клиент в таком сеансе всегда будет игнорироваться, потому что контекстом исполнения является исключительно Сервер или Внешнее соединение.Таким образом, вызвать клиентский код напрямую из Web-сервиса невозможно «от слова совсем». Но как быть, если бизнес-логика требует передать информацию из Web-сервиса конкретному пользователю, работающему в данный момент в программе?
Рассмотрим три профессиональных архитектурных шаблона для решения этой задачи.
Если вы работаете на современной версии платформы (начиная с 8.3.26), задача решается максимально элегантно с помощью встроенного механизма асинхронной отправки уведомлений с сервера на клиент.
Проанализируем шаги реализации:
Процедура ПриНачалеРаботыСистемы()
УведомленияКлиента.ПодключитьОбработчик("WS_Уведомления", "ОбработкаСобытийWS");
КонецПроцедуры
Процедура ОбработкаСобытийWS(ИмяСобытия, ДанныеУведомления) Экспорт
// Этот код сработает на клиенте при получении сигнала с сервера
ПоказатьОповещениеПользователя("Получено сообщение из WS",, ДанныеУведомления.Текст);
КонецПроцедуры
Процедура ПолучитьДанныеИзWS(ПараметрыСообщения) Экспорт
// Серверная логика обработки данных веб-сервиса...
ДанныеУведомления = Новый Структура;
ДанныеУведомления.Вставить("Текст", ПараметрыСообщения.Текст);
// Отправляем уведомление конкретному пользователю по его ИмяПользователя
Адресат = ПользователиИнформационнойБазы.НайтиПоИмени("ИванИванов");
Если Адресат <> Неопределено Тогда
МенеджерУведомленийКлиента.ОтправитьУведомление(Адресат, "WS_Уведомления", ДанныеУведомления);
КонецЕсли;
КонецПроцедуры
Платформа самостоятельно доставит этот пакет данных на рабочее место пользователя «ИванИванов» по внутренним сокетам связи, и у него мгновенно сработает клиентская процедура.
Если версия вашей платформы ниже 8.3.26, но в вашей системе развернута Система взаимодействия (встроенный чат и механизм обмена сообщениями 1С), это идеальный инструмент для передачи мгновенных сигналов с сервера на клиент.
Разберем по шагам логику работы:
Пример клиентского кода для подключения обработчика:
Процедура ПриНачалеРаботыСистемы()
ПодключитьОбработчикСлужебныхСообщений();
КонецПроцедуры
Процедура ПодключитьОбработчикСлужебныхСообщений() Экспорт
// Поиск служебного обсуждения (например, по уникальному ключу)
КлючОбсуждения = "СлужебныеУведомленияWS";
Обсуждение = СистемаВзаимодействия.ПолучитьОбсуждениеПоКлючу(КлючОбсуждения);
Если Обсуждение <> Неопределено Тогда
СистемаВзаимодействия.НачатьПодключениеОбработчикаНовыхСообщений(Обсуждение, Новый ОписаниеОповещения("ПриПолученииСообщенияWS", ЭтотОбъект));
КонецЕсли;
КонецПроцедуры
Процедура ПриПолученииСообщенияWS(Сообщение, ДополнительныеПараметры) Экспорт
// Сюда управление придет мгновенно при записи сообщения на сервере
ПоказатьОповещениеПользователя("Новое событие!",, Сообщение.Текст);
КонецПроцедуры
Пример серверного кода внутри метода Web-сервиса:
Процедура ЗарегистрироватьСобытиеВWS(ТекстСобытия) Экспорт
КлючОбсуждения = "СлужебныеУведомленияWS";
Обсуждение = СистемаВзаимодействия.ПолучитьОбсуждениеПоКлючу(КлючОбсуждения);
Если Обсуждение <> Неопределено Тогда
НовоеСообщение = СистемаВзаимодействия.СоздатьСообщение(Обсуждение.Идентификатор);
НовоеСообщение.Текст = ТекстСобытия;
НовоеСообщение.Записать(); // Сообщение мгновенно "улетает" на клиенты
КонецЕсли;
КонецПроцедуры
Если первые два варианта не подходят по технологическим причинам, воспользуемся классической и самой надежной схемой, которая гарантированно работает на абсолютно любых версиях платформы 1С:Предприятие 8.
Суть метода заключается в создании промежуточного буфера хранения сообщений (очереди) в базе данных и периодическом опросе этой очереди со стороны клиента.
Разберем реализацию этого алгоритма:
ОчередьСообщенийКлиенту.
Пользователь (СправочникСсылка.Пользователи), ИдентификаторЗаписи (УникальныйИдентификатор).ТекстСообщения (Строка), ДатаСоздания (Дата).
Процедура ЗаписатьСообщениеВОчередь(ИмяПолучателя, ТекстСообщения) Экспорт
ПользовательСсылка = Справочники.Пользователи.НайтиПоРеквизиту("ИмяВхода", ИмяПолучателя);
Если ПользовательСсылка.Пустая() Тогда
Возврат;
КонецЕсли;
НаборЗаписей = РегистрыСведений.ОчередьСообщенийКлиенту.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Пользователь.Установить(ПользовательСсылка);
НоваяЗапись = НаборЗаписей.Добавить();
НоваяЗапись.Пользователь = ПользовательСсылка;
НоваяЗапись.ИдентификаторЗаписи = Новый УникальныйИдентификатор;
НоваяЗапись.ТекстСообщения = ТекстСообщения;
НоваяЗапись.ДатаСоздания = ТекущаяДатаСеанса();
НаборЗаписей.Записать(Ложь); // Добавляем запись без затирания старых
КонецПроцедуры
Процедура ПриНачалеРаботыСистемы()
// Подключаем проверку раз в 15 секунд
ПодключитьОбработчикОжидания("ПроверитьОчередьСлужебныхСообщений", 15);
КонецПроцедуры
Процедура ПроверитьОчередьСлужебныхСообщений() Экспорт
// Вызываем неконтекстный серверный метод для быстрой проверки
МассивСообщений = ОбщийМодульСервер.ПолучитьИОчиститьНовыеСообщения();
Для Каждого ТекстСообщения Из МассивСообщений Цикл
ПоказатьОповещениеПользователя("Внимание!",, ТекстСообщения);
КонецЦикла;
КонецПроцедуры
Функция ПолучитьИОчиститьНовыеСообщения() Экспорт
МассивСообщений = Новый Массив;
ТекущийПользователь = Пользователи.ТекущийПользователь(); // Получаем текущего сеансового пользователя
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ОчередьСообщенийКлиенту.ИдентификаторЗаписи КАК ИдентификаторЗаписи,
| ОчередьСообщенийКлиенту.ТекстСообщения КАК ТекстСообщения
|ИЗ
| РегистрСведений.ОчередьСообщенийКлиенту КАК ОчередьСообщенийКлиенту
|ГДЕ
| ОчередьСообщенийКлиенту.Пользователь = &ТекущийПользователь";
Запрос.УстановитьПараметр("ТекущийПользователь", ТекущийПользователь);
РезультатЗапроса = Запрос.Выполнить();
Если НЕ РезультатЗапроса.Пустой() Тогда
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
МассивСообщений.Добавить(Выборка.ТекстСообщения);
// Сразу удаляем прочитанную запись, чтобы не обрабатывать повторно
Набор = РегистрыСведений.ОчередьСообщенийКлиенту.СоздатьНаборЗаписей();
Набор.Отбор.Пользователь.Установить(ТекущийПользователь);
Набор.Отбор.ИдентификаторЗаписи.Установить(Выборка.ИдентификаторЗаписи);
Набор.Записать(Истина); // Запись пустого набора удалит строку из БД
КонецЦикла;
КонецЕсли;
Возврат МассивСообщений;
КонецФункции
Важный совет: Не делайте интервал обработчика ожидания слишком маленьким (меньше 10–15 секунд), чтобы частыми запросами не создавать лишнюю нагрузку на сервер СУБД, особенно при большом количестве одновременно работающих пользователей.
Мы проанализировали проблему вызова клиентских процедур из серверного контекста Web-сервиса и пришли к следующим выводам:
#Если Клиент Тогда.