Как отследить активность пользователя на форме в 1С:Предприятии (Управляемые формы) и реализовать автозавершение работы при бездействии?

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

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

1. Комбинация обработчика ожидания и свойства "Модифицированность"

Один из самых часто предлагаемых и достаточно эффективных подходов — это использование свойства формы Модифицированность совместно с обработчиком ожидания. Давайте разберем его подробнее.

  1. Свойство Модифицированность: Это свойство формы принимает значение Истина, как только любые данные на форме были изменены пользователем, и Ложь, если форма не изменена или изменения были записаны. Важно понимать, что Модифицированность меняется только один раз при первом изменении. Если пользователь внес изменение, а затем перестал что-либо делать, свойство останется Истина, но не будет сигнализировать о дальнейшем бездействии.
  2. Обработчик ожидания (ОбработчикОжидания): Это механизм, который позволяет нам периодически выполнять определенный код на клиенте. Мы можем настроить его так, чтобы он срабатывал с заданным интервалом (например, раз в несколько секунд).

Чтобы отслеживать длительное бездействие с учетом уже измененных данных, мы можем использовать следующую логику:

  1. Инициализация таймера: При создании формы или начале работы, мы устанавливаем таймер бездействия.
  2. Запуск ОбработчикаОжидания: В обработчике ожидания мы периодически проверяем состояние.
  3. Проверка Модифицированность:
    • Если форма НЕ модифицирована (Модифицированность = Ложь) и пользователь не совершает действий, это означает, что он просто бездействует на пустой или неизмененной форме. В этом случае мы просто сбрасываем счетчик активности при каждом срабатывании обработчика ожидания или ничего не делаем, если нам нужно отслеживать бездействие только после начала ввода данных.
    • Если форма МОДИФИЦИРОВАНА (Модифицированность = Истина), это значит, что пользователь что-то изменил. В этом случае нам нужно проверить, продолжает ли он быть активным. Мы можем запустить внутренний таймер или счетчик.
  4. Обновление счетчика активности:

    Для более точного отслеживания активности, помимо Модифицированность, мы можем использовать дополнительную переменную, например, ПоследнееВремяАктивности. При каждом предполагаемом действии пользователя (вводе в поле, клике, изменении фокуса) мы обновляем это время. В обработчике ожидания мы сравниваем текущее время с ПоследнееВремяАктивности. Если разница превышает заданный лимит, то пользователя можно считать неактивным.

Рассмотрим пример реализации:


// В модуле формы

// Переменные формы
Перем мПоследнееВремяАктивности Экспорт; // Хранит время последнего действия пользователя
Перем мСчетчикБездействия Экспорт;      // Счетчик для проверки бездействия
Перем мИнтервалБездействия Экспорт;     // Максимально допустимое время бездействия в секундах
Перем мИнтервалОбработчикаОжидания Экспорт; // Интервал для ОбработчикаОжидания в сотых долях секунды

&НаКлиенте
Процедура ПриСозданииНаСервереПосле (Отказ, СтандартнаяОбработка)
    // Устанавливаем параметры
    мИнтервалБездействия = 300; // 5 минут (300 секунд)
    мИнтервалОбработчикаОжидания = 5 * 100; // 5 секунд

    мПоследнееВремяАктивности = ТекущаяДата();
    мСчетчикБездействия = 0;

    // Подключаем обработчик ожидания
    ПодключитьОбработчикОжидания("ПроверитьАктивностьПользователя", мИнтервалОбработчикаОжидания);
КонецПроцедуры

&НаКлиенте
Процедура ПередЗакрытием (Отказ, СтандартнаяОбработка)
    // Отключаем обработчик ожидания при закрытии формы
    ОтключитьОбработчикОжидания("ПроверитьАктивностьПользователя");
КонецПроцедуры

&НаКлиенте
Процедура ПроверитьАктивностьПользователя ()
    // Устанавливаем текущее время как признак активности, чтобы обработчик ожидания сам себя не "усыпил"
    // Если пользователь был активен (т.е. произошло какое-то другое событие на форме)
    // то мПоследнееВремяАктивности будет обновлено в соответствующем обработчике.
    // Если других событий не было, то нам нужно определить бездействие.

    Если ТекущаяДата() - мПоследнееВремяАктивности > мИнтервалБездействия Тогда
        Сообщить("Пользователь бездействует более " + Строка(мИнтервалБездействия) + " секунд. Завершаем работу формы.");
        // Здесь можно добавить логику завершения работы формы или сеанса
        Закрыть();
    КонецЕсли;

    // ВНИМАНИЕ: Если нужно отслеживать активность при каждом чихе,
    // то мПоследнееВремяАктивности нужно обновлять во всех обработчиках ПриИзменении, ПриАктивизации,
    // ПриНажатии и т.д. Если же нужен только факт изменения данных, то достаточно Модифицированность.
    // Если мы хотим отслеживать именно отсутствие *любых* действий,
    // то мПоследнееВремяАктивности должно обновляться во всех возможных обработчиках интерактивных действий.
    // А если пользователь просто перемещается по полям без изменения данных,
    // то можно использовать свойство формы ТекущийЭлемент, сравнивая его с предыдущим значением.
    // Однако, для простоты и эффективности, часто используются методы из следующих пунктов.
