Back to "Een Paging View Component ASP.NET Core Tag Helper (Deel 1 de Bare-Bones)"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

ASP.NET Core PagingTagHelper TagHelper

Een Paging View Component ASP.NET Core Tag Helper (Deel 1 de Bare-Bones)

Tuesday, 11 March 2025

Inleiding

Een werkproject de andere dag vereist het implementeren van paging formulier resultaten. Mijn ga-naar paging tag helper is altijd de pagination Tag Helper door Darrel O'Neill zoals ik schreef over Hier. Maar om welke reden dan ook... het is gewoon... werkt niet meer Voor mij. Dus in plaats van te proberen te puzzelen door wat eruit ziet als een verlaten project op dit moment besloot ik er zelf een te bouwen.

Zoals gewoonlijk kunt u de bron voor deze op mijn GitHub

Ik heb een voorbeeld site voor dit project Hier gehost

Dit heeft samples van de output als volgt:

Demo zoeken met tag Helperg

Vereisten

Voor deze tag helper had ik een paar eisen:

  1. Moet naadloos werken met Achterwind en DaisyUI; mijn favoriete CSS kaders.
  2. Zou moeten werken met HTMX zonder problemen te veroorzaken.
  3. Moet een pagesize dropdown die HTMX gebruikt om te flippen (dus als je don't gebruik HTMX moet het nog steeds werken, maar je moet een knop toe te voegen).
  4. Moet gemakkelijk te configureren en te gebruiken zijn
    1. Accepteert een paging model dus het is eenvoudig te gebruiken in een Razor pagina
    2. Moet kunnen worden geconfigureerd met een paar eenvoudige parameters
  5. Moet een nuget pakket zijn, zodat al jullie geweldige mensen er mee kunnen spelen.
  6. Moet zowel een ViewComponent en een TagHelper, zodat het kan worden gebruikt in zowel Razor pagina's en views; met dat in gedachten moet het ook een overridable Dafault.cshtml uitzicht.
  7. Werkt met een eenvoudige zoekfunctie.

In de toekomst voeg ik de mogelijkheid toe aan:

  1. Voeg aangepaste CSS om te voorkomen dat gebonden aan DaisyUI en Tailwind~~ Ik heb deze mogelijkheid al toegevoegd zie de demo hier: https://taghelpersample.mostlylucid.net/Home/PlainView
  2. De mogelijkheid om paginagroottes op te geven
  3. De mogelijkheid om een aangepaste JS-oproep toe te voegen aan de dropdown van de paginagrootte (om u toe te staan geen gebruik te maken van HTMX).
  4. Gebruik Alpine om de pieper actiever en responsiever te maken (zoals ik deed in mijn vorig artikel).

Installatie

De taghelper is nu een glanzend nieuw Nuget pakket, zodat u het kunt installeren met het volgende commando:

dotnet add package mostlylucid.pagingtaghelper

Je zou dan de tag helper toevoegen aan je _ViewImports.cshtml bestand als volgt:

@addTagHelper *, mostlylucid.pagingtaghelper

Dan kunt u gewoon beginnen met het te gebruiken; Ik geef een aantal helper klassen die u kunt gebruiken om het te configureren zoals

IPagingModel

Dit is het 'basismateriaal' dat je nodig hebt om aan de slag te gaan. Het is een eenvoudige interface die u kunt implementeren op uw model om de paging werken. Merk op dat ViewType is optioneel hier is het standaard om TailwindANdDaisy maar je kunt het instellen op Custom, Plain of Bootstrap als u een andere weergave wilt gebruiken.

public enum ViewType
{
TailwindANdDaisy,
Custom,
Plain,
Bootstrap
}

OF u kunt zelfs een aangepaste weergave opgeven met behulp van de TagHelper's use-local-view eigendom.

namespace mostlylucid.pagingtaghelper.Models;

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; }
}
namespace mostlylucid.pagingtaghelper.Models;

public interface IPagingSearchModel : IPagingModel
{
    public string? SearchTerm { get; set; }
}

Ik heb deze ook in het project geïmplementeerd om een basis te bieden:

