
Закрепляем заголовки и первую колонку GridView средствами CSS
Большая таблица без закреплённой шапки быстро превращается в кашу из цифр. Разбираем, как зафиксировать заголовки и первую колонку GridView, как Excel делает это с областями.
Зачем замораживать строки и столбцы
Кто работал с длинными листами в Excel, знает функцию «Закрепить области». Пролистали таблицу на триста строк вниз, а названия столбцов по-прежнему перед глазами. Ушли вправо на двадцать колонок, а первая колонка с кодом клиента осталась на месте. Без этого сравнивать значения почти невозможно: приходится постоянно возвращаться к началу и вспоминать, что означает пятый столбец.
GridView выводит данные такой же сеткой, но готового свойства для закрепления у него нет. Нет у него и встроенной прокрутки: элемент просто растягивается на всю высоту данных, и страница становится бесконечной. Поэтому обе задачи решаются снаружи: обёрткой с прокруткой и стилями, которые удерживают нужные ячейки на месте.
Ниже разберём схему по шагам: сначала заставим GridView выдавать правильную разметку, затем добавим прокручиваемый контейнер, потом закрепим шапку и первую колонку.
Правильная разметка: th и thead
По умолчанию GridView рендерит шапку как обычную строку tr с ячейками td, а то и вовсе без отдельной секции заголовка. Для закрепления это неудобно: браузеру и CSS проще работать с thead и th. Нужны два шага. Свойство UseAccessibleHeader превращает ячейки шапки в th, а TableSection у HeaderRow заставляет вывести её внутри thead.
Важно делать это после каждого DataBind, иначе на постбэке разметка вернётся к исходному виду. Удобнее всего обработчик PreRender: он срабатывает уже после привязки данных, при любом сценарии загрузки.
Protected Sub GridView1_PreRender(sender As Object, e As EventArgs) _
Handles GridView1.PreRender
' Пустой грид не создаёт HeaderRow, проверяем
If GridView1.HeaderRow Is Nothing Then Return
' Ячейки шапки выводятся как <th scope="col">
GridView1.UseAccessibleHeader = True
' Шапка попадает в <thead>, тело в <tbody>
GridView1.HeaderRow.TableSection = TableRowSection.TableHeader
End SubКонтейнер с прокруткой
Закрепить что-то можно только относительно области, которая прокручивается. Поэтому GridView кладём в div с фиксированными размерами и overflow: auto. Именно этот блок будет давать полосы прокрутки, а таблица внутри него может быть сколько угодно широкой и длинной.
Размеры удобнее задавать классом, а не атрибутом style: так их проще менять для мобильной вёрстки. Высоту лучше ограничивать через max-height, чтобы при малом количестве строк под таблицей не оставалось пустого места. Классы для закрепляемых ячеек назначаем прямо в разметке колонок через ItemStyle-CssClass и HeaderStyle-CssClass.
<div class="grid-scroll">
<asp:GridView ID="GridView1" runat="server" CssClass="grid"
AutoGenerateColumns="false" GridLines="None">
<Columns>
<%-- первая колонка закрепляется --%>
<asp:BoundField DataField="CustomerID" HeaderText="Клиент"
ItemStyle-CssClass="frozen" HeaderStyle-CssClass="frozen" />
<asp:BoundField DataField="ShipName" HeaderText="Получатель" />
<asp:BoundField DataField="ShipAddress" HeaderText="Адрес" />
<asp:BoundField DataField="ShipCity" HeaderText="Город" />
<asp:BoundField DataField="ShipCountry" HeaderText="Страна" />
</Columns>
</asp:GridView>
</div>Как это делали в 2009 году: expression() в Internet Explorer
Во времена IE6 и IE7 свойства position: sticky не существовало. Популярный приём опирался на CSS-выражения, особенность только Internet Explorer. Ячейкам шапки задавали position: relative, а свойство top вычисляли выражением, которое читало scrollTop контейнера. При прокрутке браузер пересчитывал выражение, и шапка смещалась вниз ровно на столько, на сколько уехало содержимое. С первой колонкой то же самое, только через left и scrollLeft.
У приёма было несколько слабых мест. Выражения пересчитывались на каждое движение мыши и тормозили большие таблицы. Результат зависел от DOCTYPE: в строгом режиме вёрстка разъезжалась, поэтому страницы держали на Transitional. А начиная с IE8 в режиме стандартов expression() отключён полностью, в остальных браузерах он не поддерживался никогда.
Если вы встретили такой CSS в сопровождаемом проекте, то в любом современном браузере он просто игнорируется: шапка не закреплена, хотя код вроде бы есть. Лечение одно: заменить выражения на sticky, о котором ниже.
/* Исторический вариант, в современных браузерах не работает */
.FrozenHeader {
position: relative;
top: expression(document.getElementById("GridViewContainer").scrollTop - 2);
z-index: 10;
}
.FrozenCell {
position: relative;
left: expression(document.getElementById("GridViewContainer").scrollLeft - 2);
}Рабочий вариант: position: sticky
Сегодня задача решается несколькими строками CSS. Ячейке с position: sticky и top: 0 браузер сам не даёт уехать выше верхней границы прокручиваемого предка. Для первой колонки используем left: 0. Никаких обработчиков прокрутки и пересчётов: всё делает движок рендеринга, поэтому даже таблица на тысячи строк прокручивается плавно.
Обратите внимание на три детали. Липкость назначаем ячейкам th и td, а не строке tr: со строками таблиц sticky исторически работает нестабильно. Закреплённым ячейкам обязательно нужен непрозрачный фон, иначе под ними будет просвечивать прокручиваемый текст. Угловой ячейке, которая одновременно в шапке и в первой колонке, ставим самый высокий z-index, чтобы при диагональной прокрутке она оставалась сверху.
.grid-scroll {
max-height: 480px;
overflow: auto; /* именно этот блок прокручивается */
border: 1px solid var(--grid-border, #d0d7de);
}
.grid {
border-collapse: separate; /* с collapse границы sticky-ячеек пропадают */
border-spacing: 0;
}
.grid thead th {
position: sticky;
top: 0;
z-index: 2;
background: var(--grid-head-bg, #f3f5f7);
}
.grid .frozen {
position: sticky;
left: 0;
z-index: 1;
background: var(--grid-bg, #fff);
}
.grid thead th.frozen {
z-index: 3; /* угловая ячейка поверх всех */
}Подводные камни
Большинство проблем с закреплением возникают не в самом CSS, а в окружении таблицы. Если что-то не липнет, пройдитесь по списку ниже, прежде чем переписывать стили.
- У одного из предков между контейнером и таблицей стоит overflow: hidden. Sticky привязывается к ближайшему прокручиваемому предку, и если это не ваш div, эффекта не будет.
- Границы ячеек исчезают при прокрутке. С border-collapse: collapse граница принадлежит таблице, а не ячейке, поэтому используйте separate и рисуйте рамку через border-bottom у th.
- Закреплено несколько колонок. Для второй колонки left равен ширине первой, поэтому ширину первой лучше задать явно через width или min-width.
- Таблица внутри UpdatePanel. После асинхронного постбэка разметка пересоздаётся, но CSS продолжает действовать, так что sticky переживает обновление без дополнительного кода. А вот позиция прокрутки сбрасывается, её придётся сохранять в hidden-поле.
Если всё же нужен старый IE
Внутренние системы иногда до сих пор открывают в режиме совместимости IE11, где sticky не поддерживается. Тогда к CSS добавляют небольшой скрипт: он слушает прокрутку контейнера и сдвигает ячейки через transform. Это не так плавно, как нативное решение, но работает везде и не мешает современным браузерам, потому что подключается только при отсутствии поддержки sticky.
Скрипт вешается один раз на контейнер. Если таблица обновляется в UpdatePanel, его нужно повторно вызывать из обработчика pageLoad, чтобы получить свежие ссылки на ячейки.
function freezeGrid(box) {
// Браузер умеет sticky: ничего не делаем
if (window.CSS && CSS.supports && CSS.supports('position', 'sticky')) return;
var heads = box.querySelectorAll('thead th');
var cells = box.querySelectorAll('.frozen');
box.addEventListener('scroll', function () {
var y = box.scrollTop, x = box.scrollLeft, i;
for (i = 0; i < heads.length; i++) {
heads[i].style.transform = 'translateY(' + y + 'px)';
}
for (i = 0; i < cells.length; i++) {
var dy = cells[i].parentNode.parentNode.tagName === 'THEAD' ? y : 0;
cells[i].style.transform = 'translate(' + x + 'px,' + dy + 'px)';
}
});
}
freezeGrid(document.querySelector('.grid-scroll'));Как это делают сегодня
Вопросы и ответы
Почему шапка GridView не закрепляется, хотя в CSS есть expression()?
CSS-выражения работали только в IE7 и ниже. Все современные браузеры их игнорируют, поэтому замените их на position: sticky с top: 0 у ячеек th.
Можно ли закрепить две первые колонки?
Да. Второй колонке задайте свой класс с left, равным ширине первой. Ширину первой при этом лучше зафиксировать явно, иначе при разной длине данных колонки будут наезжать друг на друга.
Нужно ли для sticky менять DOCTYPE страницы?
Нет. Sticky работает в режиме стандартов с обычным <!DOCTYPE html>. Совет держать Transitional относился только к старому приёму с expression().