Разработка в среде «1С:Предприятие 8.3» на управляемых формах накладывает жесткие правила на взаимодействие между клиентской и серверной частями приложения. Одной из классических проблем, с которой сталкиваются как начинающие, так и опытные разработчики, является попытка вернуть ТаблицаЗначений напрямую из серверной функции на клиент. Платформа реагирует на это ошибкой передачи данных XDTO и уведомлением о недопустимом типе данных.
В рамках этой статьи мы подробно разберем, почему возникает эта ошибка, проанализируем популярные (включая небезопасные) обходные пути и рассмотрим современные стандарты разработки для передачи табличных данных и работы с файлами Microsoft Excel.
Причиной возникновения ошибки является архитектурное ограничение платформы «1С:Предприятие». Объект ТаблицаЗначений относится к так называемым мутабельным типам данных. К этой же категории относятся ДокументОбъект, СправочникОбъект и другие прикладные объекты базы данных.
Мутабельные объекты могут динамически изменять свое состояние, структуру и состав колонок. Их прямая передача по сети между сервером и клиентом заблокирована на уровне платформы по следующим причинам:
ТаблицаЗначений физически отсутствует в памяти клиентского приложения. Попытка его сериализации вызывает сбой, так как клиент не знает, как воссоздать этот объект у себя.Платформа осуществляет передачу только тех типов данных, которые могут быть сериализованы через механизм XDTO (XML-сериализация) и гарантированно поддерживаются на стороне клиента.
В ходе обсуждения проблемы часто предлагается использовать «быстрое» решение с помощью системных функций преобразования во внутреннюю строку:
&НаКлиенте
Процедура Сформировать(Команда)
// Попытка получить сериализованную строку и восстановить её
ТаблицаЗначенийДанные = ЗначениеИзСтрокиВнутр(ПолучитьДанныеВТЗ());
КонецПроцедуры
&НаСервере
Функция ПолучитьДанныеВТЗ()
Запрос = Новый Запрос;
// ... Настройка и выполнение запроса ...
ВыгруженныеДанные = РезультатЗапроса.Выгрузить();
// Преобразование во внутренний системный формат
Возврат ЗначениеВСтрокуВнутр(ВыгруженныеДанные);
КонецФункции
Внимание: данный способ является устаревшим и крайне опасным! Использовать его в современных конфигурациях категорически не рекомендуется по следующим причинам:
ТаблицаЗначений для десериализации.ЗначениеВСтрокуВнутр, зависит от версии платформы 1С. При обновлении платформы формат строки может измениться, что приведет к неработоспособности кода.Давайте рассмотрим, как решить задачу передачи данных правильно и безопасно для любого типа клиента.
Если вам требуется обработать данные таблицы на клиенте, оптимальным решением является конвертация таблицы значений на сервере в массив простых структур. Структуры и массивы не являются мутабельными типами и отлично передаются на любой клиент (включая Тонкий, Мобильный и Веб-клиент).
Рассмотрим пример универсальной функции преобразования, аналогичной механизмам Библиотеки Стандартных Подсистем (БСП):
&НаСервере
Функция ПолучитьДанныеВМассивеСтруктур()
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Документ.Дата КАК Дата,
| Документ.Номер КАК Номер,
| Документ.СуммаОбщая КАК СуммаОбщая
|ИЗ
| Документ.Документ КАК Документ";
РезультатЗапроса = Запрос.Выполнить();
ТаблицаЗначений = РезультатЗапроса.Выгрузить();
// Конвертация таблицы в массив структур
МассивРезультат = Новый Массив;
// Формируем список колонок для заполнения свойств
МассивИменКолонок = Новый Массив;
Для Каждого Колонка Из ТаблицаЗначений.Колонки Цикл
МассивИменКолонок.Добавить(Колонка.Имя);
КонецЦикла;
Для Каждого СтрокаТЗ Из ТаблицаЗначений Цикл
СтруктураСтроки = Новый Структура;
Для Каждого ИмяКолонки Из МассивИменКолонок Цикл
СтруктураСтроки.Вставить(ИмяКолонки, СтрокаТЗ[ИмяКолонки]);
КонецЦикла;
МассивРезультат.Добавить(СтруктураСтроки);
КонецЦикла;
Возврат МассивРезультат;
КонецФункции
На стороне клиента вы получите обычный массив, по которому можно осуществлять обход в цикле:
&НаКлиенте
Процедура ЗагрузитьДанные(Команда)
ДанныеДляВыгрузки = ПолучитьДанныеВМассивеСтруктур();
Для Каждого СтрокаДанных Из ДанныеДляВыгрузки Цикл
// Обработка данных на клиенте
Сообщить(СтрокаДанных.Номер + " от " + СтрокаДанных.Дата);
КонецЦикла;
EndProcedure
Если выгруженные из запроса данные предназначены для отображения на форме или должны временно храниться в ее контексте, можно использовать встроенные механизмы платформы по связыванию типов. Для этого на форме создается реквизит с типом ТаблицаЗначений.
На стороне клиента этот реквизит автоматически преобразуется платформой в тип ДанныеФормыКоллекция. Данный тип оптимизирован для передачи между клиентом и сервером. Разберем по шагам, как выполнить заполнение:
ДанныеОтчета и типом ТаблицаЗначений. Добавьте необходимые колонки.
&НаСервере
Процедура ЗаполнитьРеквизитНаСервере()
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ Дата, Номер, СуммаОбщая ИЗ Документ.Документ";
ТаблицаРезультат = Запрос.Выполнить().Выгрузить();
// Заполняем реквизит формы данными таблицы значений
ЗначениеВРеквизитФормы(ТаблицаРезультат, "ДанныеОтчета");
КонецПроцедуры
Платформа самостоятельно выполнит сериализацию и передаст заполненную коллекцию на клиент. На клиенте вы сможете работать с ней напрямую через переменную Объект.ДанныеОтчета (или просто ДанныеОтчета, если реквизит принадлежит форме, а не объекту).
Если таблица значений нужна только для того, чтобы передать ее между различными серверными вызовами без вывода пользователю, передавать ее на клиент вообще не нужно. В этом случае идеальным инструментом выступает Временное хранилище.
Проанализируем этот сценарий:
&НаСервере
Функция ПолучитьДанныеВоВременноеХранилище()
ТаблицаДанных = ПолучитьТаблицуИзЗапроса(); // Ваша серверная функция получения ТЗ
// Помещаем таблицу в хранилище и привязываем к уникальному идентификатору формы
АдресХранилища = ПоместитьВоВременноеХранилище(ТаблицаДанных, УникальныйИдентификатор);
Возврат АдресХранилища;
КонецФункции
На клиент возвращается легкая строка — адрес во временном хранилище. При следующем обращении к серверу вы можете передать этот адрес обратно и извлечь таблицу значений на стороне сервера, вообще не гоняя массив данных по сети:
&НаСервере
Процедура ОбработатьДанныеИзХранилища(АдресХранилища)
// Извлекаем таблицу на сервере
ТаблицаДанных = ПолучитьИзВременногоХранилища(АдресХранилища);
// Выполняем действия с таблицей...
КонецПроцедуры
В исходном обсуждении автор заполняет шаблон Excel построчно через COM-соединение на стороне клиента:
Эксель = Новый COMОбъект("Excel.Application");
Использование COM-объектов на клиенте или сервере в современных версиях 1С несет в себе критические архитектурные риски:
excel.exe остается висеть в памяти операционной системы, постепенно забивая ресурсы ОЗУ сервера.Для модификации сложных файлов (особенно отчетов государственных ведомств или Центробанка, где содержатся скрытые макросы и пароли на ячейках) лучшим решением будет перенос всей работы на серверную сторону с использованием двоичных данных.
Давайте выясним алгоритм действий:
Рассмотрим пример серверного заполнения шаблона с последующим возвратом файла на клиент:
&НаКлиенте
Процедура ЗаполнитьИСохранитьШаблон(Команда)
// Предположим, путь к файлу шаблона на клиенте уже выбран
ПутьКФайлуШаблона = "C:\Templates\CB_Report.xlsx";
ДвоичныеДанныеШаблона = Новый ДвоичныеДанные(ПутьКФайлуШаблона);
// Помещаем файл во временное хранилище
АдресФайлаВХранилище = ПоместитьВоВременноеХранилище(ДвоичныеДанныеШаблона, УникальныйИдентификатор);
// Вызываем сервер для обработки файла
АдресГотовогоФайла = ЗаполнитьФайлНаСервере(АдресФайлаВХранилище);
// Получаем готовый файл из хранилища и сохраняем его у пользователя
ГотовыеДвоичныеДанные = ПолучитьИзВременногоХранилища(АдресГотовогоФайла);
ПутьДляСохранения = "C:\Reports\CB_Report_Filled.xlsx";
ГотовыеДвоичныеДанные.Записать(ПутьДляСохранения);
Сообщить("Файл успешно заполнен и сохранен!");
КонецПроцедуры
&НаСервере
Функция ЗаполнитьФайлНаСервере(Знач АдресШаблона)
// Получаем двоичные данные файла из хранилища
ДвоичныеДанныеФайла = ПолучитьИзВременногоХранилища(АдресШаблона);
// Получаем имя временного файла на сервере
ИмяВременногоФайла = ПолучитьИмяВременногоФайла("xlsx");
ДвоичныеДанныеФайла.Записать(ИмяВременногоФайла);
// Получаем данные из базы данных для заполнения
ТаблицаДанных = ПолучитьДанныеИзЗапроса();
// Запускаем работу с Excel на стороне сервера (только если сервер работает под Windows)
Попытка
ExcelПриложение = Новый COMОбъект("Excel.Application");
ExcelПриложение.DisplayAlerts = Ложь;
Книга = ExcelПриложение.Workbooks.Open(ИмяВременногоФайла);
Лист = Книга.Worksheets(1);
// Построчное заполнение шаблона
ИндексСтроки = 10;
Для Каждого СтрокаТЗ Из ТаблицаДанных Цикл
Лист.Cells(ИндексСтроки, 2).Value = СтрокаТЗ.Дата;
Лист.Cells(ИндексСтроки, 3).Value = СтрокаТЗ.Номер;
Лист.Cells(ИндексСтроки, 4).Value = СтрокаТЗ.СуммаОбщая;
ИндексСтроки = ИндексСтроки + 1;
КонецЦикла;
Книга.Save();
Книга.Close();
ExcelПриложение.Quit();
Исключение
// Обязательно закрываем процесс Excel при ошибке, чтобы избежать утечек памяти
Если Не ExcelПриложение = Неопределено Тогда
ExcelПриложение.Quit();
КонецЕсли;
ВызватьИсключение "Ошибка заполнения Excel на сервере: " + ОписаниеОшибки();
КонецПопытки;
// Читаем измененный файл обратно в двоичные данные
РезультатДвоичныеДанные = Новый ДвоичныеДанные(ИмяВременногоФайла);
// Удаляем временный файл на сервере
УдалитьФайлы(ИмяВременногоФайла);
// Помещаем измененный файл в хранилище и возвращаем адрес клиенту
Возврат ПоместитьВоВременноеХранилище(РезультатДвоичныеДанные, УникальныйИдентификатор);
КонецФункции
Использование этого подхода полностью снимает проблему передачи мутабельных типов данных между клиентом и сервером. Все тяжелые вычисления, запросы и манипуляции с файлами происходят внутри быстрого серверного окружения, а клиентская часть занимается исключительно интерфейсными задачами (выбор файла-источника и сохранение результата).