Create Number Paging with Custom Tag Helper in ASP.NET Core

Create Number Paging with Custom Tag Helper in ASP.NET Core

Tag Helpers allow us to enhance HTML elements from server-side code. We will use this feature to create number based paging links in ASP.NET Core. We will be building this feature from scratch and once completed the paging links will work as shown by the below image:

asp.net core pagination video

In short, we will add a div, that is shown below, on a View:

<div class="pagingDiv" page-model="@Model.pagingInfo" page-action="Index" page-classes-enabled="true" page-class="paging" page-class-selected="active"></div>

And it will automatically be enhanced to a fully functional paging links.

Create Paging Class

Firstly, create a Paging class called PagingInfo.cs and keep it inside the Models folder. This class contains the information – like total Item, Items per page, current page and total pages and this will be used to create the paging links.

This class is given below:

public class PagingInfo
{
    public int TotalItems { get; set; }
    public int ItemsPerPage { get; set; }
    public int CurrentPage { get; set; }
    public int TotalPages
    {
        get
        {
            return (int)Math.Ceiling((decimal)TotalItems / ItemsPerPage);
        }
    }
}

Custom Tag Helper for Paging

Now coming to the main part which is to create Custom Tag Helper for paging. So create a class inside the Models folder and name it PageLinkTagHelper.cs. Add the following code to it:

[HtmlTargetElement("div", Attributes = "page-model")]
public class PageLinkTagHelper : TagHelper
{
    private IUrlHelperFactory urlHelperFactory;

    public PageLinkTagHelper(IUrlHelperFactory helperFactory)
    {
        urlHelperFactory = helperFactory;
    }

    [ViewContext]
    [HtmlAttributeNotBound]
    public ViewContext ViewContext { get; set; }

    public PagingInfo PageModel { get; set; }

    public string PageAction { get; set; }

    /*Accepts all attributes that are page-other-* like page-other-category="@Model.allTotal" page-other-some="@Model.allTotal"*/
    [HtmlAttributeName(DictionaryAttributePrefix = "page-other-")]
    public Dictionary<string, object> PageOtherValues { get; set; } = new Dictionary<string, object>();

    public bool PageClassesEnabled { get; set; } = false;

    public string PageClass { get; set; }

    public string PageClassSelected { get; set; }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        IUrlHelper urlHelper = urlHelperFactory.GetUrlHelper(ViewContext);
        TagBuilder result = new TagBuilder("div");
        string anchorInnerHtml = "";

        for (int i = 1; i <= PageModel.TotalPages; i++)
        {
            TagBuilder tag = new TagBuilder("a");
            anchorInnerHtml = AnchorInnerHtml(i, PageModel);

            if (anchorInnerHtml == "..")
                tag.Attributes["href"] = "#";
            else if (PageOtherValues.Keys.Count != 0)
                tag.Attributes["href"] = urlHelper.Action(PageAction, AddDictionaryToQueryString(i));
            else
                tag.Attributes["href"] = urlHelper.Action(PageAction, new { id = i });

            if (PageClassesEnabled)
            {
                tag.AddCssClass(PageClass);
                tag.AddCssClass(i == PageModel.CurrentPage ? PageClassSelected : "");
            }
            tag.InnerHtml.Append(anchorInnerHtml);
            if (anchorInnerHtml != "")
                result.InnerHtml.AppendHtml(tag);
        }
        output.Content.AppendHtml(result.InnerHtml);
    }

    public IDictionary<string, object> AddDictionaryToQueryString(int i)
    {
        object routeValues = null;
        var dict = (routeValues != null) ? new RouteValueDictionary(routeValues) : new RouteValueDictionary();
        dict.Add("id", i);
        foreach (string key in PageOtherValues.Keys)
        {
            dict.Add(key, PageOtherValues[key]);
        }

        var expandoObject = new ExpandoObject();
        var expandoDictionary = (IDictionary<string, object>)expandoObject;
        foreach (var keyValuePair in dict)
        {
            expandoDictionary.Add(keyValuePair);
        }

        return expandoDictionary;
    }

    public static string AnchorInnerHtml(int i, PagingInfo pagingInfo)
    {
        string anchorInnerHtml = "";
        if (pagingInfo.TotalPages <= 10)
            anchorInnerHtml = i.ToString();
        else
        {
            if (pagingInfo.CurrentPage <= 5)
            {
                if ((i <= 8) || (i == pagingInfo.TotalPages))
                    anchorInnerHtml = i.ToString();
                else if (i == pagingInfo.TotalPages - 1)
                    anchorInnerHtml = "..";
            }
            else if ((pagingInfo.CurrentPage > 5) && (pagingInfo.TotalPages - pagingInfo.CurrentPage >= 5))
            {
                if ((i == 1) || (i == pagingInfo.TotalPages) || ((pagingInfo.CurrentPage - i >= -3) && (pagingInfo.CurrentPage - i <= 3)))
                    anchorInnerHtml = i.ToString();
                else if ((i == pagingInfo.CurrentPage - 4) || (i == pagingInfo.CurrentPage + 4))
                    anchorInnerHtml = "..";
            }
            else if (pagingInfo.TotalPages - pagingInfo.CurrentPage < 5)
            {
                if ((i == 1) || (pagingInfo.TotalPages - i <= 7))
                    anchorInnerHtml = i.ToString();
                else if (pagingInfo.TotalPages - i == 8)
                    anchorInnerHtml = "..";
            }
        }
        return anchorInnerHtml;
    }
}

