Разработка интерфейсов на управляемых формах в 1С часто требует от программиста реализации интерактивных и удобных для пользователя сценариев. Один из наиболее распространенных запросов автоматизации — это изменение какого-либо реквизита документа (например, статуса, комментария или ответственного менеджера) прямо из формы списка, без необходимости открывать форму самого документа. На первый взгляд задача кажется тривиальной, однако при ее реализации разработчики регулярно сталкиваются с неочевидными платформенными нюансами: от кэширования данных динамического списка до блокировок базы данных.
В этой статье мы подробно разберем, как правильно реализовать изменение реквизита выбранного документа по кнопке, обсудим подводные камни кэширования и свойства Использовать всегда, рассмотрим вопросы безопасности транзакций и групповой обработки документов, а также выясним, почему динамический список может не обновляться автоматически при программном создании документов и как решить эту проблему в соответствии со стандартами разработки 1С.
Для того чтобы изменить реквизит существующего документа по нажатию кнопки на форме списка, нам необходимо связать клиентское действие пользователя с серверной процедурой записи объекта. Напрямую изменять реквизиты базы данных на клиенте в управляемом приложении невозможно, поэтому мы должны передать ссылку на документ на сервер, получить объект, внести изменения и записать его.
Рассмотрим классический пример реализации этой процедуры. Создадим на форме списка команду и выведем ее в виде кнопки. В обработчике команды на клиенте мы получаем текущие данные списка и передаем ссылку в серверный контекст:
&НаКлиенте
Процедура ИзменитьРеквизит(Команда)
// Получаем текущую строку динамического списка
ТекДанные = Элементы.Список.ТекущиеДанные;
Если ТекДанные = Неопределено Тогда
ПоказатьПредупреждение(, "Не выбран документ для изменения!");
Возврат;
КонецЕсли;
// Передаем ссылку на сервер для изменения реквизита
ИзменитьРеквизитНаСервере(ТекДанные.Ссылка, "Новое наименование или статус");
// Оповещаем систему об изменении объекта для принудительного обновления формы
ОповеститьОбИзменении(ТекДанные.Ссылка);
КонецПроцедуры
На стороне сервера (желательно использовать внеконтекстную процедуру &НаСервереБезКонтекста для оптимизации производительности и снижения трафика между клиентом и сервером) мы выполняем непосредственную модификацию данных:
&НаСервереБезКонтекста
Процедура ИзменитьРеквизитНаСервере(Ссылка, ЗначениеРеквизита)
// Получаем объект документа по ссылке
ОбъектДокумента = Ссылка.ПолучитьОбъект();
// Модифицируем нужный реквизит
ОбъектДокумента.Наименование = ЗначениеРеквизита;
// Записываем документ в базу данных
ОбъектДокумента.Записать();
КонецПроцедуры
Обратите внимание на вызов метода ОповеститьОбИзменении() на клиенте. Если опустить эту команду, то после успешной записи документа на сервере пользователь не увидит изменений в списке до тех пор, пока вручную не нажмет кнопку «Обновить» (F5). Метод ОповеститьОбИзменении() сообщает платформе, что объект в базе данных был изменен сторонним процессом, и динамический список автоматически перечитает измененную строку.
Иногда при попытке прочитать ссылку через конструкцию Элементы.Список.ТекущиеДанные.Ссылка разработчики обнаруживают, что свойство Ссылка возвращает Неопределено или пустое значение. Это происходит из-за того, что платформа 1С в целях оптимизации трафика не передает на клиент те колонки динамического списка, которые физически не отображаются на экране пользователя (не выведены в таблицу формы списка).
Существует два пути решения этой проблемы:
Ссылка внутри структуры динамического списка на форме (в окне настройки реквизитов формы справа сверху) и установить флажок Использовать всегда. Это принудительно заставит платформу передавать ссылку на клиент, даже если сама колонка скрыта от пользователя.Основная таблица заполнено конкретным типом документа), вы можете полностью отказаться от обращения к ТекущиеДанные и использовать свойство Элементы.Список.ТекущаяСтрока. В данном случае это свойство вернет непосредственно саму ссылку на документ типа ДокументСсылка, что исключает ошибки с отсутствием колонки на клиенте.Посмотрим, как будет выглядеть оптимализированный клиентский код без использования флага «Использовать всегда»:
&НаКлиенте
Процедура ИзменитьРеквизитОптимально(Команда)
СсылкаНаДокумент = Элементы.Список.ТекущаяСтрока;
Если СсылкаНаДокумент = Неопределено Тогда
Возврат;
КонецЕсли;
ИзменитьРеквизитНаСервере(СсылкаНаДокумент, "Новое значение");
ОповеститьОбИзменении(СсылкаНаДокумент);
КонецПроцедуры
В реальных многопользовательских информационных базах простая запись объекта через ПолучитьОбъект().Записать() может привести к серьезным техническим коллизиям и конфликтам транзакционных блокировок. Проанализируем ключевые правила безопасной серверной разработки при изменении реквизитов:
Записать() необходимо установить блокировку. Если объект уже заблокирован кем-то другим, система вернет понятное исключение, которое мы можем обработать.Записать() без параметров по умолчанию пытается провести ранее проведенный документ. Если документ крупный и содержит много строк, повторное проведение может занять много времени, вызвать нехватку остатков по складам или породить ошибки контроля партий.
Объект.Записать(РежимЗаписиДокумента.Запись);.Объект.Записать(РежимЗаписиДокумента.Проведение);.ОбщегоНазначения.УстановитьЗначениеРеквизита(Ссылка, ИмяРеквизита, Значение) автоматически заботится о блокировках, оптимизации транзакций и выполняет запись максимально быстрым способом.Проанализируем пример безопасного серверного кода с обработкой блокировок и управлением режимом записи:
&НаСервереБезContextа
Процедура БезопасноеИзменениеРеквизитаНаСервере(Ссылка, ЗначениеРеквизита)
// Попытка установить блокировку и изменить объект
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("Документ.ПриходнаяНакладная");
ЭлементБлокировки.УстановитьЗначение("Ссылка", Ссылка);
НачатьТранзакцию();
Попытка
Блокировка.Заблокировать();
ОбъектДокумента = Ссылка.ПолучитьОбъект();
ОбъектДокумента.Комментарий = ЗначениеРеквизита;
// Записываем документ БЕЗ повторного проведения, так как изменили только текстовый комментарий
ОбъектДокумента.Записать(РежимЗаписиДокумента.Запись);
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение "Не удалось изменить документ. Возможно, он заблокирован другим пользователем. Описание ошибки: " + КраткоеПредставлениеОшибки(ИнформацияОбОшибке());
КонецПопытки;
КонецПроцедуры
На практике пользователям часто требуется изменить реквизит не у одного, а сразу у нескольких выделенных в списке документов. Для этого необходимо задействовать свойство динамического списка ВыделенныеСтроки, которое возвращает массив ссылок на все строки, выделенные пользователем с помощью клавиш Shift или Ctrl.
Разберем по шагам, как реализовать такую групповую операцию:
&НаКлиенте
Процедура ИзменитьРеквизитГруппы(Команда)
ВыделенныеСтроки = Элементы.Список.ВыделенныеСтроки;
Если ВыделенныеСтроки.Количество() = 0 Тогда
ПоказатьПредупреждение(, "Пожалуйста, выделите один или несколько документов в списке!");
Возврат;
КонецЕсли;
// Вызываем серверный метод групповой обработки
ГрупповоеИзменениеНаСервере(ВыделенныеСтроки, "Согласован");
// Оповещаем систему об изменении типа объектов для массового обновления интерфейса
ОповеститьОбИзменении(Тип("ДокументСсылка.ПриходнаяНакладная"));
ПоказатьОповещениеПользователя("Успешно", , "Реквизиты выделенных документов изменены.");
КонецПроцедуры
Серверный обработчик в этом случае будет обходить массив переданных ссылок в цикле:
&НаСервереБезКонтекста
Процедура ГрупповоеИзменениеНаСервере(МассивСсылок, НовоеЗначение)
Для Каждого Ссылка Из МассивСсылок Цикл
Попытка
Объект = Ссылка.ПолучитьОбъект();
Объект.СтатусДокумента = НовоеЗначение;
Объект.Записать(РежимЗаписиДокумента.Запись);
Исключение
// В реальных задачах здесь стоит логировать ошибки доступа или блокировок
ЗаписьЖурналаРегистрации("Групповое изменение документов",
УровеньЖурналаРегистрации.Ошибка, , ,
"Ошибка изменения документа " + Строка(Ссылка) + ": " + КраткоеПредставлениеОшибки(ИнформацияОбОшибке()));
КонецПопытки;
КонецЦикла;
КонецПроцедуры
Перейдем ко второму популярному вопросу: вы программно создаете новый документ из формы списка (например, через обработку-заполнитель), записываете его в базу данных и открываете форму нового документа пользователю. Однако после того, как пользователь закрывает эту вновь открытую форму, в исходном динамическом списке этот документ не отображается до тех пор, пока пользователь вручную не обновит список. Выясним причину этого поведения.
Основная причина кроется в потере связи между формой списка и формой нового документа. Если вы открываете форму нового документа через устаревшие или неподходящие механизмы (например, через ОткрытьЗначение(Ссылка) или без явного указания владельца формы в параметрах ОткрытьФорму()), платформа 1С не знает, что эти две формы логически связаны между собой.
Для того чтобы список автоматически обновлялся при записи нового документа, необходимо соблюсти два важных условия:
ОткрытьФорму() третьим параметром обязательно нужно передавать форму-владельца (в контексте формы списка это переменная ЭтаФорма). Именно этот параметр создает связь между открываемым объектом и текущим динамическим списком. Как только в подчиненной форме произойдет событие записи, платформа автоматически отправит сигнал форме-владельцу на обновление ее динамических списков.Основная таблица (например, Документ.ПриходнаяНакладная). Если ваш динамический список построен на основе произвольного запроса, в котором не указана основная таблица, система не сможет понять, изменения каких именно типов данных должны приводить к обновлению списка.Стандарты разработки фирмы «1С» категорически не рекомендуют использовать связку методов ПолучитьФорму().Открыть(), так как она порождает избыточные серверные вызовы. Вместо этого всегда следует использовать глобальный метод ОткрытьФорму(), передавая параметры инициализации в структуру ЗначенияЗаполнения.
Рассмотрим, как правильно открыть форму нового документа с предварительным программным заполнением реквизитов (например, контрагента или склада) без предварительной записи в базу данных. Это избавит базу данных от «мусорных» пустых документов, которые пользователи могут случайно записать или оставить висеть в системе:
&НаКлиенте
Процедура СоздатьНовыйДокументСЗаполнением(Команда)
// Создаем структуру параметров для открытия формы нового документа
ПараметрыФормы = Новый Структура;
// Наполняем структуру заполнения реквизитов
СтруктураЗаполнения = Новый Структура;
СтруктураЗаполнения.Вставить("Контрагент", ТекущийКонтрагентНаФорме);
СтруктураЗаполнения.Вставить("Склад", ПредопределенноеЗначение("Справочник.Склады.ОсновнойСклад"));
СтруктураЗаполнения.Вставить("Комментарий", "Создано программно из формы списка документов");
// Добавляем структуру в параметры формы под предопределенным ключом "ЗначенияЗаполнения"
ПараметрыФормы.Вставить("ЗначенияЗаполнения", СтруктураЗаполнения);
// Открываем форму нового документа с указанием владельца формы (ЭтаФорма)
ОткрытьФорму(
"Документ.ПриходнаяНакладная.ФормаОбъекта", // Имя формы документа
ПараметрыФормы, // Параметры инициализации и автозаполнения
ЭтаФорма // ВЛАДЕЛЕЦ ФОРМЫ (отвечает за автообновление списка)
);
КонецПроцедуры
При использовании этого механизма платформа 1С выполнит следующие действия:
СтруктураЗаполнения в модуль объекта нового документа в событие ОбработкаЗаполнения(ДанныеЗаполнения, ТекстЗаполнения, СтандартнаяОбработка) в качестве параметра ДанныеЗаполнения, где вы сможете гибко дозаполнить табличные части или сопутствующие реквизиты.ЭтаФорма (форму списка), при первой же успешной интерактивной или программной записи этого документа пользователем динамический список формы-владельца мгновенно и автоматически обновится без необходимости ручного нажатия клавиш обновления.Используя данные подходы и стандарты разработки 1С, вы сможете проектировать быстрые, отказоустойчивые и интуитивно понятные интерфейсы для конечных пользователей ваших решений.