Неочевидные нюансы записи управляемой формы
Разберем несколько нюансов записи управляемой формы.
- Описание
- Подробнее
Описание
Содержание
- Предисловие
- Должна вернуть признак успеха. Но не обязана
- ПараметрыЗаписи ≠ ДополнительныеСвойства
- Нельзя просто так передать РежимЗаписи
- При вызове на сервере пропускаются клиентские события
- Урезанная ОбработкаПроверкиЗаполненияНаСервере()
- Событие После_УСПЕШНОЙ_ЗаписиНаСервере()
- Послесловие
Предисловие ^
В любом языке программирования есть свои "нюансы". Они хорошо известны тем, кто уже сталкивался, но совершенно неочевидны при этой первой встрече.
В статье пойдёт речь о записи управляемой формы. Статья будет дополняться, поэтому, если вы знаете ещё какой-то "нюанс", то пишите в комментариях.
В каждой форме, у которой основным реквизитом указан редактируемый объект базы данных, есть метод "Записать()"

Это функция, которая позволяет записать данные формы с той же логикой, как если бы пользователь самостоятельно нажал на кнопку "Записать". Например, элемент справочника:

Это самое универсальное описание метода. Но в разных типах данных появляются свои "предопределенные" значения в структуре ПараметрыЗаписи.
Больше всего для нашего изучения подойдет документ:

Для экспериментов в базе-пустышке создадим ТестовыйДокумент.
Файловая или серверная — не важно. Описываемые нюансы работают и там, и там.

Должна вернуть признак успеха. Но не обязана ^
Одно из отличий методов "Записать" формы и самого объекта заключается в этом:
Да, метод формы — это функция. Которая возвращает признак успеха записи. Но так ли это? Как показала практика, не всегда стоит доверять справке.
Допустим, нам понадобилось записать форму документа программно. В реальных ситуациях это возможно, когда разработчик сначала хочет задать вопрос перед записью формы, а потом (после ответа пользователя) запись эту продолжить. В таких случаях, после обработки вопроса потребуется программно вызвать запись с теми же параметрами. Или же отказываться от стандартных кнопок записи и создавать свои, которые также будут программно вызывать запись. Подробнее можно прочитать на ИТС.
А может нам просто нужно после какого-то действия сохранить данные формы? Вполне обычная практика.
На самом деле, для наших тестовых целей все это не важно. Давайте для начала просто создадим кнопку "Записать программно".

В обработчике кнопки простой код:
&НаКлиенте
Процедура ЗаписатьПрограммно(Команда)
УдалосьЗаписать = Записать();
Если УдалосьЗаписать Тогда
ТекстПредупреждения = "Ура, удалось записать!";
Иначе
ТекстПредупреждения = "Жаль, но не вышло!";
КонецЕсли;
ПоказатьПредупреждение(, ТекстПредупреждения);
КонецПроцедуры
Если записать форму удалось, то метод должен вернуть Истина. Проверяем эту теорию:

Прекрасно! А теперь попробуем сделать что-то, что не даст документу записаться. Например, очистим обязательный реквизит "Дата".

Упс… Мы такого в коде не писали. Нажмём "Подробно":

Что произошло? Для 1С это выглядит как обычное исключение метода. Как ошибка в коде. Как если бы метод Записать() вызывал исключение.
Но ведь в справке описано иначе:

Ещё раз. Метод формы Записать() должен вернуть Истина, если записать удалось. Это мы проверили — всё работает. Но также метод должен вернуть Ложь в противном случае.

Давайте попробуем другую ситуацию. В модуле объекта самого документа добавим процедуру ПередЗаписью и установим в ней отказ.
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
Отказ = Истина;
КонецПроцедурыПроверим, как поведёт себя метод в данном случае.

Метод опять не вернул Ложь. А просто выдал ошибку, словно это исключение в коде.
Более того. Если зайти в журнал регистрации, то можно увидеть эти ошибки:

