NOTE: Apart from
(and even then it's questionable, I'm Scottish). These are machine translated in languages I don't read. If they're terrible please contact me.
You can see how this translation was done in this article.
Monday, 17 March 2025
//Less than a minute
Als Teil meines fortlaufenden Salbeis mit meinem Paging-Tag-Helfer habe ich nun PageSize in seinen eigenen Tag-Helfer getrennt. Dies soll den Tag-Helfer flexibler machen und komplexere Szenarien ermöglichen.
Wieder einmal, das sind alles Arbeiten-in-Fortschritt. Ich bin schon da. bis zu 0,9,0 zum Zeitpunkt des Schreibens, aber nutzen Sie auf Ihrem OWN RISK. Es ist kostenlos, aber NICHT YET von Release-Qualität IMHO (Ich habe nicht einmal Vollproben noch!)
Siehe Code für den Tag-Helfer hier.
Wie bei meinen anderen Taghelpern ist dies für einen Use-Case konzipiert, auf den ich oft stoße; wie man einfach die Seitengröße in Ergebnislisten wechseln kann. Auf den ersten Blick scheint dies ziemlich einfach, aber es kann TRICKY werden, sobald Sie HTMX in den Mix hinzufügen.
So für meinen Anwendungsfall die Anforderungen waren die folgenden
In diesem Sinne habe ich den TagHelper wie folgt gestaltet:
<page-size
hx-target="#list"
hx-swap="innerHTML"
model="Model">
</page-size>
<div id="list">
<partial name="_ResultsList" model="Model"/>
</div>
Also in diesem Fall wieder mein Standard-Paging-Modell:
public interface IPagingModel
{
public int Page { get; set; }
public int TotalItems { get; set; }
public int PageSize { get; set; }
public ViewType ViewType { get; set; }
public string LinkUrl { get; set; }
}
Das stellt die Page
, TotalItems
, PageSize
, ViewType
und LinkUrl
für die Vorführung.
Sie können diese auch in Form von Attributen an das Modell spezifizieren:
<page-size
hx-target="#list"
hx-swap="innerHTML"
total-items="100"
page-size="10"
view-type="DaisyAndTailwind">
</page-size>
Wobei die hx-target
hier ist das Ziel für die HTMX-Anfrage und die hx-swap
ist das Ziel für die Antwort.
In diesem Fall wird es die angegebene PageSize
und TotalItems
und ViewType
um die Auswahl der Seitengröße zu rendern.
Leider hatte ich diesmal eine Anforderung, die ich nicht mit rein Server-Seite umgehen konnte. Nämlich wollte ich Query-String-Paramater in der Pagesize-Änderungsanfrage beibehalten. Um dies zu erreichen, habe ich zwei Teile von JS, die in die Steuerung je nachdem, HTMX verwendet wird laden ooder nicht
@if (Model.UseHtmx)
{
@Html.HTMXPageSizeChange()
}
else
{
@Html.PageSizeOnchangeSnippet()
}
Wenn HTMX verwendet wird, wird dies in die Seite gerendert:
(() => {
if (window.__pageSizeListenerAdded) return;
document.addEventListener('htmx:configRequest', event => {
const { elt } = event.detail;
if (elt?.matches('[name="pageSize"]')) {
const params = new URLSearchParams(window.location.search);
params.set('pageSize', elt.value); // This will update or add the pageSize param
event.detail.parameters = Object.fromEntries(params.entries());
}
});
window.__pageSizeListenerAdded = true;
})();
Das ist ein einfaches Stück JavaScript, das aktuelle Querystring-Parameter in die Anfrage injiziert.
Wenn HTMX NICHT aktiviert ist, wird dies auch rendern:
(function attachPageSizeListener() {
// Ensure we only attach once if this script is included multiple times
if (window.__pageSizeListenerAttached) return;
window.__pageSizeListenerAttached = true;
// Wait for the DOM to be fully loaded
document.addEventListener("DOMContentLoaded", () => {
document.body.addEventListener("change", handlePageSizeChange);
});
function handlePageSizeChange(event) {
// Check if the changed element is a page-size <select> inside .page-size-container
const select = event.target.closest(".page-size-container select[name='pageSize']");
if (!select) return;
const container = select.closest(".page-size-container");
if (!container) return;
// Default to "true" if there's no .useHtmx input
const useHtmx = container.querySelector("input.useHtmx")?.value === "true";
if (useHtmx) {
// If using HTMX, we do nothing—HTMX will handle the request
return;
}
// Either use a linkUrl from the container or the current page URL
const linkUrl = container.querySelector("input.linkUrl")?.value || window.location.href;
const url = new URL(linkUrl, window.location.origin);
// Copy existing query params from current location
const existingParams = new URLSearchParams(window.location.search);
for (const [key, value] of existingParams.entries()) {
url.searchParams.set(key, value);
}
// If user picked the same pageSize as what's already in the URL, do nothing
if (url.searchParams.get("pageSize") === select.value) {
return;
}
// Update the pageSize param
url.searchParams.set("pageSize", select.value);
// Redirect
window.location.href = url.toString();
}
})();
Eine Änderung, die ich lilely machen werde, ist die Änderung des Namens der 'Flag'-Variable; __pageSizeListenerAttached
da es ein bisschen zu generisch ist UND ich es sowohl für HTMX- als auch für Nicht-HTMX-Anfragen verwende.
Der Tag-Helfer selbst ist ziemlich einfach, der Hauptcode bevölkert nur ein paar Eigenschaften und macht dann die View Component.
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
// We want to render a <div> by default
output.TagName = "div";
// Remove any leftover attributes that we handle ourselves
RemoveUnwantedAttributes(output);
// Determine final PagerId
var pagerId = PagerId ?? PageSizeModel?.PagerId ?? $"pager-{Guid.NewGuid():N}";
// Assign ID to the outer div
output.Attributes.SetAttribute("id", pagerId);
// Build or fallback to a link URL
var finalLinkUrl = BuildLinkUrl();
if (string.IsNullOrEmpty(finalLinkUrl))
{
// If we can't build a URL, show a fallback message or short-circuit
output.Content.SetHtmlContent(
"<p style=\"color:red\">No valid link URL found for PageSize control.</p>");
return;
}
var finalPageSize = PageSize ?? PageSizeModel?.PageSize ?? 10;
// Clamp the page size to MaxPageSize
var finalTotalItems = TotalItems ?? Model?.TotalItems ?? PageSizeModel?.TotalItems ?? 0;
if (finalTotalItems == 0) throw new ArgumentNullException(nameof(finalTotalItems), "TotalItems is required");
var maxPageSize = Math.Min(finalTotalItems, MaxPageSize);
// Fallback to model's properties if not set
var finalViewType = PageSizeModel?.ViewType ?? Model?.ViewType ?? ViewType;
var pageSizeSteps = new int[] { 10, 25, 50, 75, 100, 125, 150, 200, 250, 500, 1000 };
if (!string.IsNullOrEmpty(PageSizeSteps))
pageSizeSteps = PageSizeSteps.Split(',').Select(s =>
{
if (int.TryParse(s, out var result))
return result;
else
{
throw new ArgumentException("Invalid page size step", nameof(PageSizeSteps));
}
}).ToArray();
var pageSizes = CalculatePageSizes(finalTotalItems, maxPageSize, pageSizeSteps);
var useHtmx = PageSizeModel?.UseHtmx ?? UseHtmx;
var useLocalView = PageSizeModel?.UseLocalView ?? UseLocalView;
// Acquire the IViewComponentHelper
var viewComponentHelper = ViewContext.HttpContext.RequestServices
.GetRequiredService<IViewComponentHelper>();
((IViewContextAware)viewComponentHelper).Contextualize(ViewContext);
// Construct the PageSizeModel (if not provided) with final settings
var pagerModel = new PageSizeModel
{
ViewType = finalViewType,
UseLocalView = useLocalView,
UseHtmx = useHtmx,
PageSizes = pageSizes,
PagerId = pagerId,
Model = Model,
LinkUrl = finalLinkUrl,
MaxPageSize = maxPageSize,
PageSize = finalPageSize,
TotalItems = finalTotalItems
};
// Safely invoke the "PageSize" view component
try
{
var result = await viewComponentHelper.InvokeAsync("PageSize", pagerModel);
output.Content.SetHtmlContent(result);
}
catch (Exception ex)
{
// Optional: Log or display an error
output.Content.SetHtmlContent(
$"<p class=\"text-red-500\">Failed to render PageSize component: {ex.Message}</p>"
);
}
}
Ein kniffliges Stück ist der richtige Umgang mit der PageSizeSteps
Attribut. Dies ist eine durch Komma getrennte Liste der Seitengrößen, aus denen der Benutzer auswählen kann. ODER Sie können in Ihren eigenen Schritten passieren. Sie können auch die MaxPageSize angeben, die die maximale Seitengröße ist, die ausgewählt werden kann.
private List<int> CalculatePageSizes(int totalItems, int maxxPageSize, int[] pageSizeSteps)
{
var pageSizes = new List<int>();
// 1. Include all fixed steps up to both TotalItems and MaxPageSize
foreach (var step in pageSizeSteps)
{
if (step <= totalItems && step <= maxxPageSize)
pageSizes.Add(step);
}
// 2. If TotalItems exceeds the largest fixed step, keep doubling
int lastFixedStep = pageSizeSteps.Last();
if (totalItems > lastFixedStep)
{
int next = lastFixedStep;
while (next < totalItems && next < maxxPageSize)
{
next *= 2; // double the step
if (next <= totalItems && next <= maxxPageSize)
{
pageSizes.Add(next);
}
}
}
// 3. Include TotalItems if it's not already in the list
if (totalItems <= maxxPageSize && !pageSizes.Contains(totalItems))
{
pageSizes.Add(totalItems);
}
pageSizes.Sort();
return pageSizes;
}
Wie bei meinem anderen Tag-Helfer benutze ich das TagHelper->ViewComponent-Muster, um die View-Komponente zu rendern. Dies ermöglicht es mir, den Tag Helper einfach zu halten und den View-Komponente-Komplex. Es erlaubt Ihnen auch, Ansichten für unterschiedliche Erfahrungen zu ändern; oder sogar Ihre eigene Ansicht injizieren und verwenden, dass (mit use-local-view
).
public class PageSizeViewComponent : ViewComponent
{
public IViewComponentResult Invoke(PageSizeModel model)
{
if(model.Model != null)
{
model.LinkUrl ??= model.Model.LinkUrl;
if(model.Model is IPagingSearchModel searchModel)
{
model.SearchTerm ??= searchModel.SearchTerm;
}
}
var viewName = "Components/Pager/Default";
var useLocalView = model.UseLocalView;
return (useLocalView, model.ViewType) switch
{
(true, ViewType.Custom) when ViewExists(viewName) => View(viewName, model),
(true, ViewType.Custom) when !ViewExists(viewName) => throw new ArgumentException("View not found: " + viewName),
(false, ViewType.Bootstrap) => View("/Areas/Components/Views/PageSize/BootstrapView.cshtml", model),
(false, ViewType.Plain) => View("/Areas/Components/Views/PageSize/PlainView.cshtml", model),
(false, ViewType.TailwindAndDaisy) => View("/Areas/Components/Views/PageSize/Default.cshtml", model),
_ => View("/Areas/Components/Views/PageSize/Default.cshtml", model)
};
// If the view exists in the app, use it; otherwise, use the fallback RCL view
}
/// <summary>
/// Checks if a view exists in the consuming application.
/// </summary>
private bool ViewExists(string viewName)
{
var result = ViewEngine.FindView(ViewContext, viewName, false);
return result.Success;
}
}
Nun, das war's für den PageSize-Tag-Helfer. Ich arbeite immer noch an der Dokumentation und an Beispielen, aber der Code wird täglich besser getigt.