Building pager tag helper

Tag helpers are classes that can be applied to HTML and special tags in ASP.NET Core views. They are addition to HTML helper extension methods and they provide more flexibility by having their own classes and supporting framework level dependency injection. This blog post demonstrates how to create pager tag helper to support displaying paged results in ASP.NET Core views.

Example of tag helper

Example of tag helper can be found from layout page of default ASP.NET Core application.

<environment include="Development">
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    <link rel="stylesheet" href="~/css/site.css" />
</environment>

The <environment> tag is processed by special class that checks if application is running in development mode and then writes out links to css files defined between <environment> tags.

Source code with fully working examples of different paging methods is available in my GitHub repository gpeipman/DotNetPaging.

Pager classes

Before going to pager let’s take a look at most important classes that paging uses. Pager will use the following base class as it is carrying all necessary information to draw out the pager.

public abstract class PagedResultBase
{
    public int CurrentPage { get; set; }
    public int PageCount { get; set; }
    public int PageSize { get; set; }
    public int RowCount { get; set; }
    public string LinkTemplate { get; set; }

    public int FirstRowOnPage
    {
         get { return (CurrentPage - 1) * PageSize + 1; }
    }

    public int LastRowOnPage
    {
        get { return Math.Min(CurrentPage * PageSize, RowCount); }
    }
}

We also need generic class to carry paged results. For this we define PagedResult<T> that extends PagedResultBase and adds list with results on current page.

public class PagedResult<T> : PagedResultBase
{
    public IList<T> Results { get; set; }

    public PagedResult()
    {
        Results = new List<T>();
    }
}

Now we have all classes needed to build a pager.

Pager tag helper

Pager tag helper will be simple and short in this implementation. It accepts all classes that inherit from PagedResultBase as a model. Using the model it creates the HTML output for pager. In views we will use it like show here.

<pager pager-model="@Model"></pager>

Here is the code of pager tag helper.

[HtmlTargetElement("pager", TagStructure = TagStructure.NormalOrSelfClosing)]
public class PagerTagHelper : TagHelper
{
    private readonly HttpContext _httpContext;
    private readonly IUrlHelper _urlHelper;

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

    public PagerTagHelper(IHttpContextAccessor accessor, IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory)
    {
        _httpContext = accessor.HttpContext;
        _urlHelper = urlHelperFactory.GetUrlHelper(actionContextAccessor.ActionContext);
    }

    [HtmlAttributeName("pager-model")]
    public PagedResultBase Model { get; set; }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {           
        if(Model == null)
        {
            return;
        }

        if(Model.PageCount == 0)
        {
            return;
        }

        var action = ViewContext.RouteData.Values["action"].ToString();
        var urlTemplate = WebUtility.UrlDecode(_urlHelper.Action(action, new { page = "{0}" }));
        var request = _httpContext.Request;
        foreach (var key in request.Query.Keys)
        {
            if (key == "page")
            {
                continue;
            }

            urlTemplate += "&" + key + "=" + request.Query[key];
        }

        var startIndex = Math.Max(Model.CurrentPage - 5, 1);
        var finishIndex = Math.Min(Model.CurrentPage + 5, Model.PageCount);

        output.TagName = "";
        output.Content.AppendHtml("<ul class=\"pagination\">");
        AddPageLink(output, string.Format(urlTemplate, 1), "&laquo;");

        for (var i = startIndex; i <= finishIndex; i++)
        {
            if (i == Model.CurrentPage)
            {
                AddCurrentPageLink(output, i);
            }
            else
            {
                AddPageLink(output, string.Format(urlTemplate, i), i.ToString());
            }
        }

        AddPageLink(output, string.Format(urlTemplate, Model.PageCount), "&raquo;");
        output.Content.AppendHtml("</ul>");
    }

    private void AddPageLink(TagHelperOutput output, string url, string text)
    {           
        output.Content.AppendHtml("<li><a href=\"");
        output.Content.AppendHtml(url);
        output.Content.AppendHtml("\">");
        output.Content.AppendHtml(text);
        output.Content.AppendHtml("</a>");
        output.Content.AppendHtml("</li>");
    }

