It is possible to create a reusable pagination. What we need is:
- partial view to store number of pages and links to them
- reusable methods which contains pagination logic for
IQueryable
The complete example with source code can be seen here.
So our code would like this:
The person table. I've used SQL Server:
IF NOT EXISTS ( SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'Person' AND TABLE_SCHEMA = 'dbo' ) BEGIN CREATE TABLE dbo.Person ( ID INT IDENTITY(1, 1) NOT NULL PRIMARY KEY , CreateDate DATETIME NOT NULL DEFAULT GETDATE() , Creator VARCHAR(100) NOT NULL , ModifyDate DATETIME NULL , Modifier VARCHAR(20) NULL , FirstName VARCHAR(150) NOT NULL , LastName VARCHAR(1000) NOT NULL ) ON [PRIMARY] END GO
I've used DatabaseFirst approach. This is a generated class by Entity Framework.
public partial class Person { public int ID { get; set; } public System.DateTime CreateDate { get; set; } public string Creator { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public Nullable<System.DateTime> ModifyDate { get; set; } public string Modifier { get; set; } }
This is a class which contains data and pagination information:
public class DataResultViewModel<T> { /// <summary> /// Data items /// </summary> public IEnumerable<T> Items { get; set; } /// <summary> /// Pagination /// </summary> public Pagination Pagination { get; set; } }
This is class which contains information about persons.
public class PersonViewModel { public int ID { get; set; } public string FirstName { get; set; } public string LastName { get; set; } } /// <summary> /// What route should be requested while paging /// </summary> public class RouteInfo { /// <summary> /// Name of controller /// </summary> public string ControllerName { get; set; } /// <summary> /// Name of action /// </summary> public string ActionName { get; set; } }
and Pagination class:
public class Pagination { /// <summary> /// Total count of items /// </summary> public int TotalItems { get; set; } /// <summary> /// Count of items at the page /// </summary> public int PageSize { get; set; } = 5; /// <summary> /// Current page /// </summary> public int Page { get; set; } /// <summary> /// Sorted by field name /// </summary> public string SortBy { get; set; } /// <summary> /// Is this an ascending sort? /// </summary> public bool IsSortAscending { get; set; } /// <summary> /// Information about what page should be requested /// </summary> public RouteInfo RouteInfo { get; set; } }
And the class which makes pagination. Basically, Skip() and Take() are methods which do a pagination, so we need to use these methods for all IQueryable. And C# has a very neat feature called extension methods. Extension methods allow to reuse code:
public static class IQueryableExtension { public static IQueryable<T> UseOrdering<T, TResultSelector>(this IQueryable<T> query, Pagination pagination, Expression<Func<T, TResultSelector>> field) { if (string.IsNullOrWhiteSpace(pagination.SortBy) || string.IsNullOrEmpty(pagination.SortBy)) return query; return pagination.IsSortAscending ? query.OrderBy(field) : query.OrderByDescending(field); } public static IQueryable<T> UsePagination<T>(this IQueryable<T> query, Pagination pagination) { if (pagination.Page <= 0) pagination.Page = 1; if (pagination.PageSize <= 0) pagination.PageSize = 10; return query.Skip((pagination.Page - 1) * pagination.PageSize) .Take(pagination.PageSize); } }
This is a class of service layer:
public class PersonService { public DataResultViewModel<PersonViewModel> GetWithPagination(Pagination pagination, Expression<Func<Person, DateTime>> fieldName) { var result = new DataResultViewModel<PersonViewModel>(); using (var db = new MiscellaneousEntities()) { var persons = db.Person.AsQueryable(); result.Pagination = pagination; result.Pagination.TotalItems = persons.Count(); result.Pagination.RouteInfo = new RouteInfo() { ActionName = "Index", ControllerName = "Person" }; if (pagination.SortBy == null) pagination.SortBy = "CreateDate"; persons = persons.UseOrdering(pagination, fieldName); persons = persons.UsePagination(pagination); result.Items = persons .Select(s => new PersonViewModel() { ID = s.ID, FirstName = s.FirstName, LastName = s.LastName }) .ToList(); return result; } } }
And controller of person:
public class PersonController : Controller { PersonService _personService; public PersonController() { _personService = new PersonService(); } // GET: Person public ActionResult Index(int? page, int? pageSize, string sortBy, bool? isSortAscending) { return View (_personService.GetWithPagination(new Pagination() { Page = page ?? 1, PageSize = pageSize ?? 3, SortBy = sortBy, IsSortAscending = isSortAscending ?? false }, v => v.CreateDate ) ); } }
Then we should create views.
This is a person view that should be paginated:
@model OnlyPagination.ViewModel.DataResultViewModel<OnlyPagination.ViewModel.PersonViewModel> <div class="d-flex justify-content-center"> <div> @foreach (var item in Model.Items) { <div> <p>Id is @item.ID</p> <p>FirstName is @item.FirstName</p> <p>LastName is @item.LastName</p> </div> <hr /> } </div> </div> <div class="d-flex justify-content-center"> @{ @Html.Partial("Pagination", Model.Pagination) } </div>
And it is a reusable pagination partial view:
@model OnlyPagination.Extensions.Query.Model.Pagination @{ var pagesCount = Math.Ceiling((decimal)Model.TotalItems / (decimal)Model.PageSize); var pages = new List<int>(); for (var i = 1; i <= pagesCount; i++) { pages.Add(i); } } <div> <p class="d-flex justify-content-center">Page @Model.Page of @pagesCount</p> <ul class="pagination"> <li class="page-item @( Model.Page == 1 ? "disabled" : "" )"> <a aria-label="Previous" class="page-link" href="@Url.Action (Model.RouteInfo.ActionName, Model.RouteInfo.ControllerName, new { page = Model.Page - 1, pageSize = Model.PageSize })"> <span aria-hidden="true">«</span> </a> </li> @for (int pageNumber = 1; pageNumber <= pages.Count; pageNumber++) { <li class="page-item @( Model.Page == pageNumber ? "active" : "" )"> <a class="page-link" href="@Url.Action(Model.RouteInfo.ActionName, Model.RouteInfo.ControllerName, new { page = pageNumber, pageSize = Model.PageSize })"> @pageNumber </a> </li> } <li class="page-item @(Model.Page == pages.Count ? "disabled" : "")"> <a aria-label="Next" class="page-link" href="@Url.Action (Model.RouteInfo.ActionName, Model.RouteInfo.ControllerName, new { page = Model.Page + 1, pageSize = Model.PageSize })"> <span aria-hidden="true">»</span> </a> </li> </ul> </div>
That's all!