Разработка в среде управляемого приложения 1С:Предприятие 8.3 требует от программиста четкого понимания клиент-серверной архитектуры. Одной из наиболее частых задач является заполнение реквизитов строки табличной части при изменении ключевого значения (например, при выборе номенклатуры или составляющего элемента). Начинающие разработчики часто сталкиваются с техническими трудностями и ошибками производительности, пытаясь выполнить эти операции «напрямую».
В этой статье мы подробно проанализируем типичные ошибки, которые допускают программисты при работе с табличными частями на управляемых формах, выясним причины их возникновения и разберем два правильных, наиболее производительных подхода к решению этой задачи.
Рассмотрим исходную проблему. У нас есть табличная часть Составляющие. При изменении реквизита Наименование (который имеет ссылочный тип, например, справочник) нам необходимо автоматически заполнить реквизит Производитель этой же строки, а также добавить в табличную часть подчиненные элементы из состава этого наименования.
Прежде чем перейти к правильным алгоритмам, давайте разберем, какие архитектурные ошибки часто совершаются на практике, и почему код из обсуждения на форуме считается неоптимальным.
Ошибка 1. Обращение «через точку» на клиенте. Попытка написать на клиенте конструкцию вида ТекДанные.Наименование.Производитель приводит к ошибке выполнения. В управляемом приложении на стороне клиента (в тонком или веб-клиенте) ссылочные данные представляют собой лишь легковесные идентификаторы (ссылки). Они не содержат внутри себя значений реквизитов объекта БД. Чтобы получить значение реквизита по ссылке, необходимо обратиться к серверу.
Ошибка 2. Передача клиентского объекта строки на сервер. Переменная ТекущиеДанные (получаемая через Элементы.ИмяТаблицы.ТекущиеДанные) на клиенте имеет тип ДанныеФормыСтруктура. Передавать ее напрямую в процедуры с директивой &НаСервереБезКонтекста нельзя. При попытке передать ее в контекстный вызов &НаСервере платформа будет выполнять избыточную сериализацию, нагружая сеть.
Ошибка 3. Множественные контекстные серверные вызовы. В промежуточном решении автора (из Сообщения 4) при изменении одного реквизита происходят сразу два последовательных серверных вызова подряд: сначала функция ПолучитьПроизводителя(), а затем процедура ЗаполнитьНаСервере(). При каждом контекстном вызове (&НаСервере) вся форма целиком (все ее реквизиты, настройки, элементы и строки табличных частей) преобразуется в XML/JSON, передается по сети на сервер, там распаковывается, обрабатывается, снова упаковывается и возвращается обратно на клиент. На реальной базе в многопользовательском режиме это неизбежно приведет к ощутимым задержкам интерфейса («зависаниям» формы при вводе данных).
Платформа 1С позволяет полноценно работать с коллекциями формы на клиенте. Мы можем добавлять новые строки в табличную часть (имеющую тип ДанныеФормыКоллекция) непосредственно в клиентском коде с помощью метода Добавить(). Это дает нам возможность реализовать максимально быстрый алгоритм:
&НаСервереБезКонтекста передаем эту ссылку на сервер.Разберем этот метод по шагам на примере кода:
&НаКлиенте
Процедура СоставляющиеНаименованиеПриИзменении(Элемент)
// Получаем текущую строку табличной части
ТекДанные = Элементы.Составляющие.ТекущиеДанные;
// Проверяем, заполнена ли строка и само наименование
Если ТекДанные = Неопределено Или Не ЗначениеЗаполнено(ТекДанные.Наименование) Тогда
Возврат;
КонецЕсли;
// Выполняем ОДИН легкий неконтекстный вызов сервера
ДанныеЗаполнения = ПолучитьДанныеДляЗаполнения(ТекДанные.Наименование);
// Заполняем реквизиты текущей строки на клиенте
ТекДанные.Производитель = ДанныеЗаполнения.Производитель;
// Очищать или не очищать ТЧ перед добавлением новых строк - зависит от бизнес-логики.
// Если нужно просто добавить новые элементы состава:
Для Каждого СтрокаСостава Из ДанныеЗаполнения.МассивСостава Цикл
НоваяСтрока = Объект.Составляющие.Добавить();
НоваяСтрока.Наименование = СтрокаСостава.Наименование;
НоваяСтрока.Основное = СтрокаСостава.Основное;
НоваяСтрока.Производитель = ДанныеЗаполнения.Производитель;
КонецЦикла;
КонецПроцедуры
&НаСервереБезКонтекста
Функция ПолучитьДанныеДляЗаполнения(СсылкаНаименование)
// Создаем структуру для возврата данных на клиент
Результат = Новый Структура;
Результат.Вставить("Производитель", СсылкаНаименование.Производитель);
МассивСостава = Новый Массив;
// Обходим табличную часть справочника на сервере
Для Каждого Стр Из СсылкаНаименование.СоставныеЧасти Цикл
СтрокаСтруктуры = Новый Структура;
СтрокаСтруктуры.Вставить("Наименование", Стр.Наименование);
// Получаем реквизит "Основное" составной части (тоже через точку, так как мы на сервере)
СтрокаСтруктуры.Вставить("Основное", Стр.Наименование.Основное);
МассивСостава.Добавить(СтрокаСтруктуры);
КонецЦикла;
Результат.Вставить("МассивСостава", МассивСостава);
Возврат Результат;
КонецФункции
Этот способ является наиболее производительным, так как объем данных, передаваемый между клиентом и сервером, минимален (передается только одна ссылка туда и одна структура обратно), а контекст формы не затрагивается вовсе.
Бывают ситуации, когда алгоритм заполнения очень сложен: он зависит от других реквизитов шапки документа, требует пересчета итогов формы или выполнения тяжелых процедур, жестко завязанных на контекст. В таких случаях оправдан контекстный вызов &НаСервере, но выполнять его нужно строго один раз за событие.
При этом важно правильно адресовать строку на сервере. Вместо передачи на сервер всей строки (что невозможно сделать напрямую в неконтекстные методы или неэффективно в контекстных) мы передаем числовой Идентификатор строки. На сервере мы находим нужную строку в коллекции формы при помощи метода НайтиПоИдентификатору().
Рассмотрим пример реализации такого подхода:
&НаКлиенте
Процедура СоставляющиеНаименованиеПриИзменении(Элемент)
// Получаем уникальный идентификатор текущей строки на форме (это число)
ИдентификаторСтроки = Элементы.Составляющие.ТекущаяСтрока;
Если ИдентификаторСтроки = Неопределено Тогда
Возврат;
КонецЕсли;
// Вызываем сервер ОДИН раз, передавая только числовой идентификатор
ЗаполнитьНаСервере(ИдентификаторСтроки);
КонецПроцедуры
&НаСервере
Процедура ЗаполнитьНаСервере(ИдентификаторСтроки)
// Находим конкретную строку в контексте формы Объект.Составляющие по ее ID
ТекСтрокаТЧ = Объект.Составляющие.НайтиПоИдентификатору(ИдентификаторСтроки);
Если ТекСтрокаТЧ = Неопределено Или Не ЗначениеЗаполнено(ТекСтрокаТЧ.Наименование) Тогда
Возврат;
КонецЕсли;
// На сервере мы можем безопасно обращаться к реквизитам через точку
ТекСтрокаТЧ.Производитель = ТекСтрокаТЧ.Наименование.Производитель;
// Добавляем новые строки состава непосредственно в контекст формы
Для Каждого СтрТз Из ТекСтрокаТЧ.Наименование.СоставныеЧасти Цикл
НоваяСтрока = Объект.Составляющие.Добавить();
НоваяСтрока.Наименование = СтрТз.Наименование;
НоваяСтрока.Основное = СтрТз.Наименование.Основное;
НоваяСтрока.Производитель = ТекСтрокаТЧ.Производитель;
КонецЦикла;
КонецПроцедуры
Этот способ уступает первому по скорости из-за передачи контекста всей формы, однако он значительно проще в написании, когда требуется задействовать другие реквизиты объекта (например, дату документа, склад, организацию) для расчета значений в строках.
Если в вашей конфигурации интегрирована Библиотека Стандартных Подсистем (БСП) и вам нужно просто быстро получить значение одного или нескольких реквизитов ссылки, то создавать собственные функции в модуле формы не обязательно. Можно воспользоваться готовыми функциями общего назначения.
Например, для получения одного значения (производителя) на клиенте можно вызвать встроенный серверный метод БСП:
// Получаем одно значение реквизита "Производитель" для выбранной номенклатуры
Производитель = ОбщегоНазначенияВызовСервера.ЗначениеРеквизитаОбъекта(ТекДанные.Наименование, "Производитель");
Если же требуется получить сразу несколько реквизитов за один раз, используйте метод, возвращающий структуру:
// Запрашиваем несколько реквизитов одной ссылочной сущности за один вызов
Реквизиты = ОбщегоНазначенияВызовСервера.ЗначенияРеквизитовОбъекта(ТекДанные.Наименование, "Производитель, СтранаПроисхождения");
// Заполняем реквизиты строки
ТекДанные.Производитель = Реквизиты.Производитель;
ТекДанные.Страна = Реквизиты.СтранаПроисхождения;
Использование механизмов БСП позволяет существенно сократить объем кода в модуле формы, повышает читаемость кода и сводит к минимуму вероятность совершения ошибок при ручном написании неконтекстных запросов.