GridView с шаблонами и CSS-стилями: от BoundField к TemplateField

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

BoundField хорош для простого вывода, но в реальной админке нужны выпадающие списки, флажки, свои кнопки и аккуратный внешний вид. Всё это дают шаблонные колонки и CSS-классы.

Главное. TemplateField даёт полный контроль над разметкой строки, а FindControl и DataKeys избавляют от хрупких индексов ячеек. Внешний вид держите в CSS-классах, а не в атрибутах дизайнера.

Когда BoundField перестаёт хватать

BoundField выводит значение поля как текст, а в режиме редактирования подставляет TextBox. Для справочника из двух колонок этого достаточно. Но стоит появиться реальным требованиям, и ограничения вылезают сразу: статус нужно выбирать из списка, а не вводить руками; поле «Активен» должно быть флажком; к описанию нужен валидатор; а рядом со строкой требуется кнопка «Согласовать», которой в GridView нет.

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

ItemTemplate и EditItemTemplate

TemplateField сам по себе только контейнер. Главные в нём два шаблона. ItemTemplate отображается, когда строка в обычном режиме просмотра: обычно это Label, Literal или неактивный CheckBox. EditItemTemplate показывается в строке, номер которой совпадает с EditIndex грида: здесь размещают TextBox, DropDownList, CheckBox, валидаторы и даже пользовательские контролы.

Колонку с идентификатором редактировать нельзя, поэтому для неё EditItemTemplate не задают вовсе: тогда в режиме редактирования выводится ItemTemplate. Данные в шаблон подставляются выражениями привязки: Eval для однонаправленного вывода и Bind для двусторонней, которую понимают контролы источников данных.

Categories.aspxASPX
<asp:TemplateField HeaderText="Название">
  <ItemTemplate>
    <asp:Label ID="lblName" runat="server"
        Text='<%# Eval("CategoryName") %>' />
  </ItemTemplate>
  <EditItemTemplate>
    <asp:TextBox ID="txtName" runat="server" MaxLength="15"
        Text='<%# Bind("CategoryName") %>' CssClass="input" />
    <asp:RequiredFieldValidator runat="server" ControlToValidate="txtName"
        ErrorMessage="Укажите название" CssClass="error" Display="Dynamic" />
  </EditItemTemplate>
</asp:TemplateField>

<asp:TemplateField HeaderText="Активна">
  <ItemTemplate>
    <asp:CheckBox ID="chkActiveView" runat="server" Enabled="false"
        Checked='<%# Eval("IsActive") %>' />
  </ItemTemplate>
  <EditItemTemplate>
    <asp:CheckBox ID="chkActive" runat="server"
        Checked='<%# Bind("IsActive") %>' />
  </EditItemTemplate>
</asp:TemplateField>

FindControl вместо Controls(1)

После перехода на шаблоны меняется способ чтения значений при сохранении. С BoundField значение брали так: DirectCast(row.Cells(3).Controls(0), TextBox).Text. В TemplateField нулевым элементом коллекции Controls оказывается текстовый литерал с пробелами разметки, и TextBox уезжает на индекс 1. Старые примеры так и советовали: поменять ноль на единицу.

Работает это до первой правки шаблона. Добавили валидатор, переставили колонки, убрали перенос строки в разметке, и индексы съехали. Ошибка проявится только при сохранении, причём InvalidCastException или, хуже, запись данных не в то поле. Надёжный способ один: искать контрол по ID через FindControl. Он не зависит ни от порядка колонок, ни от форматирования разметки.

Categories.aspx.vbVB.NET
Private Sub UpdateRow(row As GridViewRow)
    ' Ключ строки берём из DataKeyNames, а не из ячейки
    Dim id = CInt(GridView1.DataKeys(row.RowIndex).Value)
    Dim name = DirectCast(row.FindControl("txtName"), TextBox).Text.Trim()
    Dim active = DirectCast(row.FindControl("chkActive"), CheckBox).Checked

    Const sql = "UPDATE Categories SET CategoryName = @name, IsActive = @active " &
                "WHERE CategoryID = @id"
    Using cn As New SqlConnection(ConnString),
          cmd As New SqlCommand(sql, cn)
        cmd.Parameters.Add("@name", SqlDbType.NVarChar, 15).Value = name
        cmd.Parameters.Add("@active", SqlDbType.Bit).Value = active
        cmd.Parameters.Add("@id", SqlDbType.Int).Value = id
        cn.Open()
        cmd.ExecuteNonQuery()
    End Using
