
GridView с шаблонами и CSS-стилями: от BoundField к TemplateField
BoundField хорош для простого вывода, но в реальной админке нужны выпадающие списки, флажки, свои кнопки и аккуратный внешний вид. Всё это дают шаблонные колонки и CSS-классы.
Когда BoundField перестаёт хватать
BoundField выводит значение поля как текст, а в режиме редактирования подставляет TextBox. Для справочника из двух колонок этого достаточно. Но стоит появиться реальным требованиям, и ограничения вылезают сразу: статус нужно выбирать из списка, а не вводить руками; поле «Активен» должно быть флажком; к описанию нужен валидатор; а рядом со строкой требуется кнопка «Согласовать», которой в GridView нет.
Всё это решает TemplateField. Это колонка, внутри которой вы сами описываете разметку для разных состояний строки. GridView ничего не придумывает за вас: какие контролы положили в шаблон, такие и будут выведены.
ItemTemplate и EditItemTemplate
TemplateField сам по себе только контейнер. Главные в нём два шаблона. ItemTemplate отображается, когда строка в обычном режиме просмотра: обычно это Label, Literal или неактивный CheckBox. EditItemTemplate показывается в строке, номер которой совпадает с EditIndex грида: здесь размещают TextBox, DropDownList, CheckBox, валидаторы и даже пользовательские контролы.
Колонку с идентификатором редактировать нельзя, поэтому для неё EditItemTemplate не задают вовсе: тогда в режиме редактирования выводится ItemTemplate. Данные в шаблон подставляются выражениями привязки: Eval для однонаправленного вывода и Bind для двусторонней, которую понимают контролы источников данных.
<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. Он не зависит ни от порядка колонок, ни от форматирования разметки.
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Свои кнопки действий
Автоматические кнопки 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 { 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; ширина полей в режиме редактирования не ломает таблицу; ссылки-кнопки видимы при навигации с клавиатуры. Последний пункт часто забывают, хотя это обязательное требование доступности.
Как это делают сегодня
Вопросы и ответы
Почему FindControl возвращает Nothing?
Чаще всего строка не в том режиме: контрол из EditItemTemplate существует только в строке, номер которой равен EditIndex. Также проверьте, что ищете по ID контрола, а не по ClientID.
Чем Eval отличается от Bind?
Eval только выводит значение. Bind дополнительно умеет забирать значение обратно при обновлении, но это работает лишь в связке с SqlDataSource или ObjectDataSource. При ручном сохранении значения всё равно читают через FindControl.
Можно ли смешивать BoundField и TemplateField в одном гриде?
Да, это нормальная практика. Простые текстовые колонки оставляют на BoundField, а шаблоны используют только там, где нужна особая разметка или поведение.