КонецПроцедуры

// Пример обработчика для обновления времени активности
// Его нужно добавлять к каждому интерактивному элементу, который вы хотите считать "активностью"
&НаКлиенте
Процедура ОбновитьАктивность()
    мПоследнееВремяАктивности = ТекущаяДата();
КонецПроцедуры

// Пример для поля ввода
&НаКлиенте
Процедура ПолеВводаПриИзменении(Элемент)
    ОбновитьАктивность();
    // Дальнейшая логика поля
КонецПроцедуры

// Пример для кнопки
&НаКлиенте
Процедура КнопкаНажатие(Элемент)
    ОбновитьАктивность();
    // Дальнейшая логика кнопки
КонецПроцедуры

// Пример для табличной части (при изменении строки или выборе)
&НаКлиенте
Процедура ТабличнаяЧастьПриАктивизацииСтроки(Элемент)
    ОбновитьАктивность();
КонецПроцедуры

// Пример для табличной части (при изменении данных в ячейке)
&НаКлиенте
Процедура ТабличнаяЧастьПриИзменении(Элемент)
    ОбновитьАктивность();
КонецПроцедуры

Этот подход требует тщательной проработки, так как нам нужно определить, какие именно действия пользователя мы считаем "активностью" и соответственно обновлять мПоследнееВремяАктивности. Для фиксации действий пользователя без привязки к каждому элементу управления можно использовать компоненту для перехвата ввода с клавиатуры и мыши.

2. Использование обработчика ожидания и сравнение "снимков" реквизитов формы

Если наша основная цель — отловить именно изменение данных на форме, а не любое взаимодействие, то более надежным способом будет периодическое сравнение текущих значений реквизитов формы с их предыдущим "снимком".

  1. Создание "снимка" данных: При каждом срабатывании обработчика ожидания мы формируем текущее состояние всех редактируемых реквизитов формы. Это может быть структура, соответствие или даже XML-строка, содержащая сериализованные значения всех важных реквизитов (включая табличные части).
  2. Сравнение "снимков": Текущий снимок сравнивается с предыдущим, сохраненным ранее. Если они отличаются, это означает, что пользователь изменил данные на форме. В этом случае мы обновляем таймер активности и сохраняем текущий снимок как предыдущий.
  3. Определение бездействия: Если несколько последовательных проверок показывают, что снимки не отличаются, и при этом прошло заданное время, мы можем считать пользователя неактивным.

Преимущества этого метода:

Недостатки:

Пример реализации:


// В модуле формы

// Переменные формы
Перем мПоследнийСнимокДанных Экспорт; // Хранит сериализованные данные формы
Перем мПоследнееВремяАктивностиДляДанных Экспорт; // Время последнего изменения данных
Перем мИнтервалБездействияДляДанных Экспорт; // Максимально допустимое время бездействия для данных

&НаКлиенте
Процедура ПриСозданииНаСервереПосле(Отказ, СтандартнаяОбработка)
    мИнтервалБездействияДляДанных = 300; // 5 минут
    мПоследнееВремяАктивностиДляДанных = ТекущаяДата();
    
    // Создаем начальный снимок данных
    мПоследнийСнимокДанных = ПолучитьСнимокДанныхФормы();

    ПодключитьОбработчикОжидания("ПроверитьИзменениеДанныхФормы", 5 * 100); // Проверка каждые 5 секунд
КонецПроцедуры

&НаКлиенте
Процедура ПередЗакрытием(Отказ, СтандартнаяОбработка)
    ОтключитьОбработчикОжидания("ПроверитьИзменениеДанныхФормы");
КонецПроцедуры

&НаКлиенте
Функция ПолучитьСнимокДанныхФормы()
    // Этот метод должен собирать все значимые реквизиты формы.
    // Для простых реквизитов:
    // СтруктураДанных = Новый Структура;
    // СтруктураДанных.Вставить("Реквизит1", Реквизит1);
    // СтруктураДанных.Вставить("Реквизит2", Реквизит2);

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

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

    // Для объекта формы
    Если ТипЗнч(Объект) = Тип("ДокументОбъект") ИЛИ ТипЗнч(Объект) = Тип("СправочникОбъект") Тогда
        ПеременныеДляСнимка.Вставить("Объект", Объект); // Для сравнения всего объекта
    КонецЕсли;

    Возврат СериализаторXDTO.ЗаписатьXDTO(ПеременныеДляСнимка);
