При разработке и поддержке решений на обычных формах в платформе 1С:Предприятие 8.3 разработчики часто сталкиваются с неожиданным изменением поведения стандартных механизмов. Одним из таких неприятных сюрпризов является баг, при котором метод ВводДоступен() начинает возвращать значение Истина одновременно для родительской и для открытой поверх нее модальной (или блокирующей) дочерней формы. В результате этого внешнее событие (например, сканирование штрихкода) обрабатывается дважды — и в активном модальном окне, и в фоновом родительском, ломая всю бизнес-логику приложения.
В этой статье мы подробно разберем, почему возникает эта ошибка, как правильно её диагностировать (почему обычный отладчик здесь бессилен) и рассмотрим несколько надежных способов обхода проблемы, которые позволят вернуть стабильность вашей системе без масштабного перевода конфигурации на управляемые формы.
В платформе 1С при считывании данных торговым оборудованием (сканером штрихкода или терминалом сбора данных) драйвер генерирует внешнее событие. Платформа рассылает это событие во все открытые формы, в модулях которых присутствует предопределенная процедура ВнешнееСобытие().
Классический подход разработчиков 1С для предотвращения обработки события в фоновом (неактивном) окне всегда выглядел следующим образом:
Процедура ВнешнееСобытие(Источник, Событие, Данные)
Если Не ВводДоступен() Тогда
Возврат;
КонецЕсли;
// Код обработки штрихкода
КонецПроцедуры
Ожидается, что при открытии модального дочернего окна родительская форма теряет фокус ввода, и метод ВводДоступен() в её контексте возвращает Ложь. Однако в современных релизах платформы 1С 8.3 (начиная с веток 8.3.13 / 8.3.14 и новее) из-за глобальной переработки фокуса, асинхронности и постепенного отказа от поддержки обычных форм этот механизм перестал работать корректно. Платформа ошибочно считает ввод доступным в обеих формах, из-за чего один и тот же штрихкод считывается и обрабатывается параллельно в двух местах.
Проанализируем важный нюанс: почему эту проблему невозможно исследовать с помощью стандартной точки останова в Конфигураторе? Дело в том, что в момент срабатывания точки останова фокус операционной системы переключается на окно Конфигуратора. Так как клиентское приложение 1С теряет фокус ввода, вызов метода ВводДоступен() в табло отладчика всегда будет возвращать Ложь.
Для корректного анализа поведения системы необходимо использовать запись логов. Мы можем воспользоваться записью в Журнал регистрации, выводом информации на экран с помощью метода Сообщить() или записью в текстовый файл. Рассмотрим пример логирования:
Процедура ВнешнееСобытие(Источник, Событие, Данные)
ЗаписьЖурналаРегистрации("СканерШтрихкода",
УровеньЖурналаРегистрации.Информация, , ,
"Форма: " + ЭтотОбъект.ИмяФормы + ", Ввод доступен: " + ВводДоступен());
Если Не ВводДоступен() Тогда
Возврат;
КонецЕсли;
КонецПроцедуры
По логам станет отчетливо видно, что в момент сканирования обе формы возвращают Истина, пропуская выполнение алгоритма дальше.
---
Самый простой и наименее инвазивный метод — внедрить локальный флаг на уровне родительской формы, который будет принудительно блокировать обработку внешних событий перед вызовом модального окна и восстанавливать её после возврата управления.
Рассмотрим, как реализовать этот подход по шагам:
Шаг 1. Объявим переменную в самом начале модуля родительской формы:
Перем СканированиеРазрешено;
Шаг 2. Инициализируем эту переменную при открытии формы:
Процедура ПриОткрытии()
СканированиеРазрешено = Истина;
КонецПроцедуры
Шаг 3. В обработчике внешнего события родительской формы добавим проверку нашего флага:
Процедура ВнешнееСобытие(Источник, Событие, Данные)
Если Не СканированиеРазрешено Тогда
Возврат;
КонецЕсли;
Если Не ВводДоступен() Тогда
Возврат;
КонецЕсли;
// Логика обработки штрихкода
КонецПроцедуры
Шаг 4. Перед открытием любого дочернего модального окна сбрасываем флаг, а после закрытия возвращаем его в исходное состояние:
Процедура ОткрытьМодальнуюФорму()
СканированиеРазрешено = Ложь;
// Открытие модального окна
ОткрытьФормуМодально("Обработка.ВводДанных.Форма.ФормаВвода");
СканированиеРазрешено = Истина;
КонецПроцедуры
Этот способ надежно страхует родительское окно от случайного срабатывания, так как во время активности дочерней формы переменная СканированиеРазрешено гарантированно равна Ложь.
---
Если в конфигурации используется стандартный механизм БСП (Библиотеки Стандартных Подсистем) для работы с торговым оборудованием, мы можем временно отключать родительскую форму от прослушивания событий сканера.
Разберем пример реализации этого метода:
Процедура ОткрытьМодальноеОкноОборудования()
// Отключаем обработку событий сканера для текущей формы
Попытка
ПолучитьСерверТО().ОтключитьКлиента(ЭтаФорма);
Исключение
// Обработка потенциальных ошибок работы с сервером ТО
КонецПопытки;
// Открываем дочернюю форму
ОткрытьФормуМодально("Справочник.Номенклатура.ФормаВыбора");
// После закрытия модального окна подключаем сканер обратно
Попытка
ПолучитьСерверТО().ПодключитьКлиента(ЭтаФорма);
Исключение
// Обработка ошибок подключения
КонецПопытки;
КонецПроцедуры
При таком подходе родительская форма физически перестает быть получателем внешних событий на время отсутствия фокуса, что полностью исключает дублирование.
---
Если в системе открывается множество различных окон и прописывать локальные флаги в каждом месте вызова трудоемко, можно организовать централизованный контроль в глобальном модуле или через параметры сеанса.
Для этого в глобальном модуле (или в модуле обычного приложения) создается глобальная переменная глТекущееСобытиеОбработано. Алгоритм работы строится на следующих правилах:
ВнешнееСобытие(), проверяет состояние глобального флага.Ложь, форма устанавливает его в значение Истина и выполняет обработку штрихкода.Истина, и просто завершает работу процедуры.Ложь выполняется по обработчику ожидания с минимальной задержкой (например, 0.1 секунды), чтобы гарантировать, что все холостые вызовы в текущем такте работы платформы уже завершились.Рассмотрим код обработчика ожидания для сброса флага:
// В модуле формы
Процедура ВнешнееСобытие(Источник, Событие, Данные)
Если глТекущееСобытиеОбработано Тогда
Возврат;
КонецЕсли;
// Фиксируем, что событие взято в работу
глТекущееСобытиеОбработано = Истина;
ПодключитьОбработчикОжидания("СброситьФлагОбработкиСобытия", 0.1, Истина);
// Выполнение полезного действия
КонецПроцедуры
Процедура СброситьФлагОбработкиСобытия() Экспорт
глТекущееСобытиеОбработано = Ложь;
КонецПроцедуры
---
Этот метод основан на разрыве синхронности выполнения кода. Проблема одновременного срабатывания часто кроется в том, что платформа рассылает сообщения «веерно» в один микромомент времени, когда состояние интерфейса еще физически перерисовывается ОС.
Если мы перенесем непосредственную обработку штрихкода в процедуру, вызываемую через ПодключитьОбработчикОжидания() с интервалом в 0.1 или 0.2 секунды, операционная система и платформа успеют корректно завершить переключение контекста фокуса. К моменту вызова нашей процедуры метод ВводДоступен() отработает абсолютно штатно и вернет правильное значение.
Посмотрим на пример реализации отложенного вызова:
Перем БуферДанныхСканера;
Процедура ВнешнееСобытие(Источник, Событие, Данные)
// Сохраняем пришедшие данные во временный буфер формы
БуферДанныхСканера = Новый Структура("Источник, Событие, Данные", Источник, Событие, Данные);
// Подключаем однократный обработчик с задержкой 0.1 сек
ПодключитьОбработчикОжидания("ОтложеннаяОбработкаСобытия", 0.1, Истина);
КонецПроцедуры
Процедура ОтложеннаяОбработкаСобытия()
// В этот момент фокус окон уже гарантированно определен платформой
Если Не ВводДоступен() Тогда
Возврат;
КонецЕсли;
Если БуферДанныхСканера <> Неопределено Тогда
// Обрабатываем данные из буфера
ОбработатьШтрихкод(БуферДанныхСканера.Данные);
// Очищаем буфер
БуферДанныхСканера = Неопределено;
КонецЕсли;
КонецПроцедуры
Используя этот подход, мы избавляемся от необходимости модифицировать алгоритмы открытия дочерних окон и можем точечно исправить код только в тех формах, где наблюдаются сбои с дублированием данных.