    private void AddCurrentPageLink(TagHelperOutput output, int page)
    {
        output.Content.AppendHtml("<li class=\"active\">");
        output.Content.AppendHtml("<span>");
        output.Content.AppendHtml(page.ToString());
        output.Content.AppendHtml("</span>");
        output.Content.AppendHtml("</li>");
    }
}

This is all we need to get pager displayed in views that use paged result as model or part of model.

Registering pager tag helper

Before we can use pager tag helper we need to register it in MVC. It doesn’t get to views automatically. For this we have to edit _ViewImports.cs file.

@using DotNetPaging
@using DotNetPaging.AspNetCore
@using DotNetPaging.AspNetCore.Components
@using DotNetPaging.AspNetCore.Models
@using DotNetPaging.EFCore
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, DotNetPaging.AspNetCore

Last line is necessary to get all our tag helpers to views.

Displaying paged results

Now let’s build simple controller action to display paged results. I don’t show here all code as full working solution is available in my GitHub repository gpeipman/DotNetPaging.

public async Task<IActionResult> TagHelper(int page = 1)
{
    var releases = await _dataContext.PressReleases
                                     .OrderByDescending(p => p.ReleaseDate)
                                     .GetPagedAsync(page, 10);
    return View(releases);
}

Here is the view with pager tag helper.

@model PagedResult<PressRelease>
@{
    ViewData["Title"] = "TagHelper";
}

<h2>Tag helper paging</h2>

<p>Results on this page are queried using synchronous method calls of Entity Framework Core.</p>

<table class="table">
    <thead>
        <tr>
            <th>Date</th>
            <th>Company</th>
            <th>Title</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var release in Model.Results)
        {
            <tr>
                <td>@release.ReleaseDate.ToShortDateString()</td>
                <td>@release.Company</td>
                <td>@release.Title</td>
            </tr>
        }
    </tbody>
</table>

<pager pager-model="@Model"></pager>

When we run the solution and move to pager tag helper page then something like this is shown in browser.

Pager tag helper in action

On the screenshot below second page of results is selected.

Wrapping up

Tag helpers are nice addition to ASP.NET Core and it’s okay to consider them to be somewhere between HTML helper extension methods and view components. They are more advanced than HTML helper extensions as they support framework level dependency injection and they are less advanced than view components as they don’t have views. Also here know-your-limits applies: as markup code is generated in compiled code we must switch over to view components if there is too much markup to output. In this means I consider pager tag helper as being on the last line before view components. What we achieved was simple and clean looking pager tag that is easy to use for our fellow developers.

Gunnar Peipman

Gunnar Peipman is ASP.NET, Azure and SharePoint fan, Estonian Microsoft user group leader, blogger, conference speaker, teacher, and tech maniac. Since 2008 he is Microsoft MVP specialized on ASP.NET.

    4 thoughts on “Building pager tag helper

    • Pingback:The Morning Brew - Chris Alcock » The Morning Brew #2521

    • August 6, 2019 at 10:41 am
      Permalink

      not working my not enough code

    • March 28, 2020 at 11:58 pm
      Permalink

      nothing works … i cannot pass model from controller to view due to mismatched Models.

      Why people are acting like that … If you use external code Because there is a method used in query to data from the table PressReleases.

      why? what you gain as you hide things … if you wanna hide , you do not have to make a blog-page. I have spent my two hours for nothing. thank you ..

    • March 29, 2020 at 5:40 am
      Permalink

      Please don’t code when you are tired and it’s hard to focus. The query to PressReleases table is in *controller action* it’s clearly stated in text above. TagHelper has zero dependencies to this table. If you read TagHelper code then only dependency to paging is PagedResultBase class that has no type attribute at all.

      Also notice that there is link to demo solution in GitHub: https://github.com/gpeipman/DotNetPaging/tree/master/DotNetPaging/DotNetPaging.AspNetCore.

    Leave a Reply

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