public abstract class BasePagerModel : IPagingModel
{
    public int Page { get; set; } = 1;
    public int TotalItems { get; set; } = 0;
    public int PageSize { get; set; } = 10;
    public ViewType ViewType { get; set; } = ViewType.TailwindANdDaisy;

    public string LinkUrl { get; set; } = "";

}
public abstract class BasePagerSearchMdodel : BasePagerModel, IPagingSearchModel
{
    public string? SearchTerm { get; set; }
}

Ik zal de zoekfunctie in een volgend artikel behandelen..

De TagHelper

Toen bedacht ik hoe ik zou willen dat de TagHelper eruit zou zien als in gebruik:

<paging
    x-ref="pager"
    hx-boost="true"
    hx-indicator="#loading-modal"
    hx-target="#user-list "
    hx-swap="show:none"
    model="Model"
    pages-to-display="10"
    hx-headers='{"pagerequest": "true"}'>
</paging>

Hier zie je dat ik een paar HTMX parameters en het model voor de paging heb ingesteld. Ik heb ook het aantal pagina's ingesteld om te tonen en de headers om te verzenden met het verzoek (dit stelt me in staat om HTMX te gebruiken om de pagina te vullen).

Het onderdeel heeft ook een BUNCH van andere config elementen die ik zal doorwerken in toekomstige artikelen. Zoals je kunt zien is er een hoop mogelijke configuratie.

<paging css-class=""
        first-last-navigation=""
        first-page-text=""
        next-page-aria-label=""
        next-page-text=""
        page=""
        pages-to-display=""
        page-size=""
        previous-page-text=""
        search-term=""
        skip-forward-back-navigation=""
        skip-back-text=""
        skip-forward-text="true"
        total-items=""
        link-url=""
        last-page-text=""
        show-pagesize=""
        use-htmx=""
        use-local-view=""
        view-type="Bootstrap"
        htmx-target=""
        id=""
></paging>

De TagHelper is vrij eenvoudig maar heeft een heleboel eigenschappen die de gebruiker in staat stellen om het gedrag aan te passen (dit zie je hieronder in de weergave ) afgezien van de eigenschappen (die ik hier niet voor kortheid zal plakken) is de code vrij eenvoudig:

    /// <summary>
    /// Processes the tag helper to generate the pagination component.
    /// </summary>

    /// <param name="context">The tag helper context.</param>
    /// <param name="output">The tag helper output.</param>
    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
   
        output.TagName = "div";
        
        //Remove all the properties that are not needed for the rendered content.
        output.Attributes.RemoveAll("page");
        output.Attributes.RemoveAll("link-url");
        output.Attributes.RemoveAll("page-size");
        output.Attributes.RemoveAll("total-items");
        output.Attributes.RemoveAll("pages-to-display");
        output.Attributes.RemoveAll("css-class");
        output.Attributes.RemoveAll("first-page-text");
        output.Attributes.RemoveAll("previous-page-text");
        output.Attributes.RemoveAll("skip-back-text");
        output.Attributes.RemoveAll("skip-forward-text");
        output.Attributes.RemoveAll("next-page-text");
        output.Attributes.RemoveAll("next-page-aria-label");
        output.Attributes.RemoveAll("last-page-text");
        output.Attributes.RemoveAll("first-last-navigation");
        output.Attributes.RemoveAll("skip-forward-back-navigation");
        output.Attributes.RemoveAll("model");
        output.Attributes.RemoveAll("show-pagesize");
        output.Attributes.RemoveAll("pagingmodel");
        output.Attributes.RemoveAll("use-local-view");
        
        var pagerId =  PagerId ?? $"pager-{Guid.NewGuid():N}";
        var linkUrl = LinkUrl ?? ViewContext.HttpContext.Request.Path;
        PageSize = Model?.PageSize ?? PageSize ?? 10;
        Page = Model?.Page ?? Page ?? 1;
        ViewType = Model?.ViewType ?? ViewType;
        TotalItems = Model?.TotalItems ?? TotalItems ?? 0;
        if(Model is IPagingSearchModel searchModel)
            SearchTerm = searchModel?.SearchTerm ?? SearchTerm ?? "";
        output.Attributes.SetAttribute("id", pagerId);
        var viewComponentHelper = (IViewComponentHelper)ViewContext.HttpContext.RequestServices.GetService(typeof(IViewComponentHelper))!;
        ((IViewContextAware)viewComponentHelper).Contextualize(ViewContext);

        var pagerModel = PagerModel ?? new PagerViewModel()
        {
            
            ViewType = ViewType,
            UseLocalView = UseLocalView,
            UseHtmx = UseHtmx,
            PagerId = pagerId,
            SearchTerm = SearchTerm,
            ShowPageSize = ShowPageSize,
            Model = Model,
            LinkUrl = linkUrl,
            Page = Page,
            PageSize = PageSize,
            TotalItems = TotalItems,
            PagesToDisplay = PagesToDisplay,
            CssClass = CssClass,
            FirstPageText = FirstPageText,
            PreviousPageText = PreviousPageText,
            SkipBackText = SkipBackText,
            SkipForwardText = SkipForwardText,
            NextPageText = NextPageText,
            NextPageAriaLabel = NextPageAriaLabel,
            LastPageText = LastPageText,
            FirstLastNavigation = FirstLastNavigation,
            SkipForwardBackNavigation = SkipForwardBackNavigation,
            HtmxTarget = HtmxTarget,
            
        };

        var result = await viewComponentHelper.InvokeAsync("Pager", pagerModel);
        output.Content.SetHtmlContent(result);
    }