We will also need to import the following namespaces in this class.

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

In the above code we have created a Custom Tag Helper that will be applied to any div element given on a View file. It is having an attribute called page-model. This means if we add:

<div class="pagingDiv" page-model="@Model.pagingInfo"></div>

Then the custom Tag Helper will convert it to a number based paging links.

Explanation

This Custom Tag Helper is using IUrlHelperFactory interface for building paging URLs. We have declared a number of properties through which the tag helpers receives values from the attributes of the div on the view. These are:

  • PageModel: This property will receive an object of PagingInfo class from the view. This object will contain total Item, Items per page, current page and total pages. These will be used to create the paging links in the tag helper.
  • PageAction: This property receives the current action method or View name. In my case it is the ‘Index’ view.
  • PageClassesEnabled: This property will be used to apply a CSS to the paging links. It can have either ‘true’ or ‘false’.
  • PageClass: It is used for applying CSS class on the div element that will contain the paging links. In our case it is named as ‘paging’.
  • PageClassSelected: used for applying a specific CSS class to the current page link.

We have also defined a property called ‘ViewContext’ of type ViewContext class and applied 2 attributes to it, these are – [ViewContext] and [HtmlAttributeNotBound].

Check the code below:

[ViewContext]
[HtmlAttributeNotBound]
public ViewContext ViewContext { get; set; }

This property will provide us the rendering information of the view where the paging links are placed. So we get access to things like the HttpContext, HttpRequest, HttpResponse and so on. A great way indeed to get access to such information in a tag helper.

Want to learn some of the very hard Model binding concepts then see Advanced Model Binding Concepts in ASP.NET Core

Next see the Process method of the tag helper where we have created the ‘IUrlHelper’ object from this ViewContext property like shown below:

IUrlHelper urlHelper = urlHelperFactory.GetUrlHelper(ViewContext);

I then used it to create paging anchors like shown below:

if (anchorInnerHtml == "..")
    tag.Attributes["href"] = "#";
else if (PageOtherValues.Keys.Count != 0)
    tag.Attributes["href"] = urlHelper.Action(PageAction, AddDictionaryToQueryString(i));
else
    tag.Attributes["href"] = urlHelper.Action(PageAction, new { id = i });
Note: [HtmlAttributeNotBound] attribute says that this property (to which it is applied) isn’t the one that you intend to set via a tag helper attribute in the view.

Did you notice the PageOtherValues property? It accepts all attributes that are page-other-* like page-other-category="@Model.allTotal", page-other-some="@Model.allTotal", etc. These all get filled in the dictionary object of type <string, object>.

Check the below code:

[HtmlAttributeName(DictionaryAttributePrefix = "page-other-")]
public Dictionary<string, object> PageOtherValues { get; set; } = new Dictionary<string, object>();

So this means we can send some extra information from the View like sort & searchtext as:

page-other-searchtext="@Model.pagingInfo.SearchText" page-other-sort="@Model.pagingInfo.SortOrder"

And these are appended to the URL like:

https://localhost:44353/Home/Index/1?searchtext=5&sort=40

The function called AddDictionaryToQueryString() will return a dictionary of objects that need to be added to the routes. These are the page numbers and PageOtherValues property values. Check below:

public IDictionary<string, object> AddDictionaryToQueryString(int i)
{
//…
}

So in-short this means we can pass any number of information in the URL’s query string by using this feature.

Register the Tag Helper