КонецФункции

&НаКлиенте
Процедура ПроверитьИзменениеДанныхФормы()
    НовыйСнимокДанных = ПолучитьСнимокДанныхФормы();

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

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

3. Программное подключение обработчиков "ПриИзменении" к элементам формы

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

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

Пример реализации:


// В модуле формы

// Переменные формы
Перем мПоследнееВремяАктивностиЭкземпляраФормы Экспорт;

&НаКлиенте
Процедура ПриСозданииНаСервереПосле(Отказ, СтандартнаяОбработка)
    мПоследнееВремяАктивностиЭкземпляраФормы = ТекущаяДата();

    // Обходим элементы формы и подключаем обработчики
    Для Каждого ЭлементФормы Из Элементы Цикл
        Если ЭлементФормы.Тип = Тип("ПолеВвода") Или
           ЭлементФормы.Тип = Тип("Флажок") Или
           ЭлементФормы.Тип = Тип("ПолеВыбора") Или
           ЭлементФормы.Тип = Тип("ПолеПереключателя") Тогда

            // Проверяем, есть ли у элемента событие "ПриИзменении"
            // и не имеет ли он уже свой собственный обработчик, если мы хотим его сохранить
            Попытка
                ЭлементФормы.УстановитьДействие("ПриИзменении", "ОбновитьВремяАктивности");
            Исключение
                // Игнорируем ошибки, если у элемента нет такого события или оно уже занято
            КонецПопытки;

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

    // Также можно подключить обработчик ожидания для проверки бездействия
    ПодключитьОбработчикОжидания("ПроверитьАктивностьПользователяПоСобытиям", 5 * 100); // Каждые 5 секунд
КонецПроцедуры

&НаКлиенте
Процедура ПередЗакрытием(Отказ, СтандартнаяОбработка)
    ОтключитьОбработчикОжидания("ПроверитьАктивностьПользователяПоСобытиям");
КонецПроцедуры

&НаКлиенте
Процедура ОбновитьВремяАктивности(Элемент = Неопределено)
    // Эта процедура будет вызываться всеми элементами,
    // к которым мы программно подключили обработчик.
    мПоследнееВремяАктивностиЭкземпляраФормы = ТекущаяДата();
КонецПроцедуры

&НаКлиенте
Процедура ПроверитьАктивностьПользователяПоСобытиям()
    Если ТекущаяДата() - мПоследнееВремяАктивностиЭкземпляраФормы > 300 Тогда // 5 минут бездействия
        Сообщить("Пользователь не совершал действий более 5 минут. Завершаем работу формы.");
        Закрыть();
    КонецЕсли;
КонецПроцедуры

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

4. Централизованный механизм отслеживания активности

Для более крупных систем с множеством форм, где необходимо отслеживать активность пользователя в целом по сеансу, а не только в рамках одной формы, имеет смысл создать централизованный механизм.

  1. Общий модуль/Функция: Создадим общий модуль (например, "УправлениеАктивностьюПользователей"), который будет содержать логику отслеживания.
  2. Подписка на события форм: При открытии каждой формы мы можем программно подписать ее на централизованный механизм. Например, в процедуре ПриСозданииНаСервереПосле всех форм вызывать общую процедуру, которая регистрирует форму и подключает к ней ОбработчикОжидания, вызывающий централизованную функцию.
  3. Хранение последнего действия: В общем модуле или даже в регистре сведений (для серверного отслеживания) мы можем хранить время последнего зарегистрированного действия пользователя.
  4. Обработчик ожидания на сеансе: Если нам нужно отслеживать активность по всему сеансу, а не только по одной открытой форме, мы можем использовать ОбработчикОжидания, который подключается при старте клиентского приложения и работает на уровне всего сеанса. Он будет периодически проверять время последней активности.
  5. Обновление активности: Любое значимое действие пользователя (открытие формы, запись документа, выполнение отчета, нажатие на кнопку на любой форме) будет вызывать централизованную функцию, которая обновит время последней активности.

Такой подход обеспечивает масштабируемость и уменьшает дублирование кода, но требует более сложной начальной настройки.

5. Серверные тайм-ауты для веб-клиентов

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

  1. Настройки веб-сервера: Веб-сервер (например, IIS для Windows, Apache для Linux) или сервер приложений 1С:Предприятия (если используется) имеет настройки тайм-аута сеанса. Если от клиента нет запросов к серверу в течение заданного времени, сеанс на сервере автоматически завершается.
  2. Принцип работы: Веб-клиент постоянно взаимодействует с сервером (например, для обновления данных, отправки команд). Если пользователь бездействует, клиент перестает отправлять запросы, и сервер через некоторое время "считает" сеанс неактивным и обрывает его.

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

Заключение

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

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

← На главную