Редактирование и удаление строк GridView через SqlDataSource

РазделGridViewУровеньНовичок Чтение8 минОпубликовано ТегиASP.NETGridViewSqlDataSourceVB.NET

SqlDataSource позволяет включить правку и удаление строк GridView почти без кода. Разбираем, как связать команды с параметрами, где прячутся ошибки и как вмешаться в процесс из code-behind.

Главное. SqlDataSource даёт редактирование GridView почти без кода, если соблюдены три условия: задан DataKeyNames, имена параметров совпадают с полями, а проверки и обработка ошибок вынесены в события RowUpdating и Updated.

Когда SqlDataSource действительно удобен

В Web Forms есть два способа наполнить GridView: вручную, через DataSource и DataBind в code-behind, или декларативно, через контрол источника данных. SqlDataSource относится ко второму варианту. Он хранит SQL-команды прямо в разметке страницы и сам вызывает их, когда пользователь нажимает «Изменить», «Сохранить» или «Удалить».

Для внутренних справочников, админок и простых таблиц это очень быстрый путь: страница с полноценным редактированием собирается за десять минут, а code-behind остаётся почти пустым. GridView и SqlDataSource договариваются между собой через события, поэтому вам не нужно вручную искать изменённые TextBox в строке, переключать EditIndex и заново привязывать данные.

Обратная сторона тоже известна: SQL в разметке трудно переиспользовать и тестировать, а бизнес-логика расползается по страницам. Поэтому SqlDataSource хорош для небольших форм, а в крупном проекте его обычно заменяют на ObjectDataSource или ручную привязку к слою доступа к данным.

Настраиваем источник данных

Источнику нужны строка подключения и по одной команде на каждую операцию: SelectCommand, UpdateCommand, DeleteCommand и при необходимости InsertCommand. Строку подключения держите в web.config и ссылайтесь на неё выражением ConnectionStrings, тогда смена сервера не потребует правки страниц.

Параметры в командах записываются с префиксом @. Для Update и Delete их нужно объявить в коллекциях UpdateParameters и DeleteParameters с правильным типом. Если тип не указать, значение придёт строкой, и SQL Server будет преобразовывать его сам, что для дат и десятичных чисел легко заканчивается ошибкой формата.

Products.aspxASPX
<asp:SqlDataSource ID="dsProducts" runat="server"
    ConnectionString="<%$ ConnectionStrings:Northwind %>"
    SelectCommand="SELECT ProductID, ProductName, UnitPrice, UnitsInStock
                   FROM Products ORDER BY ProductName"
    UpdateCommand="UPDATE Products SET ProductName = @ProductName,
                   UnitPrice = @UnitPrice, UnitsInStock = @UnitsInStock
                   WHERE ProductID = @ProductID"
    DeleteCommand="DELETE FROM Products WHERE ProductID = @ProductID">
    <UpdateParameters>
        <asp:Parameter Name="ProductName" Type="String" />
        <asp:Parameter Name="UnitPrice" Type="Decimal" />
        <asp:Parameter Name="UnitsInStock" Type="Int16" />
        <asp:Parameter Name="ProductID" Type="Int32" />
    </UpdateParameters>
    <DeleteParameters>
        <asp:Parameter Name="ProductID" Type="Int32" />
    </DeleteParameters>
</asp:SqlDataSource>

Подключаем GridView

Сам грид связывается с источником через DataSourceID. Ключевой атрибут здесь DataKeyNames: он сообщает GridView, какие поля составляют первичный ключ. Значения ключа сохраняются в ViewState и при обновлении передаются в параметр @ProductID. Без DataKeyNames команда UPDATE выполнится с пустым ключом и не изменит ни одной строки, причём без всякой ошибки.

Кнопки управления даёт CommandField. Поле ключа делаем ReadOnly, чтобы пользователь не мог его исправить, а для остальных колонок BoundField сам создаст TextBox в режиме редактирования.

Products.aspxASPX
<asp:GridView ID="gvProducts" runat="server"
    DataSourceID="dsProducts" DataKeyNames="ProductID"
    AutoGenerateColumns="False" AllowPaging="True" PageSize="15">
    <Columns>
        <asp:CommandField ShowEditButton="True" ShowDeleteButton="True"
            EditText="Изменить" UpdateText="Сохранить"
            CancelText="Отмена" DeleteText="Удалить" />
        <asp:BoundField DataField="ProductID" HeaderText="Код"
            ReadOnly="True" />
        <asp:BoundField DataField="ProductName" HeaderText="Товар" />
        <asp:BoundField DataField="UnitPrice" HeaderText="Цена"
            DataFormatString="{0:N2}" ApplyFormatInEditMode="False" />
        <asp:BoundField DataField="UnitsInStock" HeaderText="На складе" />
    </Columns>
