Перевод интерфейса прикладных решений с обычных форм на управляемые часто сопровождается серьезным пересмотром механизмов динамической фильтрации данных. В обычных формах стандартом де-факто было использование объекта ПостроительЗапроса и его свойства Отбор, которые легко выводились в табличное поле формы. В управляемом интерфейсе эта технология уступила место более гибкому и мощному инструменту — Системе компоновки данных (СКД) и связанному с ней объекту КомпоновщикНастроекКомпоновкиДанных.
В этой статье мы подробно разберем, как решить две взаимосвязанные прикладные задачи, часто возникающие при миграции кода на управляемые формы:
ХранилищеЗначения.Рассмотрим весь процесс по шагам, проанализируем неочевидные тонкости платформы и разберем лучшие практики оптимизации серверных вызовов.
Для того чтобы предоставить пользователю возможность интерактивно настраивать отборы на управляемой форме по произвольному тексту запроса, нам необходимо программно сконструировать объект СхемаКомпоновкиДанных, добавить в него набор данных, а затем проинициализировать компоновщик настроек.
Рассмотрим процедуру создания динамической схемы компоновки данных. Создадим пустую схему, добавим источник данных и набор данных типа "Запрос":
// Создаем объект схемы компоновки данных
СхемаКомпоновкиДанных = Новый СхемаКомпоновкиДанных;
// Добавляем источник данных (локальный)
ИсточникДанных = СхемаКомпоновкиДанных.ИсточникиДанных.Добавить();
ИсточникДанных.Имя = "ИсточникДанных1";
ИсточникДанных.ТипИсточникаДанных = "Local";
// Добавляем набор данных - запрос
НаборДанных = СхемаКомпоновкиДанных.НаборыДанных.Добавить(Тип("НаборДанныхЗапросСхемыКомпоновкиДанных"));
НаборДанных.Имя = "НаборДанных1";
НаборДанных.ИсточникДанных = ИсточникДанных.Имя;
НаборДанных.Запрос = ТекстЗапроса; // Передаем наш произвольный текст запроса
Важная деталь об автозаполнении полей: Обратите внимание на свойство АвтоЗаполнениеДоступныхПолей. В обсуждении на форуме высказывалось мнение, что его лучше устанавливать в значение Ложь, если в запросе нет фигурных скобок {}. Это критическое заблуждение! Если вы установите данное свойство в значение Ложь, платформа не станет автоматически считывать структуру полей из секции ВЫБРАТЬ вашего запроса. В результате список доступных полей для отбора в компоновщике окажется пустым. Для автоматического заполнения доступных полей по запросу свойство АвтоЗаполнениеДоступныхПолей обязательно должно быть установлено в значение Истина (что является поведением по умолчанию).
После создания схемы ее необходимо "скормить" реквизиту нашей управляемой формы с типом КомпоновщикНастроекКомпоновкиДанных. Напрямую присвоить схему компоновщику на клиенте или сервере нельзя — инициализация выполняется через временное хранилище и специальный объект-источник доступных настроек.
Реализуем процедуру инициализации компоновщика на сервере:
&НаСервере
Процедура ИнициализироватьКомпоновщикНаСервере(ТекстЗапроса)
// 1. Создаем динамическую схему (код из предыдущего шага)
СхемаКомпоновкиДанных = Новый СхемаКомпоновкиДанных;
ИсточникДанных = СхемаКомпоновкиДанных.ИсточникиДанных.Добавить();
ИсточникДанных.Имя = "ИсточникДанных1";
ИсточникДанных.ТипИсточникаДанных = "Local";
НаборДанных = СхемаКомпоновкиДанных.НаборыДанных.Добавить(Тип("НаборДанныхЗапросСхемыКомпоновкиДанных"));
НаборДанных.Имя = "НаборДанных1";
НаборДанных.ИсточникДанных = ИсточникДанных.Имя;
НаборДанных.АвтоЗаполнениеДоступныхПолей = Истина; // Обязательно Истина!
НаборДанных.Запрос = ТекстЗапроса;
// Очистим настройки по умолчанию для гарантии чистоты структуры
СхемаКомпоновкиДанных.НастройкиПоУмолчанию.Отбор.Элементы.Очистить();
// 2. Помещаем схему во временное хранилище, привязывая к УИД формы
URLСхемыКомпоновкиДанных = ПоместитьВоВременноеХранилище(СхемаКомпоновкиДанных, УникальныйИдентификатор);
// 3. Инициализируем компоновщик настроек (реквизит формы "Компоновщик")
ИсточникНастроек = Новый ИсточникДоступныхНастроекКомпоновкиДанных(URLСхемыКомпоновкиДанных);
Компоновщик.Инициализировать(ИсточникНастроек);
// 4. Загружаем дефолтные настройки
Компоновщик.ЗагрузитьНастройки(СхемаКомпоновкиДанных.НастройкиПоУмолчанию);
Компоновщик.Восстановить(СпособВосстановленияНастроекКомпоновкиДанных.Полное);
КонецПроцедуры
Для вывода настроек на управляемую форму добавьте таблицу формы и свяжите её путь к данным с реквизитом Компоновщик.ПользовательскиеНастройки (или Компоновщик.Настройки.Отбор для упрощенного варианта быстрого отбора). Пользователь получит полноценный и привычный интерфейс для выбора полей, видов сравнения и значений отбора.
Вторая проблема, с которой столкнулся автор темы, заключается в необходимости сохранять настроенные отборы индивидуально для каждой строки табличной части справочника. Реквизит табличной части имеет тип ХранилищеЗначения.
Выясним причину, почему при попытке напрямую обратиться к реквизиту строки табличной части на форме возникает ошибка Поле объекта не обнаружено (СписокУсловий). Это связано с внутренней архитектурой управляемого интерфейса 1С. Платформа в целях минимизации сетевого трафика и экономии оперативной памяти клиентов полностью исключает данные с типом ХранилищеЗначения из объектов типов ДанныеФормыКоллекция и ДанныеФормыСтруктура. Физически этих колонок на клиенте нет, и даже контекстные серверные вызовы, работающие с данными формы, "не видят" эти свойства напрямую в коллекции формы.
Рассмотрим два способа обхода этого ограничения: быстрый способ (через преобразование объекта) и оптимальный с точки зрения производительности (best practice с использованием временного хранилища).
Чтобы получить доступ к реальным реквизитам базы данных (включая ХранилищеЗначения), необходимо конвертировать контекстный объект формы в полноценный прикладной объект на сервере. При этом важно конвертировать весь объект целиком, а не только его табличную часть.
Посмотрим на пример реализации сохранения настроек при изменении строки на форме:
&НаСервере
Процедура СохранитьНастройкиВСтрокуТЧНаСервере(НомерСтроки, АдресВременногоХранилища)
// Преобразуем объект формы в полноценный прикладной объект СправочникОбъект
СправочникОбъект = РеквизитФормыВЗначение("Объект");
// Получаем структуру настроек из временного хранилища
НастройкиДляСохранения = ПолучитьИзВременногоХранилища(АдресВременногоХранилища);
// Находим нужную строку в реальном объекте (индексация с 0)
ТекущаяСтрокаТЧ = СправочникОбъект.ТабличнаяЧасть[НомерСтроки - 1];
// Записываем настройки в реквизит с типом ХранилищеЗначения
ТекущаяСтрокаТЧ.СписокУсловий = Новый ХранилищеЗначения(НастройкиДляСохранения);
// Возвращаем измененные данные обратно в структуру формы
ЗначениеВРеквизитФормы(СправочникОбъект, "Объект");
КонецПроцедуры
Этот способ прост в реализации и отлично подходит для редких интерактивных операций. Однако у него есть существенный недостаток: метод РеквизитФормыВЗначение вынуждает платформу собирать весь объект целиком (со всеми реквизитами, табличными частями и проверками), что при частом вызове (например, при каждом изменении отбора пользователем) может приводить к заметным задержкам интерфейса и избыточной нагрузке на сервер.
Проанализируем более профессиональный подход, который повсеместно применяется в современных типовых конфигурациях 1С (БСП). Суть паттерна состоит в том, что мы не работаем напрямую с базой данных при каждом чихе пользователя. Вместо этого мы используем транзитную строковую колонку на форме для хранения адресов во временном хранилище, а реальную перезапись в ХранилищеЗначения производим единожды — в момент записи всего документа или элемента справочника.
Разберем данный алгоритм по шагам:
АдресНастроек с типом Строка.ПриСозданииНаСервере) прочитаем существующие хранилища значений из базы данных, поместим их во временное хранилище и запишем полученные адреса в колонку АдресНастроек.ПередЗаписьюНаСервере мы один раз переносим все данные из адресов временного хранилища обратно в реквизиты табличной части объекта БД перед их непосредственной записью.Рассмотрим программную реализацию этого паттерна.
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
// При открытии формы один раз считываем объект из БД
Для Каждого СтрокаТЧ Из Объект.ТабличнаяЧасть Цикл
// Получаем значение из ХранилищаЗначения, сохраненного в БД
НастройкиИзБД = СтрокаТЧ.СписокУсловий.Получить();
Если НастройкиИзБД <> Неопределено Тогда
// Помещаем во временное хранилище и записываем адрес в форму
СтрокаТЧ.АдресНастроек = ПоместитьВоВременноеХранилище(НастройкиИзБД, УникальныйИдентификатор);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Когда пользователь выбирает строку табличной части или изменяет настройки в компоновщике, мы оперативно сохраняем их во временное хранилище на клиенте/сервере без перезаписи всего объекта:
&НаКлиенте
Процедура ТабличнаяЧастьПриАктивизацииСтроки(Элемент)
ТекущиеДанные = Элементы.ТабличнаяЧасть.ТекущиеДанные;
Если ТекущиеДанные = Неопределено Тогда
Возврат;
КонецЕсли;
// Загружаем настройки из адреса, сохраненного в строке
Если ЗначениеЗаполнено(ТекущиеДанные.АдресНастроек) Тогда
ПодключитьКомпоновщикПоАдресу(ТекущиеДанные.АдресНастроек);
Иначе
// Если настроек еще нет, инициализируем пустой компоновщик
ИнициализироватьКомпоновщикНаСервере(Объект.ТекстЗапроса);
КонецЕсли;
КонецПроцедуры
&НаСервере
Процедура ПодключитьКомпоновщикПоАдресу(АдресНастроек)
// Получаем сохраненные настройки из временного хранилища
Настройки = ПолучитьИзВременногоХранилища(АдресНастроек);
// Восстанавливаем настройки в компоновщике формы
Компоновщик.ЗагрузитьНастройки(Настройки);
КонецПроцедуры
Когда пользователь меняет настройки в интерфейсе компоновщика, мы обновляем информацию во временном хранилище:
&НаКлиенте
Процедура ПриИзмененииНастроекКомпоновщика()
ТекущиеДанные = Элементы.ТабличнаяЧасть.ТекущиеДанные;
Если ТекущиеДанные = Неопределено Тогда
Возврат;
КонецЕсли;
// Получаем текущие настройки компоновщика и отправляем в хранилище
АдресСХранилищем = СохранитьКомпоновщикВоВременноеХранилище(ТекущиеДанные.АдресНастроек);
ТекущиеДанные.AddressНастроек = АдресСХранилищем;
КонецПроцедуры
&НаСервере
Функция СохранитьКомпоновщикВоВременноеХранилище(ТекущийАдрес)
// Получаем настройки компоновщика (НастройкиКомпоновкиДанных)
ТекущиеНастройки = Компоновщик.ПолучитьНастройки();
// Перезаписываем данные по старому адресу или создаем новый адрес
Если ЗначениеЗаполнено(ТекущийАдрес) Тогда
ПоместитьВоВременноеХранилище(ТекущиеНастройки, ТекущийАдрес);
Возврат ТекущийАдрес;
Иначе
Возврат ПоместитьВоВременноеХранилище(ТекущиеНастройки, УникальныйИдентификатор);
EndIf;
КонецФункции
В самый последний момент, перед стандартной записью прикладного объекта на сервере, платформа предоставляет нам удобное событие ПередЗаписьюНаСервере. Здесь мы трансформируем адреса временного хранилища обратно в бинарные данные ХранилищаЗначения:
&НаСервере
Процедура ПередЗаписьюНаСервере(Отказ, ТекущийОбъект, ПараметрыЗаписи)
Для Каждого СтрокаФормы Из Объект.ТабличнаяЧасть Цикл
// Находим соответствующую строку в записываемом прикладном объекте БД
СтрокаОбъекта = ТекущийОбъект.ТабличнаяЧасть[Объект.ТабличнаяЧасть.Индекс(СтрокаФормы)];
// Проверяем наличие временных данных на форме
Если ЗначениеЗаполнено(СтрокаФормы.АдресНастроек) И ЭтоАдресВременногоХранилища(СтрокаФормы.АдресНастроек) Тогда
// Извлекаем настройки из временного хранилища на сервере
НастройкиИзХранилища = ПолучитьИзВременногоХранилища(СтрокаФормы.АдресНастроек);
// Упаковываем настройки в ХранилищеЗначения
СтрокаОбъекта.СписокУсловий = Новый ХранилищеЗначения(НастройкиИзХранилища);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Преимущества оптимального паттерна:
СправочникОбъект собирается и проверяется бизнес-логикой платформы ровно один раз — при физическом сохранении элемента в базу данных.Используя предложенные методики, вы сможете легко перевести устаревшие формы отборов с использованием ПостроителяЗапроса обычного приложения на современные стандарты динамических настроек СКД в управляемом интерфейсе 1С.