
RowCommand в GridView: кнопки «Согласовать», «Активировать» и другие
Кроме правки и удаления, строке таблицы часто нужны свои действия: согласовать заявку, заблокировать пользователя, отправить письмо. Всё это проходит через одно событие RowCommand.
Одно событие на все кнопки
Любой контрол с интерфейсом IButtonControl внутри GridView (Button, LinkButton, ImageButton) при нажатии поднимает событие RowCommand у грида. Это механизм всплытия команд: кнопка не обрабатывает клик сама, а передаёт наверх два значения. CommandName говорит, что нужно сделать, CommandArgument уточняет, с чем именно.
Благодаря этому на сколько угодно кнопок в сотне строк нужен ровно один обработчик. Он смотрит на имя команды и выбирает ветку. Ни назначать обработчик каждой кнопке, ни генерировать уникальные ID вручную не требуется.
Имена команд: не пересекайтесь со встроенными
GridView сам понимает несколько зарезервированных имён: Edit, Update, Cancel, Delete, Select, Page и Sort. Если кнопка в шаблоне называется CommandName="Delete", грид кроме RowCommand поднимет ещё и RowDeleting, и при ручной привязке вы получите исключение «событие RowDeleting не обработано». Такие пересечения часто ломают код после невинного переименования кнопки.
Поэтому для собственных действий выбирайте уникальные имена: Approve, Activate, SendInvoice, DelRow. Регистр в сравнении учитывается, если вы используете обычный Select Case, так что договоритесь о стиле и держите имена в константах.
Список типичных задач, для которых пишут свои команды:
- согласование или отклонение заявки в модуле документооборота;
- активация и блокировка учётной записи в управлении пользователями;
- повторная отправка письма или уведомления по конкретной записи;
- копирование записи как основы для новой;
- переход к подробной карточке с передачей ключа в адресе.
Разметка кнопок
Кнопки кладём в TemplateField. Ключ записи передаём через CommandArgument: так обработчику не придётся искать его в ячейках. Видимость кнопок привязываем к данным: «Активировать» показываем только неактивным записям, «Заблокировать» только активным. Для необратимых действий добавляем подтверждение через OnClientClick.
<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="false"
DataKeyNames="UserID" CssClass="grid">
<Columns>
<asp:BoundField DataField="Login" HeaderText="Логин" />
<asp:BoundField DataField="Email" HeaderText="E-mail" />
<asp:TemplateField HeaderText="Действия">
<ItemTemplate>
<asp:LinkButton runat="server" Text="Активировать"
CommandName="Activate"
CommandArgument='<%# Eval("UserID") %>'
Visible='<%# Not CBool(Eval("IsActive")) %>' />
<asp:LinkButton runat="server" Text="Заблокировать"
CommandName="Block"
CommandArgument='<%# Eval("UserID") %>'
Visible='<%# CBool(Eval("IsActive")) %>'
OnClientClick="return confirm('Заблокировать пользователя?');" />
<asp:LinkButton runat="server" Text="Выслать пароль"
CommandName="ResendPassword"
CommandArgument='<%# Container.DataItemIndex %>' />
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>Как получить строку внутри RowCommand
В отличие от RowDataBound, у аргументов RowCommand нет свойства e.Row. Это частая ошибка в старых примерах: код с e.Row в этом событии просто не скомпилируется. Строку добывают через источник команды. Раньше писали DirectCast(btn.Parent.Parent, GridViewRow): кнопка лежит в ячейке, ячейка в строке. Но стоит обернуть кнопку в Panel или PlaceHolder, и цепочка Parent ломается.
Надёжнее свойство NamingContainer: для любого контрола внутри шаблона строки оно возвращает именно GridViewRow, сколько бы обёрток ни было вокруг. Получив строку, можно читать соседние контролы через FindControl и ключ записи через DataKeys(row.RowIndex).
Есть и третий путь: передать в CommandArgument номер строки через Container.DataItemIndex. Но учтите, что при включённой пагинации это индекс в полном наборе данных, а не на странице. Для доступа к Rows нужен остаток от деления на PageSize, поэтому передавать ключ записи обычно проще.
Обработчик RowCommand
Обработчик начинается с выбора по имени команды. Команды пейджера и сортировки тоже проходят через RowCommand, поэтому неизвестные имена просто пропускаем, а не бросаем исключение. После действия перепривязываем грид, чтобы пользователь сразу увидел новое состояние записи.
Protected Sub GridView1_RowCommand(sender As Object,
e As GridViewCommandEventArgs) Handles GridView1.RowCommand
Select Case e.CommandName
Case "Activate", "Block"
Dim userId = CInt(e.CommandArgument)
SetUserActive(userId, e.CommandName = "Activate")
BindGrid()
Case "ResendPassword"
' Строка через NamingContainer, без цепочки Parent.Parent
Dim btn = DirectCast(e.CommandSource, Control)
Dim row = DirectCast(btn.NamingContainer, GridViewRow)
Dim userId = CInt(GridView1.DataKeys(row.RowIndex).Value)
Dim email = row.Cells(1).Text
SendPasswordReset(userId, email)
lblStatus.Text = "Письмо отправлено на " & Server.HtmlEncode(email)
Case Else
' Page, Sort и встроенные команды обрабатываются отдельно
End Select
End SubТипичные проблемы
Если RowCommand не срабатывает или срабатывает странно, причина обычно в одном из трёх мест. Первое: грид перепривязывается в Page_Load на каждом постбэке без проверки IsPostBack. Привязка пересоздаёт строки до обработки событий, и команда теряется. Второе: ViewState грида отключён, а данные не восстановлены до момента обработки событий. Третье: у кнопки задано встроенное имя вроде Delete, и вместо вашей ветки срабатывает механизм GridView.
Ещё одна неприятность касается Server.Transfer и Response.Redirect из обработчика: при переходе к карточке записи удобнее использовать обычную ссылку HyperLinkField с DataNavigateUrlFormatString. Постбэк ради простого перехода лишний, а ссылку можно открыть в новой вкладке.
Как это делают сегодня
Вопросы и ответы
Почему в RowCommand нет e.Row?
GridViewCommandEventArgs не содержит строку, это свойство есть только у RowCreated и RowDataBound. Получайте строку через e.CommandSource и NamingContainer.
Почему вместе с RowCommand срабатывает RowDeleting?
У кнопки задан CommandName="Delete", это зарезервированное имя. Переименуйте команду, например в DelRow, и обрабатывайте её только в RowCommand.
Как передать в CommandArgument несколько значений?
Склейте их через разделитель, например Eval("Id") & "|" & Eval("Type"), и разберите Split в обработчике. Но чаще проще передать ключ и прочитать остальное из DataKeys с несколькими полями в DataKeyNames.