Как исправить ошибки разделения контекста НаКлиенте и НаСервере при печати в 1С?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

Переход с обычных форм на управляемое приложение в платформе "1С:Предприятие 8" часто сопровождается трудностями, связанными с изменением парадигмы выполнения кода. В старых версиях (или в толстом клиенте) разработчики привыкли писать весь код «в одном месте» — прямо в модуле формы выполнялись и запросы к базе данных, и вывод печатных форм на экран. В современных управляемых формах такая архитектура приводит к критическим ошибкам выполнения.

В этой статье мы подробно разберем, почему исходный код автора темы не работает ни на клиенте, ни на сервере, проанализируем причины ошибок компиляции и полностью перепишем процедуру печати в соответствии со стандартами разработки фирмы «1С».

В чем заключается главная проблема исходного кода?

Давайте внимательно проанализируем исходный код из Сообщения 0. Процедура объявлена с директивой компиляции &НаКлиенте. При этом внутри нее выполняются следующие действия:

  1. Попытка получить макет документа напрямую через метаданные: Документы.ЭкзаменационнаяВедомость.ПолучитьМакет("Печать").
  2. Прямое обращение к реквизитам объекта (Дата, Семестр, Год, Группа, ТабличнаяЧасть1) без указания контекста формы.
  3. Создание объектов типа Новый Запрос и обращение к таблицам базы данных.
  4. Обращение к менеджерам перечислений (Перечисления.Оценки.Неявка) и констант (Константы.Декан.Получить()).
  5. Вызов интерактивного метода показа печатной формы: ТабДок.Показать().

Такой код успешно выполнится только в толстом клиенте (обычные формы). В управляемом приложении (тонкий клиент, веб-клиент) он работать не будет. Тонкий клиент физически "не знает" о существовании базы данных напрямую, он не умеет выполнять SQL-подобные запросы и не хранит в памяти макеты метаданных. Всю эту работу должен делать сервер.

С другой стороны, если мы просто перенесем эту процедуру на сервер (например, под директиву &НаСервере или в модуль менеджера), мы получим целый букет ошибок компиляции, описанных в Сообщении 1. На сервере недоступен метод ТабДок.Показать(), поскольку у сервера нет графического интерфейса. Кроме того, в модуле менеджера отсутствует контекст конкретной формы, поэтому переменные типа ЭтотОбъект, Дата, Семестр и другие реквизиты документа там неопределены.

Анализ неэффективности запросов (Антипаттерн N+1)

Прежде чем исправлять контекстные ошибки, давайте обратим внимание на качество самого кода работы с базой данных. В исходном варианте автор делает пять отдельных запросов к одной и той же табличной части документа, чтобы посчитать количество различных оценок («Неявка/Недопуск», «Отлично», «Хорошо», «Удовлетворительно», «Неудовлетворительно»).

Каждое обращение к объекту Запрос — это отправка запроса СУБД, его парсинг, планирование и выполнение. Выполнять пять однотипных запросов вместо одного — грубейшая ошибка оптимизации. Кроме того, в каждом запросе используется избыточное и нелогичное левое соединение (ЛЕВОЕ СОЕДИНЕНИЕ) табличной части с шапкой документа. Табличная часть любого документа в 1С по умолчанию содержит индексированное поле Ссылка, связывающее ее с родителем. Соединять ее с шапкой нет никакой необходимости.

Рассмотрим, как можно оптимизировать получение всех данных шапки, подвала и результатов подсчета оценок с помощью одного-единственного запроса. Мы применим конструкцию ВЫБОР КОГДА ... ТОГДА ... ИНАЧЕ ... КОНЕЦ совместно с агрегатной функцией СУММА. Это позволит СУБД обработать выборку за один проход.

Пошаговый рефакторинг: разделяем код на Клиент и Сервер

Правильная архитектурная схема формирования печатных форм в управляемом приложении выглядит следующим образом:

  1. На клиенте: Пользователь нажимает кнопку печати. Вызывается клиентская процедура, которая инициирует процесс. В ней же в самом конце мы покажем готовый табличный документ пользователю.
  2. На сервере (без контекста): Вызывается функция, которая обращается к базе данных, получает макет, выполняет один оптимальный запрос, заполняет области табличного документа и возвращает готовый заполненный ТабличныйДокумент обратно на клиент.

