Каждый разработчик на платформе «1С:Предприятие 8», особенно на этапе перехода от обычных форм к управляемым, сталкивается с неочевидным поведением элементов интерфейса. Одной из классических проблем является ошибка вызова метода контекста Удалить() при попытке программно удалить строку из табличной части. Симптомы этой проблемы выглядят крайне загадочно: при первом открытии формы строка удаляется без каких-либо нареканий, но стоит повторить это действие или выполнить его после добавления новых строк, как платформа выдает критическую ошибку.
В этой статье мы подробно разберем, почему возникает эта ошибка, проанализируем внутренние механизмы работы платформы с коллекциями управляемых форм и рассмотрим несколько надежных способов решения этой задачи.
Для начала давайте детально проанализируем ситуацию, которую описал автор темы. В обработчике формы выполняется проверка на уникальность типа адреса в табличной части КонтактнаяИнформация. Если пользователь пытается ввести дублирующий тип адреса, система выводит предупреждение и пытается удалить текущую строку табличной части. Код выглядит следующим образом:
Совпадение = Ложь;
Для Каждого Адрес Из Объект.КонтактнаяИнформация Цикл
Если Элементы.КонтактнаяИнформация.ТекущиеДанные.ТипАдреса = Адрес.ТипАдреса
И НЕ Адрес.НомерСтроки = Элементы.КонтактнаяИнформация.ТекущиеДанные.НомерСтроки Тогда
Совпадение = Истина;
КонецЕсли;
КонецЦикла;
Если Совпадение = Истина Тогда
ОчиститьСообщения();
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = Строка(Элементы.КонтактнаяИнформация.ТекущиеДанные.TipAdresa) + " уже заполнен. Вы можете исправить информацию...";
Сообщение.Сообщить();
Объект.КонтактнаяИнформация.Удалить(Элементы.КонтактнаяИнформация.ТекущаяСтрока);
КонецЕсли;
При повторном вызове этого кода на той же форме интерактивное удаление падает с системной ошибкой:
«Ошибка при вызове метода контекста (Удалить) по причине: Недопустимое значение параметра (параметр номер '1')»
Давайте выясним причину такого поведения. Для этого нам потребуется разобраться, чем отличаются понятия «индекс строки» и «идентификатор строки» в управляемом интерфейсе 1С.
В управляемом приложении (платформа 1С 8.2 и 8.3) таблица на форме представлена объектом типа ТаблицаФормы (в нашем случае это Элементы.КонтактнаяИнформация). Данные, которые отображаются в этой таблице, связаны с реквизитом формы, имеющим тип ДанныеФормыКоллекция (это Объект.КонтактнаяИнформация).
Метод Удалить() объекта ДанныеФормыКоллекция имеет два варианта синтаксиса:
Удалить(<Индекс>).ДанныеФормыЭлементКоллекции). Сигнатура: Удалить(<Элемент>).Теперь посмотрим на свойство ТекущаяСтрока нашей таблицы формы (Элементы.КонтактнаяИнформация.ТекущаяСтрока). Согласно официальной документации 1С, это свойство возвращает идентификатор строки (число), а вовсе не индекс и не саму строку!
Идентификатор строки — это внутреннее уникальное число, которое присваивается каждой строке таблицы в рамках текущего сеанса работы формы. Он необходим платформе для того, чтобы однозначно идентифицировать строку при сортировках, фильтрациях и других динамических изменениях данных в интерфейсе.
Когда вы пишите: Объект.КонтактнаяИнформация.Удалить(Элементы.КонтактнаяИнформация.ТекущаяСтрока);, вы передаете в метод Удалить() число (идентификатор). Платформа определяет тип параметра как «Число» и пытается применить первый вариант синтаксиса — удаление по индексу.
Почему же это работает в первый раз?
При создании формы или при её первоначальном заполнении идентификаторы строк генерируются последовательно, начиная с нуля. В этот момент идентификатор строки совпадает с её физическим индексом в таблице:
Если мы удаляем строку с идентификатором 1, платформа ищет строку с индексом 1, находит её и успешно удаляет. Нам кажется, что код написан правильно.
Почему происходит сбой во второй раз?
Представим, что мы удалили строку с индексом 1. В таблице осталось две строки. Теперь их индексы сместились:
Затем мы добавляем новую строку. Платформа присваивает ей следующий по порядку уникальный идентификатор (например, 3), но её физический индекс в коллекции становится равен 2:
Если теперь мы попытаемся удалить эту новую строку, свойство ТекущаяСтрока вернет число 3. Метод Удалить(3) попытается удалить элемент с индексом 3. Но в нашей коллекции всего 3 элемента (индексы 0, 1 и 2)! Элемента с индексом 3 не существует. Платформа генерирует исключение: «Недопустимое значение параметра (параметр номер '1')».
Внимание, скрытая угроза!
Если бы в нашей таблице после добавления новых строк физически существовал индекс, равный идентификатору удаляемой строки, то платформа удалила бы строку с этим индексом. При этом визуально была бы выбрана совершенно другая строка! Это приводит к тихим ошибкам порчи данных, когда программа без предупреждения удаляет не ту строку, которую выбрал пользователь.
---
Самый простой и лаконичный способ избежать путаницы с индексами и идентификаторами — использовать свойство таблицы формы ТекущиеДанные. Это свойство сразу возвращает объект строки (тип ДанныеФормыЭлементКоллекции), который можно напрямую передать в метод Удалить().
Давайте разберем по шагам, как изменить код автора:
ТекущаяСтрокаТЧ = Элементы.КонтактнаяИнформация.ТекущиеДанные;
Если ТекущаяСтрокаТЧ <> Неопределено Тогда
// Проверяем наши условия
Совпадение = Ложь;
Для Каждого СтрокаАдреса Из Объект.КонтактнаяИнформация Цикл
Если СтрокаАдреса.ТипАдреса = ТекущаяСтрокаТЧ.ТипАдреса
И НЕ СтрокаАдреса.НомерСтроки = ТекущаяСтрокаТЧ.НомерСтроки Тогда
Совпадение = Истина;
Прервать; // Выходим из цикла досрочно, так как совпадение уже найдено
КонецЕсли;
КонецЦикла;
Если Совпадение Тогда
ОчиститьСообщения();
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = Строка(ТекущаяСтрокаТЧ.ТипАдреса) + " уже заполнен!";
Сообщение.Сообщить();
// Удаляем строку, передавая сам объект строки ТЧ
Объект.КонтактнаяИнформация.Удалить(ТекущаяСтрокаТЧ);
КонецЕсли;
КонецЕсли;
Этот способ является наиболее предпочтительным для простых интерактивных операций удаления, выполняемых непосредственно на клиенте.
---
Что делать, если в вашем алгоритме логика завязана именно на хранение числовых идентификаторов (например, вы передаете идентификатор строки между клиентскими и серверными процедурами)? В таком случае нам поможет метод коллекции НайтиПоИдентификатору().
Проанализируем пример реализации данного подхода:
ИдентификаторСтроки = Элементы.КонтактнаяИнформация.ТекущаяСтрока;
Если ИдентификаторСтроки <> Неопределено Тогда
// Получаем сам объект строки по его уникальному идентификатору
СтрокаКоллекции = Объект.КонтактнаяИнформация.НайтиПоИдентификатору(ИдентификаторСтроки);
Если СтрокаКоллекции <> Неопределено Тогда
// Теперь мы можем безопасно удалить строку, передав объект в метод Удалить
Объект.КонтактнаяИнформация.Удалить(СтрокаКоллекции);
КонецЕсли;
КонецЕсли;
Этот метод гарантирует стопроцентную надежность. Мы сначала находим строку по её аппаратному идентификатору формы, проверяем, существует ли она физически в коллекции (защита от коллизий), и только после этого удаляем.
---
Поскольку мы обсуждаем тему удаления строк из табличной части, нельзя обойти стороной еще одну классическую ошибку начинающих разработчиков — удаление строк непосредственно во время обхода этой же коллекции в цикле Для Каждого.
Рассмотрим ошибочный шаблон кода:
// ТАК ДЕЛАТЬ НЕЛЬЗЯ!
Для Каждого СтрокаАдреса Из Объект.КонтактнаяИнформация Цикл
Если СтрокаАдреса.ТипАдреса = ПредопределенноеЗначение("Перечисление.ТипыАдресов.Неиспользуемый") Тогда
Объект.КонтактнаяИнформация.Удалить(СтрокаАдреса);
КонецЕсли;
КонецЦикла;
В чем проблема этого кода?
При удалении строки из коллекции все последующие элементы смещаются "вверх", изменяя свои индексы. Цикл Для Каждого этого изменения не замечает и на следующей итерации обращается к элементу по старому внутреннему счетчику. В результате этого:
Чтобы выполнить массовое или групповое удаление строк по условию, правильным решением будет предварительное накопление ссылок на строки в отдельный вспомогательный массив. Рассмотрим правильный алгоритм:
// 1. Создаем массив для хранения найденных строк
СтрокиДляУдаления = Новый Массив;
// 2. Обходим коллекцию и фиксируем строки, подлежащие удалению
Для Каждого СтрокаАдреса Из Объект.КонтактнаяИнформация Цикл
Если СтрокаАдреса.ТипАдреса = ПредопределенноеЗначение("Перечисление.ТипыАдресов.Неиспользуемый") Тогда
СтрокиДляУдаления.Добавить(СтрокаАдреса);
КонецЕсли;
КонецЦикла;
// 3. Отдельным циклом обходим массив и удаляем строки из реальной коллекции
Для Каждого СтрокаИзМассива Из СтрокиДляУдаления Цикл
Объект.КонтактнаяИнформация.Удалить(СтрокаИзМассива);
КонецЦикла;
При использовании данного подхода смещение индексов внутри коллекции ДанныеФормыКоллекция во втором цикле уже никак не влияет на правильность удаления, поскольку мы удаляем элементы напрямую по ссылкам на объекты строк, сохраненные в массиве.
Подводя итоги нашего разбора, сформулируем основные правила работы с удалением строк табличных частей в управляемых формах 1С:
ТекущаяСтрока напрямую в метод Удалить(), так как оно возвращает идентификатор строки, а не индекс.ТекущиеДанные. Это избавляет от необходимости писать лишний код поиска.НайтиПоИдентификатору() перед вызовом удаления.