CAdminUiList в 1С-Битрикс: как сделать нативный админ-грид и не сломать AJAX, фильтр и сортировку

Веб разработка
3 июня 2026
CAdminUiList в 1С-Битрикс: как сделать нативный админ-грид и не сломать AJAX, фильтр и сортировку

Иногда задача звучит невинно: «У нас есть таблица в админке, сделайте её как стандартный список товаров в Битриксе». То есть чекбоксы, массовые действия, гамбургер, шестерёнка настройки колонок, сортировка по заголовкам, фильтр, пагинация и AJAX без перезагрузки страницы.

На словах — обычная доработка интерфейса. На практике — переезд с самописной HTML-таблицы на полноценную подсистему административного грида 1С-Битрикс.

В этом кейсе мы разбираем, почему «просто прикрутить шестерёнку» не получится, зачем нужен CAdminUiList, почему AJAX может «работать один раз», чем опасен неправильный BASE_LINK и почему агрегаты лучше материализовать заранее, а не считать на лету в PHP.

Содержание

  1. Контекст задачи
  2. Архитектура: HL-блок, D7 ORM и нестандартный путь
  3. Скелет правильной страницы CAdminUiList
  4. Грабля №1: AJAX работает только один раз
  5. Грабля №2: фильтр не привязывается к гриду
  6. Грабля №3: агрегатные колонки и масштабирование
  7. Отладка AJAX без браузера
  8. Окружение и воспроизводимость
  9. Выводы
  10. FAQ

Контекст задачи

Исходная ситуация: в административной части проекта был самописный раздел «Правила первичной отбраковки». Данные выводились обычной HTML-таблицей. Таблица работала, но по ощущениям была чужеродной для Битрикса: без стандартной настройки колонок, без нормальных массовых действий, без привычной сортировки и фильтрации.

Запрос клиента был понятный: сделать интерфейс «как в стандартных списках товаров». То есть не просто красиво нарисовать таблицу, а получить поведение штатного административного списка:

  • чекбоксы для выбора строк;
  • гамбургер действий;
  • шестерёнку настройки колонок;
  • сортировку кликом по заголовку;
  • фильтр;
  • пагинацию;
  • AJAX-навигацию без полной перезагрузки страницы.

И вот здесь начинается неприятная правда: нативный админ-грид Битрикса — это не набор отдельных кнопок. Это связанная подсистема ядра. Если пытаться прикрутить её кусками к самописной таблице, быстро появляются странные баги: сортировка ведёт не туда, фильтр живёт отдельно, AJAX ломает URL, пагинация начинает накапливать служебные параметры.

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

Архитектура: HL-блок, D7 ORM и нестандартный путь

В нашем случае данные хранились не в обычном инфоблоке, а в Highload-блоке. Доступ к ним шёл через D7 ORM: getList() для выборки и getCount() для общего количества записей.

Это важный момент. В стандартных примерах часто подразумевается, что страница лежит в привычном месте, работает с типовой сущностью и использует типовой путь. В реальном проекте всё было сложнее:

  • данные находились в Highload-блоке;
  • страница физически лежала в /local/;
  • в админке она открывалась как /bitrix/admin/... через urlrewrite.php;
  • часть логики была завязана на кастомные классы проекта;
  • требовалось сохранить поведение, похожее на стандартные списки ядра.

Поэтому ориентироваться только на документацию было недостаточно. Более полезный источник — живые эталоны ядра:

  • iblock/admin/iblock_element_admin.php;
  • highloadblock/admin/highloadblock_rows_list.php.

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

Скелет правильной страницы CAdminUiList

Первое, что важно принять: у страницы на CAdminUiList критичен порядок выполнения.

Упрощённая логика такая:

  1. в самом начале подключается prolog_admin_before.php;
  2. после этого выполняется вся серверная логика страницы;
  3. готовятся фильтр, сортировка, выборка, навигация, строки грида;
  4. только потом подключается prolog_admin_after.php;
  5. после этого вызывается вывод фильтра и списка.

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

Отдельная грабля — проверка прав. Если конструктор вашего класса данных требует администратора и при отсутствии доступа выбрасывает исключение, неавторизованный пользователь может получить 500 ошибку вместо формы логина.

Поэтому проверку лучше делать явно до создания объектов, которые зависят от прав:

