Как получить заголовок колонки на управляемых формах и сохранить данные динамической таблицы-шахматки?

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

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

Для реализации этой задачи в базе данных обычно создают простую и надежную структуру: плоскую табличную часть документа с тремя реквизитами: Измерение1, Измерение2 и Значение. На саму форму эту табличную часть не выводят. Вместо этого программно создают динамическую таблицу значений, колонками которой становятся реальные объекты базы данных из второго измерения. Однако при попытке сохранить измененные пользователем данные обратно в документ разработчики натыкаются на серьезную проблему платформы 1С: при конвертации данных формы на сервере заголовки колонок бесследно исчезают.

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

Почему пропадают заголовки при выгрузке? Разбор технической причины

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

ТЗ_ТаблицаШахматка = РеквизитФормыВЗначение("ТаблицаШахматка");

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

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

Почему поиск по текстовым заголовкам — это антипаттерн?

Некоторые начинающие разработчики пытаются обойти проблему, передавая заголовки на сервер в виде текстовых массивов и выполняя поиск элементов базы данных по их наименованию с помощью метода НайтиПоНаименованию(). Проанализируем, почему этого делать ни в коем случае нельзя:

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

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

---

Способ А. Шифрование Уникального Идентификатора (UUID) в имени колонки

Это самый лаконичный и производительный способ, который не требует создания дополнительных объектов или реквизитов формы. Имя колонки таблицы значений гарантированно сохраняется при вызове метода РеквизитФормыВЗначение(). Мы можем использовать это поведение платформы и зашить ссылку на объект непосредственно в имя колонки.

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

Рассмотрим пример программной генерации колонок на сервере:


&НаСервере
Процедура СоздатьШахматкуНаСервере()
    
    // Получаем список объектов для колонок (например, склады)
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |	Склады.Ссылка КАК Ссылка,
        |	Склады.Наименование КАК Наименование
        |ИЗ
        |	Справочник.Склады КАК Склады";
    
    Выборка = Запрос.Выполнить().Выбрать();
    
    МассивРеквизитов = Новый Массив;
    
    Пока Выборка.Следующий() Цикл
        
        // Преобразуем UUID ссылки в валидный идентификатор колонки
        СтрокаУИД = СтрЗаменить(Строка(Выборка.Ссылка.УникальныйИдентификатор()), "-", "_");
        ИмяКолонки = "Кол_" + СтрокаУИД;
        
        // Добавляем реквизит формы (имя хранит UUID, заголовок - понятное имя для пользователя)
        НовыйРеквизит = Новый РеквизитФормы(
            ИмяКолонки, 
            Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(5, 2)), 
            "ТаблицаШахматка", 
            Выборка.Наименование
        );
        МассивРеквизитов.Добавить(НовыйРеквизит);
        
    КонецЦикла;
    
    // Изменяем состав реквизитов формы
    ИзменитьРеквизиты(МассивРеквизитов);
    
    // Создаем элементы управления на форме для отображения колонок
    Для Каждого Реквизит Из МассивРеквизитов Цикл
        ЭлементКолонка = Элементы.Добавить(Реквизит.Имя, Тип("ПолеФормы"), Элементы.ТаблицаШахматка);
        ЭлементКолонка.Вид = ВидПоляФормы.ПолеВвода;
        ЭлементКолонка.ПутьКДанным = "ТаблицаШахматка." + Реквизит.Имя;
    КонецЦикла;
    
КонецПроцедуры

Теперь разберем, как прочитать эти данные в процедуре записи документа на сервере и восстановить ссылки на объекты:


&НаСервере
Процедура ПриЗаписиНаСервере(Отказ, ТекущийОбъект, ПараметрыЗаписи)
    
    // Преобразуем коллекцию формы в обычную таблицу значений
    ТЗ_Шахматка = РеквизитФормыВЗначение("ТаблицаШахматка");
    
    // Очищаем табличную часть документа перед перезаписью
    ТекущийОбъект.ИсходныеДанные.Очистить();
    
    Для Каждого СтрокаТЗ Из ТЗ_Шахматка Цикл
        
        Для Каждого Колонка Из ТЗ_Шахматка.Колонки Цикл
            
            // Фильтруем системные колонки, обрабатываем только динамические
            Если Лев(Колонка.Имя, 4) = "Кол_" Тогда
                
                // Восстанавливаем дефисы в UUID из имени колонки
                СтрокаУИД = СтрЗаменить(Сред(Колонка.Имя, 5), "_", "-");
                
                Попытка
                    УИД = Новый УникальныйИдентификатор(СтрокаУИД);
                    // Получаем реальную ссылку на объект из базы данных без запросов
                    СсылкаИзмерение2 = Справочники.Склады.ПолучитьСсылку(УИД);
                Исключение
                    Продолжить; // Защита от непредвиденных ошибок приведения типов
                КонецПопытки;
                
                ЗначениеПроцента = СтрокаТЗ[Колонка.Имя];
                
                // Записываем только заполненные ячейки таблицы-шахматки
                Если ЗначениеПроцента <> 0 Тогда
                    НоваяСтрока = ТекущийОбъект.ИсходныеДанные.Добавить();
                    НоваяСтрока.Измерение1 = СтрокаТЗ.Измерение1; // Значение по вертикали
                    НоваяСтрока.Измерение2 = СсылкаИзмерение2;    // Восстановленный склад
                    НоваяСтрока.Значение   = ЗначениеПроцента;
                КонецЕсли;
                
            КонецЕсли;
            
        КонецЦикла;
        
    КонецЦикла;
    
