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
Som en del av min pågående salvia med min personsöktagshjälpare har jag nu separerat PageSize till sin egen tagghjälpare. Detta för att göra tagghjälpen mer flexibel och för att möjliggöra mer komplexa personsökningsscenarier.
Återigen; allt detta är pågående arbeten. Jag är redan upp till 0,9.0 vid tidpunkten för skrivandet men använda på din egen risk. Det är gratis men INTE YET av release kvalitet IMHO (Jag har inte ens Fullständiga prover än!)
Se tabellen nedan. kod för tagghjälp här.
Som med mina andra taghelpers är detta utformat för en användning-fall jag ofta stöter på; hur man enkelt byta sidstorlek i resultatlistor. Vid första anblicken verkar detta ganska enkelt men det kan bli TRICKY när du lägger HTMX i blandningen.
Så för mitt användningsfall var kraven följande:
Med detta i åtanke utformade jag TagHelper att vara följande:
<page-size
hx-target="#list"
hx-swap="innerHTML"
model="Model">
</page-size>
<div id="list">
<partial name="_ResultsList" model="Model"/>
</div>
Så i det här fallet igen tar det min vanliga personsökning 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; }
}
Vilket ger Page
, TotalItems
, PageSize
, ViewType
och LinkUrl
för personsökningen.
Du kan också ange dessa i form av attribut till modellen:
<page-size
hx-target="#list"
hx-swap="innerHTML"
total-items="100"
page-size="10"
view-type="DaisyAndTailwind">
</page-size>
där hx-target
här är målet för HTMX begäran och hx-swap
är målet för svaret.
I det här fallet kommer den att använda den angivna PageSize
och TotalItems
och ViewType
för att göra sidstorleksväljaren.
Tyvärr hade jag den här gången ett krav som jag inte kunde hantera enbart serversidan. Nämligen jag ville bevara frågesträng paramater i sidstorlek ändra begäran. För att uppnå detta har jag två bitar av JS som kommer att laddas in i kontrollen beroende på att HTMX används oor inte
@if (Model.UseHtmx)
{
@Html.HTMXPageSizeChange()
}
else
{
@Html.PageSizeOnchangeSnippet()
}
Om HTMX används återger det detta på sidan:
(() => {
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;
})();
Vilket är en enkel del av JavaScript som kommer att injicera aktuella frågesträng parametrar i begäran.
Alternativt om HTMX inte är aktiverat kommer det att göra detta:
(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();
}
})();
En förändring jag kommer att göra är att ändra namnet på "flagga" variabeln; __pageSizeListenerAttached
eftersom det är lite för generiskt och jag använder det för både HTMX och icke-HTMX förfrågningar.
Tag-hjälparen själv är ganska enkel, huvudkoden bara befolkar några egenskaper och återger sedan visningskomponenten.
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>"
);
}
}
En knepig bit hanterar korrekt PageSizeSteps
Egenskap. Det här är en lista med sidstorlekar som användaren kan välja mellan. ELLER så kan du passera i dina egna steg. Du kan också ange MaxPageSize som är den maximala sidstorlek som kan väljas.
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;
}
Som med min andra tagghjälpare använder jag TagHelper->ViewKomponent-mönstret för att återge visningskomponenten. Detta gör att jag kan hålla tagghjälpen enkel och visningskomponenten komplex. Det tillåter dig också att ändra åsikter för olika erfarenheter; eller till och med att injicera din egen uppfattning och använda den (med hjälp av 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;
}
}
Det var allt för PageSize-tagg-hjälparen. Jag jobbar fortfarande på dokumentation och exempel men koden är bättre dagligen.