Сортировка GridView с помощью DataView: по возрастанию и обратно

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

AllowSorting="true" делает заголовки GridView ссылками, но сам ничего не сортирует. Разбираем, как довести сортировку до рабочего состояния с DataView и не наступить на типичные грабли.

Главное. AllowSorting лишь включает событие Sorting, сортировать при ручной привязке приходится самому. DataView.Sort плюс сохранение колонки и направления в ViewState дают предсказуемое поведение без лишнего кода.

Что на самом деле делает AllowSorting

Сортировка в GridView заявлена как встроенная возможность, и это вводит в заблуждение. Свойство AllowSorting только превращает текст в шапке колонок в ссылки и включает генерацию события Sorting. Если грид привязан к SqlDataSource или ObjectDataSource, источник данных сам выполнит сортировку. Но в большинстве реальных проектов данные привязываются вручную через DataSource и DataBind, и тогда при первом же клике по заголовку вылетает исключение: событие Sorting не обработано.

Значит, работу придётся сделать самим: принять имя колонки, отсортировать данные, запомнить направление и снова привязать грид. Хорошая новость в том, что для DataTable у ADO.NET уже есть удобный инструмент: DataView со свойством Sort.

Настраиваем разметку

Каждой колонке, по которой разрешена сортировка, задаём SortExpression. Обычно это имя поля в источнике данных, именно оно придёт в обработчик в e.SortExpression. Колонки без SortExpression остаются обычным текстом: так можно запретить сортировку, например, по столбцу с описанием или кнопками.

При AutoGenerateColumns="true" SortExpression заполняется автоматически именем поля, но в рабочих проектах колонки почти всегда описывают явно, чтобы управлять заголовками и форматом.

Default.aspxASPX
<asp:GridView ID="GridView1" runat="server" AllowSorting="true"
    AutoGenerateColumns="false" CssClass="grid">
  <Columns>
    <asp:BoundField DataField="ProductName" HeaderText="Товар"
        SortExpression="ProductName" />
    <asp:BoundField DataField="UnitPrice" HeaderText="Цена"
        SortExpression="UnitPrice" DataFormatString="{0:N2}" />
    <%-- без SortExpression колонка не сортируется --%>
    <asp:BoundField DataField="QuantityPerUnit" HeaderText="Фасовка" />
  </Columns>
</asp:GridView>

DataView и свойство Sort

DataView можно получить у любой DataTable через DefaultView. Свойство Sort принимает строку в знакомом по SQL формате: имя колонки и необязательный суффикс ASC или DESC, можно перечислить несколько колонок через запятую. Сортировка выполняется в памяти, без нового запроса к базе, а сам DataView отлично подходит в качестве DataSource для GridView.

Отсюда следует важный вывод про производительность. Если на каждый клик заново тянуть всю таблицу из SQL Server, чтобы потом отсортировать её в памяти, мы платим дважды. Разумнее закешировать DataTable на время работы со страницей (в Cache или Session для небольших наборов), а при больших объёмах сортировать сразу в SQL через ORDER BY.

Направление сортировки: где была ошибка

Пользователь ожидает простого поведения: первый клик по колонке сортирует по возрастанию, повторный по той же колонке разворачивает порядок. Клик по другой колонке снова начинает с возрастания. Чтобы это обеспечить, между постбэками нужно хранить две вещи: текущую колонку и текущее направление. Для этого идеально подходит ViewState.

Во многих старых примерах хранили только направление и переключали его на каждый клик. В итоге сортировка «прыгает»: отсортировали цены по возрастанию, кликнули по названию и получили названия по убыванию. Ниже исправленный вариант: направление меняется, только если колонка та же.

Default.aspx.vbVB.NET
Private Property SortColumn As String
    Get
        Return CStr(If(ViewState("SortColumn"), ""))
    End Get
    Set(value As String)
        ViewState("SortColumn") = value
    End Set
End Property

Private Property SortDir As String
    Get
        Return CStr(If(ViewState("SortDir"), "ASC"))
    End Get
    Set(value As String)
        ViewState("SortDir") = value
    End Set
