Как правильно настроить отображение занятости номеров в календаре 1С и избежать зависаний интерфейса?

Программист 1С v8.3 (Обычные формы) Управленческий учет Сфера услуг, туризм и социальный сектор
← На главную

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

В этой статье мы подробно разберем практический кейс из реальной практики автоматизации гостиничного бизнеса. Мы проанализируем исходный код, выявим шесть критических архитектурных и логических ошибок, которые приводят к нестабильной работе системы, и по шагам реализуем оптимизированное, быстрое и отказоустойчивое решение.

Исходная задача и суть проблемы

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

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


Детальный анализ ошибок исходного кода

Давайте подробно проанализируем представленный на форуме код и разберем, почему он работает неэффективно и со сбоями.

Ошибка 1. Критическая проблема производительности — Вызовы сервера в обработчике отрисовки интерфейса

Самая опасная ошибка в коде — это вызов функции ПолучитьСведенияОЗанятостиНомера(), содержащей запрос к базе данных, непосредственно внутри процедуры КалендарьПриВыводеПериода.

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

Выполнять сетевой вызов к серверу и обращаться с тяжелым запросом к СУБД при каждом таком микро-движении — это верный способ намертво заблокировать работу пользователя («зафризить» интерфейс). Нагрузка на сервер приложений и базу данных вырастет лавинообразно даже при работе одного пользователя, а в многопользовательском режиме система просто откажется работать.

Ошибка 2. Ограничение платформы — Передача таблицы значений с сервера на клиент

В коде серверная функция возвращает объект типа ТаблицаЗначений напрямую на клиента:


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

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

Ошибка 3. Неоптимальное условие связи в запросе к виртуальной таблице

Обратим внимание на текст запроса к регистру накопления:


|ИЗ
|    РегистрНакопления.ПП_Загруженность.Остатки(&ДТ, ) КАК ПП_ЗагруженностьОстатки
|ГДЕ
|    ПП_ЗагруженностьОстатки.НомерПроживания = &НомерПроживания

Здесь фильтр по номеру проживания накладывается в секции ГДЕ, то есть уже после того, как виртуальная таблица СУБД рассчитала остатки. СУБД сначала выберет и рассчитает остатки по абсолютно всем номерам в базе данных, загрузит этот гигантский объем информации в оперативную память и только потом отфильтрует строки по нужному номеру. При росте базы данных этот запрос начнет выполняться чрезвычайно медленно.

Ошибка 4. Дублирование дат и логическое наложение цветов

Как справедливо заметили участники обсуждения в Сообщении 11, в таблице результатов запроса на один и тот же день может приходиться несколько строк. Например, в номере может быть установлено несколько коек (мест), либо в один и тот же день один гость выезжает утром, а другой заезжает после обеда.

В исходном коде обход выполняется следующим образом:


Для каждого ТекДата Из ОформлениеПериода.Даты Цикл    
    Для каждого стр из загруженность Цикл
        Если (ТекДата.Дата = стр.День) Тогда
            // Установка цвета...
        КонецЕсли;    
    КонецЦикла;
КонецЦикла;

Если в таблице загруженность на дату 10-е число есть три строки (например: «Бронь», «Проживание» и «Свободно»), то в цикле цвет этой даты будет последовательно перезаписан три раза. В результате день окрасится в тот цвет, строка которого случайно оказалась последней в таблице. Это приводит к абсолютно непредсказуемому поведению интерфейса.

Ошибка 5. Отсутствие сброса оформления (очистки цвета)

В коде отсутствует ветка сброса цвета для дат, по которым нет записей в базе данных. Если пользователь сначала выбрал занятый номер (календарь окрасился), а затем переключился на абсолютно свободный номер, платформа может оптимизировать отрисовку и «забыть» стереть старые цвета. В результате свободный номер будет отображаться как занятый.

Для исправления этого поведения необходимо явно указывать стандартный цвет для незанятых дней, о чем предупреждали в Сообщении 3.

