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