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), "«");
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), "»");
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.
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.
Pingback:The Morning Brew - Chris Alcock » The Morning Brew #2521
not working my not enough code
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 ..
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.