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

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

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

Ограничения метода Состояние() и проблемы синхронной загрузки

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

  1. Метод Состояние() на клиенте: Метод Состояние(), вызванный на клиенте, действительно позволяет отобразить текст в строке состояния окна приложения. Однако он может быть обновлен только тогда, когда управление возвращается на клиент. Если вся длительная операция выполняется в одной серверной функции, текст состояния на клиенте не будет обновляться до ее завершения.
  2. Серверные вызовы в цикле и производительность: Мы могли бы рассмотреть вариант, при котором клиент в цикле по элементам файла вызывает серверную функцию для каждого элемента, а затем на клиенте обновляет Состояние(). Представьте себе такой алгоритм, предложенный в сообщении 6:
    1. Вызываем серверную функцию, которая возвратит номенклатуру (или открывает файл на сервере).
    2. На клиенте в цикле проходимся по данным файла (или по порциям данных).
    3. Поля кладем в структуру, вызываем серверную функцию записи номенклатуры для каждого элемента.
    4. На клиенте вызываем Состояние().
    5. Переходим к следующему элементу цикла.

    Этот подход, хотя технически и позволяет обновлять Состояние(), является крайне неэффективным и не рекомендован для длительных операций. Каждый вызов серверной функции инициирует клиент-серверный запрос. Это влечет за собой задержки, связанные с передачей данных по сети, обработкой запроса на сервере и возвратом ответа. Если таких вызовов тысячи или десятки тысяч, общая производительность системы будет катастрофически низкой. Как справедливо замечено в сообщении 8, "многое будет зависеть от качества связи клиента с сервером", и в сообщении 11, такой подход "будет достаточно бредовой затеей". Под качеством связи здесь подразумевается совокупность технических факторов, влияющих на задержку при каждом серверном вызове.

  3. Блокировка интерфейса: Если операция действительно длительная (более 8-10 секунд), пользовательский интерфейс клиента будет полностью заблокирован. Это может привести к появлению предупреждений от браузера (в веб-клиенте) о "длительном скрипте", а также к таймаутам веб-сервера (ошибка 504), если используется веб-публикация.

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

Рекомендованный подход: Использование фоновых заданий для длительных операций

Платформа 1С:Предприятие предоставляет мощный механизм для выполнения длительных операций асинхронно — фоновые задания. Это основной и рекомендованный способ для решения нашей задачи, как указано в сообщениях 2 и 14.

Преимущества использования фоновых заданий:

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

Общий подход к асинхронному выполнению операции с прогрессом:

  1. Размещение кода обработки: Код, выполняющий длительную загрузку данных (чтение файла, запись в базу), необходимо поместить в процедуру или функцию общего модуля или модуля менеджера объекта, доступного для вызова из фоновых заданий. Важно, чтобы модуль был серверным и мог быть вызван с помощью ВызватьФоновоеЗадание().
  2. Запуск фонового задания с клиента: С клиентской части мы запускаем фоновое задание, передавая ему необходимые параметры (например, путь к файлу, идентификатор временного хранилища для результатов).
  3. Ожидание и опрос состояния на клиенте: После запуска фонового задания клиент не ждет его завершения синхронно. Вместо этого он запускает обработчик ожидания (ПодключитьОбработчикОжидания()), который будет периодически (например, раз в 1-5 секунд) опрашивать сервер о текущем состоянии фонового задания и его прогрессе.
  4. Отображение прогресса на клиенте: На время выполнения фонового задания мы отображаем пользователю индикатор прогресса. Это может быть специальная форма с прогресс-баром, анимация или просто регулярное обновление текста в строке состояния (используя Состояние() на клиенте с данными, полученными от фонового задания).
  5. Получение результатов: После завершения фонового задания (когда опрос покажет, что оно выполнено), клиент получает результаты операции из временного хранилища или другого места, где они были сохранены фоновым заданием.

Передача прогресса с сервера на клиент

Самый важный аспект — как фоновое задание, работающее на сервере, может сообщить клиенту о своем текущем прогрессе. Существует несколько основных способов:

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

Реализация прогресс-бара с фоновыми заданиями: пошаговое руководство

Давайте подробно разберем, как мы можем реализовать этот механизм на практике.

  1. Подготовка серверной процедуры (фонового задания):

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

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

    
    // Общий модуль: ЗагрузкаДанныхСервер
    // Должен быть серверным и доступным для вызова сервера
    Процедура ВыполнитьЗагрузкуНоменклатуры(ПараметрыЗагрузки) Экспорт
        Перем КоличествоОбработано, КоличествоВсего;
        
        // Получаем параметры
        ИмяФайла = ПараметрыЗагрузки.ИмяФайла;
        АдресРезультата = ПараметрыЗагрузки.АдресДляРезультата;
    
        Попытка
            Файл = Новый ТекстовыйДокумент;
            Файл.Прочитать(ИмяФайла);
            
            СтрокиФайла = РазложитьТекстовыйДокументНаСтроки(Файл); // Ваша функция для разбивки
            КоличествоВсего = СтрокиФайла.Количество();
            КоличествоОбработано = 0;
            
            СписокОшибок = Новый СписокЗначений;
            
            Для Каждого СтрокаФайла Из СтрокиФайла Цикл
                КоличествоОбработано = КоличествоОбработано + 1;
                Процент = 0;
                Если КоличествоВсего > 0 Тогда
                    Процент = Окр(КоличествоОбработано * 100 / КоличествоВсего, 0);
                КонецЕсли;
                
                ТекстПрогресса = "Обрабатывается строка " + КоличествоОбработано + " из " 
                                 + КоличествоВсего + ". " + СтрокаФайла;
                
                // Отправляем прогресс на клиент
                СообщитьПрогресс(Процент, ТекстПрогресса);
                
                Попытка
                    // Здесь ваша логика разбора строки и создания/обновления элемента номенклатуры
                    // Например:
                    // НоменклатураОбъект = СоздатьИлиНайтиНоменклатуру(СтрокаФайла);
                    // НоменклатураОбъект.Записать();
                Исключение
                    СписокОшибок.Добавить(СтрокаФайла, ОписаниеОшибки());
                КонецПопытки;
            КонецЦикла;
    
            РезультатЗагрузки = Новый Структура;
            РезультатЗагрузки.Вставить("Успешно", Истина);
            РезультатЗагрузки.Вставить("КоличествоОбработано", КоличествоОбработано);
            РезультатЗагрузки.Вставить("КоличествоОшибок", СписокОшибок.Количество());
            РезультатЗагрузки.Вставить("СписокОшибок", СписокОшибок);
            
            ПоместитьВоВременноеХранилище(РезультатЗагрузки, АдресРезультата);
    
        Исключение
            РезультатЗагрузки = Новый Структура;
            РезультатЗагрузки.Вставить("Успешно", Ложь);
            РезультатЗагрузки.Вставить("Ошибка", ОписаниеОшибки());
            ПоместитьВоВременноеХранилище(РезультатЗагрузки, АдресРезультата);
        КонецПопытки;
    КонецПроцедуры
    
    // Вспомогательная функция (может быть в том же модуле или другом)
    Функция РазложитьТекстовыйДокументНаСтроки(ТекстовыйДокумент)
        Результат = Новый Массив;
        Для Индекс = 0 По ТекстовыйДокумент.КоличествоСтрок() - 1 Цикл
            Результат.Добавить(ТекстовыйДокумент.ПолучитьСтроку(Индекс));
        КонецЦикла;
        Возврат Результат;
    КонецФункции
    
  2. Инициация фонового задания на клиенте:

    С формы клиента мы вызываем серверную функцию, которая инициирует фоновое задание. Важно сохранить уникальный идентификатор фонового задания (УникальныйИдентификатор), так как он потребуется для опроса его состояния.

    
    // На форме (клиентская процедура, например, по кнопке)
    &НаКлиенте
    Процедура ЗагрузитьДанныеИзФайла()
        ДиалогВыбораФайла = Новый ДиалогВыбораФайла(РежимДиалогаВыбораФайла.Открытие);
        ДиалогВыбораФайла.ПолноеИмяФайла = "";
        ДиалогВыбораФайла.Фильтр = "Текстовые файлы (*.txt)|*.txt|Все файлы (*.*)|*.*";
        ДиалогВыбораФайла.Заголовок = "Выберите файл для загрузки";
        
        Если Не ДиалогВыбораФайла.Выбрать() Тогда
            Возврат;
        КонецЕсли;
        
        ИмяФайлаНаКлиенте = ДиалогВыбораФайла.ПолноеИмяФайла;
        
        // Передаем файл на сервер во временное хранилище
        АдресВХИсходногоФайла = ПоместитьВоВременноеХранилище(Новый ДвоичныеДанные(ИмяФайлаНаКлиенте));
        
        // Подготовим адрес для сохранения результатов фонового задания
        АдресВХРезультата = ПолучитьУникальныйИдентификатор(); // Идентификатор для временного хранилища результата
        
        ПараметрыЗагрузки = Новый Структура;
        ПараметрыЗагрузки.Вставить("ИмяФайла", АдресВХИсходногоФайла); // Будет преобразовано на сервере
        ПараметрыЗагрузки.Вставить("АдресДляРезультата", АдресВХРезультата);
        
        // Запускаем фоновое задание
        ИдентификаторФоновогоЗадания = ЗапуститьФоновоеЗаданиеНаСервере(ПараметрыЗагрузки);
        
        Если ИдентификаторФоновогоЗадания <> Неопределено Тогда
            // Сохраняем идентификатор задания и адрес результата для дальнейшего опроса
            ЭлементыФормы.ИдентификаторЗадания = ИдентификаторФоновогоЗадания; // Пример сохранения в реквизит формы
            ЭлементыФормы.АдресВХРезультата = АдресВХРезультата;
            
            // Показываем индикатор прогресса и начинаем опрос
            ПоказатьИндикаторПрогресса(ИдентификаторФоновогоЗадания);
        Иначе
            Сообщить("Не удалось запустить фоновое задание.");
        КонецЕсли;
    КонецПроцедуры
    
    // Серверная функция, запускающая фоновое задание
    &НаСервереБезКонтекста
    Функция ЗапуститьФоновоеЗаданиеНаСервере(ПараметрыЗагрузки)
        Попытка
            // Из временного хранилища получим двоичные данные файла и сохраним на сервере
            ДвоичныеДанныеФайла = ПолучитьИзВременногоХранилища(ПараметрыЗагрузки.ИмяФайла);
            КаталогВременныхФайлов = ПолучитьКаталогВременныхФайлов();
            ИмяФайлаНаСервере = КаталогВременныхФайлов + "\" + ПолучитьИмяВременногоФайла("txt");
            ДвоичныеДанныеФайла.Записать(ИмяФайлаНаСервере);
            
            ПараметрыЗагрузки.ИмяФайла = ИмяФайлаНаСервере; // Теперь фоновое задание будет работать с серверным файлом
            
            // Запускаем фоновое задание
            Задание = ФоновыеЗадания.Выполнить("ЗагрузкаДанныхСервер.ВыполнитьЗагрузкуНоменклатуры", ПараметрыЗагрузки);
            Возврат Задание.УникальныйИдентификатор;
        Исключение
            Возврат Неопределено;
        КонецПопытки;
    КонецФункции
    
  3. Опрос состояния фонового задания на клиенте:

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

    
    // На форме (клиентская процедура)
    Перем ИдентификаторФоновогоЗадания;
    Перем АдресВХРезультата; // Если нужно получать результат
    Перем ИдентификаторОбработчикаОжидания; // Для управления обработчиком
    
    &НаКлиенте
    Процедура ПоказатьИндикаторПрогресса(ИдЗадания)
        // Инициализация формы прогресса или просто обновление Состояние()
        // Например, можно открыть отдельную форму с индикатором прогресса
        
        // Сохраняем ИдентификаторЗадания в реквизит формы или переменную
        ИдентификаторФоновогоЗадания = ИдЗадания;
        
        // Подключаем обработчик ожидания, который будет периодически опрашивать состояние
        ИдентификаторОбработчикаОжидания = ПодключитьОбработчикОжидания("ОбновитьПрогресс", 1, Истина); // 1 секунда, однократно (будем переподключать)
        Состояние("Начата загрузка данных...");
    КонецПроцедуры
    
    &НаКлиенте
    Процедура ОбновитьПрогресс()
        Если ИдентификаторФоновогоЗадания = Неопределено Тогда
            ОтключитьОбработчикОжидания(ИдентификаторОбработчикаОжидания);
            Возврат;
        КонецЕсли;
    
        СтатусЗадания = ПолучитьСтатусФоновогоЗаданияНаСервере(ИдентификаторФоновогоЗадания);
        
        Если СтатусЗадания = Неопределено Тогда
            Сообщить("Ошибка получения статуса фонового задания.");
            ЗавершитьОпросПрогресса(Ложь);
            Возврат;
        КонецЕсли;
    
        // Обновляем прогресс на клиенте
        ТекстПрогресса = "";
        Если СтатусЗадания.Свойство("СообщениеПользователю") И НЕ ПустаяСтрока(СтатусЗадания.СообщениеПользователю) Тогда
            ТекстПрогресса = СтатусЗадания.СообщениеПользователю;
        Иначе
            ТекстПрогресса = "Выполняется... " + СтатусЗадания.ПроцентВыполнения + "%";
        КонецЕсли;
        
        Состояние(ТекстПрогресса); // Обновляем строку состояния
        
        // Если у вас есть элемент формы для прогресса, обновите его
        // Элементы.ПрогрессБар.Значение = СтатусЗадания.ПроцентВыполнения;
        
        Если СтатусЗадания.Состояние = "Выполнено" Тогда
            Сообщить("Загрузка данных успешно завершена.");
            ПолучитьИОбработатьРезультаты(Истина);
            ЗавершитьОпросПрогресса(Истина);
        ИначеЕсли СтатусЗадания.Состояние = "Ошибка" Тогда
            Сообщить("Ошибка при загрузке данных: " + СтатусЗадания.ПодробноеПредставлениеОшибки);
            ПолучитьИОбработатьРезультаты(Ложь);
            ЗавершитьОпросПрогресса(Ложь);
        ИначеЕсли СтатусЗадания.Состояние = "Отменено" Тогда
            Сообщить("Загрузка данных была отменена.");
            ЗавершитьОпросПрогресса(Ложь);
        Иначе
            // Продолжаем опрос
            ПодключитьОбработчикОжидания("ОбновитьПрогресс", 1, Истина); // Переподключаем
        КонецЕсли;
    КонецПроцедуры
    
    // Серверная функция для получения статуса фонового задания
    &НаСервереБезКонтекста
    Функция ПолучитьСтатусФоновогоЗаданияНаСервере(ИдентификаторЗадания)
        Попытка
            Задание = ФоновыеЗадания.НайтиПоУникальномуИдентификатору(ИдентификаторЗадания);
            Если Задание = Неопределено Тогда
                Возврат Неопределено;
            КонецЕсли;
            
            Статус = Новый Структура;
            Статус.Вставить("Состояние", Задание.Состояние); // Ожидание, Выполняется, Выполнено, Ошибка, Отменено
            Статус.Вставить("ПроцентВыполнения", Задание.ПроцентВыполнения);
            Статус.Вставить("СообщениеПользователю", Задание.СообщениеПользователю);
            Статус.Вставить("ПодробноеПредставлениеОшибки", Задание.ПодробноеПредставлениеОшибки);
            
            Возврат Статус;
        Исключение
            Возврат Неопределено;
        КонецПопытки;
    КонецФункции
    
    &НаКлиенте
    Процедура ПолучитьИОбработатьРезультаты(Успешно)
        Если Успешно И ИдентификаторФоновогоЗадания <> Неопределено Тогда
            // Получаем результаты с сервера, если они были сохранены во временном хранилище
            Результат = ПолучитьРезультатыЗагрузкиНаСервере(АдресВХРезультата);
            Если Результат <> Неопределено Тогда
                Если Результат.Успешно Тогда
                    Сообщить("Обработано: " + Результат.КоличествоОбработано + ", Ошибок: " + Результат.КоличествоОшибок);
                    // Дополнительная обработка списка ошибок, если нужно
                Иначе
                    Сообщить("Внутренняя ошибка загрузки: " + Результат.Ошибка);
                КонецЕсли;
            Иначе
                Сообщить("Не удалось получить результаты фонового задания.");
            КонецЕсли;
        КонецЕсли;
    КонецПроцедуры
    
    &НаСервереБезКонтекста
    Функция ПолучитьРезультатыЗагрузкиНаСервере(АдресВХРезультата)
        Попытка
            Возврат ПолучитьИзВременногоХранилища(АдресВХРезультата);
        Исключение
            Возврат Неопределено;
        КонецПопытки;
    КонецФункции
    
    &НаКлиенте
    Процедура ЗавершитьОпросПрогресса(Успешно)
        ОтключитьОбработчикОжидания(ИдентификаторОбработчикаОжидания);
        ИдентификаторФоновогоЗадания = Неопределено;
        Состояние(НСтр("ru = 'Готово.'")); // Очищаем строку состояния
        // Скрываем форму прогресса или индикатор
    КонецПроцедуры
    
  4. Отображение итогов:

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

Важные аспекты и советы по оптимизации

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

  1. Оптимизация серверного кода: Код внутри фонового задания должен быть максимально оптимизирован. Избегайте лишних запросов к базе данных в цикле, используйте пакетные операции записи/чтения, кэширование данных.
  2. Частота опроса: Установите разумный интервал опроса состояния фонового задания (например, 1-3 секунды). Слишком частый опрос может создавать излишнюю нагрузку на сервер.
  3. Возможность отмены: Предусмотрите возможность отмены фонового задания пользователем. Для этого на форме прогресса добавьте кнопку "Отмена", которая будет вызывать серверную функцию ФоновыеЗадания.Отменить(УникальныйИдентификаторЗадания).
  4. Обработка ошибок: Тщательно обрабатывайте исключения как в фоновом задании, так и в клиентском коде, чтобы корректно информировать пользователя о возникших проблемах.
  5. Массовая запись данных: Для ускорения массовой записи данных в базу можно использовать транзакции, отложенные записи объектов (если это применимо), или, в более сложных сценариях, многопоточную обработку и параллельные фоновые задания (разбивая большой файл на части и обрабатывая каждую часть отдельным заданием). Однако это значительно усложняет логику сбора прогресса и результатов.
  6. Очистка временных файлов: Не забывайте удалять временные файлы, созданные на сервере при передаче данных из файла с клиента, после завершения фонового задания.

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

← На главную