Универсальный пейджер для ASP.NET MVC

11.07.2011 21:29 / Артём Волк / 910 просмотров / ...

Практически в каждом веб-проекте необходим т.н. пейджер — элемент интерфейса, с помощью обеспечивается навигация по страницам списка каких-либо элементов. Для подобной функциональности, которая повторяется из проекта в проект, удобно сделать более-менее универсальное решение. Наша реализация должна была удовлетворять таким требованиям:

  • Нумерация страниц с единицы
  • 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">&larr;&nbsp;<span>Предыдущая</span></a></li>
			 } 
			 @if ((Model.PageIndex - Model.PageDelta - 1) > 0) { 		  
				<li>&hellip;</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>&hellip;</li>
			} 				
			@if (Model.HasNextPage) { 
				<li><a href="@Model.NextLink" class="next-link"><span>Следующая</span>&nbsp;&rarr;</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-код, осуществляющий выборку части данных на уровне БД.

Скачать исходный код (3,2 Кб).