RowCommand в GridView: кнопки «Согласовать», «Активировать» и другие

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

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

Главное. Собственные действия в строках GridView делаются через CommandName и CommandArgument с одним обработчиком RowCommand. Строку ищите через NamingContainer, ключ берите из DataKeys, а встроенные имена команд не используйте.

Одно событие на все кнопки

Любой контрол с интерфейсом 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.

Users.aspxASPX
<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, поэтому неизвестные имена просто пропускаем, а не бросаем исключение. После действия перепривязываем грид, чтобы пользователь сразу увидел новое состояние записи.

Users.aspx.vbVB.NET
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
Перед выполнением действия проверяйте права текущего пользователя на сервере. Скрытая кнопка не защита: постбэк с нужным CommandName и CommandArgument можно отправить вручную.

Типичные проблемы

Если RowCommand не срабатывает или срабатывает странно, причина обычно в одном из трёх мест. Первое: грид перепривязывается в Page_Load на каждом постбэке без проверки IsPostBack. Привязка пересоздаёт строки до обработки событий, и команда теряется. Второе: ViewState грида отключён, а данные не восстановлены до момента обработки событий. Третье: у кнопки задано встроенное имя вроде Delete, и вместо вашей ветки срабатывает механизм GridView.

Ещё одна неприятность касается Server.Transfer и Response.Redirect из обработчика: при переходе к карточке записи удобнее использовать обычную ссылку HyperLinkField с DataNavigateUrlFormatString. Постбэк ради простого перехода лишний, а ссылку можно открыть в новой вкладке.

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

В Blazor QuickGrid кнопка внутри TemplateColumn вызывает метод напрямую через @onclick="() => Activate(context)", и объект строки уже под рукой: ни CommandName, ни поиска строки не нужно. В Razor Pages и MVC такие действия оформляют как отдельные POST-обработчики (asp-page-handler) с ключом записи в маршруте и антифорджери-токеном. Проверка прав на сервере при этом остаётся обязательной в любом стеке.

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

Почему в RowCommand нет e.Row?

GridViewCommandEventArgs не содержит строку, это свойство есть только у RowCreated и RowDataBound. Получайте строку через e.CommandSource и NamingContainer.

Почему вместе с RowCommand срабатывает RowDeleting?

У кнопки задан CommandName="Delete", это зарезервированное имя. Переименуйте команду, например в DelRow, и обрабатывайте её только в RowCommand.

Как передать в CommandArgument несколько значений?

Склейте их через разделитель, например Eval("Id") & "|" & Eval("Type"), и разберите Split в обработчике. Но чаще проще передать ключ и прочитать остальное из DataKeys с несколькими полями в DataKeyNames.

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