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