Почему реквизит формы 1С не обновляется после выбора другого значения и вызова серверной процедуры?

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

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

Анализ исходной проблемы и кода

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

Изначальный код, представленный в сообщении 1, выглядел следующим образом:


&НаСервереБезКонтекста Процедура ПолучательПриИзмененииНаСервере(Получатель, ПочтаПолучателя)
    //ПочтаПолучателя = Справочники.Получатели.ПустаяСсылка;
    Если Получатель.ПочтовыеЯщики.Количество > 0 Тогда
        Запрос = Новый Запрос;
        Запрос.Текст = "ВЫБРАТЬ 
                        | ПолучателиПочтовыеЯщики.Почта КАК Почта
                        |ИЗ 
                        | Справочник.Получатели.ПочтовыеЯщики КАК ПолучателиПочтовыеЯщики
                        |ГДЕ 
                        | ПолучателиПочтовыеЯщики.ПоУмолчанию = &Истина";
        Запрос.УстановитьПараметр("Истина", Истина);
        РезультатЗапроса = Запрос.Выполнить().Выбрать();
        РезультатЗапроса.Следующий();
        ПочтаПолучателя = РезультатЗапроса.Почта;
    Иначе
        Сообщить("У выбранного пользователя не указана почта");
        ПочтаПолучателя = "";
    КонецЕсли;
КонецПроцедуры

&НаКлиенте
Процедура ПолучательПриИзменении(Элемент)
    ПолучательПриИзмененииНаСервере(Получатель, ПочтаПолучателя);
    //ЭтаФорма.ОбновитьОтображениеДанных;
КонецПроцедуры

В этом коде мы видим клиентскую процедуру ПолучательПриИзменении, которая вызывает серверную процедуру ПолучательПриИзмененииНаСервере. Серверная процедура помечена атрибутом &НаСервереБезКонтекста, что означает, что она выполняется на сервере без передачи всего контекста формы, что часто улучшает производительность. В нее передаются текущие значения реквизитов Получатель и ПочтаПолучателя. Внутри серверной процедуры формируется запрос к табличной части ПочтовыеЯщики справочника Получатели, чтобы найти почту, помеченную как "по умолчанию".

Почему исходный код не работал корректно?

Мы анализируем несколько причин, почему исходный код мог работать нестабильно:

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

Корректируем запрос к табличной части справочника

Первым шагом к решению проблемы является корректировка запроса. Мы должны ясно указать, для какого именно Получателя мы ищем почтовый ящик по умолчанию. Для этого мы передаем ссылку на выбранного Получателя в запрос как параметр.

В сообщении 4 была предпринята попытка улучшить запрос, однако в нем также была допущена неточность, которая может вызвать проблемы:


Запрос.Текст = "ВЫБРАТЬ 
                | Получатели.Наименование КАК Наименование, 
                | Получатели.ПочтовыеЯщики.( 
                | Почта КАК Почта 
                | ) КАК ПочтовыеЯщики 
                |ИЗ 
                | Справочник.Получатели КАК Получатели 
                |ГДЕ 
                | Получатели.ПочтовыеЯщики.Ссылка = &Ссылка 
                | И Получатели.ПочтовыеЯщики.ПоУмолчанию = &Истина";
Запрос.УстановитьПараметр("Ссылка", Получатель);

Здесь проблема в условии Получатели.ПочтовыеЯщики.Ссылка = &Ссылка. Правильнее было бы фильтровать по владельцу табличной части. Каждый элемент табличной части связан с владельцем через поле, которое обычно называется Ссылка или Владелец в зависимости от контекста. Для табличных частей, находящихся непосредственно под объектом, связь идет через внутреннее поле. Корректный запрос должен использовать поле Ссылка табличной части, которое ссылается на запись табличной части, но для связи с владельцем мы должны использовать поле Владелец (или Ссылка.Владелец).

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


&НаСервереБезКонтекста Процедура ПолучательПриИзмененииНаСервере(Получатель, ПочтаПолучателя)
    Если Получатель.ПочтовыеЯщики.Количество > 0 Тогда // Проверяем наличие почтовых ящиков, хотя запрос сам по себе обработает отсутствие
        Запрос = Новый Запрос;
        Запрос.Текст = "ВЫБРАТЬ
                        |    ПочтовыеЯщики.Почта КАК Почта
                        |ИЗ
                        |    Справочник.Получатели.ПочтовыеЯщики КАК ПочтовыеЯщики
                        |ГДЕ
                        |    ПочтовыеЯщики.Ссылка.Владелец = &Получатель
                        |    И ПочтовыеЯщики.ПоУмолчанию = ИСТИНА";
        Запрос.УстановитьПараметр("Получатель", Получатель);
        
        РезультатЗапроса = Запрос.Выполнить().Выбрать();
        
        Если РезультатЗапроса.Следующий() Тогда
            ПочтаПолучателя = РезультатЗапроса.Почта;
        Иначе
            // Если запрос не нашел почту по умолчанию, очищаем поле
            Сообщить("У выбранного получателя не указана почта по умолчанию.");
            ПочтаПолучателя = "";
        КонецЕсли;
    Иначе
        // Если у получателя вообще нет почтовых ящиков
        Сообщить("У выбранного получателя не указана почта.");
        ПочтаПолучателя = "";
    КонецЕсли;
КонецПроцедуры

В этом варианте мы используем ПочтовыеЯщики.Ссылка.Владелец = &Получатель, чтобы точно связать записи табличной части с конкретным элементом справочника Получатели. Это наиболее надежный способ фильтрации.

Решение, предложенное автором темы

Автор темы в сообщении 8 пришел к рабочему решению, которое также использует запрос, но с небольшой особенностью. Мы посмотрим на его код и проанализируем, почему он сработал.


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