Ошибка 6. Архитектурное несоответствие — Использование регистра накопления для хранения состояний

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

Для таких задач идеально подходит Регистр сведений (периодический, в пределах дня или секунды). Он не создает избыточных таблиц итогов, которые перегружают базу данных, и позволяет мгновенно получать срез последних состояний на любую дату.


Пошаговое руководство по правильной реализации

Теперь, когда мы детально изучили все слабые места, разработаем надежное и быстрое решение, лишенное этих недостатков. Мы перенесем тяжелую работу на сервер, внедрим систему кэширования на клиенте и сделаем код совместимым с тонким клиентом.

Шаг 1. Оптимизируем и переписываем запрос на сервере

Для начала напишем правильный запрос к базе данных. Нам необходимо:

  1. Передать параметры фильтрации непосредственно в параметры виртуальной таблицы остатков.
  2. Реализовать приоритезацию состояний на уровне базы данных, чтобы исключить дубли дат и наложение цветов. Приоритет будет следующим: Текущее заселение (самый высокий приоритет, вес 3) -> Проживание (вес 2) -> Бронь (вес 1) -> Свободно (вес 0).

Рассмотрим структуру запроса:


&НаСервере
Функция ПолучитьТаблицуЗанятостиНаСервере(Знач НомерПроживания, Знач ДатаНачала, Знач ДатаОкончания)
    
    лЗапрос = Новый Запрос;
    лТекст = "
    |ВЫБРАТЬ
    |    ПП_ЗагруженностьОстатки.День КАК День,
    |    // Задаем числовой приоритет для каждого вида занятости
    |    ВЫБОР
    |        КОГДА ПП_ЗагруженностьОстатки.ВидЗанятостиНомера = ЗНАЧЕНИЕ(Перечисление.ВидЗанятостиНомера.Проживание) ТОГДА 2
    |        КОГДА ПП_ЗагруженностьОстатки.ВидЗанятостиНомера = ЗНАЧЕНИЕ(Перечисление.ВидЗанятостиНомера.Бронь) ТОГДА 1
    |        ИНАЧЕ 0
    |    КОНЕЦ КАК Приоритет
    |ПОМЕСТИТЬ ВТ_ИсходныеДанные
    |ИЗ
    |    // Фильтруем данные внутри параметров виртуальной таблицы остатков!
    |    РегистрНакопления.ПП_Загруженность.Остатки(&Период, НомерПроживания = &НомерПроживания) КАК ПП_ЗагруженностьОстатки
    |ГДЕ
    |    ПП_ЗагруженностьОстатки.День МЕЖДУ &ДатаНачала И &ДатаОкончания
    |;
    |
    |////////////////////////////////////////////////////////////////////////////////
    |// Сворачиваем данные по дням, выбирая для каждого дня только максимальный приоритет
    |ВЫБРАТЬ
    |    ВТ_ИсходныеДанные.День КАК День,
    |    МАКСИМУМ(ВТ_ИсходныеДанные.Приоритет) КАК МаксПриоритет
    |ИЗ
    |    ВТ_ИсходныеДанные КАК ВТ_ИсходныеДанные
    |
    |СГРУППИРОВАТЬ ПО
    |    ВТ_ИсходныеДанные.День
    |";
    
    лЗапрос.Текст = лТекст;
    лЗапрос.УстановитьПараметр("Период", ТекущаяДата()); // или Объект.Дата
    лЗапрос.УстановитьПараметр("НомерПроживания", НомерПроживания);
    лЗапрос.УстановитьПараметр("ДатаНачала", НачалоДня(ДатаНачала));
    лЗапрос.УстановитьПараметр("ДатаОкончания", КонецДня(ДатаОкончания));
    
    Выборка = лЗапрос.Выполнить().Выбрать();
    
    // Вместо ТаблицыЗначений возвращаем структуру "Соответствие" (Map).
    // Ключ - Дата (Начало дня), Значение - числовой приоритет состояния.
    // Этот тип данных прекрасно передается на любой клиент (Тонкий, Веб) без ошибок.
    Результат = Новый Соответствие;
    Пока Выборка.Следующий() Цикл
        Результат.Вставить(НачалоДня(Выборка.День), Выборка.МаксПриоритет);
    КонецЦикла;
    
    Возврат Результат;
    
