En sökvy komponent ASP.NET Core Tag Helper (Del 2, sidstorlek) (Svenska (Swedish))

En sökvy komponent ASP.NET Core Tag Helper (Del 2, sidstorlek)

Comments

NOTE: Apart from English (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

Inledning

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.

Tagghjälparen för sidstorlek

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.

Krav

Så för mitt användningsfall var kraven följande:

  1. Tillåter enkel användning för teh utvecklare (så mycket som möjligt konfigureras med hjälp av en enkel sidmodell)
  2. Stöder uppdatering av sidstorleken med hjälp av HTMX - vilket innebär att den kan posta tillbaka medan alla söksträngsparametrar behålls. Detta är viktigt eftersom det gör det möjligt för användaren att ändra sidstorlek utan att förlora några andra filter som de har tillämpat.
  3. Har ett något dynamiskt utbud av sidstorlekar (inklusive ett "Alla"-alternativ).

Taggens hjälpare

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.

JS Shenanigans Ordförande

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.

Kodexen

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;
        }
    }

Slutsatser

Det var allt för PageSize-tagg-hjälparen. Jag jobbar fortfarande på dokumentation och exempel men koden är bättre dagligen.

logo

©2024 Scott Galloway