Давайте реализуем эту схему на практике. Перенесем логику формирования печатной формы в модуль менеджера документа ЭкзаменационнаяВедомость. Это позволит в дальнейшем легко использовать эту же печатную форму в обработках групповой печати или при интеграции с БСП (Библиотекой Стандартных Подсистем).

Шаг 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, ВыборкаОценки.Неуд);
    КонецЕсли;
    
    // Получаем константу "Декан" на сервере
    ОбластьПодвал.Параметры.Декан = Константы.Декан.Получить();
    
    ТабДок.Вывести(ОбластьПодвал);
    
    // Настраиваем свойства табличного документа перед отправкой на клиент
    ТабДок.ОриентацияСтраницы = ОриентацияСтраницы.Портрет;
    ТабДок.ОтображатьСетку     = Ложь;
    ТабДок.Защита              = Ложь;
    ТабДок.ТолькоПросмотр      = Истина; // Для печатных форм рекомендуется ставить ТолькоПросмотр
    ТабДок.ОтображатьЗаголовки = Ложь;
    
    Возврат ТабДок;

КонецФункции

Разберем подробнее, почему этот серверный код эффективен и безопасен:

Шаг 2. Пишем клиентский вызов в Модуле Формы документа

Теперь вернемся в форму нашего документа. Нам необходимо привязать команду формы (например, нажатие кнопки "Печать") к вызову нашей серверной функции и отобразить результат на клиенте.

В модуле формы код будет выглядеть лаконично и красиво:


&НаКлиенте
Процедура Печать(Команда)
    
    // Проверяем, записан ли документ. Печать незаписанного документа не имеет смысла,
    // так как серверный запрос не найдет его в базе данных.
    Если Не ЗначениеЗаполнено(Объект.Ссылка) Тогда
        ПоказатьПредупреждение(, "Документ необходимо записать перед печатью!");
        Возврат;
    КонецЕсли;
    
    // Вызываем серверный метод получения заполненного макета
    ТабличныйДокумент = ПолучитьПечатнуюФормуНаСервере();
    
    // Показываем готовый документ пользователю на клиенте
    ТабличныйДокумент.Показать("Печать экзаменационной ведомости");

КонецПроцедуры

&НаСервереБезКонтекста
Функция ПолучитьПечатнуюФормуНаСервере(СсылкаНаДокумент)
    
    // Вызываем экспортную функцию из модуля менеджера
    Возврат Документы.ЭкзаменационнаяВедомость.СформироватьПечатнуюФорму(СсылкаНаДокумент);
    
КонецФункции

Проанализируем структуру клиент-серверного вызова:

  1. Процедура Печать выполняется на клиенте. В качестве параметра команды обычно передается элемент формы, но мы переименовали его в соответствии со стандартами разработки в Команда.
  2. Перед формированием печатной формы мы выполняем важнейшую проверку — проверяем, заполнена ли Объект.Ссылка. Если пользователь создал новый документ и нажал печать, не записав его, то база данных ничего не знает о такой ведомости. В этом случае мы выводим интерактивное окно с помощью метода ПоказатьПредупреждение().
  3. Для связи с сервером мы используем легковесный вызов &НаСервереБезКонтекста. Директива БезКонтекста критически важна для оптимизации: при таком вызове платформа не передает на сервер все данные формы (контекст), что существенно экономит интернет-трафик и ускоряет отклик интерфейса.
  4. На сервере функция обращается к модулю менеджера и возвращает готовый ТабличныйДокумент обратно на клиент. На клиенте мы вызываем интерактивный метод ТабличныйДокумент.Показать(). Среда выполнения не выдаст ошибок, так как клиент имеет графическую оболочку для визуализации таблиц.

Заключение

Мы полностью переработали устаревший и некорректно написанный код. Теперь программа разделена на уровни абстракции согласно стандартам клиент-серверной архитектуры "1С:Предприятия". Логика базы данных и вычисления осуществляются на стороне сервера в рамках одного оптимизированного пакета запросов, а визуализация и интерфейсные предупреждения происходят строго на стороне клиента.

Следуя этим правилам при разработке любых печатных форм в управляемом приложении, вы не только избежите ошибок компиляции, но и обеспечите высокую скорость работы системы даже при работе пользователей через веб-клиент или при медленном интернет-соединении.

← На главную