Click here to Skip to main content
15,064,806 members
Articles / Web Development / ASP.NET
Article
Posted 5 Aug 2021

Stats

3.1K views
113 downloads
9 bookmarked

Reusable Pagination in ASP.NET MVC

Rate me:
Please Sign up or sign in to vote.
5.00/5 (3 votes)
5 Aug 2021CPOL2 min read
How to set up pagination to reuse code without third party libraries
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:

SQL
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.

C#
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:

C#
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.

C#
public class PersonViewModel
{
    public int ID { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }
}
C#
/// <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:

C#
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() 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:

C#
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:

C#
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:

C#
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:

C#
@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:

Razor
@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">&laquo;</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">&raquo;</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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Stepan Markakov
Russian Federation Russian Federation
No Biography provided

Comments and Discussions

 
-- There are no messages in this forum --