Back to "A Paging View Component ASP.NET Core Tag Helper (Μέρος 1, κάπως...Μια Flippy Tag Helper)"

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 Paging View Component ASP.NET Core Tag Helper (Μέρος 1, κάπως...Μια Flippy Tag Helper)

Monday, 17 March 2025

Εισαγωγή

Έτσι, ενώ η οικοδόμηση ενός έργου αρχικά έφτιαξα το paging tag helper για Επίσης, κάνω κάμερα σε όλη την άλλη ανάγκη. Ένας τρόπος για να οικοδομήσουμε εύκολα τη λειτουργικότητα διαλογής για έναν πίνακα αποτελεσμάτων με υποστήριξη HtMX.

Οπότε... σας παρουσιάζω τον βοηθό του Φλίπι Τραπεζιού. Αυτό είναι απλά ένα γρήγορο άρθρο με πολύ κώδικα. Μπορείς πάντα να δεις. τα δείγματα εδώ. Και ο πηγαίος κώδικας όπως πάντα Είναι εδώ..

Είμαι αρκετά απαίσια παραδείγματα κτιρίων (είναι ένα slog innit) αλλά θα προσπαθήσω να προσθέσω όλο και περισσότερα καθώς προχωράω. Για εγκατάσταση:

dotnet add package mostlylucid.pagingtaghelper

Flippy TagHelper

Ο βοηθός ετικετών Flippy

Λοιπόν, τι είναι αυτό το πράγμα; Με λίγα λόγια είναι ένας τρόπος για να δημιουργήσετε μια κεφαλίδα τραπεζιού (ή οπουδήποτε αλλού πραγματικά) που θα σας επιτρέψει να ταξινομήσετε έναν πίνακα αποτελεσμάτων.

Στο πιο απλό (και χωρίς ενσωμάτωση HTMX) σας επιτρέπει να το κάνετε αυτό.

    <sortable-header column="@nameof(FakeDataModel.CompanyCity)"
                             current-order-by="@Model.OrderBy"
                             descending="@Model.Descending"
                             controller="Home"
                             use-htmx="false"
                             action="PageSortTagHelperNoHtmx"
                       >@Html.DisplayNameFor(x => x.Data.First().CompanyCity)
</sortable-header>

Εδώ μπορείτε να δείτε να καθορίσετε το όνομα στήλης, τη δοκιμή για την κεφαλίδα και ένα μέρος για να τοποθετήσετε πίσω (χάρη σε JetBrains.Σημειώσεις τα οποία μεταξύ πολλών άλλων, τα οποία μόλις και μετά βίας έχω γρατζουνίσει την επιφάνεια του να δώσει intellisense για το Controller και Action ονόματα).

