Un componente di visualizzazione di Paging ASP.NET Core Tag Helper (Parte 2, PageSize) (Italiano (Italian))

Un componente di visualizzazione di Paging ASP.NET Core Tag Helper (Parte 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

Introduzione

Come parte della mia salvia in corso con il mio aiutante di tag paging ho ora separato PageSize nel proprio aiutante di tag. Questo è per rendere il tag helper più flessibile e per consentire scenari di paginaggio più complessi.

Ancora una volta; questi sono tutti lavori in corso. Lo sono gia'. fino a 0,90 al momento della scrittura ma UTILIZZO A TUO RISCHIO. E 'gratuito ma non ancora di qualità di rilascio IMHO (Non ho nemmeno Campioni completi Non ancora!)

Vedere il codice per il tag helper qui.

The Page Size Tag Helper

Come con i miei altri taghelper questo è progettato per un uso-caso che spesso incontro; come cambiare facilmente la dimensione della pagina nelle liste dei risultati. A prima vista questo sembra abbastanza semplice, ma può diventare TRICKY una volta che si aggiunge HTMX nel mix.

Requisiti

Quindi per il mio caso d'uso i requisiti erano i seguenti:

  1. Consente un facile utilizzo per lo sviluppatore di teh (il più possibile è configurato utilizzando un semplice modello di pagina)
  2. Supporta l'aggiornamento della dimensione della pagina utilizzando HTMX - significa che è in grado di inviare indietro mantenendo tutti i parametri della stringa di interrogazione. Questo è importante in quanto consente all'utente di modificare la dimensione della pagina senza perdere altri filtri che hanno applicato.
  3. Ha una gamma piuttosto dinamica di dimensioni delle pagine (compresa un'opzione 'Tutti').

L'aiutante per il tag

Con questo in mente ho progettato il TagHelper per essere come segue:

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

Quindi in questo caso ci vuole di nuovo il mio modello di ricerca standard:

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

Il che fornisce il Page, TotalItems, PageSize, ViewType e LinkUrl per la chiamata.

Puoi anche specificarli sotto forma di attributi al modello:


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

Dove: hx-target qui è l'obiettivo per la richiesta HTMX e la hx-swap è l'obiettivo della risposta.

In questo caso userà il specificato PageSize e TotalItems e ViewType per rendere il selettore delle dimensioni della pagina.

JS ShenanigansCity name (optional, probably does not need a translation)

Purtroppo questa volta avevo un requisito che non potevo gestire puramente lato server. Vale a dire che ho voluto preservare la stringa di query paramater nella richiesta di modifica pagesize. Per ottenere questo ho due pezzi di JS che verranno caricati nel controllo a seconda dell'utilizzo di HTMX o non

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

Se viene utilizzato HTMX, lo rende nella 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;
})();

Che è un semplice pezzo di JavaScript che inietterà i parametri correnti di querystring nella richiesta.

In alternativa, se HTMX NON è abilitato, renderà questo:

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

Un cambiamento che faro' senza problemi e' cambiare il nome della variabile 'flag'; __pageSizeListenerAttached in quanto è un po' troppo generico e lo uso sia per le richieste HTMX che per quelle non HTMX.

Il CODICE

Il tag helper stesso è abbastanza semplice, il codice principale popola solo alcune proprietà e poi rende il componente vista.

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

Un pezzo complicato è la gestione corretta del PageSizeSteps attributo. Questa è una lista separata da virgola delle dimensioni delle pagine da cui l'utente può scegliere. Oppure puoi passare ai tuoi passi. È inoltre possibile specificare il MaxPageSize che è la dimensione massima della pagina che può essere selezionata.

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

Come con l'altro aiutante di tag, uso il modello TagHelper->ViewComponent per visualizzare il componente della vista. Questo mi permette di mantenere il tag helper semplice e il complesso componente vista. Esso consente inoltre di modificare le opinioni per diverse esperienze; o anche di iniettare la propria visione e l'uso che (utilizzando 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;
        }
    }

In conclusione

Beh, e' tutto per l'aiutante dei tag PageSize. Sto ancora lavorando sulla documentazione e gli esempi, ma il codice è ottenere migliore ogni giorno.

logo

©2024 Scott Galloway