Как работать с табличными данными в клиентском коде управляемой формы

· 9 мин

Содержание

В тонком и веб-клиенте 1С 8.3 объект ТаблицаЗначений не существует: строка Новый ТаблицаЗначений в процедуре с директивой &НаКлиенте даёт ошибку Тип не определен (ТаблицаЗначений). Таблица значений доступна на сервере, во внешнем соединении и в толстом клиенте. На клиенте с табличными данными работают через реквизит формы типа «Таблица значений» — в клиентском коде он виден как ДанныеФормыКоллекция.

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

Где таблица значений есть, а где её нет

Доступность типа зависит от того, где выполняется код. Это первое, что нужно держать в голове.

Где выполняется кодТаблицаЗначенийЧем работать с таблицей
Сервер (&НаСервере, &НаСервереБезКонтекста, серверный общий модуль)✅ естьСама таблица значений
Тонкий клиент, веб-клиент❌ нетДанныеФормыКоллекция, массив структур
Толстый клиент, обычное приложение✅ естьТаблица значений, табличное поле
Толстый клиент, управляемое приложение✅ формально естьЛучше как в тонком клиенте
Внешнее соединение (COM)✅ естьТаблица значений

Последний пункт важен. В толстом клиенте управляемого приложения код с Новый ТаблицаЗначений на клиенте скомпилируется и сработает. Но тот же модуль откроют в тонком клиенте или в браузере — и форма сломается.

Если клиентский код всё же нужен только для толстого клиента, оберните его инструкцией препроцессора #Если ТолстыйКлиентОбычноеПриложение Тогда … #КонецЕсли. Без неё модуль не пройдёт синтаксический контроль для тонкого клиента.

Почему так сделано. Тонкий клиент — лёгкое приложение без прикладных объектов. Он умеет показывать форму и выполнять простые операции с её данными. Свёртка, итоги, индексы и прочая работа с таблицами — задача сервера.

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

Чтобы на клиенте появилась таблица, создают реквизит формы. Есть два пути: в конфигураторе и программно.

В конфигураторе

  1. Откройте форму в конфигураторе, вкладка Реквизиты.
  2. Добавьте реквизит, например Товары, тип — ТаблицаЗначений.
  3. Раскройте реквизит и добавьте колонки: Наименование (Строка), Количество (Число 15.3), Цена (Число 15.2), Сумма (Число 15.2).
  4. Перетащите реквизит на вкладку Элементы. Появится таблица формы со всеми колонками.

Колонки задаются только здесь или на сервере. На клиенте добавить колонку в реквизит нельзя: у ДанныеФормыКоллекция нет свойства Колонки.

💡 Табличная часть объекта (Объект.Товары в форме документа) на клиенте ведёт себя так же, как реквизит типа «Таблица значений». Всё, что написано ниже, относится и к ней.

Программно, когда состав колонок заранее неизвестен

Колонки по данным (склады, периоды, виды цен) создают на сервере методом ИзменитьРеквизиты(). После этого реквизит нужно вывести на форму элементами.

&НаСервере
Процедура СоздатьТаблицуОстатков()

    Добавляемые = Новый Массив;
    Добавляемые.Добавить(Новый РеквизитФормы("Остатки",
        Новый ОписаниеТипов("ТаблицаЗначений"), , "Остатки"));
    Добавляемые.Добавить(Новый РеквизитФормы("Товар",
        Новый ОписаниеТипов("Строка", , Новый КвалификаторыСтроки(150)), "Остатки", "Товар"));
    Добавляемые.Добавить(Новый РеквизитФормы("Количество",
        Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 3)), "Остатки", "Количество"));
    ИзменитьРеквизиты(Добавляемые);

    ТаблицаФормы = Элементы.Добавить("Остатки", Тип("ТаблицаФормы"));
    ТаблицаФормы.ПутьКДанным = "Остатки";

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

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

Вызывайте такую процедуру из ПриСозданииНаСервере. Повторный вызов с тем же именем реквизита даст ошибку — проверяйте, создан ли он уже.

Заполнение таблицы данными на клиенте

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

