In this article, we will set up pagination where code can be reused without third-party libraries, and should be placed into a partial view and should be customizable
Introduction
While I was developing a news web application, there was a strong need to have pagination.
Background
So many tutorials and libraries were investigated. One very good and nice library has all the features that satisfies all requirements. However, this library would lock a database while querying. It was not eligible for us.
So, our requirements of pagination are:
- Code can be reused
- Without third-party libraries
- It should be placed into a partial view and should be customizable
Our code will focus only on pagination. We will not create Repository layer as it is better to post another article about it.
Using the Code
Let's consider that we are developing a web application of persons.
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; }
}
Our view model classes.
This is a class which contains data and pagination information:
public class DataResultViewModel<T>
{
public IEnumerable<T> Items { get; set; }
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; }
}
public class RouteInfo
{
public string ControllerName { get; set; }
public string ActionName { get; set; }
}
and Pagination
class:
public class Pagination
{
public int TotalItems { get; set; }
public int PageSize { get; set; } = 5;
public int Page { get; set; }
public string SortBy { get; set; }
public bool IsSortAscending { get; set; }
public RouteInfo RouteInfo { get; set; }
}
And the class which makes pagination. Basically, Skip()
and Take()
methods 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();
}
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!
How It Looks
Download
The complete code example is attached to this article. I've deleted some default libraries from the example it is a rule to have less than 10 MB of attached file. You can create your own project and copy code to work. Or try to restore packages. Thanks for your attention.
History
- 5th August, 2021: Initial version
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.