Таблица значений на клиенте 1С: что делать, если её нельзя вернуть напрямую

· 11 мин

Содержание

Таблицу значений нельзя передать с сервера на клиент как есть: в тонком и веб-клиенте типа ТаблицаЗначений просто нет. Поэтому на сервере таблицу превращают во что-то, что клиент понимает: в массив структур, в реквизит формы типа «Таблица значений» (на клиенте он виден как ДанныеФормыКоллекция), в строку JSON или оставляют её на сервере во временном хранилище и передают клиенту только адрес.

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

Почему таблицу значений не вернуть на клиент и какие есть способы передачи

Клиентский код управляемого приложения работает в тонком клиенте, веб-клиенте или мобильном. Набор типов там урезан. ТаблицаЗначений, ДеревоЗначений, РезультатЗапроса, Запрос, объекты документов и справочников живут только на сервере.

Если написать Новый ТаблицаЗначений в процедуре с директивой &НаКлиенте, модуль не пройдёт проверку: платформа сообщит, что тип не определён. Если вернуть таблицу из серверной функции, вызов упадёт уже при выполнении.

С сервера на клиент свободно проходят:

  • 🔹 примитивы: Строка, Число, Дата, Булево, Неопределено;
  • 🔹 ссылки на объекты базы и УникальныйИдентификатор;
  • 🔹 коллекции Массив, Структура, Соответствие и их фиксированные версии, если внутри тоже только передаваемые типы;
  • 🔹 данные формы: реквизиты формы, в том числе табличные.

Отсюда пять рабочих способов:

СпособКогда подходитМинусы
Массив структур из функцииКлиенту нужно пройти по строкам в кодеКаждая строка тянет имена полей, тяжелее на больших объёмах
Реквизит формы «Таблица значений»Данные надо показать пользователю в таблицеУтяжеляет каждый контекстный вызов сервера
Строка JSONДанные уходят дальше: в веб-сервис, файл, внешний компонентСсылки и даты требуют преобразования
XML или ЗначениеВСтрокуВнутрТаблицу надо «заморозить» и восстановить на сервереНа клиенте прочитать как таблицу нельзя
Временное хранилищеТаблица большая, клиенту нужна по частям или потомАдрес живёт ограниченное время

Возврат таблицы из серверной функции в виде массива структур

Самый частый приём. Серверная функция выполняет запрос, раскладывает строки таблицы по структурам и возвращает массив. На клиенте с ним работают как с обычной коллекцией.

Серверная часть, универсальное преобразование:

&НаСервереБезКонтекста
Функция ТаблицаВМассивСтруктур(Таблица)

    ИменаКолонок = Новый Массив;
    Для Каждого Колонка Из Таблица.Колонки Цикл
        ИменаКолонок.Добавить(Колонка.Имя);
    КонецЦикла;
    СтрокаКлючей = СтрСоединить(ИменаКолонок, ",");

    Результат = Новый Массив;
    Для Каждого СтрокаТаблицы Из Таблица Цикл
        Элемент = Новый Структура(СтрокаКлючей);
        ЗаполнитьЗначенияСвойств(Элемент, СтрокаТаблицы);
        Результат.Добавить(Элемент);
    КонецЦикла;

    Возврат Результат;

КонецФункции

ЗаполнитьЗначенияСвойств копирует значения по совпадающим именам, поэтому вложенный цикл по колонкам не нужен. Структура с заранее заданными ключами создаётся быстрее, чем структура с вызовами Вставить на каждое поле.

В конфигурациях на БСП уже есть готовая функция ОбщегоНазначения.ТаблицаЗначенийВМассив. Свою имеет смысл писать, только если нужно отобрать часть колонок или переименовать поля.

Функция, которую вызывает клиент:

&НаСервереБезКонтекста
Функция КонтрагентыБезИНН()

    Запрос = Новый Запрос;
    Запрос.Текст =
    "ВЫБРАТЬ
    |    Контрагенты.Ссылка КАК Ссылка,
    |    Контрагенты.Наименование КАК Наименование
    |ИЗ
    |    Справочник.Контрагенты КАК Контрагенты
    |ГДЕ
    |    НЕ Контрагенты.ЭтоГруппа
    |    И Контрагенты.ИНН = """"";

    Возврат ТаблицаВМассивСтруктур(Запрос.Выполнить().Выгрузить());

КонецФункции

Клиентская часть:

&НаКлиенте
Процедура ПроверитьКонтрагентов(Команда)

    Список = КонтрагентыБезИНН();
    Для Каждого Элемент Из Список Цикл
        Сообщить(СтрШаблон("Не заполнен ИНН: %1", Элемент.Наименование));
    КонецЦикла;

КонецПроцедуры

Наименование берём из отдельного поля, а не через Элемент.Ссылка.Наименование. В тонком клиенте обращаться через точку к реквизитам ссылки нельзя. Всё, что нужно клиенту, кладите в структуру заранее, ещё на сервере.

Если клиенту нужна одна колонка, структуры не нужны вовсе: Таблица.ВыгрузитьКолонку("Ссылка") сразу даёт массив.

Модификатор Знач в параметрах серверных процедур

Таблицу почти всегда получают по каким-то условиям, которые клиент отдаёт на сервер: период, склад, список ссылок. Тут легко потерять трафик на ровном месте.

Когда клиент вызывает серверную процедуру, параметр без Знач передаётся «по ссылке». Для клиент-серверного вызова это значит, что значение сериализуется на клиенте, уходит на сервер, а после вызова платформа везёт его обратно, чтобы клиент увидел возможные изменения. С Знач значение идёт только в одну сторону.

// Массив из нескольких тысяч ссылок уйдёт на сервер и не вернётся обратно
&НаСервереБезКонтекста
Функция ДанныеПоДокументам(Знач МассивСсылок, Знач ДатаОстатков)
    // ...
КонецФункции

Правило простое:

  1. Параметр только читается на сервере, ставьте Знач.
  2. Сервер должен вернуть клиенту результат, верните его через Возврат, а не через изменение параметра.
  3. Возвращать несколько значений удобно одной структурой: Новый Структура("Таблица, Итог, Ошибки", ...).

С Знач изменения параметра на сервере клиент не увидит. Если старый код рассчитывал, что сервер «допишет» строки в переданный массив, после добавления Знач он молча перестанет работать.

Директива тоже влияет на объём. &НаСервере везёт на сервер данные формы, &НаСервереБезКонтекста нет. Для функции, которая только читает базу и возвращает массив, контекст формы не нужен.

Таблица в реквизите формы: передача для показа пользователю

Если таблицу надо показать в форме, массив не нужен. Создайте реквизит формы с типом ТаблицаЗначений, добавьте ему колонки в редакторе реквизитов и перетащите в дерево элементов: получится элемент «Таблица формы».

На сервере реквизит заполняют двумя равноценными способами:

&НаСервере
Процедура ЗаполнитьНаСервере()

    Таблица = ПолучитьТаблицуИзЗапроса(); // обычная ТаблицаЗначений

    // Вариант 1: метод коллекции данных формы
    ТаблицаДанных.Загрузить(Таблица);

    // Вариант 2: метод формы
    // ЗначениеВРеквизитФормы(Таблица, "ТаблицаДанных");

КонецПроцедуры

Колонки сопоставляются по именам, поэтому псевдонимы в запросе должны совпадать с именами колонок реквизита. Лишние колонки запроса просто не попадут в форму.

Обратное преобразование нужно, когда на сервере удобнее работать с настоящей таблицей значений: ТаблицаДанных.Выгрузить() или РеквизитФормыВЗначение("ТаблицаДанных").

На клиенте реквизит доступен как коллекция строк:

&НаКлиенте
Функция СуммаПоКонтрагенту(Контрагент)

    Итого = 0;
    Отбор = Новый Структура("Контрагент", Контрагент);
    Для Каждого Строка Из ТаблицаДанных.НайтиСтроки(Отбор) Цикл
        Итого = Итого + Строка.Сумма;
    КонецЦикла;
    Возврат Итого;

КонецФункции

Цена удобства: реквизит участвует в каждом контекстном вызове &НаСервере. Таблица на десятки тысяч строк в форме, где кнопки постоянно дёргают сервер с контекстом, заметно тормозит.

Сериализация таблицы в JSON и XML

JSON: когда данные идут дальше клиента

ЗаписатьJSON не принимает таблицу значений напрямую. Таблицу сначала превращают в массив структур, а ссылки заменяют строками: идентификатором или представлением.

&НаСервереБезКонтекста
Функция ДокументыВJSON(Знач Начало, Знач Конец)

    Таблица = ВыбратьДокументы(Начало, Конец); // колонки: Номер, Дата, Сумма, Контрагент

    Массив = Новый Массив;
    Для Каждого Стр Из Таблица Цикл
        Массив.Добавить(Новый Структура("Номер, Дата, Сумма, Контрагент",
            Стр.Номер, Стр.Дата, Стр.Сумма, Строка(Стр.Контрагент)));
    КонецЦикла;

    Запись = Новый ЗаписьJSON;
    Запись.УстановитьСтроку();
    ЗаписатьJSON(Запись, Массив);
    Возврат Запись.Закрыть();

КонецФункции

На клиенте строку читают обратно. Даты в JSON хранятся текстом, поэтому имя поля с датой передаём третьим параметром, иначе получим строку вместо даты:

&НаКлиенте
Процедура ПрочитатьДокументы(ТекстJSON)

    Чтение = Новый ЧтениеJSON;
    Чтение.УстановитьСтроку(ТекстJSON);
    Документы = ПрочитатьJSON(Чтение, Ложь, "Дата");
    Чтение.Закрыть();

    // Документы — массив структур с полями Номер, Дата, Сумма, Контрагент

КонецПроцедуры

Для обмена внутри одной формы JSON избыточен: массив структур передаётся и без него. JSON нужен, когда строку дальше отправляют HTTP-запросом, пишут в файл или отдают внешнему компоненту.

XML и внутренняя строка: «заморозить» таблицу

Таблица значений сериализуется в XDTO целиком, с колонками и типами:

&НаСервереБезКонтекста
Функция ТаблицаВXML(Таблица)
    Запись = Новый ЗаписьXML;
    Запись.УстановитьСтроку();
    СериализаторXDTO.ЗаписатьXML(Запись, Таблица);
    Возврат Запись.Закрыть();
КонецФункции

Восстанавливается она на сервере через СериализаторXDTO.ПрочитатьXML. Короче вариант с ЗначениеВСтрокуВнутр и ЗначениеИзСтрокиВнутр: оба метода вызывайте на сервере.

Клиент такую строку только хранит, например в строковом реквизите формы, и возвращает на сервер при следующем вызове. Работать с ней как с таблицей клиент не может.

Временное хранилище: таблица остаётся на сервере

Когда таблица большая, а клиенту она нужна не сразу или по частям, её вообще не стоит гонять туда-обратно. Таблицу кладут во временное хранилище, клиенту отдают строку-адрес.

&НаСервере
Процедура ПодготовитьДанныеНаСервере()

    Таблица = ПолучитьТаблицуИзЗапроса();
    АдресТаблицы = ПоместитьВоВременноеХранилище(Таблица, УникальныйИдентификатор);
    ВсегоСтрок = Таблица.Количество();

КонецПроцедуры

АдресТаблицы и ВсегоСтрок здесь реквизиты формы. Второй параметр, уникальный идентификатор формы, привязывает значение к форме: после её закрытия платформа удалит его сама. Без этого параметра значение может исчезнуть раньше, чем вы за ним вернётесь.

Дальше клиент запрашивает данные порциями:

&НаСервереБезКонтекста
Функция СтраницаТаблицы(Знач Адрес, Знач НомерСтраницы, Знач РазмерСтраницы)

    Таблица = ПолучитьИзВременногоХранилища(Адрес);
    Если Таблица = Неопределено Тогда
        Возврат Неопределено;
    КонецЕсли;

    Результат = Новый Массив;
    Начало = (НомерСтраницы - 1) * РазмерСтраницы;
    Конец = Мин(Начало + РазмерСтраницы, Таблица.Количество()) - 1;

    Для Индекс = Начало По Конец Цикл
        Стр = Таблица[Индекс];
        Результат.Добавить(Новый Структура("Наименование, Сумма",
            Стр.Наименование, Стр.Сумма));
    КонецЦикла;

    Возврат Результат;

КонецФункции

Проверка на числах: 250 строк, страница 100 строк. Первая страница берёт индексы 0–99, третья 200–249. Для четвёртой начало 300 больше конца 249, цикл не выполнится, вернётся пустой массив.

Ещё два случая для хранилища:

  • 📦 результат фонового задания: задание кладёт таблицу по заранее выданному адресу, форма забирает её после завершения;
  • 📦 файл от пользователя: клиент помещает двоичные данные в хранилище, сервер читает их оттуда и разбирает в таблицу.

Когда данные больше не нужны, освободите память вызовом УдалитьИзВременногоХранилища(Адрес).

Как ускорить передачу таблиц между сервером и клиентом

Медленная передача обычно складывается из лишних колонок, лишних строк и лишних вызовов. Что проверить:

  1. Запрос выбирает только нужные поля. ВЫБРАТЬ * из документа тянет все реквизиты, а клиенту нужны три.
  2. Строк столько, сколько человек увидит. Для просмотра хватит ВЫБРАТЬ ПЕРВЫЕ 500 или постраничной выдачи из хранилища.
  3. Один вызов вместо цикла вызовов. Сто вызовов сервера по одной строке медленнее одного вызова на сто строк, даже если данных столько же.
  4. &НаСервереБезКонтекста, где форма не нужна. И Знач для входных параметров.
  5. Представления готовятся на сервере. Через ПРЕДСТАВЛЕНИЕ() в запросе или Строка(Ссылка) при упаковке.
  6. Повторно не запрашивать. Неизменный справочник для формы достаточно получить один раз при открытии и держать в реквизите.

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

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

Пример: таблица документов реализации в форме обработки

Задача: в обработке для 1С:Бухгалтерии 3.0 показать проведённые реализации за период, посчитать итог и открыть выбранные документы.

Реквизиты формы:

  • ДатаНачала, ДатаОкончания, тип Дата;
  • Документы, тип ТаблицаЗначений с колонками Ссылка (ДокументСсылка.РеализацияТоваровУслуг), Дата, Номер, Контрагент, Сумма;
  • ИтогоСумма, тип Число.

Заполнение на сервере:

&НаКлиенте
Процедура Заполнить(Команда)
    ЗаполнитьНаСервере();
КонецПроцедуры

&НаСервере
Процедура ЗаполнитьНаСервере()

    Запрос = Новый Запрос;
    Запрос.Текст =
    "ВЫБРАТЬ
    |    Реализация.Ссылка КАК Ссылка,
    |    Реализация.Дата КАК Дата,
    |    Реализация.Номер КАК Номер,
    |    Реализация.Контрагент КАК Контрагент,
    |    Реализация.СуммаДокумента КАК Сумма
    |ИЗ
    |    Документ.РеализацияТоваровУслуг КАК Реализация
    |ГДЕ
    |    Реализация.Проведен
    |    И Реализация.Дата МЕЖДУ &Начало И &Конец
    |
    |УПОРЯДОЧИТЬ ПО
    |    Реализация.Дата";
    Запрос.УстановитьПараметр("Начало", НачалоДня(ДатаНачала));
    Запрос.УстановитьПараметр("Конец", КонецДня(ДатаОкончания));

    Таблица = Запрос.Выполнить().Выгрузить();
    Документы.Загрузить(Таблица);
    ИтогоСумма = Таблица.Итог("Сумма");

КонецПроцедуры

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

Открытие выделенных строк целиком на клиенте, без обращения к серверу за данными строк:

&НаКлиенте
Процедура ОткрытьВыделенные(Команда)

    Для Каждого Идентификатор Из Элементы.Документы.ВыделенныеСтроки Цикл
        Строка = Документы.НайтиПоИдентификатору(Идентификатор);
        Если Строка <> Неопределено Тогда
            ПоказатьЗначение(, Строка.Ссылка);
        КонецЕсли;
    КонецЦикла;

КонецПроцедуры

Для множественного выделения у элемента таблицы формы включите свойство «Режим выделения» в значение «Множественный».

Если те же документы нужны не для показа, а, например, для выгрузки во внешний сервис, вместо реквизита формы верните из &НаСервереБезКонтекста массив структур или строку JSON из разделов выше.

Типовые ошибки при передаче таблиц и ответы на вопросы

Диагностика: выберите симптом

Ещё одна ловушка: изменение структуры из массива на клиенте не меняет таблицу на сервере. Клиент получил копию. Чтобы сохранить правки, отправьте изменённые данные на сервер отдельным вызовом.

А в толстом клиенте таблицу значений можно передать на клиент?
В обычном приложении на толстом клиенте тип ТаблицаЗначений доступен и на клиенте. Ограничение касается управляемого приложения: тонкого, веб- и мобильного клиента.
Как передать дерево значений?
Так же, как таблицу. Для показа создайте реквизит формы типа «Дерево значений», на клиенте он виден как ДанныеФормыДерево. Для обработки в коде разверните дерево в массив структур с вложенными массивами строк.
Можно ли вернуть на клиент результат запроса или выборку?
Нет, РезультатЗапроса и выборка существуют только на сервере. Выгрузите результат в таблицу значений и преобразуйте её одним из способов выше.
Что быстрее: массив структур или реквизит формы?
Для разового получения данных в код разница небольшая. Реквизит выгоднее, когда данные нужно показать и редактировать, массив, когда клиенту нужно один раз пройти по строкам без формы.
Как передать таблицу в обратную сторону, с клиента на сервер?
Табличный реквизит формы сервер видит сам при вызове &НаСервере. Массив структур передайте параметром с Знач и на сервере загрузите в новую таблицу значений через ЗаполнитьЗначенияСвойств.