При переходе на управляемые формы в 1С 8.3 начинающие разработчики часто сталкиваются с непониманием клиент-серверной архитектуры. Одна из самых распространенных ошибок при попытке заполнить данные в строке табличной части — это «Ошибка отображения типов: Отсутствует отображение для типа 'ДанныеФормыЭлементКоллекции'». В этой статье мы подробно разберем причины возникновения этой проблемы, проанализируем неверные подходы и рассмотрим два правильных способа решения в соответствии со стандартами разработки фирмы «1С».
Для начала давайте разберемся, что представляет собой тип ДанныеФормыЭлементКоллекции. В управляемом приложении этот тип является клиентским «отражением» строки таблицы на форме. Он предназначен исключительно для визуализации данных в интерфейсе на стороне клиента и тесно связан со свойствами самого элемента управления на форме.
Когда мы пытаемся передать переменную, содержащую текущую строку табличной части (например, полученную через Элементы.ИмяТаблицы.ТекущиеДанные), в процедуру или функцию на сервере, платформа выполняет процесс сериализации. Сериализация — это преобразование объекта в XML/XDTO-формат для его передачи по сети между клиентом и сервером. Поскольку тип ДанныеФормыЭлементКоллекции жестко привязан к контексту отображения формы на клиенте, платформа физически не может передать его на сервер «как есть». В результате этого процесса мы и получаем ошибку преобразования XDTO.
Ключевое правило управляемого приложения: нельзя передавать элементы интерфейса и связанные с ними типы данных напрямую с клиента на сервер во внеконтекстные вызовы или общие модули.
Рассмотрим код, который часто пытаются написать начинающие разработчики:
&НаКлиенте
Процедура НоменклатураПриИзменении(Элемент)
СтрокаТабличнойЧасти = Элементы.ПМ.ТекущиеДанные;
// Попытка передать строку ТЧ напрямую на сервер в общий модуль
ПеремещДок.ПеремещМат(СтрокаТабличнойЧасти);
КонецПроцедуры
В данном случае переменная СтрокаТабличнойЧасти как раз имеет тип ДанныеФормыЭлементКоллекции. Попытка передать ее в процедуру общего модуля гарантированно вызовет ошибку отображения типов. Более того, выполнение запросов или работа с таблицами значений непосредственно на клиенте также невозможны, так как эти объекты существуют исключительно на стороне сервера.
Теперь рассмотрим правильные архитектурные решения этой задачи.
Согласно стандартам разработки «1С:Предприятия», наиболее эффективным и быстрым способом является использование внеконтекстных серверных вызовов. Для этого мы не передаем саму строку, а собираем нужные для запроса параметры в простую Структура на клиенте, передаем ее в функцию с директивой &НаСервереБезКонтекста, получаем результат обратно в виде структуры и заполняем строку ТЧ прямо на клиенте.
Давайте разберем этот процесс по шагам.
Шаг 1. Пишем клиентскую процедуру в модуле формы
На клиенте мы получаем текущую строку нашей табличной части, подготавливаем структуру параметров и вызываем серверную функцию.
&НаКлиенте
Процедура НоменклатураПриИзменении(Элемент)
// Получаем текущую строку табличной части
ТекущаяСтрока = Элементы.ПМ.ТекущиеДанные;
Если ТекущаяСтрока = Неопределено Тогда
Возврат;
КонецЕсли;
// Создаем легкую структуру для передачи параметров на сервер
ПараметрыЗапроса = Новый Структура;
ПараметрыЗапроса.Вставить("Номенклатура", ТекущаяСтрока.Номенклатура);
ПараметрыЗапроса.Вставить("Дата", Объект.Дата); // Используем дату документа
// Получаем данные с сервера без передачи контекста всей формы
РезультатЗаполнения = ПолучитьДанныеДляСтрокиНаСервере(ПараметрыЗапроса);
// Заполняем реквизиты строки ТЧ полученными значениями на клиенте
ЗаполнитьЗначенияСвойств(ТекущаяСтрока, РезультатЗаполнения);
КонецПроцедуры
Шаг 2. Реализуем внеконтекстную функцию на сервере
Создаем функцию с директивой &НаСервереБезКонтекста. Она выполнит запрос к базе данных и вернет структуру с ценой и остатком обратно на клиент.
&НаСервереБезКонтекста
Функция ПолучитьДанныеДляСтрокиНаСервере(ПараметрыЗапроса)
// Инициализируем структуру возврата дефолтными значениями
Результат = Новый Структура("ЦенаЗаЕдиницу, ОстатокНаСкладе", 0, 0);
// Если номенклатура не выбрана, возвращаем пустые показатели
Если Не ЗначениеЗаполнено(ПараметрыЗапроса.Номенклатура) Тогда
Возврат Результат;
КонецЕсли;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЕСТЬNULL(ЦеныСрезПоследних.Цена, 0) КАК ЦенаЗаЕдиницу,
| ЕСТЬNULL(ОстаткиМатериаловОстатки.КоличествоОстаток, 0) КАК ОстатокНаСкладе
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.Цены.СрезПоследних(&Дата, Номенклатура = &Номенклатура) КАК ЦеныСрезПоследних
| ПО (ИСТИНА)
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ОстаткиМатериалов.Остатки(&Дата, Материал = &Номенклатура) КАК ОстаткиМатериаловОстатки
| ПО (ИСТИНА)
|ГДЕ
| Номенклатура.Ссылка = &Номенклатура";
Запрос.УстановитьПараметр("Дата", ?(ЗначениеЗаполнено(ПараметрыЗапроса.Дата), ПараметрыЗапроса.Дата, ТекущаяДатаСеанса()));
Запрос.УстановитьПараметр("Номенклатура", ПараметрыЗапроса.Номенклатура);
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Следующий() Тогда
// Заполняем структуру результатами запроса
ЗаполнитьЗначенияСвойств(Результат, Выборка);
КонецЕсли;
Возврат Результат;
КонецФункции
Этот метод является наиболее предпочтительным, поскольку он минимизирует объем данных, передаваемых между клиентом и сервером. Мы не гоняем тяжелую форму по сети, а передаем только структуру из двух полей и получаем обратно такой же компактный объект.
Бывают ситуации, когда логика заполнения строки зависит от множества других реквизитов формы или табличных частей, собирать которые в структуру неудобно и нецелесообразно. В таких случаях мы можем использовать контекстный вызов сервера (директива &НаСервере).
Поскольку контекстный вызов передает всю форму на сервер целиком, мы имеем прямой доступ к данным объекта через Объект. Передавать строку ТЧ параметром все еще нельзя, но мы можем передать ее уникальный идентификатор.
Рассмотрим, как реализовать этот подход:
&НаКлиенте
Процедура НоменклатураПриИзменении(Элемент)
ТекущаяСтрока = Элементы.ПМ.ТекущиеДанные;
Если ТекущаяСтрока = Неопределено Тогда
Возврат;
КонецЕсли;
// Получаем уникальный числовой идентификатор строки в таблице формы
ИдентификаторСтроки = ТекущаяСтрока.ПолучитьИдентификатор();
// Передаем число на контекстный сервер
ЗаполнитьСтрокуТЧНаСервере(ИдентификаторСтроки);
КонецПроцедуры
&НаСервере
Процедура ЗаполнитьСтрокуТЧНаСервере(ИдентификаторСтроки)
// Находим строку в табличной части Объект по полученному идентификатору
СтрокаТЧ = Объект.ПМ.НайтиПоИдентификатору(ИдентификаторСтроки);
Если СтрокаТЧ <> Неопределено Тогда
// Здесь мы находимся на сервере и можем напрямую работать с базой данных
// Выполняем аналогичный запрос к регистрам сведений и накопления
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЕСТЬNULL(ЦеныСрезПоследних.Цена, 0) КАК ЦенаЗаЕдиницу,
| ЕСТЬNULL(ОстаткиМатериаловОстатки.КоличествоОстаток, 0) КАК ОстатокНаСкладе
|ИЗ
| РегистрСведений.Цены.СрезПоследних(&Дата, Номенклатура = &Номенклатура) КАК ЦеныСрезПоследних
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ОстаткиМатериалов.Остатки(&Дата, Материал = &Номенклатура) КАК ОстаткиМатериаловОстатки
| ПО ОстаткиМатериаловОстатки.Материал = ЦеныСрезПоследних.Номенклатура";
Запрос.УстановитьПараметр("Дата", Объект.Дата);
Запрос.УстановитьПараметр("Номенклатура", СтрокаТЧ.Номенклатура);
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Следующий() Тогда
// Напрямую меняем данные строки внутри контекста формы
СтрокаТЧ.ЦенаЗаЕдиницу = Выборка.ЦенаЗаЕдиницу;
СтрокаТЧ.ОстатокНаСкладе = Выборка.ОстатокНаСкладе;
КонецЕсли;
КонецЕсли;
КонецПроцедуры
В этом примере метод ПолучитьИдентификатор() возвращает уникальный числовой индекс строки таблицы формы. На сервере встроенный метод коллекции НайтиПоИдентификатору() позволяет мгновенно позиционироваться на нужной строке коллекции Объект.ПМ и внести изменения, которые автоматически улетят обратно на клиент.
Обратим внимание на частую ошибку в конструировании запросов для получения цен и остатков по одной позиции номенклатуры. В исходном обсуждении на форуме предлагался следующий вариант связи таблиц:
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.Цены.СрезПоследних(&Дата, ) КАК ЦеныСрезПоследних
ПО (ИСТИНА)
ГДЕ ОстаткиМатериаловОстатки.Материал = ЦеныСрезПоследних.Номенклатура
Никогда не используйте конструкцию ПО (ИСТИНА) совместно с условиями в секции ГДЕ для виртуальных таблиц!
Проанализируем эту ситуацию: при такой записи СУБД сначала выполнит полное Декартово произведение (соединит каждую строку остатков со всеми строками цен без ограничений), создав гигантскую временную таблицу в оперативной памяти (или TempDB), и только после этого применит фильтр ГДЕ. На рабочих базах данных с тысячами товаров это приведет к катастрофическому падению производительности и зависанию системы.
Правильный подход — всегда передавать отбор по конкретной номенклатуре непосредственно в параметры виртуальных таблиц: СрезПоследних(&Дата, Номенклатура = &Номенклатура) и Остатки(&Дата, Материал = &Номенклатура). Это заставит СУБД выбрать только одну нужную запись, обеспечивая мгновенный отклик интерфейса при изменении строки.
Используйте предложенные паттерны проектирования, разделяйте логику интерфейса и серверных расчетов, и ваши управляемые формы будут работать быстро и без досадных ошибок отображения типов.