# Un componente per la visuale di Paging ASP.NET Core Tag Helper (Parte 1.1, una specie di ...Un Flippy tag HelperM SK4

<datetime class="hidden">2025-03-17T22:12</datetime>

<!--category-- ASP.NET Core, TagHelper, PagingTagHelper -->
# Introduzione

Quindi mentre stavo costruendo un progetto inizialmente ho costruito [Paging tag helper per ](https://www.mostlylucid.net/blog/category/PagingTagHelper) Ho anche superato un'altra necessità. Un modo per costruire facilmente la funzione di classificazione per una tabella di risultati con il supporto HtMX.

Quindi...Voglio presentarvi il Flippy Table Header tag helper.
Questo è solo un rapido articolo con un sacco di codice. Potete sempre vedere [I campioni qui](https://taghelpersample.mostlylucid.net/). E il codice sorgente come sempre. [è qui.](https://github.com/scottgal/mostlylucid.pagingtaghelper).

I' sono esempi piuttosto terribili di costruzione M SK1it' è un problema innit ) ma ioMSC4 cercherò di aggiungere sempre di più mentre vado avanti
Per installare:

```bash
dotnet add package mostlylucid.pagingtaghelper
```

![Flippy TagHelper](flippyheaders.png?width=1000&format=webp)

[TOC]

# L'aiuto della targhetta Flippy

Quindi, cos'è questa cosa?

E' il più semplice (e senza integrazione HTMX) ti permette di fare questo .

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

Qui potete vedere che si indica il nome della colonna, l'esperimento per la testa e un posto dove mandare indietro ( grazie a [JetBrains.Note](https://www.nuget.org/packages/Jetbrains.Annotations) che tra tante altre cose, che io' ho scarcemente schiacciato la superficie di dare intellisense per i nomi del Controller e dell'Action ).

Questo genererà un link che ritornerà al sito. `PageSortTagHelperNoHtmx` L'azione sulla `Home` controller con il nome della colonna e l'ordine di сортazione attuale e qualsiasi altro parametro nella URL (controllato con `auto-append-querystring` attribute. Questo vi permette di ottenere facilmente un link utile per il replico , voiM SK2 vedrete sotto l'ho fatto abbastanza flessibile dove potete especificare href / azione & controller e ottenere un link indietro con i parametri delle stringhe di query aggiustati.

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

```

Questo è il metodo che aggiunge i parametri delle stringhe di query alla URL. E' piuttosto semplice, genera una URL basata sulla `Controller` e `Action` attributi o se avete stabilito la `href` l'attributo userà quel. Se avete' `AutoAppend` per falsificare si usano solo i `href` attribute come (meanign potete ruotare il vostro per un uso specificoM SK1cases).

## Crazy Config

Per renderlo il più utile possibile con / senza HTMX. con \/ senza Tailwind & DaisyUI etc IM SK4 vi ho dato una BUNCH di proprietà da usare per configurere questo controllo relativamente semplice.

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

Potete vedere qui che ho proprietà per PRETTY MUCH tutto nel controllo (IM SK1 non le passerò tutte qui dovrebbero essere piuttosto autosufficienti

# Con HTMX

Ora, come forse avete imparato io' sono un HTMX NUT così come di solito questo supporta l'HTMX abbastanza facilmente:
FALLisce `use-htmx` true che riempie il `hx-vals` attribute. Oltre a questo uso il[ Aiutori di Tag HTMX](https://github.com/khalidabuhakmeh/Htmx.Net/tree/bbc9de911a723bff7cd0884aa566ed68502fa94b) per rendere il codice il più semplice possibile.

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

```

E bene, questo è quello che è successo. *I lavori* con HTMX senza fili.

# Il Controller MVC

Poi lo riporto a un semplice MVC Controller che genera alcuni dati di campione:

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

```

## L'informazione figura nella parte dispositiva. `OrderByField` Metoda di estensione

Oh e ho un piccolo metodo di estensione che applica un campo di ordine chiamato ai dati (or `IQueryable` etc). T|l'intero metodo di estensione è sottoM SK2 Potete vedere che questo ha MST3 metodiMSST4 uno per i nomi delle colonne forte da scrivere MST5 uno dei nomi delle stringhe IQueryable e uno degli IEnumerabiliMSC6

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

# Conclusione

Quella ' è veramente quella ., la "M SK2" è solo una cosa di cui avevo bisogno, così l'ho costruita e l' ho fissata nel pacco del nuget.