Βοηθός ετικέτας πυρήνα ASP.NET (Part 2, PageSize) ( ελληνικά (Greek_)

Βοηθός ετικέτας πυρήνα ASP.NET (Part 2, PageSize)

Comments

NOTE: Apart from English (and even then it's questionable, I'm Scottish). These are machine translated in languages I don't read. If they're terrible please contact me.
You can see how this translation was done in this article.

Monday, 17 March 2025

//

Less than a minute

Εισαγωγή

Ως μέρος της συνεχιζόμενης σοφίας μου με paging tag helper έχω τώρα χωρίσει PageSize στο δικό του βοηθό ετικέτας. Αυτό είναι για να κάνει την ετικέτα βοηθός πιο ευέλικτη και να επιτρέψει για πιο περίπλοκα σενάρια τηλεειδοποίησης.

Και πάλι, όλα αυτά είναι σε εξέλιξη. Είμαι ήδη... έως 0,9.0 τη στιγμή της γραφής, αλλά να χρησιμοποιείτε με δικό σας κίνδυνο. Είναι δωρεάν αλλά ΔΕΝ YET της ποιότητας απελευθέρωσης IMHO (Δεν έχω καν πλήρη δείγματα Ακόμα!

Δείτε το κωδικός για τον βοηθό ετικετών εδώ.

Βοηθός ετικέτας μεγέθους σελίδας

Όπως και με άλλες taghelpers μου αυτό έχει σχεδιαστεί για μια περίπτωση χρήσης που συναντώ συχνά? πώς να αλλάξετε εύκολα το μέγεθος σελίδας σε λίστες αποτελεσμάτων. Με την πρώτη ματιά αυτό φαίνεται αρκετά απλό, αλλά μπορεί να γίνει TRICKY μόλις προσθέσετε HTMX στο μείγμα.

Απαιτήσεις

Έτσι, για τη χρήση μου, οι απαιτήσεις ήταν οι ακόλουθες:

  1. Επιτρέπει την εύκολη χρήση για teh developer (όσο το δυνατόν περισσότερο διαμορφώνεται χρησιμοποιώντας ένα απλό μοντέλο σελίδα)
  2. Υποστηρίζει την ενημέρωση του μεγέθους της σελίδας χρησιμοποιώντας HTMX - που σημαίνει ότι είναι σε θέση να αναρτήσει πίσω διατηρώντας όλες τις παραμέτρους συμβολοσειράς ερωτημάτων. Αυτό είναι σημαντικό καθώς επιτρέπει στον χρήστη να αλλάξει το μέγεθος της σελίδας χωρίς να χάνει άλλα φίλτρα που έχουν εφαρμόσει.
  3. Έχει μια κάπως δυναμική γκάμα μεγεθών σελίδων (συμπεριλαμβανομένης μιας επιλογής 'Όλων).

Ο Βοηθός Ετικέτας

Με αυτό κατά νου σχεδίασα το TagHelper να είναι ως εξής:

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

Έτσι, σε αυτή την περίπτωση και πάλι παίρνει πρότυπο μου paging μοντέλο:

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

(') Δελτίο ΕΚ 6-1982, σημείο 2.1.78. Page, TotalItems, PageSize, ViewType και LinkUrl για το τηλεφώνημα.

Μπορείτε επίσης να τα προσδιορίσετε με τη μορφή χαρακτηριστικών του μοντέλου:


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

Πού στο... hx-target Εδώ είναι ο στόχος για το αίτημα HTMX και το hx-swap είναι ο στόχος για την απάντηση.

Σε αυτή την περίπτωση θα χρησιμοποιήσει το καθορισμένο PageSize και TotalItems και ViewType για να καταστήσει τον επιλογέα μεγέθους σελίδας.

JS Shenanigangans

Δυστυχώς αυτή τη φορά είχα μια απαίτηση που δεν μπορούσα να χειριστώ καθαρά την πλευρά του διακομιστή. Δηλαδή ήθελα να διατηρήσω τους παραματιστές εγχόρδων στις σελίδες. Για να επιτευχθεί αυτό, έχω δύο κομμάτια JS που θα φορτώσουν στον έλεγχο ανάλογα με το HTMX που χρησιμοποιείται oor δεν

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

Εάν χρησιμοποιείται HTMX, το καθιστά αυτό στη σελίδα:

(() => {
    if (window.__pageSizeListenerAdded) return;

    document.addEventListener('htmx:configRequest', event => {
        const { elt } = event.detail;
        if (elt?.matches('[name="pageSize"]')) {
            const params = new URLSearchParams(window.location.search);
            params.set('pageSize', elt.value); // This will update or add the pageSize param
            event.detail.parameters = Object.fromEntries(params.entries());
        }
    });
    window.__pageSizeListenerAdded = true;
})();

Το οποίο είναι ένα απλό κομμάτι JavaScript το οποίο θα εισάγει τις τρέχουσες παραμέτρους ερωτηματογραφίας στο αίτημα.

Εναλλακτικά, εάν το HTMX ΔΕΝ είναι ενεργοποιημένο, αυτό θα το καταστήσει:

(function attachPageSizeListener() {
    // Ensure we only attach once if this script is included multiple times
    if (window.__pageSizeListenerAttached) return;
    window.__pageSizeListenerAttached = true;

    // Wait for the DOM to be fully loaded
    document.addEventListener("DOMContentLoaded", () => {
        document.body.addEventListener("change", handlePageSizeChange);
    });

    function handlePageSizeChange(event) {
        // Check if the changed element is a page-size <select> inside .page-size-container
        const select = event.target.closest(".page-size-container select[name='pageSize']");
        if (!select) return;

        const container = select.closest(".page-size-container");
        if (!container) return;

        // Default to "true" if there's no .useHtmx input
        const useHtmx = container.querySelector("input.useHtmx")?.value === "true";
        if (useHtmx) {
            // If using HTMX, we do nothing—HTMX will handle the request
            return;
        }

        // Either use a linkUrl from the container or the current page URL
        const linkUrl = container.querySelector("input.linkUrl")?.value || window.location.href;
        const url = new URL(linkUrl, window.location.origin);

        // Copy existing query params from current location
        const existingParams = new URLSearchParams(window.location.search);
        for (const [key, value] of existingParams.entries()) {
            url.searchParams.set(key, value);
        }

        // If user picked the same pageSize as what's already in the URL, do nothing
        if (url.searchParams.get("pageSize") === select.value) {
            return;
        }

        // Update the pageSize param
        url.searchParams.set("pageSize", select.value);

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

Μια αλλαγή που θα κάνω είναι να αλλάξω το όνομα της μεταβλητής "σημαία." __pageSizeListenerAttached καθώς είναι λίγο πολύ γενικό και το χρησιμοποιώ τόσο για αιτήματα HTMX όσο και για μη HTMX.

Ο ΚΩΔΙΚΟΣ

Ο ίδιος ο βοηθός ετικετών είναι αρκετά απλός, ο κύριος κώδικας απλά κοσμεί μερικές ιδιότητες και στη συνέχεια καθιστά την προβολή Component.

 public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        // We want to render a <div> by default
        output.TagName = "div";

        // Remove any leftover attributes that we handle ourselves
        RemoveUnwantedAttributes(output);

        // Determine final PagerId
        var pagerId = PagerId ?? PageSizeModel?.PagerId ?? $"pager-{Guid.NewGuid():N}";
        // Assign ID to the outer div
        output.Attributes.SetAttribute("id", pagerId);
        // Build or fallback to a link URL
        var finalLinkUrl = BuildLinkUrl();
        if (string.IsNullOrEmpty(finalLinkUrl))
        {
            // If we can't build a URL, show a fallback message or short-circuit
            output.Content.SetHtmlContent(
                "<p style=\"color:red\">No valid link URL found for PageSize control.</p>");
            return;
        }

        var finalPageSize = PageSize ?? PageSizeModel?.PageSize ?? 10;
        // Clamp the page size to MaxPageSize

        var finalTotalItems = TotalItems ?? Model?.TotalItems ?? PageSizeModel?.TotalItems ?? 0;
        if (finalTotalItems == 0) throw new ArgumentNullException(nameof(finalTotalItems), "TotalItems is required");
        var maxPageSize = Math.Min(finalTotalItems, MaxPageSize);

        // Fallback to model's properties if not set
        var finalViewType = PageSizeModel?.ViewType ?? Model?.ViewType ?? ViewType;

        var pageSizeSteps = new int[] { 10, 25, 50, 75, 100, 125, 150, 200, 250, 500, 1000 };

        
        if (!string.IsNullOrEmpty(PageSizeSteps))
            pageSizeSteps = PageSizeSteps.Split(',').Select(s =>

            {
                if (int.TryParse(s, out var result))
                    return result;
                else
                {
                    throw new ArgumentException("Invalid page size step", nameof(PageSizeSteps));
                }
            }).ToArray();


        var pageSizes = CalculatePageSizes(finalTotalItems, maxPageSize, pageSizeSteps);

        var useHtmx = PageSizeModel?.UseHtmx ?? UseHtmx;
        var useLocalView = PageSizeModel?.UseLocalView ?? UseLocalView;

        // Acquire the IViewComponentHelper
        var viewComponentHelper = ViewContext.HttpContext.RequestServices
            .GetRequiredService<IViewComponentHelper>();
        ((IViewContextAware)viewComponentHelper).Contextualize(ViewContext);

        // Construct the PageSizeModel (if not provided) with final settings
        var pagerModel = new PageSizeModel
        {
            ViewType = finalViewType,
            UseLocalView = useLocalView,
            UseHtmx = useHtmx,
            PageSizes = pageSizes,
            PagerId = pagerId,
            Model = Model,
            LinkUrl = finalLinkUrl,
            MaxPageSize = maxPageSize,
            PageSize = finalPageSize,
            TotalItems = finalTotalItems
        };

        // Safely invoke the "PageSize" view component
        try
        {
            var result = await viewComponentHelper.InvokeAsync("PageSize", pagerModel);
            output.Content.SetHtmlContent(result);
        }
        catch (Exception ex)
        {
            // Optional: Log or display an error
            output.Content.SetHtmlContent(
                $"<p class=\"text-red-500\">Failed to render PageSize component: {ex.Message}</p>"
            );
        }
    }

Ένα δύσκολο κομμάτι χειρίζεται σωστά το PageSizeSteps γνώριμη ιδιότητα. Αυτή είναι μια ξεχωριστή λίστα με τα μεγέθη σελίδων από τα οποία μπορεί να επιλέξει ο χρήστης. Ή μπορείς να περάσεις στα δικά σου βήματα. Μπορείτε επίσης να καθορίσετε το MaxPageSize που είναι το μέγιστο μέγεθος σελίδας που μπορεί να επιλεγεί.

    private List<int> CalculatePageSizes(int totalItems, int maxxPageSize, int[] pageSizeSteps)
    {
        var pageSizes = new List<int>();

        // 1. Include all fixed steps up to both TotalItems and MaxPageSize
        foreach (var step in pageSizeSteps)
        {
            if (step <= totalItems && step <= maxxPageSize)
                pageSizes.Add(step);
        }

        // 2. If TotalItems exceeds the largest fixed step, keep doubling
        int lastFixedStep = pageSizeSteps.Last();
        if (totalItems > lastFixedStep)
        {
            int next = lastFixedStep;
            while (next < totalItems && next < maxxPageSize)
            {
                next *= 2; // double the step
                if (next <= totalItems && next <= maxxPageSize)
                {
                    pageSizes.Add(next);
                }
            }
        }

        // 3. Include TotalItems if it's not already in the list
        if (totalItems <= maxxPageSize && !pageSizes.Contains(totalItems))
        {
            pageSizes.Add(totalItems);
        }

        pageSizes.Sort();

        return pageSizes;
    }

Όπως και με τον άλλο μου βοηθό ετικετών χρησιμοποιώ το TagHelper->ViewComponent μοτίβο για να κάνω το εξάρτημα προβολής. Αυτό μου επιτρέπει να κρατήσω τον βοηθό ετικετών απλό και το σύμπλεγμα συστατικών προβολής. Σας επιτρέπει επίσης να αλλάξετε απόψεις για διαφορετικές εμπειρίες; ή ακόμη και να ενέσετε τη δική σας άποψη και να χρησιμοποιήσετε ότι (χρησιμοποιώντας use-local-view).


public class PageSizeViewComponent : ViewComponent
    {
        public IViewComponentResult Invoke(PageSizeModel model)
        {
            if(model.Model != null)
            {
                model.LinkUrl ??= model.Model.LinkUrl;
                
                if(model.Model is IPagingSearchModel searchModel)
                {
                    model.SearchTerm ??= searchModel.SearchTerm;
                }
            }
            
            var viewName = "Components/Pager/Default";

            var useLocalView = model.UseLocalView;

            return (useLocalView, model.ViewType) switch
            {
                (true, ViewType.Custom) when ViewExists(viewName) => View(viewName, model),
                (true, ViewType.Custom) when !ViewExists(viewName) => throw new ArgumentException("View not found: " + viewName),
                (false, ViewType.Bootstrap) => View("/Areas/Components/Views/PageSize/BootstrapView.cshtml", model),
                (false, ViewType.Plain) => View("/Areas/Components/Views/PageSize/PlainView.cshtml", model),
                (false, ViewType.TailwindAndDaisy) => View("/Areas/Components/Views/PageSize/Default.cshtml", model),
                _ => View("/Areas/Components/Views/PageSize/Default.cshtml", model)
            };

            // If the view exists in the app, use it; otherwise, use the fallback RCL view
        }
        /// <summary>
        /// Checks if a view exists in the consuming application.
        /// </summary>

        private bool ViewExists(string viewName)
        {
            var result = ViewEngine.FindView(ViewContext, viewName, false);
            return result.Success;
        }
    }

Συμπέρασμα

Λοιπόν, αυτό ήταν για τον βοηθό ετικέτας PageSize. Ακόμα δουλεύω πάνω στα έγγραφα και τα παραδείγματα αλλά ο κώδικας είναι καλύτερος καθημερινά.

logo

©2024 Scott Galloway