Un composant de visionnage ASP.NET aide-étiquette de base (partie 2, taille de page) (Français (French))

Un composant de visionnage ASP.NET aide-étiquette de base (partie 2, taille de page)

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

Présentation

Dans le cadre de ma sauge en cours avec mon helper tag de pagination, j'ai maintenant séparé PageSize dans son propre helper tag. Il s'agit de rendre l'aide-étiquette plus flexible et de permettre des scénarios de téléappel plus complexes.

Encore une fois, ce sont tous des travaux en cours. Je suis déjà jusqu'à 0,9.0 au moment de l'écriture, mais UTILISER À VOTRE RISQUE. C'est gratuit mais pas YET de qualité de sortie IMHO (je n'ai même pas échantillons complets Pour l'instant!)

Voir code pour le tag helper ici.

La taille de la page Tag Helper

Comme avec mes autres taghelpers, ceci est conçu pour un cas d'utilisation que je rencontre souvent; comment changer facilement la taille de la page dans les listes de résultats. À première vue cela semble assez simple mais il peut devenir TRICKY une fois que vous ajoutez HTMX dans le mélange.

Besoins

Donc, pour mon cas d'utilisation, les exigences étaient les suivantes:

  1. Permet une utilisation facile pour le développeur de teh (dans la mesure du possible est configuré à l'aide d'un modèle de page simple)
  2. Prend en charge la mise à jour de la taille de la page en utilisant HTMX - ce qui signifie qu'il est capable de poster retour tout en conservant tous les paramètres de la chaîne de requête. Ceci est important car il permet à l'utilisateur de changer la taille de la page sans perdre d'autres filtres qu'il a appliqués.
  3. A une gamme assez dynamique de tailles de pages (y compris une option 'All').

L'aide à l'étiquette

Dans cet esprit, j'ai conçu le TagHelper comme suit:

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

Donc, dans ce cas encore, il faut mon modèle de téléappel 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; }
}

Ce qui fournit le Page, TotalItems, PageSize, ViewType et LinkUrl pour la recherche.

Vous pouvez également les spécifier sous la forme d'attributs au modèle:


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

Dans les cas suivants: hx-target ici est la cible pour la requête HTMX et le hx-swap est la cible de la réponse.

Dans ce cas, il utilisera le PageSize et TotalItems et ViewType pour rendre le sélecteur de taille de page.

JS Shenanigans

Malheureusement, cette fois-ci, j'avais une exigence que je ne pouvais pas gérer du côté du serveur. À savoir, je voulais préserver les paramaters de la chaîne de requête dans la requête de modification de pagesize. Pour y parvenir, j'ai deux pièces de JS qui se chargeront dans le contrôle en fonction de HTMX étant utilisé ou non

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

Si HTMX est utilisé, il le rend dans la page :

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

Ce qui est une simple pièce de JavaScript qui injectera les paramètres actuels de la chaîne de requête dans la requête.

Sinon, si HTMX n'est PAS activé, il le rendra :

(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 changement que je ferai lilément est de changer le nom de la variable 'flag'; __pageSizeListenerAttached car c'est un peu trop générique ET je l'utilise pour les requêtes HTMX et non-HTMX.

Le CODE

Le tag helper lui-même est assez simple, le code principal ne fait que remplir quelques propriétés et rend ensuite le composant de vue.

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

Une pièce délicate est correctement gérer la PageSizeSteps attribut. Il s'agit d'une liste de tailles de pages séparées par des virgules que l'utilisateur peut sélectionner. OU vous pouvez passer dans vos propres pas. Vous pouvez également spécifier le MaxPageSize qui est la taille maximale de la page qui peut être sélectionnée.

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

Comme avec mon autre helper de tags, j'utilise le modèle TagHelper->ViewComponent pour rendre le composant view. Cela me permet de garder la balise helper simple et le complexe de composant de vue. Il vous permet également de changer de vues pour différentes expériences; ou même d'injecter votre propre vue et d'utiliser cela (en utilisant 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;
        }
    }

En conclusion

C'est tout pour l'assistant de la balise PageSize. Je travaille toujours sur la documentation et les exemples, mais le code est mieux getign tous les jours.

logo

©2024 Scott Galloway