Компонент перегляду обробки, ASP. NET Core Tag Помічник (Part 1 - Отримувачів) (Українська (Ukrainian))

Компонент перегляду обробки, ASP. NET Core Tag Помічник (Part 1 - Отримувачів)

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.

Tuesday, 11 March 2025

//

Less than a minute

Вступ

На днях проект мав впроваджено впроваджену форму результатів. Допоміжний помічник отримання міток завжди був Pagination Tag Помічник Darrel O' Neil як я писав про тут Але з будь-якої причини це так. зупинено роботу для мене. Тож, замість того, щоб намагатися замислюватися над тим, що виглядає як покинуте проект, я вирішив сам побудувати його.

Як завжди, ви можете отримати джерело для цього на GitHub

Я маю зразок для цього проекту введений тут

Тут є зразки вихідних даних на зразок цього:

Демонстрація пошуку з допоміжними мітками

Вимоги

Для цього помічника міток у мене було декілька вимог:

  1. Потрібно безперешкодно працювати з Хвіршовий вітерweather condition і DaisUI; my destended CSS оболонки.
  2. Слід працювати з HTMX, не спричиняючи жодних проблем.
  3. Має бути спадний список сторінок, який використовує HTMX для перевертання (отже, якщо ви не використовуєте HTMX, це має працювати, але вам слід додати кнопку).
  4. Мусить бути простим налаштування і використання
    1. Прийнято для розмазування моделі, тому просто використовувати її на сторінці Razor
    2. Слід налаштувати за допомогою декількох простих параметрів
  5. Повинний бути пакунком нугета, щоб усі ви, чудові люди, могли погратися з ним.
  6. Має бути BOHT ViewComonent і TagHelper так, щоб його можна було використовувати як на сторінках, так і на переглядах. Пам' ятаючи про це, він повинен також мати перевизначений Dafault.cshtml вигляд.
  7. Працює з простою функцією пошуку.

У майбутньому я додам можливості до:

  1. Додати нетиповий CSS, щоб уникнути прив' язки до DaisUI і Tailwindoff ~ ~ я додав цю можливість вже бачив демонстрацію тут: https: // helpersample. method.net/ Home/PlainView
  2. Можливість визначення розмірів сторінки
  3. Можливість додавання нетипового виклику JS до спадного списку розмірів сторінки (щоб дозволити вам НЕ використовувати HTMX).
  4. Використовуйте альпійські, щоб зробити пейджер більш активним і чуйним (так, як я робив у моєму Попередня стаття).

Встановлення

Тепер інструмент довідки tags є чудовим новим пакунком Nuget, отже ви можете встановити його за допомогою такої команди:

dotnet add package mostlylucid.pagingtaghelper

Потім ти додаєш помічника до свого. _ViewImports.cshtml файл, схожий на такий:

@addTagHelper *, mostlylucid.pagingtaghelper

Після цього ви можете просто почати користуватися ним; я надаю вам допоміжні класи, які ви можете використовувати для налаштування, зокрема

IPagingModel

Це "шахрайські речі" вам потрібно почати. Це простий інтерфейс, який ви можете реалізувати на вашій моделі, щоб зробити розбій. Зауважте, що ViewType є необов' язковим тут, це типове значення TailwindANdDaisy але ви можете встановити його Custom, Plain або Bootstrap якщо ви хочете використовувати інший вид.

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

АБО ви навіть можете вказати власний перегляд за допомогою пункту меню МіткаHelper 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; }
}

Я огляну функціональність пошуку в наступній статті.

Інструмент довідки мітками

Після цього я вирахував, як би хотів, щоб програма TagHelper виглядала у використанні:

<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 інших елементів налаштування, які я працюватиму у наступних статтях. Як ви можете бачити, це LOT можливого налаштування.

<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 досить простий, але має декілька властивостей, які надають користувачеві змогу налаштувати поведінку програми (ви можете побачити це нижче у розділі) перегляд ) окрім властивостей (які я не вставлятиму тут для короткочасності) код досить просто:

    /// <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. Встановіть pigerId у випадковий GUID, якщо його не вказано (це дійсно використовується для нетипового коду, ви можете вказати ідентифікатор або просто надати цьому коду змогу його перевірити).
  4. Встановлює посиланняUrl на поточний шлях, якщо його не вказано - це надасть вам змогу перевизначити його, якщо ви бажаєте використовувати іншу адресу URL.
  5. Встановити розмір сторінки, сторінку, Тип перегляду, Загальні Італоги і SearchTerm для моделі, якщо її вказано, або типово, якщо це не так. Це дозволяє нам просто пройти в IPagingModel і змусити пейджер працювати без додаткових налаштувань.
  6. Встановити атрибут ID для pigerId.
  7. Отримати пункт ViewComponentHelper з контейнера DI і контекстуалізувати його за допомогою поточного тексту 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 які є лише кількома фіксованими кроками, які я хочу використати для падіння. Потім я прокручу їх і додаю до списку. У мене завжди є звичка - мати параметр "all," отже, я додаю елементи до списку, якщо їх там ще немає.

@{
    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, щоб передати розмір сторінки серверу і оновити сторінку з збереженням поточного параметра і пошуку (якщо такі є).

Додатково, якщо ви визначаєте if use-htmx=false як параметр помічника теґів, він не виведе ці дані, натомість надасть вам змогу використовувати декілька JS, які я надав як Помічник 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" - для цього використовується nifty можливість hx-boust щоб перехопити подію клацання і надіслати запит за допомогою HTMX.
  • hx- indicator="# завантаження- modal " - цей параметр містить модуль завантаження, який показується під час обробки запиту.
  • hx- target=" # список користувачів " - це елемент, який виміняєте місцями відповідь, у нашому випадку список користувачів. Зауваження: у поточній версії програми є пейджер для простоти; ви можете зробити це активнішим за допомогою альпійської (як і у моєму випадку). Попередня стаття ) але цього разу він був поза межами.
  • hx- swap=" show:noone "

Завантаження модалу

Це досить просто і використовує DaisUI, box icons і Tailwind, щоб створити простий завантажувальний модуль.

<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="# завантаження- modal " потім вказує, що, якщо буде виконано запит на HTMX, буде показано swud, а потім сховано цей модуль.

Майбутні можливості

Так що це частина 1, є очевидно, що LOT більше, щоб покрити, і я буду в наступних статтях; включаючи на зразок сайт, альтернативні погляди, функціональні можливості пошуку і нетиповий CSS.

logo

©2024 Scott Galloway