
Сортировка GridView с помощью DataView: по возрастанию и обратно
AllowSorting="true" делает заголовки GridView ссылками, но сам ничего не сортирует. Разбираем, как довести сортировку до рабочего состояния с DataView и не наступить на типичные грабли.
Что на самом деле делает AllowSorting
Сортировка в GridView заявлена как встроенная возможность, и это вводит в заблуждение. Свойство AllowSorting только превращает текст в шапке колонок в ссылки и включает генерацию события Sorting. Если грид привязан к SqlDataSource или ObjectDataSource, источник данных сам выполнит сортировку. Но в большинстве реальных проектов данные привязываются вручную через DataSource и DataBind, и тогда при первом же клике по заголовку вылетает исключение: событие Sorting не обработано.
Значит, работу придётся сделать самим: принять имя колонки, отсортировать данные, запомнить направление и снова привязать грид. Хорошая новость в том, что для DataTable у ADO.NET уже есть удобный инструмент: DataView со свойством Sort.
Настраиваем разметку
Каждой колонке, по которой разрешена сортировка, задаём SortExpression. Обычно это имя поля в источнике данных, именно оно придёт в обработчик в e.SortExpression. Колонки без SortExpression остаются обычным текстом: так можно запретить сортировку, например, по столбцу с описанием или кнопками.
При AutoGenerateColumns="true" SortExpression заполняется автоматически именем поля, но в рабочих проектах колонки почти всегда описывают явно, чтобы управлять заголовками и форматом.
<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.
Во многих старых примерах хранили только направление и переключали его на каждый клик. В итоге сортировка «прыгает»: отсортировали цены по возрастанию, кликнули по названию и получили названия по убыванию. Ниже исправленный вариант: направление меняется, только если колонка та же.
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 в ноль. Иначе пользователь окажется на седьмой странице уже нового порядка, и это выглядит как ошибка.
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.
Как это делают сегодня
Вопросы и ответы
Почему при клике по заголовку появляется ошибка «событие Sorting не обработано»?
GridView привязан вручную через DataSource, а не через контрол источника данных. В таком режиме сортировку обязан выполнить ваш обработчик события Sorting.
Почему e.SortDirection всегда Ascending?
При ручной привязке GridView не отслеживает направление сам, поэтому свойство почти бесполезно. Храните колонку и направление в ViewState, как показано в статье.
Можно ли сортировать по нескольким колонкам сразу?
Да, DataView.Sort принимает список через запятую, например "Country ASC, City DESC". Для пользовательского интерфейса обычно хватает одной колонки, а вторичную сортировку задают фиксированно.