End Property

Protected Sub GridView1_Sorting(sender As Object, e As GridViewSortEventArgs) _
        Handles GridView1.Sorting
    If e.SortExpression = SortColumn Then
        ' та же колонка: разворачиваем порядок
        SortDir = If(SortDir = "ASC", "DESC", "ASC")
    Else
        SortColumn = e.SortExpression
        SortDir = "ASC"
    End If
    BindGrid()
End Sub

Привязка данных с учётом сортировки

Метод BindGrid получает данные, накладывает сохранённую сортировку и привязывает грид. Его же вызываем в Page_Load при первой загрузке и после любых операций редактирования, чтобы порядок строк не сбрасывался. Подключение и адаптер заворачиваем в Using, строку подключения берём из web.config.

Если GridView одновременно постраничный, после смены сортировки разумно вернуть PageIndex в ноль. Иначе пользователь окажется на седьмой странице уже нового порядка, и это выглядит как ошибка.

Default.aspx.vbVB.NET
Private Function LoadProducts() As DataTable
    Dim cs = ConfigurationManager.ConnectionStrings("Northwind").ConnectionString
    Dim table As New DataTable()
    Using cn As New SqlConnection(cs),
          da As New SqlDataAdapter("SELECT ProductName, UnitPrice, QuantityPerUnit FROM Products", cn)
        da.Fill(table)   ' Fill сам откроет и закроет соединение
    End Using
    Return table
End Function

Private Sub BindGrid()
    Dim view As DataView = LoadProducts().DefaultView
    If SortColumn <> "" Then
        view.Sort = SortColumn & " " & SortDir
    End If
    GridView1.DataSource = view
    GridView1.DataBind()
End Sub

Protected Sub Page_Load(sender As Object, e As EventArgs) Handles Me.Load
    If Not IsPostBack Then BindGrid()
End Sub

Стрелка в заголовке и защита от подмены

Без визуальной подсказки пользователь не понимает, по какой колонке и в каком порядке отсортирована таблица. Самый простой способ показать это: в RowCreated для строки заголовка найти ячейку текущей колонки и добавить ей CSS-класс sort-asc или sort-desc. Стрелку рисуем псевдоэлементом ::after, а для доступности выставляем атрибут aria-sort со значением ascending или descending.

И ещё одно правило безопасности. Если вы решите сортировать в SQL и подставлять e.SortExpression в ORDER BY, параметризовать имя колонки не получится. Значение приходит с клиента и теоретически может быть подменено. Поэтому сверяйте его с белым списком допустимых колонок и только после этого вставляйте в запрос.

  • Класс на th: Cells(i).CssClass = "sort-" & SortDir.ToLower().
  • Для экранных дикторов: Cells(i).Attributes("aria-sort") = "ascending" или "descending".
  • Белый список: New HashSet(Of String) From {"ProductName", "UnitPrice"} и проверка Contains перед построением SQL.

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

В Blazor QuickGrid сортировка включается одним параметром Sortable="true" у колонки, а при работе с IQueryable из EF Core она уходит в SQL автоматически. В API-подходе клиент передаёт имя поля и направление, сервер проверяет их по белому списку и применяет OrderBy. DataView и ViewState в новых проектах уже не нужны, но для сопровождения Web Forms описанная схема остаётся самой простой.

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

Почему при клике по заголовку появляется ошибка «событие Sorting не обработано»?

GridView привязан вручную через DataSource, а не через контрол источника данных. В таком режиме сортировку обязан выполнить ваш обработчик события Sorting.

Почему e.SortDirection всегда Ascending?

При ручной привязке GridView не отслеживает направление сам, поэтому свойство почти бесполезно. Храните колонку и направление в ViewState, как показано в статье.

Можно ли сортировать по нескольким колонкам сразу?

Да, DataView.Sort принимает список через запятую, например "Country ASC, City DESC". Для пользовательского интерфейса обычно хватает одной колонки, а вторичную сортировку задают фиксированно.

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