Add the below code to the _ViewImports.cshtml file to register the tag helper you just made.

@addTagHelper PagingCore.Models.*, PagingCore

Note: ‘PagingCore’ is my project’s name which you need to change if you have a different name for the project.

Finally notice the AnchorInnerHtml() function which has just one work which is to create the href values of the page link anchors.

When a user forgets his/her password then he/she should be able to reset the password. Check How this feature is created – Creating Password Reset feature in ASP.NET Core Identity

Adding the Paging to a View and testing

Let’s now check how it works. So I will create some data that will be held in a ‘Customer’ class. I will then show this data in a view along with paging.

So create 2 classes called ‘Customer’ & ‘CustomerList’ in the Models folder.

public class Customer
{
    [Required]
    public int Id { get; set; }

    [Required]
    public string Name { get; set; }

    [Required]
    public string City { get; set; }
}

public class CustomerList
{
    public IEnumerable<Customer> Customer { get; set; }
    public PagingInfo PagingInfo { get; set; }
}

Next, go to your Controller and add the Index action method and a function called CreateDummyData() as shown below. In our project we are adding the code to the HomeController.

using Microsoft.AspNetCore.Mvc;
using PagingCore.Models;

namespace PagingCore.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index(int? id)
        {
            CustomerList customerList = new CustomerList();
            customerList = CreateDummyData(Convert.ToInt32(id));
            return View(customerList);
        }

        CustomerList CreateDummyData(int page)
        {
            int pageSize = 5;
            List<Customer> cL = new List<Customer>
            {
                new Customer {Id=1, Name = "Jacky", City = "Boston"},
                new Customer {Id=2, Name = "Jack", City = "New Jersey"},
                new Customer {Id=3, Name = "Sheena", City = "Ohio"},
                new Customer {Id=4, Name = "Rohit", City = "Albany"},
                new Customer {Id=5, Name = "Mila", City = "NYC"},
                new Customer {Id=6, Name = "Sharon", City = "San Francisco"},
                new Customer {Id=7, Name = "Mark", City = "NY"},
                new Customer {Id=8, Name = "Bill", City = "Boston"},
                new Customer {Id=9, Name = "Maggi", City = "Lucknow"},
                new Customer {Id=10, Name = "Jacobs", City = "Mumbai"},
                new Customer {Id=11, Name = "Amanda", City = "Sao Paulo"},
                new Customer {Id=12, Name = "Mak", City = "Nagpur"},
                new Customer {Id=13, Name = "Rick", City = "New Jersey"},
                new Customer {Id=14, Name = "Chump", City = "Shanghai"},
                new Customer {Id=15, Name = "Sajal", City = "Albany"},
                new Customer {Id=16, Name = "Sharon", City = "San Francisco"},
                new Customer {Id=17, Name = "Mark", City = "NY"},
                new Customer {Id=18, Name = "Bill", City = "Boston"},
                new Customer {Id=19, Name = "Maggi", City = "Lucknow"},
                new Customer {Id=20, Name = "Jacobs", City = "Mumbai"},
                new Customer {Id=21, Name = "Jacky", City = "Boston"},
                new Customer {Id=22, Name = "Jack", City = "New Jersey"},
                new Customer {Id=23, Name = "Sheena", City = "Ohio"},
                new Customer {Id=24, Name = "Rohit", City = "Albany"},
                new Customer {Id=25, Name = "Mila", City = "NYC"},
                new Customer {Id=26, Name = "Sharon", City = "San Francisco"},
                new Customer {Id=27, Name = "Mark", City = "NY"},
                new Customer {Id=28, Name = "Bill", City = "Boston"},
                new Customer {Id=29, Name = "Maggi", City = "Lucknow"},
                new Customer {Id=30, Name = "Jacobs", City = "Mumbai"},
                new Customer {Id=31, Name = "Amanda", City = "Sao Paulo"},
                new Customer {Id=32, Name = "Mak", City = "Nagpur"},
                new Customer {Id=33, Name = "Rick", City = "New Jersey"},
                new Customer {Id=34, Name = "Chump", City = "Shanghai"},
                new Customer {Id=35, Name = "Sajal", City = "Albany"},
                new Customer {Id=36, Name = "Sharon", City = "San Francisco"},
                new Customer {Id=37, Name = "Mark", City = "NY"},
                new Customer {Id=38, Name = "Bill", City = "Boston"},
                new Customer {Id=39, Name = "Maggi", City = "Lucknow"},
                new Customer {Id=40, Name = "Jacobs", City = "Mumbai"}
            };
            PagingInfo pagingInfo = new PagingInfo();
            pagingInfo.CurrentPage = page == 0 ? 1 : page;
            pagingInfo.TotalItems = cL.Count();
            pagingInfo.ItemsPerPage = pageSize;

            var skip = pageSize * (Convert.ToInt32(page) - 1);
            CustomerList customerList = new CustomerList();
            customerList.PagingInfo = pagingInfo;
            customerList.Customer = cL.Skip(skip).Take(pageSize).ToList();

            return customerList;
        }
    }
}

