Add Pagination Tag Helpers post
This commit is contained in:
		
							parent
							
								
									7db728e7da
								
							
						
					
					
						commit
						1c5c7f06be
					
				
							
								
								
									
										215
									
								
								src/posts/PaginationTagHelpers.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										215
									
								
								src/posts/PaginationTagHelpers.md
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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
 | 
				
			||||||
 | 
					<pager-details results="this.Model.MyResults.ToObjectPaginatedResults()" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@foreach (var result in this.Model.MyResults.Results)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						...
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<pager results="@this.Model.Accounts.ToObjectPaginatedResults()"
 | 
				
			||||||
 | 
						page-number-key="@nameof(this.Model.PageNumber)" />
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// <summary>
 | 
				
			||||||
 | 
					/// Displays details about pagination in the form "Showing results x - y of z" on the left and
 | 
				
			||||||
 | 
					/// "Page x of y" on the right
 | 
				
			||||||
 | 
					/// </summary>
 | 
				
			||||||
 | 
					public class PagerDetailsTagHelper : TagHelper
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						public PaginatedResults<object> 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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// <summary>
 | 
				
			||||||
 | 
					/// 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"/>.
 | 
				
			||||||
 | 
					/// </summary>
 | 
				
			||||||
 | 
					/// <remarks>
 | 
				
			||||||
 | 
					/// Page 1 is assumed to be the default and the page number parameter will be excluded
 | 
				
			||||||
 | 
					/// </remarks>
 | 
				
			||||||
 | 
					public class PagerTagHelper : TagHelper
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						/// <summary>The name of the page number parameter in the query string</summary>
 | 
				
			||||||
 | 
						public string PageNumberKey { get; set; }
 | 
				
			||||||
 | 
						public PaginatedResults<object> Results { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						[ViewContext, HtmlAttributeNotBound]
 | 
				
			||||||
 | 
						public ViewContext ViewContext { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private string CurrentPagePath { get => this.ViewContext.HttpContext.Request.Path; }
 | 
				
			||||||
 | 
						private ImmutableDictionary<string, StringValues> 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
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						/// <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);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
		Loading…
	
		Reference in a new issue