</asp:GridView>
Имя параметра в UpdateCommand должно совпадать с DataField колонки. Именно по имени GridView раскладывает введённые значения по параметрам.

Как значения попадают в параметры

При нажатии «Сохранить» GridView собирает три словаря: Keys с ключом строки, OldValues со значениями до правки и NewValues с введёнными. Затем он вызывает Update у источника, а SqlDataSource сопоставляет элементы словарей с параметрами по имени.

Отсюда типичные сюрпризы. Колонка с ReadOnly не попадает в NewValues, поэтому для неё параметр получит значение по умолчанию. Колонка, которой нет в гриде вовсе, тоже ничего не передаст. Если в SelectCommand есть поле, которое не показывается, но должно сохраниться, его нужно либо вывести скрытым TemplateField, либо убрать из UpdateCommand.

Для оптимистичной блокировки у SqlDataSource есть режим ConflictDetection=«CompareAllValues». Тогда старые значения тоже передаются в команду, а их имена строятся по шаблону OldValuesParameterFormatString, например original_{0}. В WHERE остаётся сравнить их с текущими данными: если другой пользователь успел изменить запись, UPDATE затронет ноль строк.

Вмешиваемся из code-behind

Декларативная схема не мешает добавить свою логику. Событие RowUpdating грида срабатывает до вызова команды и даёт доступ к NewValues: здесь удобно обрезать пробелы, проверить диапазон и при необходимости отменить сохранение. Событие Updated источника сообщает, сколько строк реально изменилось и было ли исключение.

Products.aspx.vbVB.NET
Protected Sub gvProducts_RowUpdating(sender As Object, _
        e As GridViewUpdateEventArgs) Handles gvProducts.RowUpdating
    ' Чистим ввод и проверяем цену до обращения к базе
    Dim name = CStr(e.NewValues("ProductName")).Trim()
    e.NewValues("ProductName") = name
    Dim price As Decimal
    If Not Decimal.TryParse(CStr(e.NewValues("UnitPrice")), price) _
            OrElse price < 0 Then
        lblMessage.Text = "Цена указана неверно"
        e.Cancel = True
    End If
End Sub

Protected Sub dsProducts_Updated(sender As Object, _
        e As SqlDataSourceStatusEventArgs) Handles dsProducts.Updated
    If e.Exception IsNot Nothing Then
        lblMessage.Text = "Не удалось сохранить: " & e.Exception.Message
        e.ExceptionHandled = True   ' страница не упадёт
    ElseIf e.AffectedRows = 0 Then
        lblMessage.Text = "Запись изменена другим пользователем"
    End If
End Sub

Удаление и частые ошибки

Удаление работает по той же схеме: кнопка Delete берёт ключ из DataKeys и передаёт его в DeleteCommand. Подтверждение лучше добавить на клиенте, заменив CommandField на TemplateField с LinkButton и атрибутом OnClientClick, который вызывает confirm. Если на запись ссылаются другие таблицы, SQL Server вернёт ошибку внешнего ключа, и её стоит перехватить в событии Deleted так же, как выше.

Список проблем, на которые чаще всего жалуются при первой настройке:

  • «Must declare the scalar variable @ProductID»: не задан DataKeyNames или параметр называется иначе;
  • исключение о форматировании числа: DataFormatString применён в режиме редактирования, отключите ApplyFormatInEditMode;
  • сохраняются старые значения: грид заново привязывается в Page_Load при каждом postback и затирает ввод, вызывайте DataBind только при Not IsPostBack или не вызывайте вовсе;
  • пустое поле превращается в NULL: за это отвечает ConvertEmptyStringToNull у BoundField и параметра.

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

В новых проектах на .NET 8 таблицу выводят компонентом QuickGrid в Blazor, а сохранение идёт через EditForm и сервис на EF Core, где проверку берут на себя DataAnnotations. Оптимистичную блокировку EF Core делает через поле rowversion и исключение DbUpdateConcurrencyException. SQL в разметке больше не держат: запросы живут в сервисах, которые легко покрыть тестами.

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

Почему после нажатия «Сохранить» данные не меняются?

Чаще всего не задан DataKeyNames, и параметр ключа приходит пустым, либо грид перепривязывается в Page_Load при postback. Проверьте AffectedRows в событии Updated.

Можно ли использовать хранимую процедуру вместо SQL-текста?

Да. Укажите имя процедуры в UpdateCommand и UpdateCommandType=«StoredProcedure». Имена параметров процедуры должны совпадать с полями грида.

Как не дать редактировать отдельные колонки?

Поставьте колонке ReadOnly=«True» и уберите соответствующий параметр из UpdateCommand, иначе он получит пустое значение.

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