Пост

Неочевидные нюансы записи управляемой формы

Разберем несколько нюансов записи управляемой формы.


Предисловие

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

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

В каждой форме, у которой основным реквизитом указан редактируемый объект базы данных, есть метод Записать()

Скрин

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

Скрин

Это самое универсальное описание метода. Но в разных типах данных появляются свои “предопределенные” значения в структуре ПараметрыЗаписи.

Больше всего для нашего изучения подойдет документ:

Скрин

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

Файловая или серверная - не важно. Описываемые нюансы работают и там, и там.

Скрин

Должна вернуть признак успеха. Но не обязана

Одно из отличий методов “Записать” формы и самого объекта заключается в этом:

Скрин

Да, метод формы - это функция. Которая возвращает признак успеха записи. Но так ли это? Как показала практика, не всегда стоит доверять справке.

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

А может нам просто нужно после какого-то действия сохранить данные формы? Вполне обычная практика.

На самом деле, для наших тестовых целей все это не важно. Давайте для начала просто создадим кнопку Записать программно.

Скрин

В обработчике кнопки простой код:

1
2
3
4
5
6
7
8
9
10
11
12
13
&НаКлиенте
Процедура ЗаписатьПрограммно(Команда)
	
	УдалосьЗаписать = Записать();
	Если УдалосьЗаписать Тогда
		ТекстПредупреждения = "Ура, удалось записать!";
	Иначе
		ТекстПредупреждения = "Жаль, но не вышло!";
	КонецЕсли;
	
	ПоказатьПредупреждение(, ТекстПредупреждения);
	
КонецПроцедуры

Если записать форму удалось, то метод должен вернуть Истина. Проверяем эту теорию:

Скрин

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

Скрин

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

Скрин

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

Но ведь в справке описано иначе:

Скрин

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

Скрин

Давайте попробуем другую ситуацию. В модуле объекта самого документа добавим процедуру ПередЗаписью и установим в ней отказ.

1
2
3
4
5
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
	
	Отказ = Истина;
	
КонецПроцедуры

Проверим, как поведёт себя метод в данном случае.

Скрин

Метод опять не вернул Ложь. А просто выдал ошибку, словно это исключение в коде.

Более того. Если зайти в журнал регистрации, то можно увидеть эти ошибки:

Скрин

Каждая такая “ошибка” фиксируется в журнале регистрации

То есть, если разработчик, опираясь на описание метода платформы, не оборачивает Записать() в Попытка`Исключение, то отказы в ПередЗаписью()` будут фиксироваться в ЖР. И засорять его бессмысленными ошибками. Почему бессмысленными? Потому что чаще всего это будет незаполненность какого-то обязательного реквизита, или какой-нибудь отказ в обработчике объекта. Это всё обычные штатные ситуации, которые не нужны администраторам. Это просто ошибки, которые выводятся пользователю, чтобы он мог поправить свой документ.

Неужели описанная функция возвращает только Истина, а при неудаче всегда падает в ошибку?

А вот и нет! Всё ещё интереснее =)

Я провёл эксперименты и составил табличку. Что будет, если присвоить Отказ = Истина в одном из методов.

Поведение метода Форма.Записать() при отказе в событии

Вот таблица в формате Markdown:

МодульСобытиеПоведение
Модуль ОбъектаОбработкаПроверкиЗаполненияВозвращает Ложь
Модуль ОбъектаПередЗаписьюВызывает исключение
Модуль ОбъектаПриЗаписиВызывает исключение
Модуль ОбъектаОбработкаПроведенияВызывает исключение
Модуль ФормыОбработкаПроверкиЗаполненияНаСервереВозвращает Ложь
Модуль ФормыПередЗаписьюВозвращает Ложь
Модуль ФормыПередЗаписьюНаСервереВозвращает Ложь
Модуль ФормыПриЗаписиНаСервереВозвращает Ложь

Какие выводы?

  • Метод возвращает Ложь, если сделать отказ в любом из событии формы.
  • Но при отказе в событиях объекта - падает в исключение.
  • Но если это событие объекта ОбработкаПроверкиЗаполнения(), то тоже вернет Ложь
  • Но если отказ происходит самой платформой, то будет вызвано исключение.

Что такое “отказ самой платформой”? Это, как мы приводили пример выше, платформенная проверка заполнения реквизита. Или, например, попытка провести документ, который помечен на удаление. Или попытка изменить документ, который уже кто-то поменял ранее, пока пользователь держал форму открытой.

Почему так работает метод? Неизвестно. Я вел жаркую переписку с сотрудником поддержки. Как показалось, он сам не знал о таком поведении, и в процессе переписки мы находили новые возможные ситуации, когда метод вызовет исключение.

Но все ответы сводились примерно к этому:

Скрин

