diff --git a/src/posts/CsPaginationTools.md b/src/posts/CsPaginationTools.md index e761428..8a40355 100644 --- a/src/posts/CsPaginationTools.md +++ b/src/posts/CsPaginationTools.md @@ -32,21 +32,11 @@ namespace ASIS.Shared; [DataContract(IsReference = true)] public class PaginationOptions { - /// - /// This constructor exists so that MVC can instantiate the object before mapping is contents - /// - public PaginationOptions() { } - - public PaginationOptions(int pageNumber, int? resultsPerPage) - { - this.PageNumber = pageNumber; - this.ResultsPerPage = resultsPerPage; - } - private int? _resultsPerPage; private int _pageNumber; /// The number of results per page + /// null for unlimited [DataMember] public int? ResultsPerPage { @@ -61,9 +51,7 @@ public class PaginationOptions } } - /// - /// The page number to get - /// + /// The page number to get /// One indexed [DataMember] public int PageNumber @@ -78,8 +66,39 @@ public class PaginationOptions this._pageNumber = value; } } + + /// + /// This constructor exists so that MVC can instantiate the object before mapping is contents + /// + public PaginationOptions() { } + + public PaginationOptions(int pageNumber, int? resultsPerPage) + { + this.PageNumber = pageNumber; + this.ResultsPerPage = resultsPerPage; + } } +/// +/// This exists because s can't be generic, so passing them an actual +/// wouldn't work +/// +public interface IPaginatedResults +{ + public int PageNumber { get; } + public int LastPageNumber { get; } + public int? ResultsPerPage { get; } + public int CurrentPageResultsCount { get; } + public int TotalResultsCount { get; } + public int FirstResultNumber { get; } + public int LastResultNumber { get; } + public bool IsFirstPage { get; } + public bool IsLastPage { get; } + public int PreviousPageNumber { get; } + public int NextPageNumber { get; } +} + + /// /// An extension of that contains the results from a query and uses /// them to calculate additional pagination info @@ -89,7 +108,7 @@ public class PaginationOptions /// /// The data type of the results [DataContract(IsReference = true)] -public class PaginatedResults : PaginationOptions where T : class +public class PaginatedResults : PaginationOptions, IPaginatedResults where T : class { [JsonConstructor] public PaginatedResults(int pageNumber, int? resultsPerPage, IEnumerable results, @@ -118,21 +137,21 @@ public class PaginatedResults : PaginationOptions where T : class /// The number of results on the current page /// [DataMember] - public int? CurrentPageResultsCount { get => this.Results.Count(); } + public int CurrentPageResultsCount { get => this.Results.Count(); } /// /// The number of the first result on the current page. This is calculated using /// , , and . /// [DataMember] - public int? FirstResultNumber + public int FirstResultNumber { get { if (this.ResultsPerPage is null) return 1; - return (this.ResultsPerPage * (this.PageNumber - 1)) + 1; + return ((int)this.ResultsPerPage * (this.PageNumber - 1)) + 1; } } @@ -141,14 +160,14 @@ public class PaginatedResults : PaginationOptions where T : class /// , , and . /// [DataMember] - public int? LastResultNumber + public int LastResultNumber { get { if (this.ResultsPerPage is null) return this.Results.Count(); - return (this.ResultsPerPage * (this.PageNumber - 1)) + this.CurrentPageResultsCount; + return ((int)this.ResultsPerPage * (this.PageNumber - 1)) + this.CurrentPageResultsCount; } } @@ -157,15 +176,14 @@ public class PaginatedResults : PaginationOptions where T : class /// /// [DataMember] - public int? LastPageNumber + public int LastPageNumber { get { if (this.ResultsPerPage is null) return 1; - return (int)Math.Ceiling(this.TotalResultsCount - / (double)this.ResultsPerPage); + return (int)Math.Ceiling(this.TotalResultsCount / (double)this.ResultsPerPage); } } diff --git a/src/posts/PaginationTagHelpers.md b/src/posts/PaginationTagHelpers.md index 6ccab03..716d4a1 100644 --- a/src/posts/PaginationTagHelpers.md +++ b/src/posts/PaginationTagHelpers.md @@ -27,28 +27,36 @@ The query parameter given as `page-number-key` will be changed or added to each The `pager-details` tag helper: ```csharp -using ASIS.SecretsManagement.Shared; +using ASIS.Shared; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.TagHelpers; using Microsoft.AspNetCore.Razor.TagHelpers; using System.Text.Encodings.Web; +namespace ASIS.Shared.TagHelpers; + /// /// 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 class PaginationDetailsTagHelper : TagHelper { - public PaginatedResults Results { get; set; } + public required IPaginatedResults Results { get; set; } + + /// + /// The name for the item to be displayed, e.g. results. This will be used in the format + /// "Showing results 1-10 of 100". + /// + public string ResultName { get; set; } = "results"; public override void Process(TagHelperContext context, TagHelperOutput output) { output.TagName = "div"; output.TagMode = TagMode.StartTagAndEndTag; - output.AddClass("paginationDetails", HtmlEncoder.Default); + output.AddClass("pagination-details", HtmlEncoder.Default); TagBuilder resultsNumDetails = new("span"); - resultsNumDetails.InnerHtml.Append("Showing results " + resultsNumDetails.InnerHtml.Append($"Showing {this.ResultName} " + $"{this.Results.FirstResultNumber} - {this.Results.LastResultNumber} " + $"of {this.Results.TotalResultsCount}"); output.Content.AppendHtml(resultsNumDetails); @@ -64,7 +72,6 @@ public class PagerDetailsTagHelper : TagHelper 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; @@ -72,6 +79,8 @@ using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Primitives; using System.Collections.Immutable; +namespace ASIS.Shared.TagHelpers; + /// /// 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 . @@ -79,56 +88,64 @@ using System.Collections.Immutable; /// /// Page 1 is assumed to be the default and the page number parameter will be excluded /// -public class PagerTagHelper : TagHelper +public class PaginationTagHelper : TagHelper { + public required IPaginatedResults Results { get; set; } + /// The name of the page number parameter in the query string - public string PageNumberKey { get; set; } - public PaginatedResults Results { get; set; } + public required string PageNumberKey { get; set; } [ViewContext, HtmlAttributeNotBound] - public ViewContext ViewContext { get; set; } + public required ViewContext ViewContext { get; set; } - private string CurrentPagePath { get => this.ViewContext.HttpContext.Request.Path; } + private string CurrentPagePath + { + get => this.ViewContext.HttpContext.Request.PathBase + + this.ViewContext.HttpContext.Request.Path; + } private ImmutableDictionary QueryStringValues { get; set; } + = ImmutableDictionary.Create(); public override void Process(TagHelperContext context, TagHelperOutput output) { output.TagName = "nav"; output.TagMode = TagMode.StartTagAndEndTag; + output.AddClass("wsu-pagination", HtmlEncoder.Default); output.Attributes.Add("aria-label", "pagination"); - var paginationList = new TagBuilder("ul"); - paginationList.AddCssClass("pagination"); + TagBuilder paginationList = new("ul"); + paginationList.AddCssClass("wsu-pagination__menu"); 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(this.CreatePreviousPageItem()); output.Content.AppendHtml(paginationList); + output.Content.AppendHtml(this.CreateNextPageItem()); } private TagBuilder CreatePageItem(int pageNumber, string linkText) { - var listItem = new TagBuilder("li"); - var link = new TagBuilder("a"); + TagBuilder listItem = new("li"); + listItem.AddCssClass("wsu-pagination__menu-page"); + + TagBuilder link = new("a"); + link.Attributes.Add("aria-label", $"Goto Page {pageNumber}"); if (pageNumber == this.Results.PageNumber) - link.AddCssClass("currentPage"); + link.Attributes.Add("aria-current", "true"); link.InnerHtml.AppendHtml(linkText); // Leave off the number for page 1 - var queryStringWithPageNumber = pageNumber == 1 + ImmutableDictionary queryStringWithPageNumber = pageNumber == 1 ? this.QueryStringValues : this.QueryStringValues.Add(this.PageNumberKey, pageNumber.ToString()); @@ -141,59 +158,54 @@ public class PagerTagHelper : TagHelper private TagBuilder CreatePreviousPageItem() { - var listItem = new TagBuilder("li"); - var link = new TagBuilder("a"); + TagBuilder link = new("a"); + link.AddCssClass("wsu-pagination__previous"); + link.AddCssClass("wsu-button"); + link.AddCssClass("wsu-button--style-outline"); + link.Attributes.Add("aria-label", "Goto Previous Page"); if (this.Results.IsFirstPage) - link.AddCssClass("disabled"); + link.Attributes.Add("disabled", "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()); + ImmutableDictionary 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("«"); + link.InnerHtml.AppendHtml("Previous"); - listItem.InnerHtml.AppendHtml(link); - return listItem; + return link; } private TagBuilder CreateNextPageItem() { - var listItem = new TagBuilder("li"); - var link = new TagBuilder("a"); + TagBuilder link = new("a"); + link.AddCssClass("wsu-pagination__next"); + link.AddCssClass("wsu-button"); + link.AddCssClass("wsu-button--style-outline"); + link.Attributes.Add("aria-label", "Goto Next Page"); if (this.Results.IsLastPage) - link.AddCssClass("disabled"); + link.Attributes.Add("disabled", "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()); + ImmutableDictionary 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.Attributes.Add("href", QueryHelpers + .AddQueryString(this.CurrentPagePath, queryStringWithPageNumber)); - link.InnerHtml.AppendHtml("»"); + link.InnerHtml.AppendHtml("Next"); - 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); + return link; } } ```