Каждая такая "ошибка" фиксируется в журнале регистрации.
То есть, если разработчик, опираясь на описание метода платформы, не оборачивает Записать() в Попытка\Исключение, то отказы в ПередЗаписью() будут фиксироваться в ЖР. И засорять его бессмысленными ошибками. Почему бессмысленными? Потому что чаще всего это будет незаполненность какого-то обязательного реквизита, или какой-нибудь отказ в обработчике объекта. Это всё обычные штатные ситуации, которые не нужны администраторам. Это просто ошибки, которые выводятся пользователю, чтобы он мог поправить свой документ.
Неужели описанная функция возвращает только Истина, а при неудаче всегда падает в ошибку?
А вот и нет! Всё ещё интереснее =)
Я провёл эксперименты и составил табличку. Что будет, если присвоить Отказ = Истина в одном из методов.
Поведение метода Форма.Записать() при отказе в событии
| Модуль | Событие | Поведение |
|---|---|---|
| МодульОбъекта | ОбработкаПроверкиЗаполнения | Возвращает Ложь |
| МодульОбъекта | ПередЗаписью | Вызывает исключение |
| МодульОбъекта | ПриЗаписи | Вызывает исключение |
| МодульОбъекта | ОбработкаПроведения | Вызывает исключение |
| МодульФормы | ОбработкаПроверкиЗаполненияНаСервере | Возвращает Ложь |
| МодульФормы | ПередЗаписью | Возвращает Ложь |
| МодульФормы | ПередЗаписьюНаСервере | Возвращает Ложь |
| МодульФормы | ПриЗаписиНаСервере | Возвращает Ложь |
Какие выводы?
- Метод возвращает Ложь, если сделать отказ в любом из событии формы.
- Но при отказе в событиях объекта — падает в исключение.
- Но если это событие объекта ОбработкаПроверкиЗаполнения(), то тоже вернет Ложь
- Но если отказ происходит самой платформой, то будет вызвано исключение.
Что такое "отказ самой платформой"? Это, как мы приводили пример выше, платформенная проверка заполнения реквизита. Или, например, попытка провести документ, который помечен на удаление. Или попытка изменить документ, который уже кто-то поменял ранее, пока пользователь держал форму открытой.
Почему так работает метод? Неизвестно. Я вел жаркую переписку с сотрудником поддержки. Как показалось, он сам не знал о таком поведении, и в процессе переписки мы находили новые возможные ситуации, когда метод вызовет исключение.
Но все ответы сводились примерно к этому:

Если подвести итоги ответа, то позиция 1С такая: "это нештатная ситуация".
В данном случае "нештатной" считается:
- Установка Отказ = Истина в событиях модуля объекта (кроме ОбработкаПроверкиЗаполнения)
- Неуказанный пользователем обязательный реквизит
- Попытка пользователем провести помеченный на удаление документ
- Попытка изменить документ, который уже поменял другой пользователь
- ???
В таких ситуациях, если использовать метод формы Записать(), то он вызовет исключение. И каждая такая неудачная попытка записи будет фиксироваться как ошибка в ЖР.

