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