ASP.NET Core Tag Help (Osa 2, sivukoko) (Suomi (Finnish))

ASP.NET Core Tag Help (Osa 2, sivukoko)

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

Johdanto

Olen nyt erottanut PageSizen omaksi tunnisteavustajakseen osana jatkuvaa salviaani hakutunnisteen avulla. Näin tunnisteavustin on joustavampi ja mahdollistaa monimutkaisemmat hakuskenaariot.

Nämä ovat kaikki käynnissä olevia toimia. Olen jo enintään 0,9,0 Kun kirjoitat, mutta käytät omaa riskiäsi. Se on ilmaista, mutta EI vielä julkaisulaatuista Imhoa (minulla ei edes ole täydelliset näytteet vielä!)

Katso tunnisteen auttajan koodi täällä.

Sivun kokoisen tagin auttaja

Kuten muidenkin taghelpeideni kohdalla, tämäkin on suunniteltu usein kohtaamaani käyttökertaa varten, kuinka voit helposti vaihtaa sivukokoa tulosluetteloissa. Ensi silmäyksellä tämä vaikuttaa melko suoraviivaiselta, mutta siitä voi tulla TRICKY, kun siihen lisätään HTMX.

Vaatimukset

Joten minun käyttötapauksessani vaatimukset olivat seuraavat:

  1. Mahdollistaa helpon käytön teh-kehittäjälle (niin paljon kuin mahdollista on määritelty yksinkertaisella sivumallilla)
  2. Tukee sivun koon päivittämistä HTMX:n avulla - näin se pystyy postaamaan takaisin säilyttäen kaikki kyselyn merkkijonon parametrit. Tämä on tärkeää, koska sen avulla käyttäjä voi muuttaa sivun kokoa menettämättä muita suodattimia, joita hän on käyttänyt.
  3. Sivukokojen valikoima on melko dynaaminen (mukaan lukien "Kaikki"-vaihtoehto).

Tagin auttaja

Tätä silmällä pitäen suunnittelin TagHelper-ohjelman seuraavasti:

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

Tässäkin tapauksessa se vaatii vakiohakumallini:

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

Tästä seuraa, että Page, TotalItems, PageSize, ViewType sekä LinkUrl Hakemista varten.

Voit myös määritellä ne mallin attribuuttien muodossa:


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

Jossa hx-target Tässä on kohde HTMX-pyynnölle ja hx-swap on vastauskohteena.

Tässä tapauksessa se käyttää määritettyä PageSize sekä TotalItems sekä ViewType sivun kokovalitsinta varten.

JS Shenanigans

Valitettavasti tällä kertaa minulla oli vaatimus, jota en pystynyt käsittelemään pelkästään palvelinpuolella. Halusin erityisesti säilyttää kyselyn merkkijonon parameteraattoreiden sivuille muutospyynnön. Tämän saavuttamiseksi minulla on kaksi kappaletta JS:ää, jotka lataavat ohjaimeen riippuen siitä, käytetäänkö HTMX:ää vai ei

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

Jos HTMX:ää käytetään, se kääntää tämän sivulle:

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

Mikä on yksinkertainen Javascript-kappale, joka ruiskuttaa nykyiset kyselyparametrit pyyntöön.

Vaihtoehtoisesti, jos HTMX ei ole käytössä, se tekee tämän:

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

Yksi muutos, jonka teen lilely, on "lippumuuttujan" nimen muuttaminen. __pageSizeListenerAttached Koska se on hieman liian yleinen JA käytän sitä sekä HTMX- että ei-HTMX-pyyntöihin.

KOODI

Itse tunnisteavustin on melko yksinkertainen, pääkoodi vain kansoittaa muutaman ominaisuuden ja kääntää sitten näkymän Componentiksi.

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

Yksi hankala osa on käsitellä oikein PageSizeSteps Ominaisuus. Tämä on pilkkueroteltu lista sivukokoista, joista käyttäjä voi valita. OR voit kulkea omia askeleitasi. Voit myös määrittää MaxPageSize-koon, joka on suurin mahdollinen sivun koko.

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

Kuten muidenkin tag-auttajieni kohdalla, käytän TagHelper->ViewComponent-mallia näkymän komponentin tekemiseen. Näin pystyn pitämään tunnisteavustimen yksinkertaisena ja näkymän komponenttikompleksina. Sen avulla voit myös muuttaa näkemyksiäsi erilaisiin kokemuksiin tai jopa ruiskuttaa omaa näkemystäsi ja käyttää sitä (käyttäen 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;
        }
    }

Johtopäätöksenä

Siinä kaikki PageSize-tagin auttajalle. Työstän edelleen dokumentteja ja esimerkkejä, mutta koodi on päivä päivältä parempi.

logo

©2024 Scott Galloway