В процессе разработки на платформе 1С:Предприятие 8.3 (начиная с версий 8.3.12 и 8.3.13) программисты часто сталкиваются с крайне неприятным поведением системы: при выполнении вполне стандартного кода тонкий клиент полностью «застывает». При этом приложение перестает реагировать на любые клики мыши, клавиатурные сочетания, не закрывается по нажатию на крестик и лишь издает монотонный системный звук при попытке нажать на любое место экрана. Единственным способом закрыть программу остается принудительное завершение процесса через Диспетчер задач Windows.
В этой статье мы подробно разберем, почему возникает эта ошибка, проанализируем внутренние механизмы взаимодействия оконной системы платформы 1С с операционной системой и реализуем надежное архитектурное решение этой проблемы без использования сомнительных костылей.
Давайте рассмотрим классический пример, в котором воспроизводится зависание. Предположим, у нас есть основная форма, из которой мы открываем вспомогательное окно оплаты (например, форму авторизации банковского терминала или выбора фискального регистратора). Это вспомогательное окно открывается с режимом блокировки всего интерфейса или окна владельца. После успешного завершения операции вспомогательное окно закрывается, возвращает результат в процедуру оповещения, и система должна сразу же распечатать чек или квитанцию.
Ниже представлен исходный программный код, который гарантированно приводит к зависанию интерфейса на платформе 1С:
&НаКлиенте
Процедура Повесить(Команда)
// Открываем блокирующее окно формы оплаты
ОткрытьForm = ОткрытьФорму(
"ВнешняяОбработка.Вешаем8313.Форма.ФормаОплаты",
,
ЭтотОбъект,
,
,
,
Новый ОписаниеОповещения("ЗавершениеОплаты", ЭтотОбъект, Неопределено),
РежимОткрытияОкнаФормы.БлокироватьОкноВладельца
);
КонецПроцедуры
&НаКлиенте
Процедура ЗавершениеОплаты(РезультатОплаты, ДополнительныеПараметры) Экспорт
// Формируем табличный документ для печати
ТабДок = Новый ТабличныйДокумент;
ТабДок.Область("R1C1").Текст = "Печать чека ККМ... Успешно!";
ТабДок.ИмяПринтера = "Microsoft Print to PDF";
// Отправляем документ напрямую на принтер
Список = Новый СписокЗначений();
Список.Добавить(TabДок);
УправлениеПечатьюКлиент.РаспечататьТабличныеДокументы(Список, Новый СписокЗначений(), Ложь);
КонецПроцедуры
Код самой вспомогательной формы оплаты предельно прост:
&НаКлиенте
Процедура КомандаЗакрыть(Команда)
Закрыть();
КонецПроцедуры
При запуске этого кода форма оплаты закрывается, но в ту же секунду все окна 1С становятся недоступными. Давайте разберемся, почему это происходит.
Чтобы понять природу мертвой блокировки интерфейса, нам необходимо заглянуть под капот оконного менеджера платформы 1С:Предприятие 8.3. Начиная с версии 8.3.12, разработчики платформы существенно переработали механизмы отображения окон и блокировок для обеспечения корректной работы немодального интерфейса и улучшения стабильности веб-клиента.
Когда мы открываем форму с параметром РежимОткрытияОкнаФормы.БлокироватьОкноВладельца или РежимОткрытияОкнаФормы.БлокироватьВесьИнтерфейс, платформа накладывает на родительские окна специальный блокирующий слой (overlay), который перехватывает все события ввода пользователя.
Далее происходит следующая цепочка событий:
ОписаниеОповещения (в нашем случае — ЗавершениеОплаты).ЗавершениеОплаты вспомогательное окно уже визуально исчезло, но физическое снятие блокировки с основного интерфейса еще не завершено! Сообщение о снятии блокирующего слоя добавлено в очередь системных сообщений операционной системы, но поток выполнения 1С еще до него не добрался, так как он занят выполнением нашей процедуры оповещения.ЗавершениеОплаты мы вызываем метод ПакетОтображаемыхДокументов.Напечатать(РежимИспользованияДиалогаПечати.НеИспользовать) (который вызывается неявно внутри функций библиотеки стандартных подсистем).В пользу этой гипотезы говорят результаты практических экспериментов, проведенных разработчиками:
РежимОткрытияОкнаФормы.Независимый, зависания не происходит. В этом режиме изначально нет блокирующего слоя, поэтому очередь сообщений операционной системы не содержит отложенных команд на разблокировку окон.На просторах форумов часто предлагают использовать промежуточное предупреждение с таймаутом для решения этой проблемы. Выглядит этот «костыль» следующим образом:
&НаКлиенте
Процедура ЗавершениеОплатыПромежуток(РезультатОплаты, ДополнительныеПараметры) Экспорт
// Вывод предупреждения на 1 секунду для искусственной разблокировки
ПоказатьПредупреждение(
Новый ОписаниеОповещения("ЗавершениеОплаты", ЭтотОбъект, Неопределено),
"Выполняется печать...",
1,
"Печать"
);
КонецПроцедуры
Почему этот метод работает? Метод ПоказатьПредупреждение является асинхронным. Его вызов заставляет платформу принудительно запустить внутренний цикл обработки сообщений для показа нового диалогового окна. Во время этого микро-цикла платформа успевает обработать зависшее в очереди сообщение о разблокировке родительского интерфейса от предыдущей закрытой формы. Когда управление передается в процедуру ЗавершениеОплаты, основной интерфейс уже полностью разблокирован, и прямая печать проходит успешно.
Недостатки костыля с предупреждением:
Наиболее изящный, надежный и архитектурно правильный способ обойти взаимную блокировку оконного менеджера — это использование однократного обработчика ожидания с минимальной задержкой.
Идея метода заключается в том, чтобы позволить процедуре оповещения завершиться штатно и максимально быстро. Сразу после этого платформа вернет управление операционной системе, которая мгновенно обработает накопившуюся очередь сообщений и полностью снимет блокирующий слой с интерфейса. И только на следующем «тике» системных событий выполнится наш обработчик ожидания, который безопасно произведет отправку документа на печать.
Давайте реализуем эту схему на практике. Перепишем наш исходный код правильно:
&НаКлиенте
Процедура Повесить(Команда)
// Открываем форму с блокировкой интерфейса
ПараметрыОповещения = Новый ОписаниеОповещения("ЗавершениеОплаты", ЭтаФорма);
ОткрытьФорму(
"ВнешняяОбработка.Вешаем8313.Форма.ФормаОплаты",
,
ЭтотОбъект,
,
,
,
ПараметрыОповещения,
РежимОткрытияОкнаФормы.БлокироватьВесьИнтерфейс
);
КонецПроцедуры
&НаКлиенте
Процедура ЗавершениеОплаты(РезультатОплаты, ДополнительныеПараметры) Экспорт
// Вместо мгновенного запуска печати подключаем однократный обработчик ожидания.
// Интервал в 0.1 секунды визуально неощутим для пользователя,
// но этого времени платформе гарантированно хватит для разблокировки окон.
ПодключитьОбработчикОжидания("ВыполнитьПечатьПослеРазблокировкиОкон", 0.1, Истина);
КонецПроцедуры
&НаКлиенте
Процедура ВыполнитьПечатьПослеРазблокировкиОкон() Экспорт
// Данный метод выполняется в абсолютно чистом контексте,
// когда все блокирующие интерфейсы уже успешно сняты платформой.
ТабДок = Новый ТабличныйДокумент;
ТабДок.Область("R1C1").Текст = "Печать чека ККМ... Успешно!";
ТабДок.ИмяПринтера = "Microsoft Print to PDF";
Список = Новый СписокЗначений();
Список.Добавить(TabДок);
// Безопасно отправляем табличный документ на печать
УправлениеПечатьюКлиент.РаспечататьТабличныеДокументы(Список, Новый СписокЗначений(), Ложь);
КонецПроцедуры
Этот подход гарантирует стабильную работу программы на любых релизах платформы «1С:Предприятие 8.3», начиная с самых ранних и заканчивая современными версиями.
Описанный баг взаимной блокировки потоков в процедурах оповещения закрытия окон актуален не только для печати табличных документов. Нам, как разработчикам, необходимо придерживаться правила: любые тяжелые синхронные операции на клиенте внутри обработчиков закрытия блокирующих окон должны выполняться через отложенный вызов.
Рассмотрим список типовых операций, которые могут намертво повесить интерфейс при вызове из процедур оповещения:
НачатьПомещениеФайла, в старых конфигурациях все еще можно встретить синхронный код).Использование однократного подключения обработчика ожидания с помощью метода ПодключитьОбработчикОжидания с интервалом 0.1 секунды является универсальным и наиболее надежным паттерном проектирования интерфейса в управляемом приложении 1С, позволяющим навсегда забыть о зависании интерфейса клиентских приложений.