Back to "A Paging View Component ASP.NET Core Tag Helper (Deel 1.1, soort van...A 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 (Deel 1.1, soort van...A Flippy Tag Helper)

Monday, 17 March 2025

Inleiding

Dus tijdens het bouwen van een project bouwde ik oorspronkelijk de paging tag helper voor Ik kijk ook naar een andere behoefte. Een manier om eenvoudig sorteerfunctionaliteit te bouwen voor een tabel met resultaten met HtMX ondersteuning.

Ik presenteer je de Flippy Table Header tag helper dingetje. Dit is gewoon een snel artikel met een LOT van code. Je kunt altijd zien de monsters hier. En de broncode zoals altijd is hier.

Ik ben behoorlijk verschrikkelijk gebouw voorbeelden (het is een slog innit) maar ik zal proberen om meer en meer toe te voegen als ik ga mee. Te installeren:

dotnet add package mostlylucid.pagingtaghelper

Flippy TagHelper

The Flippy Tag Helper

Wat is dit voor ding? Kortom, het is een manier om een tabelkop te genereren (of waar dan ook) waarmee je een tabel met resultaten kunt sorteren.

Op zijn eenvoudigste (en zonder HTMX integratie) laat het je dit doen.

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

Hier kunt u zien dat u de kolomnaam, de test voor de koptekst en een plaats om terug te posten (dankzij JetBrains.Annotaties die onder vele andere dingen, die ik heb nauwelijks gekrast het oppervlak van geven intellisense voor de Controller en Actie namen).

Dit zal leiden tot een link die zal post terug naar de PageSortTagHelperNoHtmx De Commissie heeft een voorstel voor een richtlijn betreffende de onderlinge aanpassing van de wettelijke en bestuursrechtelijke bepalingen der Lid-Staten inzake de indeling, de verpakking en het kenmerken van gevaarlijke stoffen ingediend. Home controller met de kolomnaam en de huidige sorteervolgorde en alle andere parameters in de URL (bediend met de auto-append-querystring attribuut. Hierdoor kunt u gemakkelijk een nuttige postback link krijgen, zie je hieronder Ik maakte het vrij flexibel waar u kunt opgeven href / actie & controller en krijg een link terug met de querystring parameters toegevoegd.

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

Dit is de methode die de querystring parameters toevoegt aan de URL. Het is vrij eenvoudig, het genereert een URL op basis van de Controller en Action attributen of als u de href attribuut dat het zal gebruiken. Als u heeft ingesteld AutoAppend om vals het zal gewoon gebruik maken van de href attribuut zoals is (meanign kun je je eigen roll voor specifieke use-cases).

Crazy-configuratie

Om dit zo handig mogelijk te maken met / zonder HTMX. met / zonder Tailwind & DaisyUI etc Ik heb je een BUNCH van eigenschappen gegeven om deze relatief eenvoudige bediening te configureren

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

Jullie kunnen hier zien dat IK eigenschappen heb voor PRETY Much alles in de controle (ik ga hier niet allemaal doorheen ze zouden vrij zelfverklaarbaar moeten zijn).

Met HTMX

Zoals je wellicht hebt geleerd ben ik een HTMX NUT dus zoals gebruikelijk ondersteunt dit HTMX vrij naadloos: Het is van mening dat de use-htmx Waarheid die invult in de hx-vals attribuut. Samen met dit gebruik ik de HTMX Tag Helpers om de code zo eenvoudig mogelijk te maken.

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

En dat is het echt. Het is gewoon... werken met HTMX naadloos.

De MVC-controller

Ik post dit dan terug naar een eenvoudige MVC Controller die enkele monstergegevens genereert:

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

De OrderByField Uitbreidingsmethode

Oh en ik hebben een kleine extensie methode verpakt waarin past een genoemde order veld op de gegevens (of IQueryable enz.). De hele extensie methode is hieronder. Je kunt zien dat dit 3 methoden heeft, een voor sterke getypte kolomnamen, een voor IQueryable string column namen en een voor IEnumerables.

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

Conclusie

Dat is het echt. Het is gewoon iets wat ik nodig had dus ik bouwde het een gestoken in het nuget pakket.

logo

©2024 Scott Galloway