Een Paging View Component ASP.NET Core Tag Helper (Deel 2, PageSize) (Nederlands (Dutch))

Een Paging View Component ASP.NET Core Tag Helper (Deel 2, PageSize)

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

Inleiding

Als onderdeel van mijn lopende salie met mijn paging tag helper heb ik nu gescheiden PageSize in zijn eigen tag helper. Dit is om de tag helper flexibeler te maken en om complexere paging scenario's mogelijk te maken.

Nogmaals; dit zijn allemaal werken-in vooruitgang. Dat ben ik al. tot 0.9.0 op het moment van schrijven maar gebruik op je eigen risico. Het is gratis maar NIET YET van de release kwaliteit IMHO (Ik heb niet eens volledige monsters Nog niet!)

Zie de code voor de tag helper hier.

The Page Size Tag Helper

Zoals met mijn andere taghelpers is dit ontworpen voor een use-case die ik vaak tegenkom; hoe eenvoudig paginagrootte te wisselen in resultatenlijsten. Op het eerste gezicht lijkt dit vrij eenvoudig, maar het kan worden TRICKY zodra u HTMX in de mix.

Vereisten

Dus voor mijn gebruik geval de vereisten waren de volgende

  1. Hiermee eenvoudig te gebruiken voor teh ontwikkelaar (zoveel mogelijk is geconfigureerd met behulp van een eenvoudig paginamodel)
  2. Ondersteunt het bijwerken van de paginagrootte met behulp van HTMX - wat betekent dat het in staat is om terug te posten met behoud van alle query string parameters. Dit is belangrijk omdat het de gebruiker in staat stelt om de paginagrootte te wijzigen zonder het verliezen van andere filters die ze hebben toegepast.
  3. Heeft een enigszins dynamisch bereik van paginagroottes (inclusief een 'All' optie).

De Tag Helper

Met dit in gedachten ontwierp ik de TagHelper als volgt:

<page-size
    hx-target="#list"
    hx-swap="innerHTML"
    model="Model">
    
</page-size>
<div id="list">
    <partial name="_ResultsList" model="Model"/>
</div>

Dus in dit geval neemt het weer mijn standaard paging model:

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

Wat voorziet in de Page, TotalItems, PageSize, ViewType en LinkUrl voor de oproep.

U kunt deze ook specificeren in de vorm van attributen aan het model:


<page-size
    hx-target="#list"
    hx-swap="innerHTML"
    total-items="100"
    page-size="10"
    view-type="DaisyAndTailwind">
</page-size>

Waar de hx-target hier is het doel voor het HTMX verzoek en de hx-swap is het doel voor de reactie.

In dit geval zal het gebruik van de opgegeven PageSize en TotalItems en ViewType om de paginagrootte-selector weer te geven.

JS Shenanigans

Helaas had ik deze keer een vereiste die ik niet kon verwerken puur server kant. Namelijk wilde ik query string paramaters in de pagina wijzigen verzoek te behouden. Om dit te bereiken heb ik twee stukken JS die in de controle zal worden geladen, afhankelijk van HTMX wordt gebruikt oor niet

@if (Model.UseHtmx)
{
@Html.HTMXPageSizeChange()
}
else
{
    @Html.PageSizeOnchangeSnippet()
}

Als HTMX wordt gebruikt wordt dit weergegeven in de pagina:

(() => {
    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;
})();

Dat is een eenvoudig stukje JavaScript dat de huidige querystring parameters in het verzoek zal injecteren.

Als HTMX NIET is ingeschakeld, zal dit ook worden weergegeven:

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

Een verandering die ik zal maken is het veranderen van de naam van de 'vlag' variabele; __pageSizeListenerAttached want het is een beetje te generiek en ik gebruik het voor zowel HTMX als niet-HTMX verzoeken.

De CODE

De tag helper zelf is vrij eenvoudig, de hoofdcode gewoon populeert een paar eigenschappen en vervolgens maakt de weergave 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>"
            );
        }
    }

Een lastig stuk is de juiste behandeling van de PageSizeSteps attribuut. Dit is een komma gescheiden lijst van paginagroottes waaruit de gebruiker kan kiezen. OR u kunt passeren in uw eigen stappen. U kunt ook de MaxPageSize opgeven die de maximale paginagrootte is die geselecteerd kan worden.

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

Net als met mijn andere tag helper gebruik ik het TagHelper->ViewComponent patroon om de weergave component te renderen. Dit stelt me in staat om de tag helper eenvoudig te houden en de view component complex. Het stelt u ook in staat om van mening te veranderen voor verschillende ervaringen; of om zelfs uw eigen mening te injecteren en dat te gebruiken (met behulp van 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;
        }
    }

Conclusie

Nou dat is het voor de PageSize tag helper. Ik werk nog steeds aan de documentatie en voorbeelden, maar de code wordt dagelijks beter.

logo

©2024 Scott Galloway