Добавление строки по кнопке

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

    НоваяСтрока = Товары.Добавить();
    НоваяСтрока.Наименование = "Бумага А4";
    НоваяСтрока.Количество = 5;
    НоваяСтрока.Цена = 350;
    НоваяСтрока.Сумма = НоваяСтрока.Количество * НоваяСтрока.Цена; // 1750

    Элементы.Товары.ТекущаяСтрока = НоваяСтрока.ПолучитьИдентификатор();

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

Для позиционирования используйте идентификатор строки, а не индекс. Индекс меняется при удалении и вставке строк, идентификатор — нет.

Пересчёт при изменении поля

Обработчик ПриИзменении колонки работает с текущими данными таблицы формы:

&НаКлиенте
Процедура ТоварыКоличествоПриИзменении(Элемент)
    ТекущиеДанные = Элементы.Товары.ТекущиеДанные;
    ТекущиеДанные.Сумма = ТекущиеДанные.Количество * ТекущиеДанные.Цена;
КонецПроцедуры

Такой пересчёт не требует обращения к серверу. Форма отвечает мгновенно даже в веб-клиенте.

Заполнение из запроса

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

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

&НаСервере
Процедура ЗаполнитьНаСервере()
    Запрос = Новый Запрос("ВЫБРАТЬ ... ");
    ЗначениеВРеквизитФормы(Запрос.Выполнить().Выгрузить(), "Товары");
КонецПроцедуры

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

Методы обработки строк в клиентском коде

У ДанныеФормыКоллекция набор методов меньше, чем у таблицы значений. На клиенте уверенно работают:

  • ➕ Добавить(), Вставить(Индекс) — новая строка в конец или в позицию;
  • ➖ Удалить(Строка или Индекс), Очистить();
  • 🔢 Количество(), Индекс(Строка), обращение по индексу Товары[0];
  • 🔍 НайтиСтроки(Структура) — возвращает массив строк по отбору;
  • 🆔 НайтиПоИдентификатору(Ид) и у строки ПолучитьИдентификатор();
  • ↕️ Сдвинуть(Строка, Смещение) — перемещение строки вверх или вниз.

Поиск строк по отбору

Отбор = Новый Структура("Наименование", "Бумага А4");
Найденные = Товары.НайтиСтроки(Отбор);
Если Найденные.Количество() > 0 Тогда
    Найденные[0].Количество = Найденные[0].Количество + 1;
КонецЕсли;

Удаление строк по условию

Удаляют с конца, иначе после каждого удаления индексы съезжают и часть строк пропускается. Границу цикла считают один раз до начала:

Всего = Товары.Количество();
Для Сч = 1 По Всего Цикл
    Строка = Товары[Всего - Сч];
    Если Строка.Количество = 0 Тогда
        Товары.Удалить(Строка);
    КонецЕсли;
КонецЦикла;

При пяти строках индексы пойдут 4, 3, 2, 1, 0. Удаление строки с индексом 3 не трогает строки 0–2, которые ещё впереди.

Итог по колонке

Метода Итог() на клиенте нет. Для подвала таблицы или проверки перед записью сумму считают циклом:

ВсегоСумма = 0;
Для Каждого Строка Из Товары Цикл
    ВсегоСумма = ВсегоСумма + Строка.Сумма;
КонецЦикла;

💡 Чтобы показать итог под таблицей, включите у таблицы формы свойство Подвал и укажите у колонки путь к данным подвала — числовой реквизит формы, в который этот цикл кладёт сумму.

Что делать только на сервере

Свернуть(), Итог(), ВыгрузитьКолонку(), Скопировать(), Колонки.Добавить(), Индексы.Добавить() — методы таблицы значений, на клиенте их нет. Сортировку и группировку больших объёмов тоже надёжнее делать на сервере: там доступна полная таблица значений и запрос с СГРУППИРОВАТЬ ПО.

Передача табличных данных на сервер и обратно

Есть четыре рабочих способа. Выбор зависит от объёма данных и от того, нужен ли серверу контекст формы.

СпособКогда подходитМинус
Контекстный вызов &НаСервереДанные уже в реквизите формыНа сервер уходят все данные формы
&НаСервереБезКонтекста + массив структурНужна часть строк, форма тяжёлаяКод преобразования туда и обратно
Временное хранилищеБольшая таблица живёт между вызовамиНужно следить за адресом
ХранилищеЗначенияСохранить таблицу в реквизите объектаНа клиенте содержимое не прочитать

