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
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
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).
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).
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.
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;
}
OrderByField
Méthode d'extensionOh 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";
}
C'est vraiment ça. C'est juste quelque chose dont j'avais besoin, alors je l'ai construit dans le paquet nuget.