Постраничный вывод в ListView через DataPager при привязке из кода

РазделДанные и привязкаУровеньСредний Чтение8 минОпубликовано ТегиASP.NETListViewDataPagerVB.NET

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

Главное. При ручной привязке ListView к данным DataPager сам ничего не перезагружает: в событии PagePropertiesChanging нужно вызвать SetPageProperties и затем повторить DataBind, а для больших таблиц выбирать страницу прямо в SQL.

Как устроена связка 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. В обработчике нужно сделать две вещи: применить эти значения к пейджеру и перепривязать данные. Тогда вывод и номер страницы всегда совпадают.

Catalog.aspxASPX
<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 передан весь набор записей, ведь общее количество он берёт из размера источника.

Catalog.aspx.vbVB.NET
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
Третий аргумент SetPageProperties со значением False говорит пейджеру не запускать привязку самому: её выполняет наш BindProducts.

Большие таблицы: страница прямо из SQL

Описанное решение каждый раз загружает все записи, а показывает только двенадцать. Для сотни товаров это незаметно, для ста тысяч уже нет. Правильнее выбирать только нужную страницу на стороне SQL Server, а ListView сообщать общее количество строк.

Сделать это с ручной привязкой можно, но DataPager тогда нужно обмануть: он ждёт, что источник содержит все строки. Проще всего использовать ObjectDataSource с EnablePaging=«True», SelectMethod и SelectCountMethod. Источник сам передаст в ваш метод startRowIndex и maximumRows, а количество возьмёт из отдельного метода. Внутри метода выборка делается через ROW_NUMBER() в SQL Server 2005 и 2008 или через OFFSET и FETCH начиная с SQL Server 2012.

GetProductsPage.sqlSQL
-- 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 у пейджера: тогда номер страницы берётся из адреса, и событие ведёт себя иначе.

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

В Blazor на .NET 8 постраничность даёт QuickGrid вместе с компонентом Paginator: состояние страницы хранится в PaginationState, а при подключении ItemsProvider в базу уходит только запрос нужного диапазона. В EF Core для этого есть Skip и Take, а на очень больших таблицах лучше перейти на keyset-пагинацию по ключу последней записи.

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

Почему пейджер отстаёт на один клик?

Данные привязываются раньше, чем DataPager меняет индексы. Добавьте обработчик PagePropertiesChanging с SetPageProperties и DataBind.

Можно ли поставить DataPager внутрь LayoutTemplate?

Да, и тогда PagedControlID указывать не нужно. Но доступ к нему из кода придётся получать через FindControl у ListView.

Как сделать адреса страниц понятными поисковикам?

Задайте у DataPager свойство QueryStringField, например page. Ссылки станут обычными GET-адресами вида ?page=3 вместо postback.

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