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