Pagination in ASP.NET Core Razor Pages using custom Tag Helper

ASP.Net Core doesn’t have a built-in paging feature. Paging is essential when you have thousands of records to display. This quick tutorial shows how you can add pagination to an ASP.Net Core Razor Page application. We will create a custom Tag Helper for pagination.  

The sample web application included with this article can be downloaded from GitHub. Click here to download 

Project Setup

You need to have the following tools locally to run this application successfully.

The sample web application included with this article uses .NET Core 5.0 and SQL Server 2019. When you download and run the sample application, it will create a database automatically and insert sample data to it.

Paging Logic

Firstly, we have to create a class file that has some basic properties associated with the paging. I added PagingData.cs class file for that.

Pagingdata.cs

using System;
namespace MCTPaging.Model
{
    public class PagingData
    {
        public int TotalRecords { get; set; }
        public int RecordsPerPage { get; set; }
        public int CurrentPage { get; set; }
        public int TotalPages => (int)Math.Ceiling((decimal)TotalRecords / RecordsPerPage);
        public string UrlParams { get; set; }
        public int LinksPerPage { get; set; }
    }
}

Most of the properties in this class are self-explanatory. “UrlParams” Property holds the URL and query string parameters of the page where the paging Tag Helper is implemented. These parameters will be used when generating the paging links. “LinksPerPage” property stores the number of paging links to be displayed per page. Value of “TotalPages” property is calculated at run time based on the values of “TotalRecords” and RecordsPerPage properties.

Paging.CS

This class contains the logic to generate the paging links and the necessary code to make this a custom Tag Helper.

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using System;
using MCTPaging.Model;
using Microsoft.AspNetCore.Mvc.Routing;

namespace MCTPaging.TagHelpers
{
    [HtmlTargetElement("div", Attributes = "paging-model")]
    public class Pageing : TagHelper
    {
        private IUrlHelperFactory _urlHelperFactory;

        public Pageing(IUrlHelperFactory urlHelperFactory)
        {
            _urlHelperFactory = urlHelperFactory;
        }

        [ViewContext]
        [HtmlAttributeNotBound]
        public ViewContext ViewContext { get; set; }
        public PagingData PagingModel { get; set; }     
        public string PageClass { get; set; }
        public string PageClassNormal { get; set; }
        public string PageClassSelected { get; set; }
        public string PageClassLabel { get; set; }
        public string PageClassLinks { get; set; }


        public override void Process(TagHelperContext helperContext, TagHelperOutput helperOutput)
        {
            IUrlHelper urlHelper = _urlHelperFactory.GetUrlHelper(ViewContext);
            TagBuilder outerDiv = new TagBuilder("div");

            int startPage;
            int endPage;
            if (PagingModel.TotalPages > 1)
            {
                if (PagingModel.TotalPages <= PagingModel.LinksPerPage)
                {
                    startPage = 1;
                    endPage = PagingModel.TotalPages;
                }
                else
                {
                    if (PagingModel.CurrentPage + PagingModel.LinksPerPage - 1 > PagingModel.TotalPages)
                    {
                        startPage = PagingModel.CurrentPage - ((PagingModel.CurrentPage + PagingModel.LinksPerPage - 1) 
                            - PagingModel.TotalPages);
                        endPage = (PagingModel.CurrentPage + PagingModel.LinksPerPage - 1) - 
                            ((PagingModel.CurrentPage + PagingModel.LinksPerPage - 1) - PagingModel.TotalPages);
                    }
                    else
                    {                      
                        if (PagingModel.LinksPerPage !=2)
                        {
                            startPage = PagingModel.CurrentPage - (PagingModel.LinksPerPage / 2);
                            if (startPage < 1)
                            {
                                startPage = 1;
                            }
                            endPage = startPage + PagingModel.LinksPerPage - 1;
                        }                    
                        else
                        {
                            startPage = PagingModel.CurrentPage;
                            endPage = PagingModel.CurrentPage + PagingModel.LinksPerPage - 1;
                        }

                    }

                }
                TagBuilder labelDiv;
                labelDiv = new TagBuilder("div");
                labelDiv.AddCssClass(PageClassLabel);
                labelDiv.InnerHtml.Append($"Showing {PagingModel.CurrentPage} of { PagingModel.TotalPages}");
                outerDiv.InnerHtml.AppendHtml(labelDiv);
                TagBuilder linkDiv = new TagBuilder("div");
                linkDiv.InnerHtml.AppendHtml(GeneratePageLinks("First", 1));
                for (int i = startPage; i <= endPage; i++)
                {
                    linkDiv.InnerHtml.AppendHtml(GeneratePageLinks(i.ToString(), i));
                }

                linkDiv.InnerHtml.AppendHtml(GeneratePageLinks("Last", PagingModel.TotalPages));
                linkDiv.AddCssClass(PageClassLinks);
                outerDiv.InnerHtml.AppendHtml(linkDiv);
                helperOutput.Content.AppendHtml(outerDiv.InnerHtml);
            }
        }