КонецПроцедуры

---

Способ Б. Использование таблицы соответствия в реквизитах формы

Если вам неудобно преобразовывать UUID в имена колонок (например, вы хотите использовать простые имена по типу Колонка1, Колонка2), отличным решением будет создание скрытого вспомогательного реквизита на форме.

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

Рассмотрим по шагам, как работать с картой соответствия:

  1. При генерации динамических колонок мы записываем пару «ИмяКолонки — СсылкаНаОбъект» в нашу таблицу соответствия.
  2. При записи документа мы обращаемся к этой таблице для мгновенного получения ссылок.

Пример реализации генерации колонок с заполнением карты соответствия:


&НаСервере
Процедура СоздатьШахматкуСКартойСоответствия()
    
    // Получаем данные для колонок
    Запрос = Новый Запрос("ВЫБРАТЬ Ссылка, Наименование ИЗ Справочник.Склады");
    Выборка = Запрос.Выполнить().Выбрать();
    
    МассивРеквизитов = Новый Массив;
    Счетчик = 1;
    
    // Очищаем карту колонок перед заполнением
    КартаКолонок.Очистить();
    
    Пока Выборка.Следующий() Цикл
        
        ИмяКолонки = "ДинамическаяКолонка_" + Счетчик;
        
        // Регистрируем соответствие в скрытом реквизите формы
        НовоеСоответствие = КартаКолонок.Добавить();
        НовоеСоответствие.ИмяКолонки = ИмяКолонки;
        НовоеСоответствие.СсылкаНаОбъект = Выборка.Ссылка;
        
        НовыйРеквизит = Новый РеквизитФормы(
            ИмяКолонки, 
            Новый ОписаниеТипов("Число"), 
            "ТаблицаШахматка", 
            Выборка.Наименование
        );
        МассивРеквизитов.Добавить(НовыйРеквизит);
        
        Счетчик = Счетчик + 1;
        
    КонецЦикла;
    
    ИзменитьРеквизиты(МассивРеквизитов);
    
    // Добавление элементов управления на форму...
    
КонецПроцедуры

А теперь посмотрим, насколько просто извлекать данные при записи на сервере с использованием метода НайтиСтроки():


&НаСервере
Процедура СохранитьДанныеЧерезКартуКолонок(ТекущийОбъект)
    
    ТЗ_Шахматка = РеквизитФормыВЗначение("ТаблицаШахматка");
    
    ТекущийОбъект.ИсходныеДанные.Очистить();
    
    Для Каждого СтрокаТЗ Из ТЗ_Шахматка Цикл
        
        Для Каждого Колонка Из ТЗ_Шахматка.Колонки Цикл
            
            // Ищем колонку в нашей карте соответствий
            ПараметрыОтбора = Новый Структура("ИмяКолонки", Колонка.Имя);
            НайденныеСтроки = КартаКолонок.НайтиСтроки(ПараметрыОтбора);
            
            Если НайденныеСтроки.Количество() > 0 Тогда
                // Ссылка найдена мгновенно и без обращений к базе данных!
                СсылкаИзмерение2 = НайденныеСтроки[0].СсылкаНаОбъект;
                
                ЗначениеПроцента = СтрокаТЗ[Колонка.Имя];
                
                Если ЗначениеПроцента <> 0 Тогда
                    НоваяСтрока = ТекущийОбъект.ИсходныеДанные.Добавить();
                    НоваяСтрока.Измерение1 = СтрокаТЗ.Измерение1;
                    НоваяСтрока.Измерение2 = СсылкаИзмерение2;
                    НоваяСтрока.Значение   = ЗначениеПроцента;
                КонецЕсли;
            КонецЕсли;
            
        КонецЦикла;
        
    КонецЦикла;
    
КонецПроцедуры

---

Способ В. Получение оригинальных заголовков через элементы формы

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

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

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


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

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


Для Каждого Колонка Из ТЗ_Шахматка.Колонки Цикл
    ЗаголовокВизуальный = ПолучитьЗаголовокПоляНаФорме(Колонка.Имя);
    // Теперь у нас есть и имя колонки в данных, и ее отображаемый заголовок!
КонецЦикла;

---

Сравнение и выбор оптимального решения

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

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

← На главную