Почему сбрасываются изменения в табличной части при возврате с сервера на клиент в 1С и как это исправить?

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

В практике разработки на платформе «1С:Предприятие 8» (особенно на управляемых формах) программисты часто сталкиваются с загадочным поведением: данные в табличной части, измененные в серверной процедуре, визуально сбрасываются до исходных значений сразу после возврата управления на клиент. При этом в отладчике на сервере изменения фиксируются корректно.

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

Исходная ситуация и проблемный код

Рассмотрим типовой сценарий. У нас есть обработчик изменения номенклатуры в строке табличной части. На клиенте мы проверяем заполнение документа-основания, а затем передаем управление на сервер для выполнения запроса и корректировки количества. Проанализируем исходный код разработчика:


&НаКлиенте
Процедура ТоварыНоменклатураПриИзменении(Элемент)
    Если НЕ ЗначениеЗаполнено(Объект.Основание) Тогда
        Предупреждение("Сначала заполните Основание");
        Возврат;
    КонецЕсли;
    Тек = Элементы.Товары.ТекущиеДанные;
    ТоварыНоменклатураПриИзмененииНаСервере(Тек.Номенклатура, Элементы.Товары.ТекущаяСтрока, Тек.Количество);
КонецПроцедуры

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

Проблема: В строке Объект.Товары[ТекСтрока].Количество = Результат[0].Количество; значение меняется. Но как только управление возвращается на клиент, количество в строке снова становится прежним. Однако, если объявить параметры процедуры с ключевым словом Знач, всё начинает работать корректно. Выясним, почему так происходит.

Причина 1. Механизм Copy-Restore (копирование-восстановление) при вызове сервера

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

Когда вы вызываете серверную процедуру и передаете в нее параметры «по ссылке» (то есть без ключевого слова Знач), платформа 1С имитирует передачу по ссылке с помощью механизма Copy-Restore (копирование-восстановление):

  1. При вызове сервера (Copy): Платформа сериализует значения параметров на клиенте, передает их на сервер и создает там локальные копии переменных. В нашем примере локальная переменная сервера Количество получает значение, например, 10 (исходное количество в строке).
  2. Выполнение на сервере: В процессе работы серверного кода вы напрямую обращаетесь к контексту формы и меняете значение в табличной части: Объект.Товары[...].Количество = 5;. Значение в табличной части успешно изменилось. Но локальная переменная-параметр Количество на сервере по-прежнему хранит исходное значение 10!
  3. Возврат на клиент (Restore): Так как параметр Количество передан без ключевого слова Знач, платформа обязана вернуть его измененное (или неизмененное) значение обратно в ту переменную, которая передавалась на клиенте (в нашем случае — в свойство Тек.Количество). Платформа берет значение серверной переменной Количество (которое равно 10) и **принудительно записывает его поверх** обновленного значения табличной части на клиенте.

Таким образом, платформа сама затирает ваши изменения старым значением параметра принудительно при выходе из серверной процедуры. Как только мы пишем Знач Количество, мы указываем платформе, что обратное копирование делать не нужно. Данные, измененные через объект, благополучно остаются на клиенте.

Важная рекомендация стандартов разработки: Из соображений производительности всегда передавайте параметры с клиента на сервер с ключевым словом Знач, если только вам действительно не требуется вернуть измененное значение переменной назад на клиент. Это избавляет от лишней сериализации данных в сетевом трафике и предотвращает подобные трудноуловимые баги.

Причина 2. Грубая ошибка адресации строк: Идентификатор vs Индекс

Внимательно посмотрим на строку изменения данных:

Объект.Товары[ТекСтрока].Количество = ...;

И на строку удаления:

Объект.Товары.Удалить(ТекСтрока);

В переменной ТекСтрока у нас хранится значение свойства Элементы.Товары.ТекущаяСтрока. Разберем, почему использовать его напрямую в квадратных скобках коллекции — это критическая ошибка, которая рано или поздно приведет к сбою или порче данных в базе.

Рассмотрим ситуацию на примере. Вы открыли документ, в табличной части 3 строки:

  1. Строка 1: Индекс = 0, Идентификатор = 0
  2. Строка 2: Индекс = 1, Идентификатор = 1
  3. Строка 3: Индекс = 2, Идентификатор = 2

Пока вы ничего не удаляли, индекс совпадает с идентификатором. Код Объект.Товары[ТекСтрока] работает "случайно" правильно.

Теперь вы удаляете первую строку (с индексом 0 и идентификатором 0). Таблица перестраивается:

  1. Бывшая Строка 2: Индекс = 0, Идентификатор = 1
  2. Бывшая Строка 3: Индекс = 1, Идентификатор = 2

Пользователь выбирает визуально вторую строку (которая сейчас имеет индекс 1). Свойство ТекущаяСтрока возвращает идентификатор 2. Вы пытаетесь обратиться к строке: Объект.Товары[2]. Происходит аварийное завершение работы системы с ошибкой "Индекс находится за пределами границы", поскольку строк в коллекции осталось всего две (с индексами 0 и 1).

Если бы строк изначально было больше (например, 10), то обращение по идентификатору 2 вернуло бы совершенно другую строку (с текущим индексом 2), и ваш код молча перезаписал бы данные в чужой строке, что привело бы к логическому повреждению данных в документе!

Правильное решение проблемы

Чтобы избежать описанных проблем, выполним следующие действия:

  1. Добавим ключевое слово Знач ко всем параметрам серверной процедуры.
  2. Будем использовать метод НайтиПоИдентификатору() для безопасного поиска строки на сервере.
  3. Будем передавать в метод удаления саму найденную строку, а не её идентификатор.

Проанализируем правильный вариант написания кода:


&НаКлиенте
Процедура ТоварыНоменклатураПриИзменении(Элемент)
    Если НЕ ЗначениеЗаполнено(Объект.Основание) Тогда
        Предупреждение("Сначала заполните Основание");
        Возврат;
    КонецЕсли;
    
    Тек = Элементы.Товары.ТекущиеДанные;
    Если Тек = Неопределено Тогда
        Возврат;
    КонецЕсли;
    
    // Передаем идентификатор строки и текущую номенклатуру
    ТоварыНоменклатураПриИзмененииНаСервере(Тек.Номенклатура, Элементы.Товары.ТекущаяСтрока);
КонецПроцедуры

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

Резюме

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

← На главную