En Paging-uppfattningskomponent ASP.NET Core Tag Helper | ( | Part | 1 | Bare |- | Bones | ) (Svenska (Swedish))

En Paging-uppfattningskomponent ASP.NET Core Tag Helper | ( | Part | 1 | Bare |- | Bones | )

Tuesday, 11 March 2025

//

14 minute read

Inleiding

Ett arbetsprojekt i gårdagen krävde att implementera sökningsformresultaten. pagination Tag-hjältaren av Darrel O'Neill som jag skrev om här men av vilken anledning som helst så är det slutade fungera för mig. Så istället för att försöka gå igenom vad som ser ut som ett övergiven projekt vid det här laget bestämde jag mig för att bygga ett själv

Som vanligt kan du få källan till detta på min GitHub

Jag har ett exemplar för det här projektet. värd här

Det här har exempel på utgången som denna:

sök demonstration med tag-helperg

Benötningar

För den här tagassistenten hade jag några krav

  1. Borde jobba ömtlöst med Tailwind och DaisyUI; mina prefererade CSS ramverk
  2. Det borde fungera med HTMX utan att orsaka några problem.
  3. Det borde finnas en webbsida som använder HTMX för att bläddra. ( så om du DON 'T använder HTTP så borde det fortfarande fungera men du behöver lägga till en knapp
  4. Det borde vara enkelt att konfigurera och använda
    1. Accepterar en webbmodell så att det är enkelt att använda i en Razor-sida
    2. Ska kunna konfigureras med några enkla parametrar
  5. Det borde vara ett nugetpaket så att alla ni bra människor kan leka med det.
  6. Det borde vara både en ViewComponent och en TagHelper så att den kan användas i både Razor-sidor och -visualer; med tanke på att den också måste ha en överridbar Dafault.cshtml synsätt.
  7. Fungerar med en enkel sökfunktion.

I framtiden kommer jag att lägga till förmågan till

  1. Lägg till anpassad CSS för att undvika att bli fast vid DaisyUI och Tailwind~~ Jag har lagt till den här möjligheten redan se demon här https://taghelpersample
  2. Möjligheterna att avgöra sidans storlekar
  3. möjligheten att lägga till en anpassad JS sändning i sidans storlek dropdown ( för att du inte behöver använda HTMX).
  4. Använd Alpine för att göra webbaren mer aktiv och responsiv ( som jag gjorde i min tidigare artikel).

Installation

taghelper är nu ett lysande nytt Nuget-paket så du kan installera det med följande kommandot:

dotnet add package mostlylucid.pagingtaghelper

Du'd lägger sedan tagassistenten till din _ViewImports.cshtml file like so:

@addTagHelper *, mostlylucid.pagingtaghelper

Då kan du bara börja använda den; Jag ger några hjälpklassar som du kan använda för att begränsa den, till exempel

IPagingModel

Det här är den ' grundläggande grejen, ' du behöver börja med, . det är en enkel gränssnitt som du kan implementera på din modell för att få utpenningen att fungera. ViewType är optionellt här det standard är att TailwindANdDaisy men du kan sätta det till Custom, Plain eller Bootstrap om du vill använda en annan syn

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

Eller så kan du till och med specifikera en anpassad syn genom att använda tagHelper's use-local-view egendom.

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

Jag har också implementerat dessa i projektet för att ge en basnivå

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

I' kommer att omfatta sökfunktionen i en framtid artikel

TagHelper

Sedan listade jag ut hur jag skulle vilja att taghelparen såg ut i användandet:

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

Här kan ni se att jag har ställt några HTMX-parameter och modellen som ska användas för utsändningen.

Komponenten har också ett BUNCH av andra konfigurationselement som jag kommer att jobba med i framtida artiklar.

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

TagHelper är ganska enkelt men har en massa egenskaper som låter användaren anpassa beteendet ( du kan se det här i syn ) bortsett från egenskaperna | ( | som jag vann |' | skrev inte här för korthets skull ♫ ) | koden är rätt enkel ♫

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

Det består av dessa steg

  1. Inställ utgångens tag namn till div; det här är behållaren för pagern
  2. Ta bort alla egenskaper som inte är nödvändiga för den utformata innehållet ( men lämna kvar alla de som användaren har tillhandahållit
  3. Sätt den pagerId till en slumpmässig GUID om den inte är tillgänglig. ( det här används verkligen för kundenspezifische kod.
  4. Inställ linkUrl till den nuvarande vägen om det inte är tillgängligt - detta låter dig överrida det här om du vill använda en annan URL
  5. Seta PageSize , Page, ViewType, TotalItems and SearchTerm till modellen om den är tillgänglig eller standarden om den inte är tillgänglig IPagingModel och få skrivaren att fungera utan att konfigurera längre.
  6. Se ID-attributet till pagerId.
  7. Ta ViewComponentHelper från DI-containern och kontextualisera den med den nuvarande ViewContext.
  8. Skep en ny PagerViewModel med egenskaperna ställda till de vi har eller standardvärdena om de inte är tillgängliga.
  9. Invokera Pager Att se komponenten med PagerViewModel och sätter utgångens innehåll i resultatet.

Allt rätt enkelt igen.

Att titta på komponenten

Synen

Ansichten för ViewComponenten är ganska enkel; det ' är bara en loop genom sidorna och några länkar till de första

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>
}
Detta är uppdelat i några sektioner:
  1. Landning av sidans storlek
  2. Den första, sistaM SK1 följande och tidigare länkar
  3. Springa tillbaka och springa fram till länkarna
  4. På sidans länkar
  5. Den sidans information text

Bladsystorleken

En sak jag saknade från den ursprungliga taghelparen var en sidastorlek Det här är en enkel lista att välja , ni kan se att jag börjar med att definiera fixedSteps som är bara några små fixera steg jag vill använda för att dra ner.. Jag loopar igenom dem och lägger dem till listan.

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

Jag återskapar den till sidan

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

Ni kan se att jag optionellt använder några HTMX-attribute för att skicka sidans storlek till servern och uppdatera sidan medan jag behåller den nuvarande sidan och sökparametern.

Dessutom om du specifikerar use-htmx=false som en parameter på tagassistenten så kommer det inte ut ‘'’, men istället låter du använda lite JS som jag ger som HTML hjälpprogram för att uppdatera sidans storlek.

@Html.PageSizeOnchangeSnippet()
    

Det här är ett enkelt skript som kommer att uppdatera sidans storlek och ladda upp sidan igen. ( lägg märke till att detta inte fungerar.

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 integrering

HTMX-integrationen är ganska enkel eftersom HT MX kaskader till barnelement vi kan definiera HTX-parameterna på moderelementet och de kommer att härta sig

  • hx-boost="true" - detta använder det fina hx-boost-fenskap att interceptera den klickade händelsen och skicka begäran via HTMX.
  • hx-indikatorn="# laddandeM SK2modalt " | | detta har en laddande modal som visar medan applikationen bearbetas
  • hx-target="#användarenM SK2listanMska3 M Ska4 detta är elementet som svaret kommer att byta ut Mska5 i det här fallet är användarlistan M Ska6 Anteckningen Mske7 Det här innefattar just nu Pager för enkelhet ; du kan göra detta mer aktivt med Alpine M ska9 som i min tidigare artikel ) men den var bortom ramen den här gången
  • hx-swap

Loading Modal

Detta är ganska enkelt och använder DaisyUI, boxicons och Tailwind för att skapa en enkel laddande modal

<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-indikatorM SK1 laddande - modalt" så specifikerar att när en HTMX utmaning utförs ska den visa och sedan gömma den modalen

Framtidens egenskaper

Så det är uppenbarligen mycket mer att täcka på den här delen, 1, och ' som jag kommer att lägga till i framtida artiklar, m.in ;, inklusive exemplarsitet, , och m.i.m. alternativa synsätt,,, sökfunktionen och custom CSS.

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.