End Sub
Чтобы DataKeys работал, задайте у GridView DataKeyNames="CategoryID". Тогда ключ не нужно выводить в видимую колонку или прятать в HiddenField.

Свои кнопки действий

Автоматические кнопки AutoGenerateEditButton и AutoGenerateDeleteButton удобны, но в реальных модулях действий больше: «Согласовать» в маршруте документов, «Активировать» в справочнике пользователей, «Дублировать» в каталоге. Поэтому кнопки тоже выносят в TemplateField: в ItemTemplate кладут «Изменить» и «Удалить», в EditItemTemplate «Сохранить» и «Отмена».

У каждой кнопки задают CommandName с собственным именем, например EditRow или Approve, и CommandArgument с ключом записи. Все нажатия приходят в одно событие RowCommand, где по CommandName выбирается нужная ветка. Подробно этот механизм, включая выбор имён команд и получение строки, разобран в отдельной статье о RowCommand.

Оформление: классы вместо атрибутов

Дизайнер Visual Studio охотно предлагает «Автоформат», который вписывает в разметку десятки атрибутов BackColor, ForeColor и Font-Bold. Результат трудно поддерживать: чтобы поменять цвет шапки во всём приложении, придётся править каждую страницу. Правильный путь: у грида и его стилей задавать только CssClass, а внешний вид описывать в одном файле CSS.

Для каждого состояния строки у GridView есть свой стиль: HeaderStyle, RowStyle, AlternatingRowStyle, EditRowStyle, SelectedRowStyle, PagerStyle, FooterStyle. Назначаем им классы и дальше работаем только с CSS. Свойство GridLines="None" убирает атрибут rules и старые границы, которые иначе перебивают ваши стили. Выравнивание чисел по правому краю задаётся классом через ItemStyle-CssClass у колонки.

grid.cssCSS
.grid { width: 100%; border-collapse: collapse; font: 14px/1.4 system-ui, sans-serif; }
.grid th { background: var(--grid-head-bg, #eef1f4); text-align: left; }
.grid th, .grid td { padding: 8px 12px; border-bottom: 1px solid #dde2e7; }
.grid .row-alt { background: #f8fafb; }
.grid .row-edit { background: #fff8e1; }
.grid .num { text-align: right; font-variant-numeric: tabular-nums; }
.grid .input { width: 100%; box-sizing: border-box; }
.grid .error { color: #b00020; font-size: 12px; }
.grid a:focus-visible { outline: 2px solid #1a73e8; outline-offset: 2px; }

Перед релизом

Когда редактирование работает, грид ещё не готов к пользователям. Проверьте короткий список: валидаторы стоят на всех обязательных полях и не срабатывают при нажатии «Отмена» (у кнопки CausesValidation="false"); у полей ввода есть MaxLength, совпадающий с размером колонки в базе; кнопка удаления спрашивает подтверждение через OnClientClick; ширина полей в режиме редактирования не ломает таблицу; ссылки-кнопки видимы при навигации с клавиатуры. Последний пункт часто забывают, хотя это обязательное требование доступности.

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

В Blazor QuickGrid аналог TemplateField называется TemplateColumn: внутри него обычная Razor-разметка с доступом к объекту строки через context, без FindControl и привязки по индексам. Стили изолируют через CSS isolation (файл .razor.css) или дизайн-токены в CSS-переменных. Редактирование чаще выносят в отдельную форму или модальное окно с EditForm и валидацией через DataAnnotations.

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

Почему FindControl возвращает Nothing?

Чаще всего строка не в том режиме: контрол из EditItemTemplate существует только в строке, номер которой равен EditIndex. Также проверьте, что ищете по ID контрола, а не по ClientID.

Чем Eval отличается от Bind?

Eval только выводит значение. Bind дополнительно умеет забирать значение обратно при обновлении, но это работает лишь в связке с SqlDataSource или ObjectDataSource. При ручном сохранении значения всё равно читают через FindControl.

Можно ли смешивать BoundField и TemplateField в одном гриде?

Да, это нормальная практика. Простые текстовые колонки оставляют на BoundField, а шаблоны используют только там, где нужна особая разметка или поведение.

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