При разработке информационных систем на платформе 1С:Предприятие 8.3 мы часто сталкиваемся с необходимостью выполнения длительных операций, таких как загрузка большого объема данных из внешнего файла. В таких случаях критически важно обеспечить обратную связь с пользователем, показывая текущий прогресс выполнения операции. Это помогает избежать впечатления "зависания" программы и позволяет пользователю оценивать оставшееся время. Рассмотрим, как эффективно реализовать индикатор прогресса в управляемых формах, учитывая особенности клиент-серверного взаимодействия и возможности платформы.
Начнем с анализа первоначальных предположений и существующих ограничений. В управляемых формах 1С:Предприятие клиентская часть приложения не имеет постоянного открытого соединения с базой данных или сервером 1С. Взаимодействие происходит по запросу: клиент отправляет вызов на сервер, сервер выполняет операцию и возвращает результат. Это означает, что пока серверная функция выполняется, клиентский интерфейс ожидает ответа и может быть заблокирован.
Состояние() на клиенте: Метод Состояние(), вызванный на клиенте, действительно позволяет отобразить текст в строке состояния окна приложения. Однако он может быть обновлен только тогда, когда управление возвращается на клиент. Если вся длительная операция выполняется в одной серверной функции, текст состояния на клиенте не будет обновляться до ее завершения.
Состояние(). Представьте себе такой алгоритм, предложенный в сообщении 6:
Состояние().Этот подход, хотя технически и позволяет обновлять Состояние(), является крайне неэффективным и не рекомендован для длительных операций. Каждый вызов серверной функции инициирует клиент-серверный запрос. Это влечет за собой задержки, связанные с передачей данных по сети, обработкой запроса на сервере и возвратом ответа. Если таких вызовов тысячи или десятки тысяч, общая производительность системы будет катастрофически низкой. Как справедливо замечено в сообщении 8, "многое будет зависеть от качества связи клиента с сервером", и в сообщении 11, такой подход "будет достаточно бредовой затеей". Под качеством связи здесь подразумевается совокупность технических факторов, влияющих на задержку при каждом серверном вызове.
Таким образом, для решения задачи отображения прогресса при длительной загрузке данных нам необходим асинхронный подход, который позволит серверу выполнять работу, не блокируя клиентский интерфейс, и при этом регулярно сообщать о ходе выполнения.
Платформа 1С:Предприятие предоставляет мощный механизм для выполнения длительных операций асинхронно — фоновые задания. Это основной и рекомендованный способ для решения нашей задачи, как указано в сообщениях 2 и 14.
Преимущества использования фоновых заданий:
Общий подход к асинхронному выполнению операции с прогрессом:
ВызватьФоновоеЗадание().
ПодключитьОбработчикОжидания()), который будет периодически (например, раз в 1-5 секунд) опрашивать сервер о текущем состоянии фонового задания и его прогрессе.
Состояние() на клиенте с данными, полученными от фонового задания).
Самый важный аспект — как фоновое задание, работающее на сервере, может сообщить клиенту о своем текущем прогрессе. Существует несколько основных способов:
СообщитьПрогресс(): Внутри процедуры фонового задания мы можем использовать глобальный метод СообщитьПрогресс(Процент, ТекстСостояния). Этот метод позволяет регистрировать числовой процент выполнения и текстовое сообщение о текущем этапе. На клиенте, при опросе состояния фонового задания, мы можем получить эту информацию с помощью метода ПолучитьСостояниеЗадания(), который возвращает структуру с полями ПроцентВыполнения и СообщениеПользователю. Это наиболее простой и рекомендуемый способ для вывода общего прогресса.
// Пример использования СообщитьПрогресс() в фоновом задании
Процедура ВыполнитьЗагрузкуДанныхФайла(ИмяФайла, ПараметрыЗагрузки) Экспорт
// ... Инициализация ...
ОбщееКоличество = ПолучитьКоличествоЭлементовВФайле(ИмяФайла);
Обработано = 0;
Для Каждого СтрокаФайла Из ДанныеФайла Цикл
// ... Логика обработки строки ...
Обработано = Обработано + 1;
Процент = ОбщееКоличество > 0 ? Окр(Обработано * 100 / ОбщееКоличество, 0) : 0;
СообщениеПрогресса = "Обрабатывается строка " + Обработано + " из " + ОбщееКоличество + "...";
// Сообщаем прогресс
СообщитьПрогресс(Процент, СообщениеПрогресса);
// ... Запись данных в базу ...
КонецЦикла;
// ... Завершение, сохранение результатов ...
КонецПроцедуры
ВременноеХранилище.
АдресВременногоХранилища). Этот адрес затем можно передать клиенту через СообщитьПрогресс() (в текстовом сообщении) или как параметр фонового задания. Клиент периодически считывает данные из этого хранилища.
Давайте подробно разберем, как мы можем реализовать этот механизм на практике.
Создайте новый общий модуль (например, "ЗагрузкаДанныхСервер") с серверным вызовом. В нем определим процедуру, которая будет выполнять всю логику загрузки. Обязательно добавьте параметр Экспорт, чтобы процедура была доступна для вызова из фонового задания. Внутри этой процедуры реализуем цикл по элементам файла, и на каждой итерации или через определенные интервалы вызываем СообщитьПрогресс().
Если нам нужно вернуть результат (например, количество загруженных элементов, список ошибок, адрес временного хранилища с результатами), мы можем сохранить его во временное хранилище и вернуть его адрес, либо обновить состояние самого фонового задания, которое клиент затем прочитает.
// Общий модуль: ЗагрузкаДанныхСервер
// Должен быть серверным и доступным для вызова сервера
Процедура ВыполнитьЗагрузкуНоменклатуры(ПараметрыЗагрузки) Экспорт
Перем КоличествоОбработано, КоличествоВсего;
// Получаем параметры
ИмяФайла = ПараметрыЗагрузки.ИмяФайла;
АдресРезультата = ПараметрыЗагрузки.АдресДляРезультата;
Попытка
Файл = Новый ТекстовыйДокумент;
Файл.Прочитать(ИмяФайла);
СтрокиФайла = РазложитьТекстовыйДокументНаСтроки(Файл); // Ваша функция для разбивки
КоличествоВсего = СтрокиФайла.Количество();
КоличествоОбработано = 0;
СписокОшибок = Новый СписокЗначений;
Для Каждого СтрокаФайла Из СтрокиФайла Цикл
КоличествоОбработано = КоличествоОбработано + 1;
Процент = 0;
Если КоличествоВсего > 0 Тогда
Процент = Окр(КоличествоОбработано * 100 / КоличествоВсего, 0);
КонецЕсли;
ТекстПрогресса = "Обрабатывается строка " + КоличествоОбработано + " из "
+ КоличествоВсего + ". " + СтрокаФайла;
// Отправляем прогресс на клиент
СообщитьПрогресс(Процент, ТекстПрогресса);
Попытка
// Здесь ваша логика разбора строки и создания/обновления элемента номенклатуры
// Например:
// НоменклатураОбъект = СоздатьИлиНайтиНоменклатуру(СтрокаФайла);
// НоменклатураОбъект.Записать();
Исключение
СписокОшибок.Добавить(СтрокаФайла, ОписаниеОшибки());
КонецПопытки;
КонецЦикла;
РезультатЗагрузки = Новый Структура;
РезультатЗагрузки.Вставить("Успешно", Истина);
РезультатЗагрузки.Вставить("КоличествоОбработано", КоличествоОбработано);
РезультатЗагрузки.Вставить("КоличествоОшибок", СписокОшибок.Количество());
РезультатЗагрузки.Вставить("СписокОшибок", СписокОшибок);
ПоместитьВоВременноеХранилище(РезультатЗагрузки, АдресРезультата);
Исключение
РезультатЗагрузки = Новый Структура;
РезультатЗагрузки.Вставить("Успешно", Ложь);
РезультатЗагрузки.Вставить("Ошибка", ОписаниеОшибки());
ПоместитьВоВременноеХранилище(РезультатЗагрузки, АдресРезультата);
КонецПопытки;
КонецПроцедуры
// Вспомогательная функция (может быть в том же модуле или другом)
Функция РазложитьТекстовыйДокументНаСтроки(ТекстовыйДокумент)
Результат = Новый Массив;
Для Индекс = 0 По ТекстовыйДокумент.КоличествоСтрок() - 1 Цикл
Результат.Добавить(ТекстовыйДокумент.ПолучитьСтроку(Индекс));
КонецЦикла;
Возврат Результат;
КонецФункции
С формы клиента мы вызываем серверную функцию, которая инициирует фоновое задание. Важно сохранить уникальный идентификатор фонового задания (УникальныйИдентификатор), так как он потребуется для опроса его состояния.
// На форме (клиентская процедура, например, по кнопке)
&НаКлиенте
Процедура ЗагрузитьДанныеИзФайла()
ДиалогВыбораФайла = Новый ДиалогВыбораФайла(РежимДиалогаВыбораФайла.Открытие);
ДиалогВыбораФайла.ПолноеИмяФайла = "";
ДиалогВыбораФайла.Фильтр = "Текстовые файлы (*.txt)|*.txt|Все файлы (*.*)|*.*";
ДиалогВыбораФайла.Заголовок = "Выберите файл для загрузки";
Если Не ДиалогВыбораФайла.Выбрать() Тогда
Возврат;
КонецЕсли;
ИмяФайлаНаКлиенте = ДиалогВыбораФайла.ПолноеИмяФайла;
// Передаем файл на сервер во временное хранилище
АдресВХИсходногоФайла = ПоместитьВоВременноеХранилище(Новый ДвоичныеДанные(ИмяФайлаНаКлиенте));
// Подготовим адрес для сохранения результатов фонового задания
АдресВХРезультата = ПолучитьУникальныйИдентификатор(); // Идентификатор для временного хранилища результата
ПараметрыЗагрузки = Новый Структура;
ПараметрыЗагрузки.Вставить("ИмяФайла", АдресВХИсходногоФайла); // Будет преобразовано на сервере
ПараметрыЗагрузки.Вставить("АдресДляРезультата", АдресВХРезультата);
// Запускаем фоновое задание
ИдентификаторФоновогоЗадания = ЗапуститьФоновоеЗаданиеНаСервере(ПараметрыЗагрузки);
Если ИдентификаторФоновогоЗадания <> Неопределено Тогда
// Сохраняем идентификатор задания и адрес результата для дальнейшего опроса
ЭлементыФормы.ИдентификаторЗадания = ИдентификаторФоновогоЗадания; // Пример сохранения в реквизит формы
ЭлементыФормы.АдресВХРезультата = АдресВХРезультата;
// Показываем индикатор прогресса и начинаем опрос
ПоказатьИндикаторПрогресса(ИдентификаторФоновогоЗадания);
Иначе
Сообщить("Не удалось запустить фоновое задание.");
КонецЕсли;
КонецПроцедуры
// Серверная функция, запускающая фоновое задание
&НаСервереБезКонтекста
Функция ЗапуститьФоновоеЗаданиеНаСервере(ПараметрыЗагрузки)
Попытка
// Из временного хранилища получим двоичные данные файла и сохраним на сервере
ДвоичныеДанныеФайла = ПолучитьИзВременногоХранилища(ПараметрыЗагрузки.ИмяФайла);
КаталогВременныхФайлов = ПолучитьКаталогВременныхФайлов();
ИмяФайлаНаСервере = КаталогВременныхФайлов + "\" + ПолучитьИмяВременногоФайла("txt");
ДвоичныеДанныеФайла.Записать(ИмяФайлаНаСервере);
ПараметрыЗагрузки.ИмяФайла = ИмяФайлаНаСервере; // Теперь фоновое задание будет работать с серверным файлом
// Запускаем фоновое задание
Задание = ФоновыеЗадания.Выполнить("ЗагрузкаДанныхСервер.ВыполнитьЗагрузкуНоменклатуры", ПараметрыЗагрузки);
Возврат Задание.УникальныйИдентификатор;
Исключение
Возврат Неопределено;
КонецПопытки;
КонецФункции
Мы используем ПодключитьОбработчикОжидания() для периодического вызова клиентской процедуры, которая будет опрашивать сервер о статусе фонового задания. Эта клиентская процедура, в свою очередь, будет вызывать тонкую серверную функцию, чтобы получить актуальное состояние задания.
// На форме (клиентская процедура)
Перем ИдентификаторФоновогоЗадания;
Перем АдресВХРезультата; // Если нужно получать результат
Перем ИдентификаторОбработчикаОжидания; // Для управления обработчиком
&НаКлиенте
Процедура ПоказатьИндикаторПрогресса(ИдЗадания)
// Инициализация формы прогресса или просто обновление Состояние()
// Например, можно открыть отдельную форму с индикатором прогресса
// Сохраняем ИдентификаторЗадания в реквизит формы или переменную
ИдентификаторФоновогоЗадания = ИдЗадания;
// Подключаем обработчик ожидания, который будет периодически опрашивать состояние
ИдентификаторОбработчикаОжидания = ПодключитьОбработчикОжидания("ОбновитьПрогресс", 1, Истина); // 1 секунда, однократно (будем переподключать)
Состояние("Начата загрузка данных...");
КонецПроцедуры
&НаКлиенте
Процедура ОбновитьПрогресс()
Если ИдентификаторФоновогоЗадания = Неопределено Тогда
ОтключитьОбработчикОжидания(ИдентификаторОбработчикаОжидания);
Возврат;
КонецЕсли;
СтатусЗадания = ПолучитьСтатусФоновогоЗаданияНаСервере(ИдентификаторФоновогоЗадания);
Если СтатусЗадания = Неопределено Тогда
Сообщить("Ошибка получения статуса фонового задания.");
ЗавершитьОпросПрогресса(Ложь);
Возврат;
КонецЕсли;
// Обновляем прогресс на клиенте
ТекстПрогресса = "";
Если СтатусЗадания.Свойство("СообщениеПользователю") И НЕ ПустаяСтрока(СтатусЗадания.СообщениеПользователю) Тогда
ТекстПрогресса = СтатусЗадания.СообщениеПользователю;
Иначе
ТекстПрогресса = "Выполняется... " + СтатусЗадания.ПроцентВыполнения + "%";
КонецЕсли;
Состояние(ТекстПрогресса); // Обновляем строку состояния
// Если у вас есть элемент формы для прогресса, обновите его
// Элементы.ПрогрессБар.Значение = СтатусЗадания.ПроцентВыполнения;
Если СтатусЗадания.Состояние = "Выполнено" Тогда
Сообщить("Загрузка данных успешно завершена.");
ПолучитьИОбработатьРезультаты(Истина);
ЗавершитьОпросПрогресса(Истина);
ИначеЕсли СтатусЗадания.Состояние = "Ошибка" Тогда
Сообщить("Ошибка при загрузке данных: " + СтатусЗадания.ПодробноеПредставлениеОшибки);
ПолучитьИОбработатьРезультаты(Ложь);
ЗавершитьОпросПрогресса(Ложь);
ИначеЕсли СтатусЗадания.Состояние = "Отменено" Тогда
Сообщить("Загрузка данных была отменена.");
ЗавершитьОпросПрогресса(Ложь);
Иначе
// Продолжаем опрос
ПодключитьОбработчикОжидания("ОбновитьПрогресс", 1, Истина); // Переподключаем
КонецЕсли;
КонецПроцедуры
// Серверная функция для получения статуса фонового задания
&НаСервереБезКонтекста
Функция ПолучитьСтатусФоновогоЗаданияНаСервере(ИдентификаторЗадания)
Попытка
Задание = ФоновыеЗадания.НайтиПоУникальномуИдентификатору(ИдентификаторЗадания);
Если Задание = Неопределено Тогда
Возврат Неопределено;
КонецЕсли;
Статус = Новый Структура;
Статус.Вставить("Состояние", Задание.Состояние); // Ожидание, Выполняется, Выполнено, Ошибка, Отменено
Статус.Вставить("ПроцентВыполнения", Задание.ПроцентВыполнения);
Статус.Вставить("СообщениеПользователю", Задание.СообщениеПользователю);
Статус.Вставить("ПодробноеПредставлениеОшибки", Задание.ПодробноеПредставлениеОшибки);
Возврат Статус;
Исключение
Возврат Неопределено;
КонецПопытки;
КонецФункции
&НаКлиенте
Процедура ПолучитьИОбработатьРезультаты(Успешно)
Если Успешно И ИдентификаторФоновогоЗадания <> Неопределено Тогда
// Получаем результаты с сервера, если они были сохранены во временном хранилище
Результат = ПолучитьРезультатыЗагрузкиНаСервере(АдресВХРезультата);
Если Результат <> Неопределено Тогда
Если Результат.Успешно Тогда
Сообщить("Обработано: " + Результат.КоличествоОбработано + ", Ошибок: " + Результат.КоличествоОшибок);
// Дополнительная обработка списка ошибок, если нужно
Иначе
Сообщить("Внутренняя ошибка загрузки: " + Результат.Ошибка);
КонецЕсли;
Иначе
Сообщить("Не удалось получить результаты фонового задания.");
КонецЕсли;
КонецЕсли;
КонецПроцедуры
&НаСервереБезКонтекста
Функция ПолучитьРезультатыЗагрузкиНаСервере(АдресВХРезультата)
Попытка
Возврат ПолучитьИзВременногоХранилища(АдресВХРезультата);
Исключение
Возврат Неопределено;
КонецПопытки;
КонецФункции
&НаКлиенте
Процедура ЗавершитьОпросПрогресса(Успешно)
ОтключитьОбработчикОжидания(ИдентификаторОбработчикаОжидания);
ИдентификаторФоновогоЗадания = Неопределено;
Состояние(НСтр("ru = 'Готово.'")); // Очищаем строку состояния
// Скрываем форму прогресса или индикатор
КонецПроцедуры
После завершения или отмены фонового задания, мы отключаем обработчик ожидания и выводим пользователю итоговое сообщение о результате операции (успешно, с ошибками, отменено). Если фоновое задание сохранило детальные результаты (например, список необработанных строк) во временное хранилище, мы можем получить их на клиенте и показать пользователю.
Для обеспечения максимальной производительности и удобства использования, мы должны учитывать несколько дополнительных моментов:
ФоновыеЗадания.Отменить(УникальныйИдентификаторЗадания).
Применяя эти подходы, мы сможем эффективно реализовать информативный и неблокирующий индикатор прогресса для длительных операций загрузки данных, что значительно повысит удобство использования нашей системы на платформе 1С:Предприятие 8.3.