Универсальный пейджер для ASP.NET MVC
Практически в каждом веб-проекте необходим т.н. пейджер — элемент интерфейса, с помощью обеспечивается навигация по страницам списка каких-либо элементов. Для подобной функциональности, которая повторяется из проекта в проект, удобно сделать более-менее универсальное решение. Наша реализация должна была удовлетворять таким требованиям:
- Нумерация страниц с единицы
- HTML-код пейджера в виде Partial View
- Отсутствие привязки к какому либо конкретному способу передачи номера текущей страницы в URL и к правилам роутинга
- Возможность использования пейджера с любым источником данных (LINQ2SQL-запрос, ADO.NET-запрос и т.п.), т.е. возможность реализации собственного алгоритма выборки страницы данных
Итогом решения будет набор из ViewModel-классов и View-файлов в двух вариантах: WebForms View Engine и Razor.
Интересно, реально ли оформить решения в виде пакета NuGet?
Пример использования
Начнём с демонстрации того, что получилось. С помощью классов из этой записи можно осуществить постраничный вывод запроса, возвращающего IQueryable<T> (т.е. запроса к LINQ2SQL или Entity Framework), для любых других источников данных (например, запросов с использованием DataReader) достаточно реализовать простой интерфейс.
Простой код action'а может выглядеть, например, так:
public ActionResult List(int? page)
{
// Какой-то LINQ2SQL-запрос
var query = from r in DbContext.Records
where r.Type == 2 // какие-то условия отбора, сортировки записей
orderby r.Title
select r;
return View(
new PagedListViewModel(query, page, 20); // 20 -- количество записей на странице
);
}
Во View вывод пейджера будет выглядеть так (в Razor-синтаксисе с использованием T4MVС для генерации ссылок):
@*
Отображение пейджера
*@
@Html.RenderPartial(
MVC.Shared.Views.UI.Pager,
new PagerViewModel(
Model,
Url.Action(MVC.Records.Index().AddRouteValues(page: PagerViewModel.PageNumberPlaceholder)))
);
@*
Вывод элементов списком, таблицей и т.п.
*@
@if (Model.TendersList.Count != 0) {
foreach (var record in Model) {
...
}
}
Значение, равное PagerViewModel.PageNumberPlaceholder будет заменено на номер страницы в ссылках пейджера.
Решение
Возможность использования пейджера с любым источником данных достигается благодаря простому интерфейсу, который представляет собой всю информацию, которая необходима для отображения пейджера:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace CrispStudio.Models.ViewModel
{
public interface IPageableViewModel
{
int PageIndex {get;} // Номер текущей страницы
int PagesCount {get;} // Количество страниц
}
}
За передачу данных в Partial View, который отображает пейджер, отвечает отдельный ViewModel-класс:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace CrispStudio.Models.ViewModel
{
public class PagerViewModel
{
public const int PageNumberPlaceholder = int.MinValue; // Это значение будет заменено в переданном URL'е на номер страницы
private IPageableViewModel _result;
private string _urlPattern;
private int _delta;
public PagerViewModel(IPageableViewModel result, string urlPattern)
{
_result = result;
_urlPattern = urlPattern;
_delta = 5; // Максимальное количество прямых ссылок на страниц, например << 1, 2, 3, 4, 5 ... >>, нужно брать из настроек\констант
}
// Общие данные
public int PageIndex
{
get
{
return _result.PageIndex;
}
}
public int PagesCount
{
get
{
return _result.PagesCount;
}
}
public int PageDelta
{
get
{
return _delta;
}
}
// Генерация ссылок
public string GenerateLink(int pageIndex)
{
return _urlPattern.Replace(PageNumberPlaceholder.ToString(), pageIndex.ToString());
}
public string NextLink
{
get
{
return GenerateLink(PageIndex + 1);
}
}
public string PreviousLink
{
get
{
return GenerateLink(PageIndex - 1);
}
}
public string FirstLink
{
get
{
return GenerateLink(1);
}
}
public string LastLink
{
get
{
return GenerateLink(PagesCount);
}
}
// Для проверок во view
public bool HasPreviousPage
{
get
{
return (PageIndex > 1);
}
}
public bool HasNextPage
{
get
{
return (PageIndex < PagesCount);
}
}
public bool IsFirstPage
{
get
{
return (PageIndex == 1);
}
}
public bool IsLastPage
{
get
{
return (PageIndex == PagesCount);
}
}
}
}
Partial View для пейджера может выглядеть, например, так (Razor-синтаксис, .aspx-версия доступна в архиве для скачивания):
@model PagerViewModel
@if (Model.PagesCount > 1) {
<div class="b-pager paging">
<ul class="paging_numbers">
@if (!Model.IsFirstPage) {
<li><a href="@Model.FirstLink" class="prev-link"><span>Первая</span></a></li>
}
@if (Model.HasPreviousPage) {
<li><a href="@Model.PreviousLink" class="prev-link">← <span>Предыдущая</span></a></li>
}
@if ((Model.PageIndex - Model.PageDelta - 1) > 0) {
<li>…</li>
}
@for (int i = Model.PageIndex - Model.PageDelta; i <= Model.PageIndex + Model.PageDelta; i++) {
if (i > 0 && i <= Model.PagesCount) {
if (i != Model.PageIndex){
<li><a href="@Model.GenerateLink(i)">@i</a></li>
} else {
<li><strong>@i</strong></li>
}
}}
@if (Model.PagesCount > (Model.PageIndex + Model.PageDelta)) {
<li>…</li>
}
@if (Model.HasNextPage) {
<li><a href="@Model.NextLink" class="next-link"><span>Следующая</span> →</a></li>
}
@if (!Model.IsLastPage) {
<li><a href="@Model.LastLink" class="next-link"><span>Последняя</span> (<strong>@Model.PagesCount</strong>)</a></li>
}
</ul>
</div>
}
Для того, чтобы всё заработало осталось только описать класс(ы) реализующие интерфейс IPageableViewModel. Решение не накладывает никаких ограничений на источник данных, что позволяет осуществлять постраничную разбивку практически любых данных. В качестве примера можно привести реализацию постраничной разбивки для IQueryable<T> (т.е. запроса к LINQ2SQL или Entity Framework):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace CrispStudio.Models.ViewModel
{
public class PagedListViewModel<T> : List<T>, IPageableViewModel
{
private int _pageIndex;
private int _pagesCount;
public PagedListViewModel(IQueryable<T> source, int? pageIndex, int pageSize)
{
_pageIndex = pageIndex ?? 1;
_pagesCount = (int)Math.Ceiling(source.Count() / (double)pageSize);
this.AddRange(source.Skip((_pageIndex - 1) * pageSize).Take(pageSize));
}
public int PageIndex
{
get
{
return _pageIndex;
}
}
public int PagesCount
{
get
{
return _pagesCount;
}
}
}
}
В этом классе используются LINQ-операторы Skip() и Take(), которые генерируют SQL-код, осуществляющий выборку части данных на уровне БД.