Het omvat de volgende stappen:

  1. Schakel de uitvoertagnaam in op div; dit is de container voor de pager.
  2. Verwijder alle eigenschappen die niet nodig zijn voor de weergegeven inhoud (maar laat alle door de gebruiker verstrekte eigenschappen, dit maakt eenvoudige aanpassing mogelijk).
  3. Stel de pagerId in op een willekeurige GUID als deze niet beschikbaar is (dit wordt echt gebruikt voor aangepaste code, je kunt de ID opgeven of gewoon deze code laten verzorgen).
  4. Stel de linkUrl in op het huidige pad als dit niet beschikbaar is - hiermee kunt u dit overschrijven als u een andere URL wilt gebruiken.
  5. Stel de PageSize, Page, ViewType, TotalItems en SearchTerm in op het model indien beschikbaar of de standaard indien niet. Dit stelt ons in staat om te passeren in de IPagingModel en laat de pager werken zonder verdere configuratie.
  6. Stel het ID-attribuut in op het pagerId.
  7. Krijg de ViewComponentHelper uit de DI container en contextualiseer het met de huidige ViewContext.
  8. Een nieuw aanmaken PagerViewModel met de eigenschappen die zijn ingesteld op de waarden die we hebben of de standaardwaarden indien niet verstrekt.
  9. Roep de Pager BeeldComponent met de PagerViewModel en stel de output inhoud in op het resultaat.

Alweer vrij eenvoudig.

De weergaveComponent

De weergave

Het uitzicht voor de ViewComponent is vrij eenvoudig; het is gewoon een lus door de pagina's en een paar links naar de eerste, laatste, volgende en vorige pagina's.