КонецФункции

Шаг 2. Реализуем кэширование данных на клиенте

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

Для этого создадим на форме реквизит (или переменную модуля в толстом клиенте) с именем КэшЗанятостиНомера.

Рассмотрим процедуру обновления кэша:


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

Эту процедуру ОбновитьКэшЗанятостиНомера() необходимо вызывать в следующих событиях:

Шаг 3. Быстрое и корректное оформление календаря на клиенте

Теперь наш обработчик вывода периода будет работать мгновенно. Он не выполняет никаких запросов и не обращается к серверу. Он просто берет готовую дату из календаря, обращается к локальному кэшу (переменной КэшЗанятостиНомера) и мгновенно красит ее в нужный цвет.

Проанализируем оптимизированный код обработчика:


&НаКлиенте
Процедура КалендарьПриВыводеПериода(Элемент, ОформлениеПериода)
    
    // Если кэш пуст, ничего не красим
    Если КэшЗанятостиНомера = Неопределено Тогда
        Возврат;
    КонецЕсли;
    
    Для каждого ТекДата Из ОформлениеПериода.Даты Цикл
        
        ТекущийДень = НачалоДня(ТекДата.Дата);
        
        // Сначала проверяем, не входит ли эта дата в диапазон оформляемого сейчас заселения
        Если ТекущийДень >= НачалоДня(Объект.ДатаНачала) И ТекущийДень <= НачалоДня(Объект.ДатаОкончания) Тогда
            // Текущее заселение имеет наивысший визуальный приоритет
            ТекДата.ЦветФона = WebЦвета.ХакиТемный;
            Продолжить;
        КонецЕсли;
        
        // Ищем состояние дня в кэше по ключу-дате (поиск в Соответствии работает мгновенно)
        ПриоритетСостояния = КэшЗанятостиНомера.Получить(ТекущийДень);
        
        Если ПриоритетСостояния = Неопределено Тогда
            // Если данных нет, обязательно сбрасываем цвет на стандартный (очищаем старое оформление)
            ТекДата.ЦветФона = Новый Цвет; 
        ИначеЕсли ПриоритетСостояния = 2 Тогда
            // Проживание
            ТекДата.ЦветФона = WebЦвета.НейтральноАквамариновый;
        ИначеЕсли ПриоритетСостояния = 1 Тогда
            // Бронь
            ТекДата.ЦветФона = WebЦвета.Хаки;
        Иначе
            // На всякий случай обрабатываем прочие неописанные состояния
            ТекДата.ЦветФона = Новый Цвет;
        КонецЕсли;
        
    КонецЦикла;
    
КонецПроцедуры

Что мы получили в результате оптимизации?

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

  1. Мгновенный отклик интерфейса. Пользователь может быстро прокручивать календарь, перетаскивать окно программы, кликать по датам — календарь перерисовывается моментально, без единой задержки, так как все данные берутся из локального кэша оперативной памяти клиента.
  2. Минимальная нагрузка на сервер и СУБД. Вместо сотен хаотичных запросов в секунду при прокрутке календаря, программа делает ровно один легкий, точечный запрос при выборе номера.
  3. Корректное отображение без наложения цветов. Благодаря группировке и приоритезации данных на уровне SQL-запроса, СУБД возвращает строго одну запись для каждого дня с наивысшим приоритетом состояния. Наложение цветов полностью исключено.
  4. Кроссплатформенность и готовность к будущему. Использование структуры Соответствие вместо ТаблицыЗначений гарантирует, что этот код без проблем запустится в тонком клиенте, веб-клиенте на Linux или в мобильном приложении.
← На главную