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