diff --git a/src/posts/PaginationTagHelpers.md b/src/posts/PaginationTagHelpers.md new file mode 100644 index 0000000..6ccab03 --- /dev/null +++ b/src/posts/PaginationTagHelpers.md @@ -0,0 +1,215 @@ +--- +title: Pagination Tag Helpers +description: A couple of tag helpers to automate creating common pagination components +tags: [ Programming, C#, ASP.NET Core ] +--- + +These tag helpers work with [`PaginatedResults`](/posts/CsPaginationTools) to automatically create things like a results header (e.g. "Showing results 1-20 of 123" and "Page 1 of 7") as well as pagination links. + +## How to use + +```csharp + + +@foreach (var result in this.Model.MyResults.Results) +{ + ... +} + + +``` + +The query parameter given as `page-number-key` will be changed or added to each the pagination link. The `page-number-key` query parameter will be left off for links to page 1. + +## Code + +The `pager-details` tag helper: + +```csharp +using ASIS.SecretsManagement.Shared; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.TagHelpers; +using Microsoft.AspNetCore.Razor.TagHelpers; +using System.Text.Encodings.Web; + +/// +/// Displays details about pagination in the form "Showing results x - y of z" on the left and +/// "Page x of y" on the right +/// +public class PagerDetailsTagHelper : TagHelper +{ + public PaginatedResults Results { get; set; } + + public override void Process(TagHelperContext context, TagHelperOutput output) + { + output.TagName = "div"; + output.TagMode = TagMode.StartTagAndEndTag; + output.AddClass("paginationDetails", HtmlEncoder.Default); + + TagBuilder resultsNumDetails = new("span"); + resultsNumDetails.InnerHtml.Append("Showing results " + + $"{this.Results.FirstResultNumber} - {this.Results.LastResultNumber} " + + $"of {this.Results.TotalResultsCount}"); + output.Content.AppendHtml(resultsNumDetails); + + TagBuilder pageNumDetails = new("span"); + pageNumDetails.InnerHtml.Append( + $"Page {this.Results.PageNumber} of {this.Results.LastPageNumber}"); + output.Content.AppendHtml(pageNumDetails); + } +} +``` + +And the `pager` tag helper: + +```csharp +using ASIS.SecretsManagement.Shared; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Primitives; +using System.Collections.Immutable; + +/// +/// Creates a list of links to pages for the current list of data. This will replace the page number +/// parameter in the query string using the . +/// +/// +/// Page 1 is assumed to be the default and the page number parameter will be excluded +/// +public class PagerTagHelper : TagHelper +{ + /// The name of the page number parameter in the query string + public string PageNumberKey { get; set; } + public PaginatedResults Results { get; set; } + + [ViewContext, HtmlAttributeNotBound] + public ViewContext ViewContext { get; set; } + + private string CurrentPagePath { get => this.ViewContext.HttpContext.Request.Path; } + private ImmutableDictionary QueryStringValues { get; set; } + + public override void Process(TagHelperContext context, TagHelperOutput output) + { + output.TagName = "nav"; + output.TagMode = TagMode.StartTagAndEndTag; + output.Attributes.Add("aria-label", "pagination"); + + var paginationList = new TagBuilder("ul"); + paginationList.AddCssClass("pagination"); + + this.QueryStringValues = QueryHelpers + .ParseQuery(this.ViewContext.HttpContext.Request.QueryString.Value) + .ToImmutableDictionary() + .Remove(this.PageNumberKey); + + paginationList.InnerHtml.AppendHtml(this.CreatePreviousPageItem()); + + foreach (int i in Enumerable.Range(1, this.Results.LastPageNumber)) + { + paginationList.InnerHtml.AppendHtml(this.CreatePageItem(i, i.ToString())); + } + + paginationList.InnerHtml.AppendHtml(this.CreateNextPageItem()); + + output.Content.AppendHtml(paginationList); + } + + private TagBuilder CreatePageItem(int pageNumber, string linkText) + { + var listItem = new TagBuilder("li"); + var link = new TagBuilder("a"); + + if (pageNumber == this.Results.PageNumber) + link.AddCssClass("currentPage"); + + link.InnerHtml.AppendHtml(linkText); + + // Leave off the number for page 1 + var queryStringWithPageNumber = pageNumber == 1 + ? this.QueryStringValues + : this.QueryStringValues.Add(this.PageNumberKey, pageNumber.ToString()); + + link.Attributes.Add("href", QueryHelpers.AddQueryString( + this.CurrentPagePath, queryStringWithPageNumber)); + + listItem.InnerHtml.AppendHtml(link); + return listItem; + } + + private TagBuilder CreatePreviousPageItem() + { + var listItem = new TagBuilder("li"); + var link = new TagBuilder("a"); + + if (this.Results.IsFirstPage) + link.AddCssClass("disabled"); + + // Leave off the number for page 1 + var queryStringWithPageNumber = this.Results.PreviousPageNumber == 1 + ? this.QueryStringValues + : this.QueryStringValues + .Add(this.PageNumberKey, this.Results.PreviousPageNumber.ToString()); + + link.Attributes.Add("href", QueryHelpers.AddQueryString( + this.CurrentPagePath, queryStringWithPageNumber)); + + link.InnerHtml.AppendHtml("«"); + + listItem.InnerHtml.AppendHtml(link); + return listItem; + } + + private TagBuilder CreateNextPageItem() + { + var listItem = new TagBuilder("li"); + var link = new TagBuilder("a"); + + if (this.Results.IsLastPage) + link.AddCssClass("disabled"); + + // Leave off the number for page 1 + var queryStringWithPageNumber = this.Results.NextPageNumber == 1 + ? this.QueryStringValues + : this.QueryStringValues.Add(this.PageNumberKey, this.Results.NextPageNumber.ToString()); + + link.Attributes.Add("href", QueryHelpers.AddQueryString( + this.CurrentPagePath, queryStringWithPageNumber)); + + link.InnerHtml.AppendHtml("»"); + + listItem.InnerHtml.AppendHtml(link); + return listItem; + } +} + +public static class PaginatedResultsExtensions +{ + /// Razor doesn't like casting, so use this instead + public static PaginatedResults ToObjectPaginatedResults( + this PaginatedResults paginatedResults) where T : class + { + return new PaginatedResults(paginatedResults.PageNumber, + paginatedResults.ResultsPerPage, paginatedResults.Results, + paginatedResults.TotalResultsCount); + } +} +``` + +And some CSS for the `pager-details`: + +```css +.paginationDetails { + display: flex; + flex-wrap: wrap; + row-gap: 0.25em; + column-gap: 1em; + margin-bottom: 1em; +} + +.paginationDetails:first-child { + margin-right: auto; +} +```