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-таблице.
Почему AJAX в админ-гриде Битрикса может работать только один раз?
Частая причина — неправильная навигация и отсутствие чистого BASE_LINK. Если грид получает AJAX-URL со служебными параметрами как базовый адрес, параметры начинают накапливаться, история браузера портится, а следующие действия ломаются. Решение — использовать CAdminUiResult и SetNavigationParams() с явным BASE_LINK.
Чем CAdminUiResult лучше CAdminResult для нового административного грида?
CAdminUiResult лучше подходит для нового UI административных списков. Он корректнее работает с современным гридом, AJAX-навигацией и механизмами CAdminUiList. Старый подход через CAdminResult и NavText(GetNavPrint()) может включить неподходящий шаблон пагинации.
Почему фильтр лучше выводить через DisplayFilter?
DisplayFilter() правильно связывает фильтр с гридом, передаёт нужный GRID_ID и согласует поведение фильтра с AJAX-механикой списка. При ручном подключении bitrix:main.ui.filter легко получить конфликт ID или фильтр, который визуально есть, но работает отдельно от грида.
Как правильно делать сортировку и фильтрацию по вычисляемым колонкам?
Если колонка рассчитывается по другой таблице или агрегату, лучше материализовать значение: заранее сохранить результат в отдельное поле записи и обновлять его при пересчёте данных. Тогда сортировка и фильтрация выполняются на уровне базы данных, а не через загрузку большого массива в PHP.
Почему нельзя фильтровать большие списки через PHP?
Потому что это плохо масштабируется. Если в системе десятки или сотни тысяч записей, загрузка всех строк в память ради сортировки или фильтрации приведёт к медленной работе, большому расходу памяти и нестабильному поведению. Для административного грида выборка должна идти через SQL с фильтром, сортировкой, лимитом и offset.
Нужны ли индексы для кастомного админ-грида?
Да. Если поля участвуют в фильтрации, сортировке или агрегации, под них нужны индексы. Без индексов страница может тормозить даже при правильно собранном UI. Лучше добавлять индексы через миграции, чтобы структура базы была воспроизводимой.
Почему прямое открытие AJAX-URL может показывать белый экран?
Потому что AJAX-ответ не обязан быть самостоятельной страницей. Он рассчитан на обработку внутри уже загруженной административной страницы Битрикса. Ошибка вроде BX is not defined при прямом открытии AJAX-URL не всегда означает проблему в серверной логике.
Григорий Фролов
Григорий Фролов
Руководитель NJ Soft

Поделиться: