Back to "A Page View 组件 ASP.NET 核心标签辅助工具(巴骨第1部分)"

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

A Page View 组件 ASP.NET 核心标签辅助工具(巴骨第1部分)

Tuesday, 11 March 2025

一. 导言 导言 导言 导言 导言 导言 一,导言 导言 导言 导言 导言 导言

另一天的一个工作项目必须执行传呼表结果。 我的传呼标签助手 一直以来都是 达雷尔·奥尼尔(Darrel O'Neill)著, 正如我所写的 在这里 不管出于什么原因,它只是 停止工作 对我来说。 因此,我不再试图去弄乱什么看似是废弃的项目, 而是决定自己建造一个。

和往常一样,你可以得到源头 我的吉他露露,我的吉他露露,我的吉他露露,我的吉他露露,我的吉卜露,我的吉卜露,我的吉卜露,我的吉卜露,我的吉卜露,

我有这个项目的样本场地 主办于此

这里有类似输出的样本 :

用标签助手搜索演示g

所需资源所需资源所需资源所需资源所需资源所需资源所需资源所需资源所需资源所需资源所需资源

对于这个标签助手,我有一些要求:

  1. 工作应顺畅地配合 反尾风DisacyUI 调音界面;我更喜欢的CSS框架。
  2. 应与HTMX合作,不造成任何问题。
  3. 应该有一个使用 HTMX 翻转的页码下载( 因此, 如果您不使用 HTMX, 它应该仍然有效, 但是您需要添加一个按钮) 。
  4. 应容易配置和使用
    1. 接受一个传呼模型, 所以在 Razor 页面中使用简单化 。
    2. 应该能够配置一些简单的参数
  5. 应该是小菜一碟 这样你们这些伟大的人 就能玩它了
  6. 应该是两个视图元件和标签帮助器, 可以在 Razor 页面和视图中使用; 考虑到这一点, 它也应该有一个压倒一切的 Dafault.cshtml 视图。
  7. 使用简单的搜索功能。

今后我将增加以下能力:

  1. 添加自定义 CSS 以避免被绑在 DaisiUI 和 Tailwind 上, http://taghelpersample. mostlylylucid.net/Home/PlainView https://taghelpersample.net/Home/PlainView 网站 https://taghelpersample. mostlylulucid.net/Home/PlainView 网站 https://taghelpersample.net/Home/PlainView https://taghelpersample.
  2. 指定页面大小的能力
  3. 能够将自定义的 JS 调用添加到页面下调大小( 允许您不使用 HTMX ) 。
  4. 利用阿尔卑斯山让呼呼机更活跃、更反应更灵敏(就像我在 前一条文前一条文).

安装安装

标签助手现在是一个闪亮的新 Giget 软件包, 这样您就可以安装它, 使用以下命令 :

dotnet add package mostlylucid.pagingtaghelper

然后你会把标签助手添加到你的 _ViewImports.cshtml 像这样的文件 :

@addTagHelper *, mostlylucid.pagingtaghelper

然后您就可以开始使用它; 我提供一些帮助者课程, 您可以用这些课程来配置它, 例如

IPagingModel

这是你需要开始的“基本材料”。 这是一个简单的界面, 你可以在您的模型上实施 使传呼工作。 请注意 ViewType 默认为 TailwindANdDaisy 但您可以设置为 Custom, PlainBootstrap 如果想要使用不同的视图,则使用不同的视图。

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

OR 您甚至可以使用“标签帮助者”来指定自定义视图 use-local-view 财产。

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

我还在项目中执行了这些要求,以提供一个基线:

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

我将在未来的文章中报道搜索功能。

标签帮助者

然后我想出了我想让"标签求助者" 看上去像在使用中的东西:

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

这里您可以看到我设置了一些 HTMX 参数和用于传呼的模型。 我还设定了要显示的页面页数和要按请求发送的页眉( 允许我使用 HTMX 来填充页面 ) 。

本部分还包含一个包含其他配置要素的BUNCH, 我将在以后的文章中研究这些要素。 正如你可以看到的,那里有很多可能的配置。

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

标签帮助器非常简单, 但具有一系列属性, 使用户能够自定义行为( 您可以在下面 视图视图视图视图视图 除了属性(我不会在这里粘贴为简洁)之外,

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

其中包括以下步骤:

  1. 将输出标签名设为 div;这是呼叫器的容器。
  2. 删除已提供内容不需要的所有属性( 但留所有用户提供的属性, 这样可以进行简单的定制 ) 。
  3. 如果未提供, 请将 pagerID 设为随机的 GUID( 这实际上用于自定义代码, 您可以指定 ID, 或者让这个代码处理它 ) 。
  4. 如果不提供的话, 设置当前路径的链接Url - 如果您想要使用不同的 URL, 允许您覆盖此路径 。
  5. 如果提供了页面大小、页面、视图类型、项目总数和搜索术语,则将其设置为模型,如果不是,则设置默认。 这使我们能够通过 IPagingModel 并在没有进一步配置的情况下使用传呼机。
  6. 将 ID 属性设为calrId 。
  7. 从 DI 容器中获取“ View Conconent Helper”, 并将其与当前“ ViewContext” 相容。
  8. 创建新 PagerViewModel 属性与我们拥有的值或默认值一起设置,如果没有提供,则与默认值一起设置。
  9. 启动 Pager 查看 PagerViewModel 并设置结果的输出内容。

再简单不过了

视图功能

视图视图

ViewComponent 的视图非常简单; 它只是浏览页面的环页, 以及第一个、最后一个、下一个和前一个页面的链接 。

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>
}
这分为几节:
  1. 页面缩放大小
  2. 第一个、最后一个、下一个和以前的链接
  3. 向后跳过和向前跳过链接
  4. 页面链接
  5. 页面信息文本

页面缩放

我从最初的标签帮手中 漏掉了一个东西 就是一个页面大小的下调。 这是一个简单选择的列表, 您可以看到我首先从定义开始 fixedSteps 这些只是一些固定的步骤,我想用来进行下调。 然后我绕过这些,把它们添加到清单中。 我总是有一个“所有”选项的习惯,所以如果还没有,我就把全部项目加到清单中。

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

您可以看到我可选择使用一些 HTMX 属性将页面大小传送到服务器并更新页面,同时保留当前页面和搜索参数(如果有的话)。

此外,如果您怀疑 use-htmx=false 作为标签助手的参数, 它不会输出这些, 而是允许您使用一些 JSI 作为 HTML 助手来更新页面大小 。

@Html.PageSizeOnchangeSnippet()
    

这是一个简单的脚本, 将更新页面大小并重新加载页面( 请注意, 这对纯 CSS / 靴带尚未有效, 因为需要我找出属性名称等 ) 。

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一体化

HTMX的整合非常简单,因为HTMX是儿童元素的级联,我们可以确定父元素的HTMX参数,这些参数将被继承。

  • hx-boost="true" - 使用微软 hx 加速特性 来拦截点击事件并通过 HTMX 发送请求。
  • hx-indicator="#loaking-modal" - 这个装货模式将在处理请求时显示 。
  • hx- 目标= "#user- list" - 这是响应要交换的元素, 在此情况下是用户列表 。 注意: 目前包括简单化的“ 页面” 。 您可以使用阿尔卑斯山( 如我 前一条 )但这次超出了范围。
  • hx-swap="显示:无"

装入模式

这很简单,使用DaisyUI、Boxicons和尾风来制造一个简单的装货模式。

<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="#loaking-modal" 然后指定,当执行 HTMX 请求时, 它会挂着显示, 然后隐藏这个模式 。

未来特点

因此,这是第一部分,显然有一个LOT要覆盖的更多, 我会在未来的文章中写上, 包括样本现场, 替代视图,搜索功能 和定制的 CSS 。

logo

©2024 Scott Galloway