Back to "Un composant de visionnage ASP.NET Core Tag Helper (Partie 1.1, en quelque sorte...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

Un composant de visionnage ASP.NET Core Tag Helper (Partie 1.1, en quelque sorte...A Flippy Tag Helper)

Monday, 17 March 2025

Présentation

Donc, tout en construisant un projet, j'ai initialement construit le tag de recherche helper pour J'ai aussi besoin d'une caméra. Une façon de construire facilement la fonctionnalité de tri pour une table de résultats avec le support HtMX.

Donc... je vous présente le Flippy Table Header tag helper thingy. C'est juste un article rapide avec un LOT de code. Tu peux toujours voir les échantillons iciC'est ce que j'ai dit. Et le code source comme toujours C'est ici..

Je suis des exemples de construction assez horribles (c'est un slog innit) mais j'essaierai d'ajouter de plus en plus au fur et à mesure. Pour installer :

dotnet add package mostlylucid.pagingtaghelper

Flippy TagHelper

L'aide à l'étiquette Flippy

Alors c'est quoi ce truc? Bref, c'est une façon de générer un en-tête de table (ou n'importe où ailleurs vraiment) qui vous permettra de trier une table de résultats.

À son plus simple (et sans intégration HTMX), il vous permet de le faire.

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

Ici vous pouvez voir que vous spécifiez le nom de la colonne, le test pour l'en-tête et un endroit pour poster en arrière (merci à JetBrains.Annotations qui entre bien d'autres choses, que j'ai à peine éraflé la surface de donner l'intelligence pour les noms de contrôleur et d'action).

Cela générera un lien qui affichera de nouveau à la PageSortTagHelperNoHtmx suite donnée à la Home controller avec le nom de la colonne et l'ordre de tri courant et tout autre paramètre dans l'URL (commandé avec le auto-append-querystring attribut. Cela vous permet d'obtenir facilement un lien post-retour utile, vous verrez ci-dessous je l'ai rendu assez flexible où vous pouvez spécifier href / action & controller et obtenir un lien de retour avec les paramètres de la chaîne de requête annexés.

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

C'est la méthode qui ajoute les paramètres de la chaîne de requête à l'URL. C'est assez simple, il génère une URL basée sur le Controller et Action attributs ou si vous avez défini le href attribut il utilisera cela. Si vous avez réglé AutoAppend pour false il utilisera juste le href attribut tel qu'il est (signifie que vous pouvez rouler votre propre pour des cas d'utilisation spécifiques).

Crazy Config

Pour rendre cela aussi utile que possible avec / sans HTMX. avec / sans Tailwind & DaisyUI etc Je vous ai donné un BUNCH de propriétés à utiliser pour configurer ce contrôle relativement simple

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

Vous pouvez voir ici que j'ai des propriétés pour PRETTY MUCH tout ce qui est sous le contrôle (je ne vais pas les traverser tous ici ils devraient être assez explicites).

Avec HTMX

Maintenant, comme vous l'avez peut-être appris, je suis un HTMX NUT donc comme d'habitude, cela supporte HTMX de manière assez transparente: Il DÉFAUT use-htmx vrai qui remplit le hx-vals attribut. Avec ça, j'utilise le Aide-étiquettes HTMX de rendre le code aussi simple que possible.

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

Et bien c'est ça qui l'est vraiment juste Travaux avec HTMX sans problème.

Le contrôleur du MVC

Je l'affiche ensuite sur un simple contrôleur MVC qui génère quelques données d'échantillon:

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

Les OrderByField Méthode d'extension

Oh et j'ai une petite méthode d'extension dans laquelle applique un champ d'ordre nommé aux données (ou IQueryable C'est ce que l'on appelle "l'Europe de l'Est", c'est-à-dire "l'Europe de l'Est", c'est-à-dire "l'Europe de l'Est" et "l'Europe de l'Est", c'est-à-dire "l'Europe de l'Est", c'est-à-dire "l'Europe de l'Est" et "l'Europe de l'Est", c'est-à-dire "l'Europe de l'Est", c'est-à-dire "l'Europe de l'Est" et "l'Europe de l'Est". La méthode d'extension complète est ci-dessous. Vous pouvez voir ceci a 3 méthodes, une pour les noms de colonnes dactylographiées, une pour les noms de colonnes de chaînes IQueryable et une pour les noms de colonnes 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";
}

En conclusion

C'est vraiment ça. C'est juste quelque chose dont j'avais besoin, alors je l'ai construit dans le paquet nuget.

logo

©2024 Scott Galloway