Контекстный вызов

В процедуре &НаСервере реквизит превращается в настоящую таблицу значений:

&НаСервере
Процедура СвернутьНаСервере()
    ТЗ = РеквизитФормыВЗначение("Товары");
    ТЗ.Свернуть("Наименование, Цена", "Количество, Сумма");
    ЗначениеВРеквизитФормы(ТЗ, "Товары");
КонецПроцедуры

Для табличной части объекта то же делают через Объект.Товары.Выгрузить() и Объект.Товары.Загрузить(ТЗ). Оба метода работают только на сервере.

Без контекста: массив структур

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

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

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

Обратное преобразование — таблицу значений в массив структур — делают на сервере перед возвратом результата:

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

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

Временное хранилище

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

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

&НаСервереБезКонтекста
Функция КоличествоСтрок(Адрес)
    ТЗ = ПолучитьИзВременногоХранилища(Адрес);
    Возврат ТЗ.Количество();
КонецФункции

АдресТаблицы — строковый реквизит формы. Второй параметр УникальныйИдентификатор привязывает данные к форме: хранилище очистится, когда форму закроют.

Сериализация в строку

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

Чем клиентская коллекция отличается от таблицы значений

Сводка ограничений, на которые чаще всего натыкаются:

  • 🚫 нельзя создать коллекцию в коде клиента — только реквизит формы;
  • 🚫 нельзя добавить или удалить колонку на клиенте;
  • 🚫 нет Свернуть, Итог, Скопировать, ВыгрузитьКолонку, индексов;
  • 🚫 функция с директивой &НаСервере не может вернуть на тонкий клиент таблицу значений;
  • ⚠️ каждый контекстный серверный вызов передаёт все данные формы, включая большие таблицы;
  • ✅ значения строк — те же типы, что у колонок реквизита, включая ссылки;
  • ✅ изменения, сделанные на клиенте, сервер увидит при следующем контекстном вызове.

Из-за третьего пункта работает простое правило. Логику, которой нужны методы таблицы значений, пишите в серверной процедуре сразу. Не пытайтесь эмулировать Свернуть() на клиенте вложенными циклами: это медленно и плохо читается.

Ошибки при работе с таблицей на клиенте и как их найти

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

Как отлаживать

  1. Поставьте точку останова в клиентской процедуре и посмотрите переменную в окне Вычислить выражение (Shift+F9). Тип покажет ДанныеФормыКоллекция — это нормально.
  2. Чтобы увидеть таблицу целиком, остановитесь в серверной процедуре после РеквизитФормыВЗначение(). Отладчик покажет полноценную таблицу значений со всеми колонками.
  3. Запустите Конфигурация → Проверка конфигурации с флажками для тонкого клиента и веб-клиента. Обращения к серверным типам в клиентском коде найдутся ещё до запуска.
  4. Если форма тормозит, включите замер производительности и посмотрите, сколько времени уходит на серверные вызовы формы. Объём передаваемых данных обычно виден по их числу и длительности.

Частые вопросы

Можно ли использовать таблицу значений в клиентском общем модуле?
Нет. Клиентский общий модуль компилируется для тонкого клиента, где этого типа нет. Таблицу собирают в серверном модуле с флажком «Вызов сервера», а на клиент возвращают массив структур.
Как передать таблицу в другую форму при открытии?
Параметры формы должны передаваться между клиентом и сервером. Передайте массив структур или адрес временного хранилища, а в ПриСозданииНаСервере новой формы восстановите таблицу значений.
Почему изменения, сделанные на клиенте, не видны в процедуре &НаСервереБезКонтекста?
Внеконтекстный вызов не получает данные формы. Нужные строки передайте параметром или используйте контекстный вызов &НаСервере.
Как сохранить таблицу значений в реквизите справочника или документа?
Заверните её на сервере в Новый ХранилищеЗначения(ТЗ) и запишите в реквизит типа «Хранилище значения». Получить таблицу обратно — метод Получить(), тоже на сервере.
Работает ли НайтиСтроки на клиенте так же быстро, как на сервере?
На клиенте нет индексов, поэтому поиск идёт перебором. Для сотен строк это незаметно. Для десятков тысяч ищите на сервере, предварительно добавив индекс по колонке отбора.