Чем это плохо? Разработчики, которые будут опираться на описание метода в справке, могут не догадаться, что обычный отказ в модуле объекта (по мнению 1С — "нештатная ситуация"), будет вызывать ошибку. И, соответственно, прерывать выполнение кода. Ведь далее (после программной записи) вполне может оказаться какой-то кусок кода, который, по мнению программиста, должен выполниться в любом случае.
Все это значит, что на возвращаемое значение стоит опираться только если Записать() обернуть в попытку. В идеале нужно хотя бы в примечании справки описать возможность возникновения исключения. Но поддержка 1С, к сожалению, отказалась добавлять примечание в справку 😣 Поэтому, остается надеяться, что программисты будут читать эту статью 😅
Для своих нужд я использую небольшой метод в общем модуле:
// Записывает объект формы. Возвращает признак успеха.
// Нужен для обхода недокументированного поведения платформы.
// В СП описано, что метод формы Записать() возвращает признак успеха.
// Истина - успешно записан; Ложь - в противном случае.
// Но это происходит не во всех случаях.
// Подробнее: //sale.itcity.ru/public/1396380/?ref=1159
//
// Параметры:
// Форма - ФормаКлиентскогоПриложения - Форма, объект которой нужно записать
// ПараметрыЗаписи - Структура - ПараметрыЗаписи метода Записать() формы
// СообщитьПриИсключении - булево - нужно ли сообщать ОписаниеОшибки() при возникновении исключения
//
// Возвращаемое значение:
// Булево - Истина - успешно записан; Ложь - в противном случае.
//
Функция ЗаписатьФорму(Форма, ПараметрыЗаписи, СообщитьПриИсключении = Истина) Экспорт
Попытка
ЗаписанУспешно = Форма.Записать(ПараметрыЗаписи);
Исключение
ЗаписанУспешно = Ложь;
Если СообщитьПриИсключении Тогда
Сообщить(ОписаниеОшибки());
КонецЕсли;
КонецПопытки;
Возврат ЗаписанУспешно;
КонецФункции
ПараметрыЗаписи ≠ ДополнительныеСвойства ^
Когда появляется задача записать объект с передачей доп.свойств, самым логичным кажется, что для этого достаточно просто передать в метод эти самые доп.свойства. Например:
ДополнительныеСвойства = Новый Структура;
ДополнительныеСвойства.Вставить("МоеСвойство", Истина);
Записать(ДополнительныеСвойства);
Но, естественно, это не так. Ведь ПараметрыЗаписи — это не ДополнительныеСвойства. Но как передать дополнительные свойства объекта в метод формы Записать()?
Платформа сама не предоставляет такой возможности, но мы можем это сделать сами в методе ПередЗаписьюНаСервере().
Например:
&НаСервере
Процедура ПередЗаписьюНаСервере(Отказ, ТекущийОбъект, ПараметрыЗаписи)
Для Каждого КлючИЗначение Из ПараметрыЗаписи Цикл
ТекущийОбъект.ДополнительныеСвойства.Вставить(КлючИЗначение.Ключ, КлючИЗначение.Значение);
КонецЦикла;
КонецПроцедуры
Нельзя просто так передать РежимЗаписи ^

Что если мы хотим программно провести форму документа? Для этого нужно просто передать РежимЗаписи:
&НаКлиенте
Процедура ЗаписатьПрограммно(Команда)
ПараметрыЗаписи = Новый Структура;
ПараметрыЗаписи.Вставить("РежимЗаписи", РежимЗаписиДокумента.Проведение);
Записать(ПараметрыЗаписи);
КонецПроцедурыДа, такой код сработает корректно. А если мы хотим программно не провести, а записать? Нужно просто поменять значение режима записи?
Не все так просто. Дело в том, что у формы документа есть "особенность".
Для эксперимента поменяем режим записи в методе ЗаписатьПрограммно. И в событии ПередЗаписью() у формы установим точку останова.

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

Режим записи подменился. Мы передали методу "Запись", а платформа заменила его на "Проведение".
Почему так? Всему виной свойство формы "ПриЗаписиПерепроводить"

Эта галочка отвечает за то, будет ли документ перепроводиться при нажатии на кнопку "Запись".
Про это поведение самой кнопки знают многие. Стандартно, если документ проведен, то нажатие кнопки "Записать" будет приводить к повторному проведению.
Но, как оказалось, это поведение распространяется не только на действие пользователя, но и на программную запись разработчиком.
И вполне логичным было бы это поведение, происходи оно только в том случае, когда разработчик НЕ передавал напрямую РежимЗаписи. Но, по факту, даже если программист настаивает на режиме записи "Запись", то платформа проигнорирует его требование. И сама подменит режим записи на "Проведение".
Можно ли как-то это обойти? Очередным костылем.
&НаКлиенте
Процедура ЗаписатьПрограммно(Команда)
ЗаписатьПрограммноНаСервере();
КонецПроцедуры
Процедура ЗаписатьПрограммноНаСервере()
ПриЗаписиПерепроводить = Ложь;
ПараметрыЗаписи = Новый Структура;
ПараметрыЗаписи.Вставить("РежимЗаписи", РежимЗаписиДокумента.Запись);
Попытка
Записать(ПараметрыЗаписи);
Исключение
Сообщить(ОписаниеОшибки());
КонецПопытки;
ПриЗаписиПерепроводить = Истина;
КонецПроцедуры
Здесь мы сначала отключаем свойство ПриЗаписиПерепроводить. А потом (после самой попытки записи) включаем снова.
Но почему это происходит на сервере? А просто это свойство на клиенте доступно только для чтения. Как хорошо, когда в примечании пишут важную информацию. Вот бы везде так 😉

Как думаете, есть более "правильный" способ? Напишите об этом в комментариях.
А пока я сразу скажу "минус" данного подхода. Дело в том, что вызов метода Форма.Записать() на сервере тоже имеет свои "нюансы"…
При вызове на сервере пропускаются клиентские события ^
Из самого заголовка можно понять смысл данного "нюанса".
Дело в том, что если мы попытаемся вызвать метод формы Записать() на сервере, то платформа не будет выполнять клиентские события.
Давайте проверим. Для эксперимента я реализовал такой код:
&НаКлиенте
Процедура ЗаписатьНаКлиенте(Команда)
Записать();
КонецПроцедуры
&НаКлиенте
Процедура ЗаписатьНаСервере(Команда)
ЗаписатьНаСервереНаСервере();
КонецПроцедуры
&НаСервере
Процедура ЗаписатьНаСервереНаСервере()
Записать();
КонецПроцедуры
&НаСервере
Процедура ОбработкаПроверкиЗаполненияНаСервере(Отказ, ПроверяемыеРеквизиты)
Сообщить("ОбработкаПроверкиЗаполненияНаСервере");
КонецПроцедуры
&НаКлиенте
Процедура ПередЗаписью(Отказ, ПараметрыЗаписи)
Сообщить("ПередЗаписью");
КонецПроцедуры
&НаСервере
Процедура ПередЗаписьюНаСервере(Отказ, ТекущийОбъект, ПараметрыЗаписи)
Сообщить("ПередЗаписьюНаСервере");
КонецПроцедуры
&НаСервере
Процедура ПриЗаписиНаСервере(Отказ, ТекущийОбъект, ПараметрыЗаписи)
Сообщить("ПриЗаписиНаСервере");
КонецПроцедуры
&НаСервере
Процедура ПослеЗаписиНаСервере(ТекущийОбъект, ПараметрыЗаписи)
Сообщить("ПослеЗаписиНаСервере");
КонецПроцедуры
&НаКлиенте
Процедура ПослеЗаписи(ПараметрыЗаписи)
Сообщить("ПослеЗаписи");
КонецПроцедуры
Теперь у нас на форме две кнопки для программной записи. Одна выполняется "на клиенте", другая "на сервере".

Записывать будем проведенный документ, чтобы отработали все события.
Вот таблица, которая показывает отличия между выполнениями этих кнопок.
События формы при вызове метода Записать()
| Событие | НаКлиенте | НаСервере |
|---|---|---|
| ПередЗаписью | + | — |
| ОбработкаПроверкиЗаполненияНаСервере | + | + |
| ПередЗаписьюНаСервере | + | + |
| ПриЗаписиНаСервере | + | + |
| ПослеЗаписиНаСервере | + | + |
| ПослеЗаписи | + | — |
Как видим, в управляемой форме есть два клиентских события, которые задействованы в записи данных. ПередЗаписью() и ПослеЗаписи(). Эти события отрабатываться не будут, если метод формы Записать() вызван на сервере.
С одной стороны это логично. Ведь мы находимся на сервере и не можем "вызвать" клиент.
Но с другой — неочевидно. Можно, не подумав об этом последствии, вызывать метод Записать() на сервере, а потом удивляться, почему часть кода не выполняется.
Такое бы стоило отражать в примечании к методу. Как думаете? 😁
Урезанная ОбработкаПроверкиЗаполненияНаСервере() ^
На этот раз "особенность" касается не только программной записи.
Для начала прочтем справку:
ФормаКлиентскогоПриложения (ClientApplicationForm)
ОбработкаПроверкиЗаполненияНаСервере (FillCheckProcessingAtServer)
Синтаксис:
ОбработкаПроверкиЗаполненияНаСервере(<Отказ>, <ПроверяемыеРеквизиты>)
Параметры:
<Отказ>