Αυτό θα δημιουργήσει ένα σύνδεσμο που θα αναρτήσει πίσω στο PageSortTagHelperNoHtmx Δράση σχετικά με την Home ελεγκτής με το όνομα στήλης και την τρέχουσα σειρά ταξινόμησης και οποιεσδήποτε άλλες παραμέτρους στο URL (ελεγχόμενη με το auto-append-querystring γνώριμη ιδιότητα. Αυτό σας επιτρέπει εύκολα να πάρετε μια χρήσιμη σύνδεση μετά την επιστροφή, θα δείτε παρακάτω Το έκανα αρκετά ευέλικτο, όπου μπορείτε να καθορίσετε href / δράση & χειριστήριο και να πάρετε ένα σύνδεσμο πίσω με τις παραμέτρους που επισυνάπτονται.

    private void AddQueryStringParameters(TagHelperOutput output, bool newDescending)
    {
        string? href = "";

        // If Controller and Action are provided, generate URL dynamically
        if (!string.IsNullOrEmpty(Controller) && !string.IsNullOrEmpty(Action))
        {
            href = Url.ActionLink(Action, Controller);
          
        }
        else if (output.Attributes.ContainsName("href")) // If href is manually set, use it
        {
            href = output.Attributes["href"].Value?.ToString() ?? "";
        }
        if(string.IsNullOrEmpty(href)) throw new ArgumentException("No href was provided or could be generated");
        
        // If AutoAppend is false or href is still empty, don't modify anything
        if (!AutoAppend && !string.IsNullOrWhiteSpace(href))
        {
            output.Attributes.RemoveAll("href");
            output.Attributes.SetAttribute("href", href);
            return;
        }

        // Parse the existing URL to append query parameters
        var queryStringBuilder = QueryString.Empty
            .Add("orderBy", Column)
            .Add("descending", newDescending.ToString().ToLowerInvariant());

        // Preserve existing query parameters from the current request
        foreach (var key in ViewContext.HttpContext.Request.Query.Keys)
        {
            var keyLower = key.ToLowerInvariant();
            if (keyLower != "orderby" && keyLower != "descending") // Avoid duplicating orderBy params
            {
             queryStringBuilder=   queryStringBuilder.Add(key, ViewContext.HttpContext.Request.Query[key]!);
            }
        }
href+= queryStringBuilder.ToString();
        
        // Remove old href and set the new one with the appended parameters
        output.Attributes.RemoveAll("href");
        output.Attributes.SetAttribute("href", href);
    }

Αυτή είναι η μέθοδος που προσθέτει τις παραμέτρους ερωτηματογράφησης στο URL. Είναι πολύ απλό, παράγει ένα URL με βάση το Controller και Action χαρακτηριστικά ή αν έχετε θέσει το href Αυτό θα το χρησιμοποιήσει. Αν έχεις κανονίσει... AutoAppend να ψεύδεται θα χρησιμοποιήσει μόνο το href χαρακτηριστικό όπως είναι (μέσος όρος μπορείτε να κυλήσει το δικό σας για συγκεκριμένες περιπτώσεις χρήσης).

Crazy Config

Για να το κάνετε όσο το δυνατόν πιο χρήσιμο με / χωρίς HTMX. με / χωρίς Tailwind & DaisyUI κ.λπ. Σας δίνω ένα BUNCH των ιδιοτήτων για να χρησιμοποιήσετε για να ρυθμίσετε αυτό το σχετικά απλό έλεγχο

    [HtmlAttributeName("hx-controller")]
    [AspMvcController] // Enables IntelliSense for controller names
    public string? HXController { get; set; }

    [HtmlAttributeName("hx-action")]
    [AspMvcAction] // Enables IntelliSense for action names
    public string? HXAction { get; set; }
    
    
    [HtmlAttributeName("action")]
    [AspMvcAction]
    public string? Action { get; set; }
    
    [HtmlAttributeName("controller")]
    [AspMvcController]
    public string? Controller { get; set; }
    
    [ViewContext]
    [HtmlAttributeNotBound]
    public ViewContext ViewContext { get; set; }
    /// <summary>
    /// The column to sort by
    /// </summary>

    [HtmlAttributeName("column")] public string Column { get; set; } = string.Empty;

    /// <summary>
    /// Whether to auto-append any query string parameters
    /// </summary>

    [HtmlAttributeName("auto-append-querystring")] public bool AutoAppend { get; set; } = true;
    
    // <summary>
    /// Whether to use htmx ; specifcally used to set hx-vals
    /// </summary>

    [HtmlAttributeName("use-htmx")] public bool UseHtmx { get; set; } = true;

    /// <summary>
    /// The currently set order by column
    /// </summary>

    [HtmlAttributeName("current-order-by")]
    public string? CurrentOrderBy { get; set; }

    /// <summary>
    /// Sort direction, true for descending, false for ascending
    /// </summary>

    [HtmlAttributeName("descending")] public bool Descending { get; set; }

    
    /// <summary>
    ///  CSS class for the chevron up icon
    /// </summary>

    [HtmlAttributeName("chevron-up-class")]
    public string? ChevronUpClass { get; set; }
    
    /// <summary>
    ///  CSS class for the chevron down icon
    /// </summary>


    [HtmlAttributeName("chevron-down-class")]
    public string? ChevronDownClass { get; set; }
    
    /// <summary>
    /// The CSS class for the chevron when unsorted
    /// </summary>

    
    [HtmlAttributeName("chevron-unsorted-class")]
    public string? ChevronUnsortedClass { get; set; }

    /// <summary>
    /// The CSS class to use for the tag.
    /// </summary>

    [HtmlAttributeName("tag-class")] public string? TagClass { get; set; }

Μπορείτε να δείτε εδώ Έχω ιδιότητες για ΠΡΑΓΜΑΤΙΚΑ ΠΟΛΛΑ τα πάντα στον έλεγχο (δεν πρόκειται να τα περάσω όλα εδώ θα πρέπει να είναι αρκετά αυτο-εξηγητικά).

Με HTMX

Τώρα, όπως μπορείτε να έχετε μάθει Είμαι ένα HTMX NUT έτσι ως συνήθως αυτό υποστηρίζει HTMX αρκετά απρόσκοπτα: ΑΝΤΙΣΤΑΘΜΙΖΕΙ use-htmx Αληθινό που συμπληρώνει το hx-vals γνώριμη ιδιότητα. Μαζί με αυτό χρησιμοποιώ το Βοηθοί ετικετών HTMX για να κάνει τον κώδικα όσο το δυνατόν πιο απλό.

            <sortable-header column="Name"
                             current-order-by="@Model.OrderBy"
                             descending="@Model.Descending"
                             hx-get
                             hx-route-pagesize="@Model.PageSize"
                             hx-route-page="@Model.Page"
                             hx-route-search="@Model.SearchTerm"
                             hx-controller="Home"
                             hx-action="PageSortTagHelper"
                             hx-indicator="#loading-modal"
                             hx-target="#list"
                             hx-push-url="true">@Html.DisplayNameFor(x => x.Data.First().Name)
            </sortable-header>

Και αυτό είναι το πραγματικό... έργα με HTMX απρόσκοπτα.

Ο ελεγκτής MVC

Στη συνέχεια, δημοσιεύω αυτό πίσω σε ένα απλό MVC Controller που παράγει κάποια δεδομένα δειγμάτων:

    [Route("PageSortTagHelper")]
    public async Task<IActionResult> PageSortTagHelper(string? search, int pageSize = 10, int page = 1, string? orderBy = "", bool descending = false)
    {
        var pagingModel = await SortResults(search, pageSize, page, orderBy, descending);

        if (Request.IsHtmxBoosted() || Request.IsHtmx())
        {
            return PartialView("_PageSortTagHelper", pagingModel);
        }
        return View("PageSortTagHelper", pagingModel);
    }
    
     private async Task<OrderedPagingViewModel> SortResults(string? search, int pageSize, int page, string? orderBy, bool descending)
    {
        search = search?.Trim().ToLowerInvariant();
        var fakeModel = await dataFakerService.GenerateData(1000);
        var results = new List<FakeDataModel>();

        if (!string.IsNullOrEmpty(search))
            results = fakeModel.Where(x => x.Name.ToLowerInvariant().Contains(search)
                                           || x.Description.ToLowerInvariant().Contains(search) ||
                                           x.CompanyAddress.ToLowerInvariant().Contains(search)
                                           || x.CompanyEmail.ToLowerInvariant().Contains(search)
                                           || x.CompanyCity.ToLowerInvariant().Contains(search)
                                           || x.CompanyCountry.ToLowerInvariant().Contains(search)
                                           || x.CompanyPhone.ToLowerInvariant().Contains(search)).ToList();
        else
        {
            results = fakeModel.ToList();
        }

        if (!string.IsNullOrWhiteSpace(orderBy))
        {
            results = results.OrderByField(orderBy, descending).ToList();
        }

        var pagingModel = new OrderedPagingViewModel();
        pagingModel.TotalItems = results.Count();
        pagingModel.Page = page;
        pagingModel.SearchTerm = search;
        pagingModel.PageSize = pageSize;
        pagingModel.Data = results.Skip((page - 1) * pageSize).Take(pageSize).ToList();
        pagingModel.OrderBy = orderBy;
        pagingModel.Descending = descending;
        return pagingModel;
    }

Η OrderByField Μέθοδος επέκτασης

Ω και έχω μια μικρή μέθοδος επέκτασης που συσκευάζεται στην οποία εφαρμόζεται ένα ονομαζόμενο πεδίο παραγγελίας στα δεδομένα (ή IQueryable κ.λπ.). Το σύνολο της μεθόδου επέκτασης είναι παρακάτω. Μπορείτε να δείτε αυτό έχει 3 μεθόδους, ένα για ισχυρά δακτυλογραφημένα ονόματα στήλης, ένα για IQueryable ονόματα στήλη συμβολοσειρών και ένα για IE αριθμημένα.

using System.Linq.Expressions;
using System.Reflection;

namespace mostlylucid.pagingtaghelper.Extensions;

public static class QueryableExtensions
{
    public static IQueryable<T> OrderByField<T, TKey>(
        this IQueryable<T> source,
        Expression<Func<T, TKey>> keySelector,
        bool descending = false)
    {
        return descending ? source.OrderByDescending(keySelector) : source.OrderBy(keySelector);
    }


    public static IQueryable<T> OrderByField<T>(
        this IQueryable<T> source,
        string sortBy,
        bool descending = false)
    {
        if (string.IsNullOrWhiteSpace(sortBy))
            return source; // No sorting applied

        var property =
            typeof(T).GetProperty(sortBy, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
        if (property == null)
            throw new ArgumentException($"Property '{sortBy}' not found on type '{typeof(T)}'.");

        var param = Expression.Parameter(typeof(T), "x");
        var propertyAccess = Expression.MakeMemberAccess(param, property);
        var lambda = Expression.Lambda(propertyAccess, param);

        var methodName = descending ? "OrderByDescending" : "OrderBy";

        var resultExpression = Expression.Call(
            typeof(Queryable),
            methodName,
            new[] { typeof(T), property.PropertyType },
            source.Expression,
            Expression.Quote(lambda)
        );

        return source.Provider.CreateQuery<T>(resultExpression);
    }
    
    public static IEnumerable<T> OrderByField<T>(
        this IEnumerable<T> source,
        string sortBy,
        bool descending = false)
    {
        var property = typeof(T).GetProperty(sortPropertyName(sortBy), BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
        if (property == null)
            throw new ArgumentException($"Property '{sortBy}' not found on type '{typeof(T)}'.");

        return descending
            ? source.OrderByDescending(x => property.GetValue(x, null))
            : source.OrderBy(x => property.GetValue(x, null));
    }

    // Helper methods for readability (optional)
    private static string sortPropertyName(string sortBy) => sortBy.Trim();
    private static string methodName(bool descending) => descending ? "OrderByDescending" : "OrderBy";
}

Συμπέρασμα

Αυτό είναι πραγματικά αυτό. Είναι απλά κάτι που χρειαζόμουν και το έφτιαξα στο πακέτο.

logo

©2024 Scott Galloway