Das Plugin Konzept von Jquery hat was. Man nehme eine HTML Table und holt sich ein JavaScript Library wie bootstrap-sortable.js ins Projekt. Dann noch die CSS Klasse sortable in jedes TH Element und fertig.
Mit Blazor geht das leider nicht so einfach. Da jede Component seinen eigenen Renderer und State hat, ist es unklug, wenn man mit einem Extension Konzept andere Components da reinpfuschen lässt. Blazor will alles per Binding im DOM erledigen.
Das Ziel: Mit Blazor eine sortable HTML Tabelle mit einer wiederverwendbaren Komponente
Der Benutzer klickt auf den Kopf der Tabelle und der Inhalt wird sortiert. Klickt der Benutzer noch einmal darauf ändert sich die Reihenfolge.
Die Idee
Zunächst kommt uns entgegen, dass eine Blazor Komponente dauerhaft seinen Status behält. Also auch eine Liste von Firmen. Einmal aus der Datenbank geladen, ist sie im Arbeitsspeicher verfügbar und per LINQ einfach und schnell sortierbar.
Ein weiteres Schmankerl von Blazor Components ist das Parameter Binding, bzw das Binden von unbekannten Parametern. So muss man nicht jeden Parameter einzeln spezifizieren, sondern lässt sich alles unbekannt per CaptureUnmatchedValues in ein Dictionary legen. Damit wird in meiner Sortierungs Lösung die TD Deklaration erledigt. Konvention SQL Feldname = Spalten Titel
1: <table class="table table-striped
table-borderd table-condensed sortable">
2: <thead>
3: <TRSortable ADR_ManagedID="ID"
4: GroupName="Firma"
5: Bearbeiter="Bearbeiter"
6: Anzahl="Anzahl"
7: LastBill="LastBill"
8: OnSort="Sorted"></TRSortable>
Lediglich eine OnSort Methode wird aus der aufrufenden Blazor Page benötigt um die Liste (aus SQL Server) neu sortieren zu können.
Der Vollständigkeit halber der Code der per Entity Framework die Daten initial lädt
1: List<AdrManagedBI> ListManaged;
2: protected override async Task OnInitializedAsync()
3: {
4: var query = from k in db.AdrManageds.
Include(managed => managed.AdrManagedAdressens)
5: select new AdrManagedBI()
6: {
7: ... };
8:
9: ListManaged = query.ToList();
10: }
Als Konvention führe ich ein (ja das ginge besser per Enum)
- 0 unsortiert
- 1 aufsteigend sortiert
- 2 absteigend sortiert
Wir haben hier also den Order State beschrieben. Fehlt nur noch welches Feld denn sortiert werden soll. Es darf ja immer nur eines.
Etwas später kommt in der TR Component eine Event Notification zum Einsatz. Genauer das eventCallBack. Dies kann an den Aufrufer nur einen Parameter übergeben. Wir brauchen eine Klasse die Order und Feldname beinhaltet.
1: public class SortStateArgs
2: {
3:
4: public string Name;
5: public int Order;
6:
7: public SortStateArgs(string name, int order)
8: {
9: Name = name;
10: Order = order;
11: }
Am nächsten LINQ Problem musste ich ein wenig kauen. Wie sortiert man in LINQ mit einem variablen String Parameter? Die Lösung findet sich hier in der sort Logik
1: public void Sorted(SortStateArgs args)
2: {
3: var propertyInfo = ListManaged.
First().GetType().GetProperty(args.Name);
4: if (args.Order == 2) //Absteigend
5: {
6: ListManaged = ListManaged.
OrderByDescending(e => propertyInfo.GetValue(e, null)).ToList();
7: }
8: else //Aufsteigend 0 / 1
9: {
10: ListManaged = ListManaged.
OrderBy(e => propertyInfo.GetValue(e, null)).ToList();
11: }
12: }
Sortierung visualisieren
Bisher habe ich die aufrufende Component beschrieben. Nun überspringe ich sozusagen eine Komponente und wende mich der Darstellung der Auf und Ab Pfeile zu. Dies um dann die eigentliche Arbeitslogik besser einordnen zu können.
Bootstrap 5 setzt auf neue Icons. Fontawesome ist obsolet. Auch hier haben wir die Wahl zwischen einer Font und SVG. Letzteres hat den Vorteil, das es unabhängig von einer eingebetteten bootstrap-icons.woff funktioniert.
Der HTML Part der SortIcon.Razor sieht auch harmlos aus. Man verzeihe den fehlenden Zeilenumbruch.
1: @if (_order == 1)
2: {
3: <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-sort-down-alt" viewBox="0 0 16 16">
4: <path d="M3.5 3.5a.5.5 0 0 0-1 0v8.793l-1.146-1.147a.5.5 0 0 0-.708.708l2 1.999.007.007a.497.497 0 0 0 .7-.006l2-2a.5.5 0 0 0-.707-.708L3.5 12.293V3.5zm4 .5a.5.5 0 0 1 0-1h1a.5.5 0 0 1 0 1h-1zm0 3a.5.5 0 0 1 0-1h3a.5.5 0 0 1 0 1h-3zm0 3a.5.5 0 0 1 0-1h5a.5.5 0 0 1 0 1h-5zM7 12.5a.5.5 0 0 0 .5.5h7a.5.5 0 0 0 0-1h-7a.5.5 0 0 0-.5.5z" />
5: </svg>
6: }
7: else if (_order == 2)
8: {
9: <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-sort-up" viewBox="0 0 16 16">
10: <path d="M3.5 12.5a.5.5 0 0 1-1 0V3.707L1.354 4.854a.5.5 0 1 1-.708-.708l2-1.999.007-.007a.498.498 0 0 1 .7.006l2 2a.5.5 0 1 1-.707.708L3.5 3.707V12.5zm3.5-9a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zM7.5 6a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1h-5zm0 3a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1h-3zm0 3a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1h-1z" />
11: </svg>
12: }
13: else
14: {
15:
16: }
Die C# Code Logik für die sortierbare HTML Tabelle ist ein wenig komplexer. Wir brauchen zwei Parameter. Die Sortierreihenfolge und natürlich welches Feld denn sortiert sein soll. Da die Komponente mehrfach existiert, muss bekannt sein, für welches Feld (Sortfield) diese aktiv ist um dann zu vergleichen ob es ident ist mit dem aktiven (SortfieldActive) ist-
1: @code {
2: private string sortFieldActivated;
3: int _order;
4: public int SortOrder { get; set; }
5: [Parameter]
6: public int Order { get; set; }
7: [Parameter]
8: public string SortField { get; set; }
9: [Parameter]
10: public string SortFieldActivated
11: {
12: get { return sortFieldActivated; }
13: set
14: {
15:
16: if (value == SortField)
17: {
18: //nötig _Order intern vs Parameter
19: //reihenfolge beachten Order="@SortOrder" SortFieldActivated="@SortField" >
20: _order = Order;
21: }
22: else //reset der anderen
23: {
24: _order = 0;
25: }
26: sortFieldActivated = value;
27: }
28: }
29:
30:
31: }
Da die Setter der Reihenfolge nach ausgelöst werden, in der sie als Attribut in der aufrufenden Komponente definiert sind, hier der Kommentarhinweis.
Sortable Table Component
So nun ans eingemachte zur TRSortable.razor Komponente. Diese erstellt per Schleife für jeden TH Element den Click Handler, das Sort Icon und außen rum das TR Element.
1: <tr>
2: @foreach (var item in Fields)
3: {
4: <th style="cursor:hand;"
@onclick='()=>Sorted(item.Key)'>
5: @item.Value
6: <SortIcon SortField="@item.Key"
Order="@SortOrder" SortFieldActivated="@SortField" ></SortIcon>
7: </th>
8: }
9: </tr>
Ziemlich cool finde ich den Trick über die Unnamed Parameter einfach das Dictionary zu füllen, das oben per foreach durchlaufen wird.
Weiterer Blazor Trick ist das gebundene Event. Besser beschrieben per EventCallback, das ein typisiertes Objekt mit den Infos Name des sortierten Feldes und Reihenfolge an den Aufrufer übergibt. Das heißt der Klick auf den Tabellenheader wird in dieser Komponenten behandelt.
1: [Parameter(CaptureUnmatchedValues = true)]
2: public Dictionary<string, object> Fields { get; set; }
3: [Parameter]
4: public EventCallback<SortStateArgs> OnSort { get; set; }
5:
6: public string SortField { get; set; }
7: int SortOrder;
Dann wird per Invoke das Event der aufrufenden Component samt Parameter aktiviert. Doch zuvor wird noch die minimal Code Programmierlogik für die sortierreihenfolge abgehandelt. Erst ist die Liste unsortiert 0, dann 1 aufsteigend, dann 2, dann wieder 1. Die Sortier-Richtung und die Spalte sind Eigentum der Komponente und werden auch dort mit den Variablen SortField und SortOrter über die Lebenszeit gehalten.
1: void Sorted(string field)
2: {
3: if (SortField == field)
4: {
5: SortOrder = SortOrder == 1 ? 2 : 1;
6: }
7: else //erstmals sort
8: {
9: SortOrder = 1; //Aufsteigend
10: SortField = field;
11: }
12: OnSort.InvokeAsync(new SortStateArgs(field, SortOrder));
13: }
An dieser Stelle des Blazor Blog Artikels scrollen sie nach oben zum Code “Sorted(“ um dann den Abschluss für die reale LINQ Sortierung per Zeichenkette als Feld nach vollziehen zu können.