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