Мы видим, что автор использовал несколько иной подход к запросу:

  1. Фильтрация по наименованию: Запрос фильтруется по Получатели.Наименование = &Наименование, а параметр устанавливается как Получатель.ПолучитьОбъект().Наименование. Это сработало, но имеет потенциальные недостатки. Наименования не всегда уникальны, и при наличии двух получателей с одинаковым наименованием, но разной почтой по умолчанию, может возникнуть ошибка или непредсказуемое поведение. Всегда предпочтительнее фильтровать по уникальному идентификатору — ссылке.
  2. Запрос с вложенной табличной частью: Запрос выбирает как поле Наименование, так и вложенную табличную часть ПочтовыеЯщики. После выполнения запроса, мы получаем объект РезультатЗапроса, из которого затем получаем выборку по вложенной табличной части с помощью РезультатЗапроса.ПочтовыеЯщики.Выбрать(). Это рабочий способ, но он немного более громоздкий, чем прямой запрос к табличной части, как мы показали выше.
  3. Причина работоспособности: Несмотря на неоптимальный выбор фильтра по наименованию, сам факт того, что переменная ПочтаПолучателя была изменена в серверной процедуре, вызванной из клиентской с передачей этой переменной как параметра, привел к автоматическому обновлению реквизита на форме. Именно этот механизм клиент-серверного взаимодействия обеспечил решение.

Рекомендованный и более эффективный подход: прямой доступ к данным объекта

Когда объект Получатель уже известен (то есть, мы имеем его ссылку), и нам нужны данные из его табличной части, зачастую гораздо более эффективным является прямой доступ к данным объекта, а не выполнение запроса к базе данных. Запрос создает дополнительную нагрузку, связанную с компиляцией и выполнением SQL. Мы можем получить объект по ссылке и напрямую пройтись по его табличной части.

Рассмотрим, как можно было бы реализовать эту логику с помощью прямого доступа к объекту. Этот вариант также был предложен автором в закомментированном виде ("Вариант №2"), что подтверждает его актуальность.


&НаСервереБезКонтекста Процедура ПолучательПриИзмененииНаСервере(ПолучательСсылка, ПочтаПолучателя)
    // Получаем объект справочника по переданной ссылке
    Если ПолучательСсылка.Пустая() Тогда
        ПочтаПолучателя = "";
        Возврат;
    КонецЕсли;

    ПочтаНайдена = Ложь;
    ПочтаПолучателя = ""; // Очищаем на случай, если не найдем по умолчанию

    Попытка
        ОбъектПолучатель = ПолучательСсылка.ПолучитьОбъект();

        Если ОбъектПолучатель <> Неопределено Тогда
            Для Каждого ПочтовыйЯщикСТрока Из ОбъектПолучатель.ПочтовыеЯщики Цикл
                Если ПочтовыйЯщикСТрока.ПоУмолчанию Тогда
                    ПочтаПолучателя = ПочтовыйЯщикСТрока.Почта;
                    ПочтаНайдена = Истина;
                    Прервать; // Если мы ожидаем только один почтовый ящик по умолчанию
                КонецЕсли;
            КонецЦикла;

            Если НЕ ПочтаНайдена Тогда
                Сообщить("У выбранного получателя не указана почта по умолчанию.");
            КонецЕсли;
        Иначе
            Сообщить("Не удалось получить объект получателя.");
            ПочтаПолучателя = "";
        КонецЕсли;
    Исключение
        Сообщить("Ошибка при получении объекта получателя: " + ОписаниеОшибки());
        ПочтаПолучателя = "";
    КонецПопытки;
КонецПроцедуры

// На клиентской процедуре вызов остается прежним
&НаКлиенте
Процедура ПолучательПриИзменении(Элемент)
    ПолучательПриИзмененииНаСервере(Получатель, ПочтаПолучателя);
КонецПроцедуры

Разберем этот подход:

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

Этот способ обращается к данным объекта, который уже может быть кэширован или быстро загружен в память, что, как правило, быстрее, чем выполнение полноценного запроса к СУБД, особенно если речь идет о единичной записи или небольшом количестве данных.

Важные моменты и лучшие практики

  1. Client-Server Interaction (Клиент-серверное взаимодействие): Мы должны всегда помнить о том, что реквизиты формы, переданные в серверную процедуру как параметры, автоматически сериализуются на клиенте, передаются на сервер, изменяются там, а затем десериализуются и возвращаются на клиент, обновляя соответствующий реквизит формы. Это ключевой механизм для таких задач.
  2. Использование &НаСервереБезКонтекста: Этот атрибут полезен для повышения производительности, так как он минимизирует объем данных, передаваемых между клиентом и сервером. Мы используем его, когда для выполнения серверной операции достаточно переданных параметров, и не нужен полный контекст формы.
  3. Отладка: При возникновении подобных проблем активно используйте отладчик 1С. Устанавливайте точки останова как на клиентской (например, в ПолучательПриИзменении), так и на серверной части кода (в ПолучательПриИзмененииНаСервере), чтобы пошагово отслеживать значения переменных, параметры, передаваемые между клиентом и сервером, а также результаты выполнения запросов. Используйте Сообщить() для быстрой проверки значений.
  4. Уникальность "По умолчанию": Наши решения предполагают, что в табличной части "ПочтовыеЯщики" есть только один элемент с признаком "ПоУмолчанию = Истина". Если таких элементов может быть несколько, мы должны адаптировать логику – например, выбрать первый найденный, собрать все и предложить пользователю выбор, или выдать ошибку.
  5. Параметры запроса: Всегда предпочтительнее передавать уникальный идентификатор (ссылку) в качестве параметра запроса, а не наименование. Наименования могут быть неуникальными, что приведет к ошибкам или непредсказуемому поведению.

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

← На главную