Если подвести итоги ответа, то позиция 1С такая: “это нештатная ситуация”.

В данном случае “нештатной” считается:

  • Установка Отказ = Истина в событиях модуля объекта (кроме ОбработкаПроверкиЗаполнения)
  • Неуказанный пользователем обязательный реквизит
  • Попытка пользователем провести помеченный на удаление документ
  • Попытка изменить документ, который уже поменял другой пользователь
  • ???

В таких ситуациях, если использовать метод формы Записать(), то он вызовет исключение. И каждая такая неудачная попытка записи будет фиксироваться как ошибка в ЖР.

Скрин

Чем это плохо? Разработчики, которые будут опираться на описание метода в справке, могут не догадаться, что обычный отказ в модуле объекта (по мнению 1С - “нештатная ситуация”), будет вызывать ошибку. И, соответственно, прерывать выполнение кода. Ведь далее (после программной записи) вполне может оказаться какой-то кусок кода, который, по мнению программиста, должен выполниться в любом случае.

Все это значит, что на возвращаемое значение стоит опираться только если Записать() обернуть в попытку. В идеале нужно хотя бы в примечании справки описать возможность возникновения исключения. Но поддержка 1С, к сожалению, отказалась добавлять примечание в справку 😣 Поэтому, остается надеяться, что программисты будут читать эту статью 😅

Для своих нужд я использую небольшой метод в общем модуле:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Записывает объект формы. Возвращает признак успеха. 
// Нужен для обхода недокументированного поведения платформы.
//  В СП описано, что метод формы Записать() возвращает признак успеха. 
//  Истина - успешно записан; Ложь - в противном случае.
//  Но это происходит не во всех случаях.
//  Подробнее: //infostart.ru/public/1396380/?ref=1159
//
// Параметры:
//  Форма - ФормаКлиентскогоПриложения - Форма, объект которой нужно записать
//  ПараметрыЗаписи  - Структура - ПараметрыЗаписи метода Записать() формы
//  СообщитьПриИсключении - булево - нужно ли сообщать ОписаниеОшибки() при возникновении исключения
//
// Возвращаемое значение:
//   Булево   - Истина - успешно записан; Ложь - в противном случае.
//
Функция ЗаписатьФорму(Форма, ПараметрыЗаписи, СообщитьПриИсключении = Истина) Экспорт
	
	Попытка
		ЗаписанУспешно = Форма.Записать(ПараметрыЗаписи);
	Исключение
	    ЗаписанУспешно = Ложь;
		Если СообщитьПриИсключении Тогда
			Сообщить(ОписаниеОшибки());
		КонецЕсли;
	КонецПопытки;
	
	Возврат ЗаписанУспешно;
	
КонецФункции

ПараметрыЗаписи ≠ ДополнительныеСвойства

Когда появляется задача записать объект с передачей доп.свойств, самым логичным кажется, что для этого достаточно просто передать в метод эти самые доп.свойства. Например:

1
2
3
ДополнительныеСвойства = Новый Структура;
ДополнительныеСвойства.Вставить("МоеСвойство", Истина);
Записать(ДополнительныеСвойства);

Но, естественно, это не так. Ведь ПараметрыЗаписи - это не ДополнительныеСвойства. Но как передать дополнительные свойства объекта в метод формы Записать()?

Платформа сама не предоставляет такой возможности, но мы можем это сделать сами в методе ПередЗаписьюНаСервере().

Например:

1
2
3
4
5
6
7
8
&НаСервере
Процедура ПередЗаписьюНаСервере(Отказ, ТекущийОбъект, ПараметрыЗаписи)
	
	Для Каждого КлючИЗначение Из ПараметрыЗаписи Цикл
	    ТекущийОбъект.ДополнительныеСвойства.Вставить(КлючИЗначение.Ключ, КлючИЗначение.Значение);
	КонецЦикла;
	
КонецПроцедуры

Нельзя просто так передать РежимЗаписи

Скрин

Что если мы хотим программно провести форму документа? Для этого нужно просто передать РежимЗаписи:

1
2
3
4
5
6
7
8
&НаКлиенте
Процедура ЗаписатьПрограммно(Команда)
	
	ПараметрыЗаписи = Новый Структура;
	ПараметрыЗаписи.Вставить("РежимЗаписи", РежимЗаписиДокумента.Проведение);
	Записать(ПараметрыЗаписи);
	
КонецПроцедуры

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

Не все так просто. Дело в том, что у формы документа есть “особенность”.

Для эксперимента поменяем режим записи в методе ЗаписатьПрограммно. И в событии ПередЗаписью() у формы установим точку останова.

Скрин

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

Скрин

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

Почему так? Всему виной свойство формы ПриЗаписиПерепроводить

Скрин

Эта галочка отвечает за то, будет ли документ перепроводиться при нажатии на кнопку Запись.

