Update pagination posts

This commit is contained in:
Neil Brommer 2024-06-05 11:42:31 -07:00
parent f7f8399b63
commit 25f32cefbb
2 changed files with 107 additions and 77 deletions

View file

@ -32,21 +32,11 @@ namespace ASIS.Shared;
[DataContract(IsReference = true)] [DataContract(IsReference = true)]
public class PaginationOptions public class PaginationOptions
{ {
/// <summary>
/// This constructor exists so that MVC can instantiate the object before mapping is contents
/// </summary>
public PaginationOptions() { }
public PaginationOptions(int pageNumber, int? resultsPerPage)
{
this.PageNumber = pageNumber;
this.ResultsPerPage = resultsPerPage;
}
private int? _resultsPerPage; private int? _resultsPerPage;
private int _pageNumber; private int _pageNumber;
/// <summary>The number of results per page</summary> /// <summary>The number of results per page</summary>
/// <remarks><c>null</c> for unlimited</remarks>
[DataMember] [DataMember]
public int? ResultsPerPage public int? ResultsPerPage
{ {
@ -61,9 +51,7 @@ public class PaginationOptions
} }
} }
/// <summary> /// <summary>The page number to get</summary>
/// The page number to get
/// </summary>
/// <remarks>One indexed</remarks> /// <remarks>One indexed</remarks>
[DataMember] [DataMember]
public int PageNumber public int PageNumber
@ -78,7 +66,38 @@ public class PaginationOptions
this._pageNumber = value; this._pageNumber = value;
} }
} }
/// <summary>
/// This constructor exists so that MVC can instantiate the object before mapping is contents
/// </summary>
public PaginationOptions() { }
public PaginationOptions(int pageNumber, int? resultsPerPage)
{
this.PageNumber = pageNumber;
this.ResultsPerPage = resultsPerPage;
} }
}
/// <summary>
/// This exists because <see cref="TagHelper"/>s can't be generic, so passing them an actual
/// <see cref="PaginatedResults{T}"/> wouldn't work
/// </summary>
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; }
}
/// <summary> /// <summary>
/// An extension of <see cref="PaginationOptions"/> that contains the results from a query and uses /// An extension of <see cref="PaginationOptions"/> that contains the results from a query and uses
@ -89,7 +108,7 @@ public class PaginationOptions
/// </typeparam> /// </typeparam>
/// <typeparam name="U">The data type of the results</typeparam> /// <typeparam name="U">The data type of the results</typeparam>
[DataContract(IsReference = true)] [DataContract(IsReference = true)]
public class PaginatedResults<T> : PaginationOptions where T : class public class PaginatedResults<T> : PaginationOptions, IPaginatedResults where T : class
{ {
[JsonConstructor] [JsonConstructor]
public PaginatedResults(int pageNumber, int? resultsPerPage, IEnumerable<T> results, public PaginatedResults(int pageNumber, int? resultsPerPage, IEnumerable<T> results,
@ -118,21 +137,21 @@ public class PaginatedResults<T> : PaginationOptions where T : class
/// The number of results on the current page /// The number of results on the current page
/// </summary> /// </summary>
[DataMember] [DataMember]
public int? CurrentPageResultsCount { get => this.Results.Count(); } public int CurrentPageResultsCount { get => this.Results.Count(); }
/// <summary> /// <summary>
/// The number of the first result on the current page. This is calculated using /// The number of the first result on the current page. This is calculated using
/// <see cref="ResultsPerPage"/>, <see cref="PageNumber"/>, and <see cref="TotalResults"/>. /// <see cref="ResultsPerPage"/>, <see cref="PageNumber"/>, and <see cref="TotalResults"/>.
/// </summary> /// </summary>
[DataMember] [DataMember]
public int? FirstResultNumber public int FirstResultNumber
{ {
get get
{ {
if (this.ResultsPerPage is null) if (this.ResultsPerPage is null)
return 1; return 1;
return (this.ResultsPerPage * (this.PageNumber - 1)) + 1; return ((int)this.ResultsPerPage * (this.PageNumber - 1)) + 1;
} }
} }
@ -141,14 +160,14 @@ public class PaginatedResults<T> : PaginationOptions where T : class
/// <see cref="ResultsPerPage"/>, <see cref="PageNumber"/>, and <see cref="TotalResults"/>. /// <see cref="ResultsPerPage"/>, <see cref="PageNumber"/>, and <see cref="TotalResults"/>.
/// </summary> /// </summary>
[DataMember] [DataMember]
public int? LastResultNumber public int LastResultNumber
{ {
get get
{ {
if (this.ResultsPerPage is null) if (this.ResultsPerPage is null)
return this.Results.Count(); 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<T> : PaginationOptions where T : class
/// <see cref="TotalResultsCount"/> /// <see cref="TotalResultsCount"/>
/// </summary> /// </summary>
[DataMember] [DataMember]
public int? LastPageNumber public int LastPageNumber
{ {
get get
{ {
if (this.ResultsPerPage is null) if (this.ResultsPerPage is null)
return 1; return 1;
return (int)Math.Ceiling(this.TotalResultsCount return (int)Math.Ceiling(this.TotalResultsCount / (double)this.ResultsPerPage);
/ (double)this.ResultsPerPage);
} }
} }

View file

@ -27,28 +27,36 @@ The query parameter given as `page-number-key` will be changed or added to each
The `pager-details` tag helper: The `pager-details` tag helper:
```csharp ```csharp
using ASIS.SecretsManagement.Shared; using ASIS.Shared;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.TagHelpers; using Microsoft.AspNetCore.Mvc.TagHelpers;
using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.AspNetCore.Razor.TagHelpers;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
namespace ASIS.Shared.TagHelpers;
/// <summary> /// <summary>
/// Displays details about pagination in the form "Showing results x - y of z" on the left and /// Displays details about pagination in the form "Showing results x - y of z" on the left and
/// "Page x of y" on the right /// "Page x of y" on the right
/// </summary> /// </summary>
public class PagerDetailsTagHelper : TagHelper public class PaginationDetailsTagHelper : TagHelper
{ {
public PaginatedResults<object> Results { get; set; } public required IPaginatedResults Results { get; set; }
/// <summary>
/// The name for the item to be displayed, e.g. <c>results</c>. This will be used in the format
/// "Showing <c>results</c> 1-10 of 100".
/// </summary>
public string ResultName { get; set; } = "results";
public override void Process(TagHelperContext context, TagHelperOutput output) public override void Process(TagHelperContext context, TagHelperOutput output)
{ {
output.TagName = "div"; output.TagName = "div";
output.TagMode = TagMode.StartTagAndEndTag; output.TagMode = TagMode.StartTagAndEndTag;
output.AddClass("paginationDetails", HtmlEncoder.Default); output.AddClass("pagination-details", HtmlEncoder.Default);
TagBuilder resultsNumDetails = new("span"); TagBuilder resultsNumDetails = new("span");
resultsNumDetails.InnerHtml.Append("Showing results " resultsNumDetails.InnerHtml.Append($"Showing {this.ResultName} "
+ $"{this.Results.FirstResultNumber} - {this.Results.LastResultNumber} " + $"{this.Results.FirstResultNumber} - {this.Results.LastResultNumber} "
+ $"of {this.Results.TotalResultsCount}"); + $"of {this.Results.TotalResultsCount}");
output.Content.AppendHtml(resultsNumDetails); output.Content.AppendHtml(resultsNumDetails);
@ -64,7 +72,6 @@ public class PagerDetailsTagHelper : TagHelper
And the `pager` tag helper: And the `pager` tag helper:
```csharp ```csharp
using ASIS.SecretsManagement.Shared;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.AspNetCore.Razor.TagHelpers;
@ -72,6 +79,8 @@ using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
using System.Collections.Immutable; using System.Collections.Immutable;
namespace ASIS.Shared.TagHelpers;
/// <summary> /// <summary>
/// Creates a list of links to pages for the current list of data. This will replace the page number /// 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 <see cref="PageNumberKey"/>. /// parameter in the query string using the <see cref="PageNumberKey"/>.
@ -79,56 +88,64 @@ using System.Collections.Immutable;
/// <remarks> /// <remarks>
/// Page 1 is assumed to be the default and the page number parameter will be excluded /// Page 1 is assumed to be the default and the page number parameter will be excluded
/// </remarks> /// </remarks>
public class PagerTagHelper : TagHelper public class PaginationTagHelper : TagHelper
{ {
public required IPaginatedResults Results { get; set; }
/// <summary>The name of the page number parameter in the query string</summary> /// <summary>The name of the page number parameter in the query string</summary>
public string PageNumberKey { get; set; } public required string PageNumberKey { get; set; }
public PaginatedResults<object> Results { get; set; }
[ViewContext, HtmlAttributeNotBound] [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<string, StringValues> QueryStringValues { get; set; } private ImmutableDictionary<string, StringValues> QueryStringValues { get; set; }
= ImmutableDictionary.Create<string, StringValues>();
public override void Process(TagHelperContext context, TagHelperOutput output) public override void Process(TagHelperContext context, TagHelperOutput output)
{ {
output.TagName = "nav"; output.TagName = "nav";
output.TagMode = TagMode.StartTagAndEndTag; output.TagMode = TagMode.StartTagAndEndTag;
output.AddClass("wsu-pagination", HtmlEncoder.Default);
output.Attributes.Add("aria-label", "pagination"); output.Attributes.Add("aria-label", "pagination");
var paginationList = new TagBuilder("ul"); TagBuilder paginationList = new("ul");
paginationList.AddCssClass("pagination"); paginationList.AddCssClass("wsu-pagination__menu");
this.QueryStringValues = QueryHelpers this.QueryStringValues = QueryHelpers
.ParseQuery(this.ViewContext.HttpContext.Request.QueryString.Value) .ParseQuery(this.ViewContext.HttpContext.Request.QueryString.Value)
.ToImmutableDictionary() .ToImmutableDictionary()
.Remove(this.PageNumberKey); .Remove(this.PageNumberKey);
paginationList.InnerHtml.AppendHtml(this.CreatePreviousPageItem());
foreach (int i in Enumerable.Range(1, this.Results.LastPageNumber)) foreach (int i in Enumerable.Range(1, this.Results.LastPageNumber))
{ {
paginationList.InnerHtml.AppendHtml(this.CreatePageItem(i, i.ToString())); 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(paginationList);
output.Content.AppendHtml(this.CreateNextPageItem());
} }
private TagBuilder CreatePageItem(int pageNumber, string linkText) private TagBuilder CreatePageItem(int pageNumber, string linkText)
{ {
var listItem = new TagBuilder("li"); TagBuilder listItem = new("li");
var link = new TagBuilder("a"); listItem.AddCssClass("wsu-pagination__menu-page");
TagBuilder link = new("a");
link.Attributes.Add("aria-label", $"Goto Page {pageNumber}");
if (pageNumber == this.Results.PageNumber) if (pageNumber == this.Results.PageNumber)
link.AddCssClass("currentPage"); link.Attributes.Add("aria-current", "true");
link.InnerHtml.AppendHtml(linkText); link.InnerHtml.AppendHtml(linkText);
// Leave off the number for page 1 // Leave off the number for page 1
var queryStringWithPageNumber = pageNumber == 1 ImmutableDictionary<string, StringValues> queryStringWithPageNumber = pageNumber == 1
? this.QueryStringValues ? this.QueryStringValues
: this.QueryStringValues.Add(this.PageNumberKey, pageNumber.ToString()); : this.QueryStringValues.Add(this.PageNumberKey, pageNumber.ToString());
@ -141,14 +158,18 @@ public class PagerTagHelper : TagHelper
private TagBuilder CreatePreviousPageItem() private TagBuilder CreatePreviousPageItem()
{ {
var listItem = new TagBuilder("li"); TagBuilder link = new("a");
var link = new TagBuilder("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) if (this.Results.IsFirstPage)
link.AddCssClass("disabled"); link.Attributes.Add("disabled", "disabled");
// Leave off the number for page 1 // Leave off the number for page 1
var queryStringWithPageNumber = this.Results.PreviousPageNumber == 1 ImmutableDictionary<string, StringValues> queryStringWithPageNumber =
this.Results.PreviousPageNumber == 1
? this.QueryStringValues ? this.QueryStringValues
: this.QueryStringValues : this.QueryStringValues
.Add(this.PageNumberKey, this.Results.PreviousPageNumber.ToString()); .Add(this.PageNumberKey, this.Results.PreviousPageNumber.ToString());
@ -156,44 +177,35 @@ public class PagerTagHelper : TagHelper
link.Attributes.Add("href", QueryHelpers.AddQueryString( link.Attributes.Add("href", QueryHelpers.AddQueryString(
this.CurrentPagePath, queryStringWithPageNumber)); this.CurrentPagePath, queryStringWithPageNumber));
link.InnerHtml.AppendHtml("«"); link.InnerHtml.AppendHtml("Previous");
listItem.InnerHtml.AppendHtml(link); return link;
return listItem;
} }
private TagBuilder CreateNextPageItem() private TagBuilder CreateNextPageItem()
{ {
var listItem = new TagBuilder("li"); TagBuilder link = new("a");
var link = new TagBuilder("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) if (this.Results.IsLastPage)
link.AddCssClass("disabled"); link.Attributes.Add("disabled", "disabled");
// Leave off the number for page 1 // Leave off the number for page 1
var queryStringWithPageNumber = this.Results.NextPageNumber == 1 ImmutableDictionary<string, StringValues> queryStringWithPageNumber =
this.Results.NextPageNumber == 1
? this.QueryStringValues ? this.QueryStringValues
: this.QueryStringValues.Add(this.PageNumberKey, this.Results.NextPageNumber.ToString()); : this.QueryStringValues
.Add(this.PageNumberKey, this.Results.NextPageNumber.ToString());
link.Attributes.Add("href", QueryHelpers.AddQueryString( link.Attributes.Add("href", QueryHelpers
this.CurrentPagePath, queryStringWithPageNumber)); .AddQueryString(this.CurrentPagePath, queryStringWithPageNumber));
link.InnerHtml.AppendHtml("»"); link.InnerHtml.AppendHtml("Next");
listItem.InnerHtml.AppendHtml(link); return link;
return listItem;
}
}
public static class PaginatedResultsExtensions
{
/// <summary>Razor doesn't like casting, so use this instead</summary>
public static PaginatedResults<object> ToObjectPaginatedResults<T>(
this PaginatedResults<T> paginatedResults) where T : class
{
return new PaginatedResults<object>(paginatedResults.PageNumber,
paginatedResults.ResultsPerPage, paginatedResults.Results,
paginatedResults.TotalResultsCount);
} }
} }
``` ```