        private TagBuilder GeneratePageLinks(string iterator, int pageNumber)
        {
            string url;
            TagBuilder tag;
            tag = new TagBuilder("a");
            url = PagingModel.UrlParams.Replace("-", pageNumber.ToString());
            tag.Attributes["href"] = url;
            tag.AddCssClass(PageClass);
            if (iterator != "First" && iterator != "Last")
            {
                tag.AddCssClass(Convert.ToInt32(iterator) == PagingModel.CurrentPage ? PageClassSelected : PageClassNormal);
            }
            else
            {
                tag.AddCssClass(pageNumber == PagingModel.CurrentPage ? PageClassSelected : PageClassNormal);
            }
            tag.InnerHtml.Append(iterator.ToString());
            return tag;

        }


    }
}

 

Code Description

The first step to create a custom Tag Helper is adding a class that inherits from "TagHelper" class. The TagHelper class has two virtual methods, synchronous (Process) and asynchronous (ProcessAsync), one of which must be implemented in our class.  We need a target element for the pagination. Here, we will use a Div for that. The next thing is mentioning comma-separated string of attribute selectors the HTML element must match for the Tag Helper to run. Here I have made the "paging-model" attribute mandatory, without which the Tag Helper won’t run.

IURLhelper factory is a factory for creating Microsoft.AspNetCore.MVC.IUrlHelper instances.  We will use dependency injection to get IURLhelper.

Next, we declare a set of attributes for the Tag Helper.

The ViewContext object is the object that provides access to things like HttpContext, HttpRequest, HttpResponse, etc. You can gain access to it in a Tag Helper via a property with a [ViewContext] attribute so that the property gets set to the current ViewContext.

Essentially, [HtmlAttributeNotBound] says that this attribute is not the one that you wish to set in the HTML with a Tag Helper attribute.

PagingModel is a mandatory attribute for the Tag Helper.  The other four properties related to CSS can be used to set CSS classes for the generated paging links and the label. 

IURLHelper retrieves the current URL from where the Tag Helper code is called. 

 TagBuilder class contains methods and properties that are used to create HTML elements. This class is used to write HTML Helpers and Tag Helpers. We will use this TagBuilder to generate HTML tags for paging links. 

The next portion of the code contains the logic to generate paging links. The number of paging links generated for each page depends on the value assigned to “LinksPerPage'' property. Based on the page number passed to the Tag Helper code, we have to calculate the start page and end page and then generate paging links in between them. Once the start page and the end page are calculated “GeneratePageLinks” function is called in a loop to generate the paging links. The generated HTML tags are appended to TagHelperOutput variable.

Registering the Tag Helper

The next step is registering the Tag Helper. It is done in _ViewImports.cshtml file. We want the code to Identify all the Tag Helpers within the application automatically.  For this, we have added the following code. 

@addTagHelper *, MCTPaging

 The first string after @addTagHelper specifies the Tag Helper to load (Use "*" for all tag helpers). MCTPaging is the assembly where the Tag Helper code resides.  

Invoking the Tag Helper

 The paging Tag Helper is called on the Index page.  The OnGet handler method retrieves all the employee records from the database. An object of PagingData class is initialized there, and values for  CurrentPage, RecordsPerPage,  TotalRecords, UrlParams, and LinksPerPage are assigned. Here, when query string parameters are assigned to QParam variable a hyphen is used after PageNumber parameter. This hyphen will be replaced with the page number in the Tag Helper class

Index.cshtml.cs

    public void OnGet(int PageNum = 1)
        {
            Employees = _db.Employee.OrderBy(m => m.EmpId).ToList<Employee>();
            StringBuilder QParam = new StringBuilder();
            if (PageNum!=0)
            {
                QParam.Append($"/Index?PageNum=-");

            }
           
            if (Employees.Count > 0)
            {
                PagingData = new PagingData
                {
                    CurrentPage = PageNum,
                    RecordsPerPage = PageSize,
                    TotalRecords = Employees.Count(),
                    UrlParams = QParam.ToString(),
                    LinksPerPage =7
                };
                Employees = Employees.Skip((PageNum - 1) * PageSize)
               .Take(PageSize).ToList();
                
            }
        }

Employees variable contains all employee records from the database. We need to get only the records for the selected page number from this. We use the following code for that.

 Employees = Employees.Skip((PageNum - 1) * PageSize)
               .Take(PageSize).ToList();

Finally, we have to call the paging Tag Helper.  We have to use kabab-casing to assign values to Tag Helper attributes. So PagingModel becomes paging-model. Visual Studio identifies the Tag Helper automatically once it is registered in _ViewImports.cshtml file and provides IntelliSense support. The following code calls the Tag Helper

    <div paging-model="@Model.PagingData"   page-class="btn border mt-3"
         page-class-links="text-center" page-class-normal="btn btn-light" page-class-selected="btn btn-dark  active" 
         page-class-label="p-2 font-weight-bold  pt-md-2  text-center" class="text-center">
    </div>

I have used a few Bootstrap classes to make the links attractive. Paging related data is passed to the custom Tag Helper through PagingData variable.  You can use custom CSS classes to completely change the look and feel of the paging links. 

Screenshot

 

 I hope this tutorial is helpful. Feel free to give your comments and opinions.

Check my article to learn about server-side paging using stored procedures and Dapper ORM

 

 


Search