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.



See also

One thought on “Building pager tag helper

Leave a Reply

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