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