The CreateDummyData function creates some dummy customer information and sets the paging info to the ‘PagInfo’ class.

The Index View receives the current page number in the ‘id’ parameter. The concept applied here is the Model Binding feature and then we return the CustomerList object which contains both the PageInfo and Customer’s data.

View

Now all remains is the View file where we put the div. The View code is given below. We are using the Index view of Home Controller.

@model CustomerList
<div id="viewContent">
    <table>
        <thead>
            <tr>
                <th>Name</th>
                <th>Status</th>
                <th>City</th>
            </tr>
        </thead>
        <tbody id="pageTbody">
            @foreach (var p in Model.Customer)
            {
                <tr>
                    <td>@p.Id</td>
                    <td>@p.Name</td>
                    <td>@p.City</td>
                </tr>

            }
        </tbody>
        <tfoot>
            <tr>
                <td colspan="3">
                    <div class="pagingDiv" page-model="@Model.PagingInfo" page-action="Index" page-classes-enabled="true" page-class="paging" page-class-selected="active"></div>
                </td>
            </tr>
        </tfoot>
    </table>
</div>

The View accepts model of ‘CustomerList’ type and shows the customers data in a table. See the footer of the table where we have placed the div to be transformed by custom tag helper:

<div class="pagingDiv" page-model="@Model.pagingInfo" page-action="Index" page-classes-enabled="true" page-class="paging" page-class-selected="active"></div>

Run your application and you will how these paging links work.

Download the full source codes:

Download

Xaero – Entity Framework Core Advanced Project is my latest project where I have created a full Movie Database based ASP.NET Core App in Entity Framework Core. In this project you will find lots and lots of reusable high quality codes.
Conclusion

This pagination feature is very powerful and easy to add to any of yours ASP.NET Core application. I have also created a CSS for the paging links:

.pagingDiv {
    background: #f2f2f2;
}

    .pagingDiv > a {
        display: inline-block;
        padding: 0px 9px;
        margin-right: 4px;
        border-radius: 3px;
        border: solid 1px #c0c0c0;
        background: #e9e9e9;
        box-shadow: inset 0px 1px 0px rgba(255,255,255, .8), 0px 1px 3px rgba(0,0,0, .1);
        font-size: .875em;
        font-weight: bold;
        text-decoration: none;
        color: #717171;
        text-shadow: 0px 1px 0px rgba(255,255,255, 1);
    }

        .pagingDiv > a:hover {
            background: #fefefe;
            background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#FEFEFE), to(#f0f0f0));
            background: -moz-linear-gradient(0% 0% 270deg,#FEFEFE, #f0f0f0);
        }

        .pagingDiv > a.active {
            border: none;
            background: #616161;
            box-shadow: inset 0px 0px 8px rgba(0,0,0, .5), 0px 1px 0px rgba(255,255,255, .8);
            color: #f0f0f0;
            text-shadow: 0px 0px 3px rgba(0,0,0, .5);
        }

SHARE THIS ARTICLE

  • linkedin
  • reddit
yogihosting

ABOUT THE AUTHOR

I hope you enjoyed reading this tutorial. If it helped you then consider buying a cup of coffee for me. This will help me in writing more such good tutorials for the readers. Thank you. Buy Me A Coffee donate

Comments

  1. Hassan Nolehdan says:

    Dear sir
    Hi
    I Read and open your code but some when i use it for my project , taghelpers for pagination does not apear .
    I read some times and check several times but there are a problem that never seen pagination links.
    please help me .
    Nolehdan

    1. yogihosting says:

      Make sure you have have Register the Tag Helper in the _ViewImports.cshtml file by adding the code:

      @addTagHelper PagingCore.Models.*, PagingCore

      Moreover please try downloading the source code of this tutorial and check if you have missed on something.

Leave a Reply

Your email address will not be published. Required fields are marked *