Закрепляем заголовки и первую колонку GridView средствами CSS

РазделGridViewУровеньСредний Чтение8 минОпубликовано ТегиASP.NETGridViewCSS

Большая таблица без закреплённой шапки быстро превращается в кашу из цифр. Разбираем, как зафиксировать заголовки и первую колонку GridView, как Excel делает это с областями.

Главное. Закрепление в GridView держится на трёх вещах: правильной разметке thead/th, прокручиваемом контейнере и position: sticky с непрозрачным фоном. Старые expression() в современных браузерах мертвы, их нужно заменять.

Зачем замораживать строки и столбцы

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

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

Ниже разберём схему по шагам: сначала заставим GridView выдавать правильную разметку, затем добавим прокручиваемый контейнер, потом закрепим шапку и первую колонку.

Правильная разметка: th и thead

По умолчанию GridView рендерит шапку как обычную строку tr с ячейками td, а то и вовсе без отдельной секции заголовка. Для закрепления это неудобно: браузеру и CSS проще работать с thead и th. Нужны два шага. Свойство UseAccessibleHeader превращает ячейки шапки в th, а TableSection у HeaderRow заставляет вывести её внутри thead.

Важно делать это после каждого DataBind, иначе на постбэке разметка вернётся к исходному виду. Удобнее всего обработчик PreRender: он срабатывает уже после привязки данных, при любом сценарии загрузки.

Default.aspx.vbVB.NET
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
Если включён ShowFooter, аналогично выставьте FooterRow.TableSection = TableRowSection.TableFooter, иначе подвал окажется внутри tbody.

Контейнер с прокруткой

Закрепить что-то можно только относительно области, которая прокручивается. Поэтому GridView кладём в div с фиксированными размерами и overflow: auto. Именно этот блок будет давать полосы прокрутки, а таблица внутри него может быть сколько угодно широкой и длинной.

Размеры удобнее задавать классом, а не атрибутом style: так их проще менять для мобильной вёрстки. Высоту лучше ограничивать через max-height, чтобы при малом количестве строк под таблицей не оставалось пустого места. Классы для закрепляемых ячеек назначаем прямо в разметке колонок через ItemStyle-CssClass и HeaderStyle-CssClass.

Default.aspxASPX
<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, о котором ниже.

legacy.css (только IE7 и ниже)CSS
/* Исторический вариант, в современных браузерах не работает */
.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.cssCSS
.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, чтобы получить свежие ссылки на ячейки.

freeze-fallback.jsJavaScript
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'));

Как это делают сегодня

В новых проектах на .NET 8 таблицы обычно строят на Blazor QuickGrid или на Razor Pages с обычной HTML-таблицей, а закрепление делают тем же position: sticky, без всяких серверных свойств. Для очень больших наборов данных используют виртуализацию (Virtualize у QuickGrid), чтобы в DOM находились только видимые строки. Готовые компоненты вроде AG Grid умеют закреплять колонки из коробки, но для типичной админки хватает десятка строк CSS.

Вопросы и ответы

Почему шапка GridView не закрепляется, хотя в CSS есть expression()?

CSS-выражения работали только в IE7 и ниже. Все современные браузеры их игнорируют, поэтому замените их на position: sticky с top: 0 у ячеек th.

Можно ли закрепить две первые колонки?

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

Нужно ли для sticky менять DOCTYPE страницы?

Нет. Sticky работает в режиме стандартов с обычным <!DOCTYPE html>. Совет держать Transitional относился только к старому приёму с expression().

Читайте также