Про это поведение самой кнопки знают многие. Стандартно, если документ проведен, то нажатие кнопки Записать будет приводить к повторному проведению.

Но, как оказалось, это поведение распространяется не только на действие пользователя, но и на программную запись разработчиком.

И вполне логичным было бы это поведение, происходи оно только в том случае, когда разработчик НЕ передавал напрямую РежимЗаписи. Но, по факту, даже если программист настаивает на режиме записи Запись, то платформа проигнорирует его требование. И сама подменит режим записи на Проведение.

Можно ли как-то это обойти? Очередным костылем.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
&НаКлиенте
Процедура ЗаписатьПрограммно(Команда)
	
	ЗаписатьПрограммноНаСервере();
	
КонецПроцедуры

Процедура ЗаписатьПрограммноНаСервере()

	ПриЗаписиПерепроводить = Ложь;
	
	ПараметрыЗаписи = Новый Структура;
	ПараметрыЗаписи.Вставить("РежимЗаписи", РежимЗаписиДокумента.Запись);
	Попытка
		Записать(ПараметрыЗаписи);
	Исключение
		Сообщить(ОписаниеОшибки());
	КонецПопытки;
	
	ПриЗаписиПерепроводить = Истина;

КонецПроцедуры

Здесь мы сначала отключаем свойство ПриЗаписиПерепроводить. А потом (после самой попытки записи) включаем снова.

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

Скрин

Как думаете, есть более “правильный” способ? Напишите об этом в комментариях.

А пока я сразу скажу “минус” данного подхода. Дело в том, что вызов метода Форма.Записать() на сервере тоже имеет свои “нюансы”…

При вызове на сервере пропускаются клиентские события

Из самого заголовка можно понять смысл данного “нюанса”.

Дело в том, что если мы попытаемся вызвать метод формы Записать() на сервере, то платформа не будет выполнять клиентские события.

Давайте проверим. Для эксперимента я реализовал такой код:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
&НаКлиенте
Процедура ЗаписатьНаКлиенте(Команда)
	Записать();
КонецПроцедуры

&НаКлиенте
Процедура ЗаписатьНаСервере(Команда)
	ЗаписатьНаСервереНаСервере();
КонецПроцедуры

&НаСервере
Процедура ЗаписатьНаСервереНаСервере()	
	Записать();
КонецПроцедуры


&НаСервере
Процедура ОбработкаПроверкиЗаполненияНаСервере(Отказ, ПроверяемыеРеквизиты)
	Сообщить("ОбработкаПроверкиЗаполненияНаСервере");
КонецПроцедуры

&НаКлиенте
Процедура ПередЗаписью(Отказ, ПараметрыЗаписи)
	 Сообщить("ПередЗаписью");
КонецПроцедуры

&НаСервере
Процедура ПередЗаписьюНаСервере(Отказ, ТекущийОбъект, ПараметрыЗаписи)
	Сообщить("ПередЗаписьюНаСервере");
КонецПроцедуры

&НаСервере
Процедура ПриЗаписиНаСервере(Отказ, ТекущийОбъект, ПараметрыЗаписи)
	Сообщить("ПриЗаписиНаСервере");
КонецПроцедуры

&НаСервере
Процедура ПослеЗаписиНаСервере(ТекущийОбъект, ПараметрыЗаписи)
	Сообщить("ПослеЗаписиНаСервере");
КонецПроцедуры

&НаКлиенте
Процедура ПослеЗаписи(ПараметрыЗаписи)
	Сообщить("ПослеЗаписи");
КонецПроцедуры

Теперь у нас на форме две кнопки для программной записи. Одна выполняется “на клиенте”, другая “на сервере”.

Скрин

Записывать будем проведенный документ, чтобы отработали все события.

Вот таблица, которая показывает отличия между выполнениями этих кнопок.

Скрин

Как видим, в управляемой форме есть два клиентских события, которые задействованы в записи данных. ПередЗаписью() и ПослеЗаписи(). Эти события отрабатываться не будут, если метод формы Записать() вызван на сервере.

С одной стороны это логично. Ведь мы находимся на сервере и не можем “вызвать” клиент.

Но с другой - неочевидно. Можно, не подумав об этом последствии, вызывать метод Записать() на сервере, а потом удивляться, почему часть кода не выполняется.

Такое бы стоило отражать в примечании к методу. Как думаете? 😁

Урезанная ОбработкаПроверкиЗаполненияНаСервере()

На этот раз “особенность” касается не только программной записи.

Для начала прочтем справку:

Скрин

Когда разработчик попытается в форме документа отменить проверку заполнения какого-то реквизита объекта, то будет ужасно огорчен. Ведь платформа это сделать не позволяет.

Давайте добавим нашему документу обязательный реквизит Сумма.

Скрин

И установим точку останова на ОбработкаПроверкиЗаполненияНаСервере()

Скрин

Да, как и описано в СП, здесь присутствует ключ Объект, который позволяет нам полностью отменить логику проверки самого объекта. Но, здесь нельзя изменить состав реквизитов объекта, которые он будет проверять.

Мы НЕ можем в форме документа отменить проверку реквизита Сумма.

Можно только отменить всю проверку целиком. Но в таком случае метод объекта ОбработкаПроверкиЗаполнения() просто вообще не выполнится. А ведь там может быть какая-то очень важная логика!

Неужели технически невозможно никак отменить проверку реквизита объекта из формы? Можно. Но никогда так не делайте!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
&НаСервере
Процедура ОбработкаПроверкиЗаполненияНаСервере(Отказ, ПроверяемыеРеквизиты)
	
	ИндексЭлемента = ПроверяемыеРеквизиты.Найти("Объект");
	Если НЕ ИндексЭлемента = Неопределено Тогда
		
		ПроверяемыеРеквизиты.Удалить(ИндексЭлемента);
		
		ТекущийОбъект = РеквизитФормыВЗначение("Объект");
		ТекущийОбъект.ДополнительныеСвойства.Вставить("НеНадоПроверятьСуммуДокументаПожалуйста", Истина);
		Отказ = НЕ ТекущийОбъект.ПроверитьЗаполнение();
		ЗначениеВРеквизитФормы(ТекущийОбъект, "Объект");
		
	КонецЕсли;
	
КонецПроцедуры

Да, Вы, наверное, уже догадались. В данном куске кода разработчик отменяет штатный вызов метода объекта ОбработкаПроверкиЗаполнения(), но далее сам же и вызывает его с передачей доп.параметра.

В самом же модулей объекта происходит такое:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Процедура ОбработкаПроверкиЗаполнения(Отказ, ПроверяемыеРеквизиты)
	
	Перем УбедительноПросятНеПроверятьСумму;
	
	ДополнительныеСвойства.Свойство("НеНадоПроверятьСуммуДокументаПожалуйста", УбедительноПросятНеПроверятьСумму);
	
	Если УбедительноПросятНеПроверятьСумму = Истина Тогда
		ИндексЭлемента = ПроверяемыеРеквизиты.Найти("Сумма");
		Если ИндексЭлемента <> Неопределено Тогда
			ПроверяемыеРеквизиты.Удалить(ИндексЭлемента);
		КонецЕсли;
	КонецЕсли;
	
КонецПроцедуры

Скрин

Да, это очень плохо и вызывает кровотечение из глаз. И поэтому не делайте так. А если найдете альтернативный способ отменить проверку реквизита объекта из самой формы, то пишите в комментариях 👍

Но зачем же тогда нужно это событие вообще, если мы не можем отменить проверку обязательного реквизита? Всё просто. Мы можем это сделать, но только с реквизитами формы.

То есть, если у нас на форме есть реквизит (не объекта, а именно формы), то мы можем сделать его “обязательным”. Или наоборот разрешить его не указывать. Но только реквизиты самой формы.

Событие После_УСПЕШНОЙ_ЗаписиНаСервере()

Да, речь идёт про событие формы ПослеЗаписиНаСервере(). Оно уже вне транзакции. В нём нельзя отменить запись, потому что она уже была завершена. Очень удобное событие, которое позволяет как-то донастроить форму сразу после записи объекта.

И удобство этого обработчика ещё в том, что он имеет доступ к записываемому объекту:

Скрин

Это позволяет форме получить из объекта какие-то данные, которые были сформированы в модуле самого объекта. Например, в ДополнительныеСвойства в одном из событии объект мог положить какие-то дополнительные данные, а форма их оттуда взять.

Но, к сожалению, разработчики забывают, что это событие срабатывает только после УСПЕШНОЙ записи. То есть, если в одном из событий был установлен Отказ, то обработчик выполняться не будет. И код, который нужен для обновления формы, не выполнится. И данные из самого объекта получить не удастся.

Об этом можно догадаться по описанию:

Описание: Вызывается после записи объекта на сервере и после завершения транзакции.

Используя слово “завершения”, авторы подразумевали, что транзакция может быть завершена только успешно. А при ошибке - она отменена.

Но всё равно, встречаются такие решения, когда разработчики, не поняв этой особенности, думают, что событие отработает всё равно.

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

В таких нештатных ситуациях можно использовать какие-нибудь хитрости. Например, при записи из формы, НЕ производить отказ в модуле объекта, а просто устанавливать флаг в ДополнительныеСвойства. А уже в модуле формы, в самом последнем транзакционном событии, устанавливать этот признак отказа.

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

Скрин

Авторский пост защищен лицензией CC BY 4.0 .