Complete source code for the Default TailwindUI & Daisy view
@model mostlylucid.pagingtaghelper.Components.PagerViewModel
@{
    var totalPages = (int)Math.Ceiling((double)Model.TotalItems! / (double)Model.PageSize!);
    var pageSizes = new List<int>();
    if (Model.ShowPageSize)
    {
        // Build a dynamic list of page sizes.

        // Fixed steps as a starting point.
        int[] fixedSteps = { 10, 25, 50, 75, 100, 125, 150, 200, 250, 500, 1000 };

        // Add only those fixed steps that are less than or equal to TotalItems.
        foreach (var step in fixedSteps)
        {
            if (step <= Model.TotalItems)
            {
                pageSizes.Add(step);
            }
        }

        // If TotalItems is greater than the largest fixed step,
        // add additional steps by doubling until reaching TotalItems.
        if (Model.TotalItems > fixedSteps.Last())
        {
            int next = fixedSteps.Last();
            while (next < Model.TotalItems)
            {
                next *= 2;
                // Only add if it doesn't exceed TotalItems.
                if (next < Model.TotalItems)
                {
                    pageSizes.Add(next);
                }
            }

            // Always include the actual TotalItems as the maximum option.
            if (!pageSizes.Contains(Model.TotalItems.Value))
            {
                pageSizes.Add(Model.TotalItems.Value);
            }
        }
    }
}
@if (totalPages > 1)
{
    <div class="@Model.CssClass flex items-center justify-center" id="pager-container">
        @if (Model.ShowPageSize)
        {
            var pagerId = Model.PagerId;
            var htmxAttributes = Model.UseHtmx
                ? $"hx-get=\"{Model.LinkUrl}\" hx-trigger=\"change\" hx-include=\"#{pagerId} [name='page'], #{pagerId} [name='search']\" hx-push-url=\"true\""
                : "";


            <!-- Preserve current page -->
            <input type="hidden" name="page" value="@Model.Page"/>
            <input type="hidden" name="search" value="@Model.SearchTerm"/>
            <input type="hidden" class="useHtmx" value="@Model.UseHtmx.ToString().ToLowerInvariant()"/>
            if (!Model.UseHtmx)
            {
                <input type="hidden" class="linkUrl" value="@Model.LinkUrl"/>
            }

            <!-- Page size select with label -->
            <div class="flex items-center mr-8">
                <label for="pageSize-@pagerId" class="text-sm text-gray-600 mr-2">Page size:</label>
                <select id="pageSize-@pagerId"
                        name="pageSize"
                        class="border rounded select select-primary select-sm pt-0 mt-0 min-w-[80px] pr-4"
                        @Html.Raw(htmxAttributes)>
                    @foreach (var option in pageSizes.ToList())
                    {
                        var optionString = option.ToString();
                        if (option == Model.PageSize)
                        {
                            <option value="@optionString" selected="selected">@optionString</option>
                        }
                        else
                        {
                            <option value="@optionString">@optionString</option>
                        }
                    }
                </select>
            </div>
        }

        @* "First" page link *@
        @if (Model.FirstLastNavigation && Model.Page > 1)
        {
            var href = $"{Model.LinkUrl}?page=1&pageSize={Model.PageSize}";
            if (!string.IsNullOrEmpty(Model.SearchTerm))
            {
                href += $"&search={Model.SearchTerm}";
            }

            <a class="btn btn-sm"
               href="@href">
                @Model.FirstPageText
            </a>
        }

        @* "Previous" page link *@
        @if (Model.Page > 1)
        {
            var href = $"{Model.LinkUrl}?page={Model.Page - 1}&pageSize={Model.PageSize}";
            if (!string.IsNullOrEmpty(Model.SearchTerm))
            {
                href += $"&search={Model.SearchTerm}";
            }

            <a class="btn btn-sm"
               href="@href">
                @Model.PreviousPageText
            </a>
        }

        @* Optional skip back indicator *@
        @if (Model.SkipForwardBackNavigation && Model.Page > Model.PagesToDisplay)
        {
            <a class="btn btn-sm btn-disabled">
                @Model.SkipBackText
            </a>
        }

        @* Determine visible page range *@
        @{
            int halfDisplay = Model.PagesToDisplay / 2;
            int startPage = Math.Max(1, Model.Page.Value - halfDisplay);
            int endPage = Math.Min(totalPages, startPage + Model.PagesToDisplay - 1);
            startPage = Math.Max(1, endPage - Model.PagesToDisplay + 1);
        }
        @for (int i = startPage; i <= endPage; i++)
        {
            var href = $"{Model.LinkUrl}?page={i}&pageSize={Model.PageSize}";
            if (!string.IsNullOrEmpty(Model.SearchTerm))
            {
                href += $"&search={Model.SearchTerm}";
            }

            <a data-page="@i" class="btn btn-sm mr-2 @(i == Model.Page ? "btn-active" : "")"
               href="@href">
                @i
            </a>
        }

        @* Optional skip forward indicator *@
        @if (Model.SkipForwardBackNavigation && Model.Page < totalPages - Model.PagesToDisplay + 1)
        {
            <a class="btn btn-sm btn-disabled mr-2">
                @Model.SkipForwardText
            </a>
        }

        @* "Next" page link *@
        @if (Model.Page < totalPages)
        {
            var href = $"{Model.LinkUrl}?page={Model.Page + 1}&pageSize={Model.PageSize}";
            if (!string.IsNullOrEmpty(Model.SearchTerm))
            {
                href += $"&search={Model.SearchTerm}";
            }

            <a class="btn btn-sm mr-2"
               href="@href"
               aria-label="@Model.NextPageAriaLabel">
                @Model.NextPageText
            </a>
        }

        @* "Last" page link *@
        @if (Model.FirstLastNavigation && Model.Page < totalPages)
        {
            var href = $"{Model.LinkUrl}?page={totalPages}&pageSize={Model.PageSize}";
            if (!string.IsNullOrEmpty(Model.SearchTerm))
            {
                href += $"&search={Model.SearchTerm}";
            }

            <a class="btn btn-sm"
               href="@href">
                @Model.LastPageText
            </a>
        }

        <!-- Page info text with left margin for separation -->
        <div class="text-sm text-neutral-500 ml-8">
            Page @Model.Page of @totalPages (Total items: @Model.TotalItems)
        </div>
    </div>
}
Dit is opgedeeld in een paar secties:
  1. Uitklapmenu voor paginagrootte
  2. De eerste, laatste, volgende en vorige links
  3. De skip back en skip forward links
  4. De pagina links
  5. De pagina-infotekst