require_once($_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_admin_before.php');

global $USER, $APPLICATION;

if (!$USER->IsAdmin()) {
    $APPLICATION->AuthForm('Access denied');
}

// дальше создаём объекты, готовим фильтр, сортировку, выборку и грид

Кастомный HTML-контент тоже лучше не выводить «голым» echo между прологом и списком. Для этого у административной страницы есть штатный механизм:

$APPLICATION->BeginPrologContent();

// ваш дополнительный контент

$APPLICATION->EndPrologContent();

Это снижает риск сломать структуру административной страницы и поведение компонентов нового UI.

Грабля №1: AJAX работает только один раз

Самая неприятная проблема в этом кейсе выглядела так: первый клик по сортировке или пагинации срабатывал нормально. Данные действительно обновлялись. Но после этого адрес в браузере превращался в AJAX-URL со служебными параметрами. Следующий клик уже работал некорректно, а принудительное обновление страницы через CMD+R могло давать белый экран.

На первый взгляд кажется, что проблема на сервере. Но серверная часть была в порядке:

  • сортировка применялась правильно;
  • данные приходили корректные;
  • AJAX-ответ формировался;
  • ошибок в самой ORM-выборке не было.

Проблема оказалась на стыке навигации и клиентской истории грида. Клиентская часть записывала в адресную строку «грязный» URL через механизм истории. В URL попадали служебные параметры вроде internal, grid_action, bxajaxid, а параметр страницы начинал накапливаться.

Причина была в том, что список собирался через CAdminResult и старый механизм NavText(GetNavPrint()) без явного BASE_LINK. В результате включался не тот шаблон пагинации, и новый UI-грид получал некорректную базу для навигации.

Правильное решение — использовать CAdminUiResult и задавать навигацию через SetNavigationParams() с чистым BASE_LINK:

$result = new CAdminUiResult($ormResult, $sTableID);

$lAdmin->SetNavigationParams($result, [
    'BASE_LINK' => '/bitrix/admin/your_page.php',
]);

Смысл BASE_LINK простой: гриду нужно явно сказать, какой URL считать нормальной базой страницы. Особенно если физически файл лежит в /local/, а в админке открывается через /bitrix/admin/... и urlrewrite.php.

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

Практический вывод: в новом административном UI Битрикса навигацию для грида лучше задавать только через CAdminUiResult и SetNavigationParams() с явным BASE_LINK.

Грабля №2: фильтр не привязывается к гриду

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

Причина — ручное подключение компонента bitrix:main.ui.filter с тем же ID, что и у грида, но без корректного GRID_ID. В результате два компонента фактически боролись за один идентификатор.

В таких случаях лучше не собирать фильтр вручную. У CAdminUiList есть штатный метод:

$lAdmin->DisplayFilter($uiFilter);

Он сам делает важные вещи:

  • правильно привязывает фильтр к гриду;
  • передаёт нужный GRID_ID;
  • включает подписи полей;
  • согласует поведение фильтра с AJAX-гридом.

При этом значения фильтра всё равно читаются отдельно через Bitrix\Main\UI\Filter\Options. Затем на их основе вручную строится ORM-фильтр для getList().

Обычно приходится обрабатывать несколько типов полей:

  • string — текстовый поиск;
  • list — выбор из списка;
  • number — числовые условия через _numsel, _from, _to;
  • date — диапазоны дат.

Здесь важно не путать две задачи. DisplayFilter() отвечает за корректное отображение и привязку UI. А построение ORM-фильтра — это уже ваша серверная логика.

Грабля №3: агрегатные колонки и масштабирование

Отдельный пласт проблем начинается, когда в гриде появляются колонки, которых физически нет в основной сущности.

Например:

  • количество срабатываний правила;
  • дата последнего срабатывания;
  • результат агрегации по другой таблице;
  • статистика, рассчитанная по связанным данным.

На маленьких объёмах очень соблазнительно решить это в PHP: получить список правил, отдельно посчитать агрегаты, отфильтровать ID, отсортировать массив и отдать результат в грид.

Это плохой путь.

В нашем кейсе ожидалось около 100 тысяч записей. Наивный подход уже на промежуточном этапе мог тянуть в память 60 тысяч и больше строк. Даже если сегодня это «терпимо», завтра такая страница станет медленной, нестабильной и плохо предсказуемой.

Правильный подход — материализация агрегатов. То есть не считать тяжёлые значения при каждом открытии списка, а сохранять их в отдельных полях записи в момент пересчёта данных.

Например:

  • UF_LAST_COUNT — количество последних срабатываний;
  • UF_LAST_DATETIME — дата последнего срабатывания.

Такие поля можно обновлять в процессе фонового пересчёта или в методе, который обновляет статистику. После этого фильтрация, сортировка и пагинация снова становятся нормальными SQL-операциями на уровне базы данных.

Это принципиально важно. Админ-грид должен получать уже подготовленную выборку с limit, offset, order и фильтром. Он не должен заставлять PHP загружать весь массив данных в память ради одной страницы.

Также нужны индексы. Без них даже аккуратный UI не спасёт:

  • индексы под поля фильтрации;
  • индексы под поля сортировки;
  • композитные индексы под частые комбинации условий;
  • индексы под поля, участвующие в агрегации;
  • префиксные индексы для длинных текстовых полей, если они участвуют в поиске.

Лучше оформлять такие изменения через миграции, например через Sprint.Migration. Тогда структура базы будет воспроизводимой, а не «где-то руками добавили индекс на проде».

Отладка AJAX без браузера

Ещё один полезный приём — отлаживать AJAX-запросы не только через браузер, но и напрямую через серверную среду.

В нашем случае помогло воспроизведение реального авторизованного AJAX-запроса через curl с общей файловой сессией. Если CLI и FPM работают в одном контейнере и используют одну директорию сессий, можно увидеть сырой ответ грида и отделить серверную проблему от клиентской.

Это особенно полезно, когда визуально кажется, что «Битрикс просто сломался», а на деле сервер отдаёт корректные данные, но клиентская часть неправильно обновляет историю или URL.

Важно понимать и другой момент: прямое открытие AJAX-URL в браузере может давать белый экран или ошибки вроде BX is not defined. Это не всегда баг. AJAX-ответ не обязан быть самостоятельной HTML-страницей. Он рассчитан на обработку внутри уже загруженной административной страницы.

Поэтому диагностику лучше строить так:

  1. проверить, что ORM-запрос возвращает правильные данные;
  2. проверить сырой AJAX-ответ;
  3. проверить, какие параметры попадают в URL;
  4. проверить, какой шаблон пагинации используется;
  5. сравнить поведение со штатными файлами ядра.

Чтение исходников ядра здесь не прихоть, а нормальный рабочий метод. В подобных задачах ответы часто лежат не в документации, а в том, как сами разработчики Битрикса собирают административные списки.

Окружение и воспроизводимость

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

В кейсе проект поднимался локально в Docker на официальных образах bitrix-tools:

  • nginx;
  • php-fpm;
  • mysql или percona;
  • cron;
  • восстановление базы из бэкапа.

При восстановлении старого или крупного проекта почти всегда всплывают технические нюансы. Например:

  • в дампе могут быть внешние ключи на таблицы, которые ещё не созданы, поэтому при импорте приходится временно отключать FOREIGN_KEY_CHECKS;
  • для MySQL 8 или Percona могут понадобиться дополнительные права, например SESSION_VARIABLES_ADMIN, если в подключении выполняется SET innodb_strict_mode=0;
  • версия PHP должна соответствовать коду проекта, особенно если используются современные возможности языка вроде типизированных констант классов.

Воспроизводимое окружение сокращает время отладки в разы. Когда можно спокойно повторить AJAX-запрос, посмотреть сырой ответ, временно включить дополнительный лог и сравнить поведение с ядром, задача перестаёт быть гаданием по интерфейсу.

Выводы

Главный вывод простой: не надо воевать с фреймворком. Если нужен административный список в стиле нового UI Битрикса, лучше сразу строить его на CAdminUiList, а не пытаться оживить самописную HTML-таблицу отдельными кнопками и компонентами.

Три вещи, которые экономят много часов отладки:

  • CAdminUiResult вместе с SetNavigationParams() и явным BASE_LINK;
  • нативный DisplayFilter() вместо ручной сборки main.ui.filter;
  • материализация агрегатов вместо расчётов и сортировки больших массивов в PHP.

И ещё одно: думать о масштабе нужно сразу. Если в системе ожидаются десятки или сотни тысяч записей, фильтрация, сортировка и пагинация должны выполняться на уровне базы данных. PHP не должен имитировать SQL поверх огромного массива.

Хороший административный интерфейс в Битриксе — это не только «чтобы было похоже на ядро». Это корректная архитектура страницы, чистая навигация, правильная связь фильтра с гридом, индексы в базе и воспроизводимое окружение для отладки.

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

FAQ

Технически можно попытаться, но это плохой путь. Шестерёнка, фильтр, сортировка, массовые действия и AJAX-навигация в новом административном UI связаны между собой. Надёжнее сразу строить список на CAdminUiList, чем прикручивать отдельные элементы к самописной HTML-таблице.

Частая причина — неправильная навигация и отсутствие чистого BASE_LINK. Если грид получает AJAX-URL со служебными параметрами как базовый адрес, параметры начинают накапливаться, история браузера портится, а следующие действия ломаются. Решение — использовать CAdminUiResult и SetNavigationParams() с явным BASE_LINK.

CAdminUiResult лучше подходит для нового UI административных списков. Он корректнее работает с современным гридом, AJAX-навигацией и механизмами CAdminUiList. Старый подход через CAdminResult и NavText(GetNavPrint()) может включить неподходящий шаблон пагинации.

DisplayFilter() правильно связывает фильтр с гридом, передаёт нужный GRID_ID и согласует поведение фильтра с AJAX-механикой списка. При ручном подключении bitrix:main.ui.filter легко получить конфликт ID или фильтр, который визуально есть, но работает отдельно от грида.

Если колонка рассчитывается по другой таблице или агрегату, лучше материализовать значение: заранее сохранить результат в отдельное поле записи и обновлять его при пересчёте данных. Тогда сортировка и фильтрация выполняются на уровне базы данных, а не через загрузку большого массива в PHP.

Потому что это плохо масштабируется. Если в системе десятки или сотни тысяч записей, загрузка всех строк в память ради сортировки или фильтрации приведёт к медленной работе, большому расходу памяти и нестабильному поведению. Для административного грида выборка должна идти через SQL с фильтром, сортировкой, лимитом и offset.

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

Потому что AJAX-ответ не обязан быть самостоятельной страницей. Он рассчитан на обработку внутри уже загруженной административной страницы Битрикса. Ошибка вроде BX is not defined при прямом открытии AJAX-URL не всегда означает проблему в серверной логике.
Григорий Фролов
Григорий Фролов
Руководитель NJ Soft

Поделиться: