
Постраничный вывод в ListView через DataPager при привязке из кода
С SqlDataSource связка ListView и DataPager работает сразу. Стоит привязать данные из code-behind, и страницы начинают переключаться со второго клика. Разбираем, почему так происходит и как это исправить.
Как устроена связка ListView и DataPager
DataPager появился в ASP.NET 3.5 вместе с ListView. В отличие от встроенного пейджера GridView, это отдельный контрол: его можно поставить в любое место страницы, хоть над списком, хоть в боковой колонке, и настроить набор кнопок полями NumericPagerField и NextPreviousPagerField.
Работает он с любым контролом, реализующим интерфейс IPageableItemContainer. ListView его реализует: у него есть свойства StartRowIndex и MaximumRows и метод SetPageProperties. DataPager при клике вычисляет новую стартовую строку и вызывает этот метод, а ListView решает, какие записи показать.
Когда ListView подключён к контролу источника через DataSourceID, после SetPageProperties он сам заново запрашивает данные и всё работает. При ручной привязке через DataSource повторный запрос никто не делает, и тут начинаются проблемы.
Симптом: страница меняется со второго клика
Типичная картина: в Page_Load список привязывается внутри If Not IsPostBack, пейджер на месте. Пользователь нажимает «2», страница перезагружается, но показывает первую страницу. Нажимает «3» и видит вторую. Пейджер всё время отстаёт на один шаг.
Причина в порядке событий. Нажатие обрабатывается после Page_Load, DataPager меняет StartRowIndex, но данные уже не перепривязываются: ListView отрисовывает то, что восстановлено из ViewState. На следующем запросе новые индексы наконец участвуют в выводе, отсюда и задержка на один клик. Если же убрать проверку IsPostBack и привязывать всегда, данные привязываются раньше, чем пейджер успевает поменять индексы, и эффект тот же.
Решение: PagePropertiesChanging
У ListView есть событие PagePropertiesChanging. Оно срабатывает, когда пейджер собирается сменить страницу, и передаёт новые StartRowIndex и MaximumRows. В обработчике нужно сделать две вещи: применить эти значения к пейджеру и перепривязать данные. Тогда вывод и номер страницы всегда совпадают.
<asp:ListView ID="lvProducts" runat="server" ItemPlaceholderID="ph">
<LayoutTemplate>
<div class="catalog">
<asp:PlaceHolder ID="ph" runat="server" />
</div>
</LayoutTemplate>
<ItemTemplate>
<article class="card">
<h3><%#: Eval("ProductName") %></h3>
<p><%# Eval("UnitPrice", "{0:N2}") %> руб.</p>
</article>
</ItemTemplate>
</asp:ListView>
<asp:DataPager ID="pager" runat="server" PagedControlID="lvProducts"
PageSize="12">
<Fields>
<asp:NextPreviousPagerField ShowFirstPageButton="True"
ShowNextPageButton="False" FirstPageText="В начало" />
<asp:NumericPagerField ButtonCount="7" />
<asp:NextPreviousPagerField ShowLastPageButton="True"
ShowPreviousPageButton="False" LastPageText="В конец" />
</Fields>
</asp:DataPager>Код страницы
Первичная привязка остаётся в Page_Load под проверкой IsPostBack, а смена страницы обрабатывается отдельно. Обратите внимание: DataPager отображает правильное число страниц, только если в ListView передан весь набор записей, ведь общее количество он берёт из размера источника.
Protected Sub Page_Load(sender As Object, e As EventArgs) Handles Me.Load
If Not IsPostBack Then BindProducts()
End Sub
Protected Sub lvProducts_PagePropertiesChanging(sender As Object, _
e As PagePropertiesChangingEventArgs) _
Handles lvProducts.PagePropertiesChanging
' Сначала применяем новые индексы, затем привязываем данные
pager.SetPageProperties(e.StartRowIndex, e.MaximumRows, False)
BindProducts()
End Sub
Private Sub BindProducts()
lvProducts.DataSource = ProductRepository.GetAll() ' DataTable или List
lvProducts.DataBind()
End SubБольшие таблицы: страница прямо из SQL
Описанное решение каждый раз загружает все записи, а показывает только двенадцать. Для сотни товаров это незаметно, для ста тысяч уже нет. Правильнее выбирать только нужную страницу на стороне SQL Server, а ListView сообщать общее количество строк.
Сделать это с ручной привязкой можно, но DataPager тогда нужно обмануть: он ждёт, что источник содержит все строки. Проще всего использовать ObjectDataSource с EnablePaging=«True», SelectMethod и SelectCountMethod. Источник сам передаст в ваш метод startRowIndex и maximumRows, а количество возьмёт из отдельного метода. Внутри метода выборка делается через ROW_NUMBER() в SQL Server 2005 и 2008 или через OFFSET и FETCH начиная с SQL Server 2012.
-- SQL Server 2012 и новее
SELECT ProductID, ProductName, UnitPrice
FROM Products
ORDER BY ProductName
OFFSET @StartRowIndex ROWS FETCH NEXT @MaximumRows ROWS ONLY;
SELECT COUNT(*) FROM Products; -- для SelectCountMethodЧек-лист при проблемах с пейджером
Если постраничный вывод ведёт себя странно, проверьте по порядку:
- задан ли PagedControlID и совпадает ли он с ID списка, если DataPager стоит вне ListView;
- есть ли обработчик PagePropertiesChanging при привязке из кода, и вызывается ли в нём SetPageProperties до DataBind;
- не перепривязываются ли данные в Page_Load при каждом postback;
- не передаётся ли в DataSource SqlDataReader: он не знает числа строк, используйте DataTable или список;
- не включён ли QueryStringField у пейджера: тогда номер страницы берётся из адреса, и событие ведёт себя иначе.
Как это делают сегодня
Вопросы и ответы
Почему пейджер отстаёт на один клик?
Данные привязываются раньше, чем DataPager меняет индексы. Добавьте обработчик PagePropertiesChanging с SetPageProperties и DataBind.
Можно ли поставить DataPager внутрь LayoutTemplate?
Да, и тогда PagedControlID указывать не нужно. Но доступ к нему из кода придётся получать через FindControl у ListView.
Как сделать адреса страниц понятными поисковикам?
Задайте у DataPager свойство QueryStringField, например page. Ссылки станут обычными GET-адресами вида ?page=3 вместо postback.