De Paginagrootte omlaag

Een ding wat ik miste in de originele tag helper was een paginagrootte dropdown. Dit is een eenvoudige selectielijst, je kunt zien dat ik eerst begin door te definiëren fixedSteps Dat zijn slechts een paar vaste stappen die ik wil gebruiken voor de dropdown. Ik loop ze dan door en voeg ze toe aan de lijst. Een gewoonte die ik altijd heb is het hebben van een 'all' optie, dus ik voeg de totale items aan de lijst als het er nog niet is.

@{
    var totalPages = (int)Math.Ceiling((double)Model.TotalItems! / (double)Model.PageSize!);
    var pageSizes = new List<int>();
    if (Model.ShowPageSize)
    {
        // Build a dynamic list of page sizes.

        // Fixed steps as a starting point.
        int[] fixedSteps = { 10, 25, 50, 75, 100, 125, 150, 200, 250, 500, 1000 };

        // Add only those fixed steps that are less than or equal to TotalItems.
        foreach (var step in fixedSteps)
        {
            if (step <= Model.TotalItems)
            {
                pageSizes.Add(step);
            }
        }

        // If TotalItems is greater than the largest fixed step,
        // add additional steps by doubling until reaching TotalItems.
        if (Model.TotalItems > fixedSteps.Last())
        {
            int next = fixedSteps.Last();
            while (next < Model.TotalItems)
            {
                next *= 2;
                // Only add if it doesn't exceed TotalItems.
                if (next < Model.TotalItems)
                {
                    pageSizes.Add(next);
                }
            }

            // Always include the actual TotalItems as the maximum option.
            if (!pageSizes.Contains(Model.TotalItems.Value))
            {
                pageSizes.Add(Model.TotalItems.Value);
            }
        }
    }
}

Ik maak dit dan weer terug naar de pagina

  @if (Model.ShowPageSize)
        {
            var pagerId = Model.PagerId;
            var htmxAttributes = Model.UseHtmx
                ? $"hx-get=\"{Model.LinkUrl}\" hx-trigger=\"change\" hx-include=\"#{pagerId} [name='page'], #{pagerId} [name='search']\" hx-push-url=\"true\""
                : "";


            <!-- Preserve current page -->
            <input type="hidden" name="page" value="@Model.Page"/>
            <input type="hidden" name="search" value="@Model.SearchTerm"/>
            <input type="hidden" class="useHtmx" value="@Model.UseHtmx.ToString().ToLowerInvariant()"/>
            if (!Model.UseHtmx)
            {
                <input type="hidden" class="linkUrl" value="@Model.LinkUrl"/>
            }

            <!-- Page size select with label -->
            <div class="flex items-center mr-8">
                <label for="pageSize-@pagerId" class="text-sm text-gray-600 mr-2">Page size:</label>
                <select id="pageSize-@pagerId"
                        name="pageSize"
                        class="border rounded select select-primary select-sm pt-0 mt-0 min-w-[80px] pr-4"
                        @Html.Raw(htmxAttributes)>
                    @foreach (var option in pageSizes.ToList())
                    {
                        var optionString = option.ToString();
                        if (option == Model.PageSize)
                        {
                            <option value="@optionString" selected="selected">@optionString</option>
                        }
                        else
                        {
                            <option value="@optionString">@optionString</option>
                        }
                    }
                </select>
            </div>
        }

