Back to "A Paging View Component ASP.NET Core Tag Helper (Teil 1, irgendwie...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 (Teil 1, irgendwie...A Flippy Tag Helper)

Monday, 17 March 2025

Einleitung

Während ich ein Projekt aufbaute, baute ich die Paging Tag Helfer für Ich habe auch eine Kamera über ANDEREN Bedarf. Eine Möglichkeit, einfach Sortierfunktionalität für eine Ergebnistabelle mit HtMX-Unterstützung zu erstellen.

Also... ich präsentiere Ihnen die Flippy Table Header Tag Helfer Ding. Dies ist nur ein kurzer Artikel mit einer Menge Code. Du kannst immer sehen die Proben hier......................................................................................................... Und der Quellcode wie eh und je ist hier.

Ich bin ziemlich schreckliche Baubeispiele (es ist ein Slog-Innit), aber ich werde versuchen, mehr und mehr hinzuzufügen, während ich mitgehe. Zum Installieren:

dotnet add package mostlylucid.pagingtaghelper

Flippy TagHelper

Der Flippy Tag Helfer

Also, was ist das für ein Ding? Kurz gesagt, es ist eine Möglichkeit, einen Tabellenkopf (oder irgendwo anders wirklich) zu generieren, mit dem Sie eine Tabelle der Ergebnisse sortieren können.

Am einfachsten (und ohne HTMX-Integration) können Sie dies tun.

    <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 sehen Sie den Spaltennamen, den Test für den Header und einen Ort, um zurück zu posten (danke an JetBrains.Anmerkungen die unter vielen anderen Dingen, die ich kaum an der Oberfläche des Gebens Intelsense für den Controller und Action-Namen gekratzt habe).

Dies wird einen Link erzeugen, der zurück zu den PageSortTagHelperNoHtmx Maßnahmen im Bereich der Home Controller mit dem Spaltennamen und der aktuellen Sortierreihenfolge und anderen Parametern in der URL (kontrolliert mit der auto-append-querystring Attribut. Auf diese Weise können Sie leicht einen nützlichen Postback-Link erhalten, Sie werden unten sehen Ich machte es ziemlich flexibel, wo Sie href / Aktion & Controller angeben können und einen Link zurück mit den Querystring-Parametern angehängt bekommen.

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

Dies ist die Methode, die die Querystring-Parameter an die URL anhängt. Es ist ziemlich einfach, es erzeugt eine URL auf der Grundlage der Controller und Action Attribute oder wenn Sie die href Attribut es verwendet, dass. Wenn Sie eingestellt haben AutoAppend zu false es wird nur die href Attribut wie es ist (bedeutet, dass Sie Ihre eigenen für bestimmte Anwendungsfälle rollen können).

Verrückte Konfig

Um dies mit / ohne HTMX so nützlich wie möglich zu machen. mit / ohne Tailwind & DaisyUI etc. Ich habe Ihnen eine BUNCH Eigenschaften zu verwenden, um diese relativ einfache Steuerung zu konfigurieren

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

Sie können hier sehen Ich habe Eigenschaften für PRETTY MUCH alles in der Kontrolle (Ich werde nicht durch sie alle hier gehen, sie sollten ziemlich selbsterklärend sein).

Mit HTMX

Jetzt, wie Sie vielleicht gelernt haben, bin ich ein HTMX NUT, so wie üblich unterstützt dies HTMX ziemlich nahtlos: Es ENDGÜLT, use-htmx true, die in der hx-vals Attribut. Zusammen mit diesem verwende ich die HTMX-Tag-Helfer um den Code so einfach wie möglich zu machen.

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

Und das ist es wirklich nur Arbeiten mit HTMX nahtlos.

Der MVC-Controller

Ich poste dies dann zurück zu einem einfachen MVC-Controller, der einige Beispieldaten generiert:

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

Das OrderByField Erweiterungsverfahren

Oh und ich habe eine kleine Erweiterungsmethode verpackt, in der ein benanntes Bestellfeld auf die Daten angewendet wird (oder IQueryable usw.). Die ganze Erweiterungsmethode ist unten. Sie können sehen, dass dies 3 Methoden hat, eine für starke eingegebene Spaltennamen, eine für IQueryable String Spaltennamen und eine für 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";
}

Schlussfolgerung

Das ist es wirklich. Es ist nur etwas, das ich brauchte, also habe ich es in das Nuget-Paket gesteckt.

logo

©2024 Scott Galloway