U kunt zien dat ik optioneel enkele HTMX-attributen gebruik om de paginagrootte door te geven aan de server en de pagina bij te werken met behoud van de huidige pagina en zoekparameter (indien aanwezig).

Bovendien als u specif use-htmx=false als een parameter op de tag helper zal het deze niet uitvoeren, maar in plaats daarvan zal u toestaan om een aantal JS die ik als een HTML Helper te gebruiken om de paginagrootte bij te werken.

@Html.PageSizeOnchangeSnippet()
    

Dit is een eenvoudig script dat de paginagrootte zal bijwerken en de pagina opnieuw zal laden (let op: dit werkt nog niet voor Plain CSS / Bootstrap omdat ik de namen van de eigendommen moet uitwerken etc).

document.addEventListener("DOMContentLoaded", function () {
    document.body.addEventListener("change", function (event) {
        const selectElement = event.target.closest("#pager-container select[name='pageSize']");
        if (!selectElement) return;

        const pagerContainer = selectElement.closest("#pager-container");
        const useHtmxInput = pagerContainer.querySelector("input.useHtmx");
        const useHtmx = useHtmxInput ? useHtmxInput.value === "true" : true; // default to true

        if (!useHtmx) {
            const pageInput = pagerContainer.querySelector("[name='page']");
            const searchInput = pagerContainer.querySelector("[name='search']");

            const page = pageInput ? pageInput.value : "1";
            const search = searchInput ? searchInput.value : "";
            const pageSize = selectElement.value;
            const linkUrl =  pagerContainer.querySelector("input.linkUrl").value ?? "";
            
            const url = new URL(linkUrl, window.location.origin);
            url.searchParams.set("page", page);
            url.searchParams.set("pageSize", pageSize);

            if (search) {
                url.searchParams.set("search", search);
            }

            window.location.href = url.toString();
        }
    });
});

HTMX-integratie

De HTMX integratie is vrij eenvoudig omdat HTMX cascading tot kind elementen kunnen we de HTMX parameters op het ouder element en ze zullen worden geërfd.

  • hx-boost="true" - dit maakt gebruik van de handige hx-boost functie om het klik-evenement te onderscheppen en het verzoek te verzenden via HTMX.
  • hx-indicator="#loading-modal" - dit heeft een laadmodus die zal tonen tijdens de behandeling van het verzoek.
  • hx-target="#user-list" - dit is het element dat het antwoord zal verwisselen, in dit geval de gebruikerslijst. Opmerking: Dit omvat momenteel de Pager voor eenvoud; je kunt dit actiever maken met behulp van Alpine (zoals in mijn Vorig artikel ) maar deze keer was het buiten bereik.
  • hx-swap="show:none"

Het laadmodaal

Dit is vrij eenvoudig en maakt gebruik van DaisyUI, boxiconen en Tailwind om een eenvoudige laadmodus te creëren.

<div id="loading-modal" class="modal htmx-indicator">
    <div class="modal-box flex flex-col items-center justify-center">
        <h2 class="text-lg font-semibold">Loading...</h2>
        <i class="bx bx-loader bx-spin text-3xl mt-2"></i>
    </div>
</div>

hx-indicator="#loading-modal" geeft vervolgens aan dat wanneer een HTMX-verzoek wordt uitgevoerd het shoould tonen dan deze modale verbergen.

Toekomstige functies

Dus dat is deel 1, er is duidelijk een LOT meer te behandelen en ik zal in toekomstige artikelen; met inbegrip van de sample site, alternatieve weergaven, de zoekfunctie en de aangepaste CSS.

logo

©2024 Scott Galloway