Introduction

Dans tout logiciel de gestion, la grille de données est le composant le plus utilisé, le plus scruté et le plus exigeant. C'est le point de convergence entre l'utilisateur et l'information : c'est ici que l'on visualise des milliers de lignes, que l'on trie, filtre, sélectionne et agit sur les données.

Un logiciel métier sans grille performante, c'est comme un tableur Excel qui rame : l'utilisateur perd confiance, patience, et finit par contourner l'outil.

La SuperDataGrid est née de ce constat. C'est un composant Blazor Server conçu et développé en interne chez Appliman pour répondre aux exigences réelles d'un logiciel de gestion professionnelle :

flowchart TD
    A["🏢 LOGICIEL MÉTIER"] --> B[Saisie]
    A --> C[Workflow]
    A --> D[Rapports]
    A --> E[Export]
    B --> F
    C --> F
    D --> F
    E --> F
    F["⚡ SUPERDATAGRID Virtualisation Tri & Filtres Sélection Édition inline Responsive"]

    style F fill:#0d6efd,color:#fff,stroke:#0a58ca,stroke-width:2px
    style A fill:#e9ecef,stroke:#6c757d,stroke-width:2px

1. Virtualisation

Le paging classique

La pagination traditionnelle (« Page 1 sur 542 ») est un héritage du Web des années 2000. Elle impose à l'utilisateur de naviguer page par page, perdant le contexte de ses données. C'est comme lire un livre où l'on doit refermer chaque page avant d'en ouvrir une nouvelle.

Le scroll virtualisé

La SuperDataGrid utilise le composant natif Virtualize<TItem> de Blazor pour implémenter un scroll continu et fluide. Le principe est simple mais puissant :

block-beta
    columns 1
    block:viewport["Zone rendue (viewport)"]
        columns 1
        overscanUp["▲ Overscan — 5 lignes au-dessus"]
        visible["Lignes visibles à l'écran\n~15-25 lignes selon la hauteur du conteneur"]
        overscanDown["▼ Overscan — 5 lignes en dessous"]
    end
    spacer["Les 99 950 autres lignes n'existent PAS dans le DOM\nSeul un spacer CSS simule leur hauteur"]

    style overscanUp fill:#ff9f43,stroke:#e17055,color:#fff
    style visible fill:#0984e3,stroke:#0652DD,color:#fff,stroke-width:2px
    style overscanDown fill:#ff9f43,stroke:#e17055,color:#fff
    style spacer fill:#636e72,stroke:#2d3436,color:#fff

Concrètement, si votre grille affiche 100 000 lignes, seules ~35 lignes existent réellement dans le DOM à tout moment (les lignes visibles + l'overscan de 5 lignes de chaque côté). Le reste est simulé par un élément <tr> spacer dont la hauteur CSS correspond au nombre total de lignes multipliée par la hauteur d'une ligne (RowHeight).

Le pattern ItemsProvider

La clé de la performance réside dans le delegate GridItemsProvider<TItem>. Plutôt que de charger toutes les données en mémoire, la grille demande uniquement la « page » de données nécessaire au moment du scroll :

public delegate ValueTask<GridItemsProviderResult<TItem>> GridItemsProvider<TItem>(
    GridItemsProviderRequest<TItem> request
);

Le GridItemsProviderRequest<TItem> transporte toutes les informations nécessaires :

public readonly record struct GridItemsProviderRequest<TItem>(
    int StartIndex,          // Index de départ demandé
    int? Count,              // Nombre de lignes souhaitées
    string? SortColumn,      // Colonne de tri active
    SortDirection SortDirection,  // Ascendant ou descendant
    IEnumerable<SuperDataGridFilterInfo> Filters,  // Filtres actifs
    CancellationToken CancellationToken  // Annulation (scroll rapide)
);

Et le provider retourne un résultat structuré :

public readonly record struct GridItemsProviderResult<TItem>(
    IEnumerable<TItem> Items,   // Les éléments demandés
    int TotalItemCount          // Le nombre total (pour le spacer)
);

Implémentation

Voici comment implémenter un ItemsProvider qui interroge une API côté serveur :

private async ValueTask<GridItemsProviderResult<Client>> LoadClientsAsync(
    GridItemsProviderRequest<Client> request)
{
    var query = _dbContext.Clients.AsQueryable();

    // Appliquer les filtres
    foreach (var filter in request.Filters)
    {
        query = ApplyFilter(query, filter);
    }

    // Compter le total (une seule fois si possible)
    var totalCount = await query.CountAsync(request.CancellationToken);

    // Appliquer le tri
    if (!string.IsNullOrEmpty(request.SortColumn))
    {
        query = request.SortDirection == SortDirection.Ascending
            ? query.OrderByProperty(request.SortColumn)
            : query.OrderByPropertyDescending(request.SortColumn);
    }

    // Récupérer uniquement la tranche demandée
    var items = await query
        .Skip(request.StartIndex)
        .Take(request.Count ?? 50)
        .ToListAsync(request.CancellationToken);

    return GridItemsProviderResult<Client>.From(items, totalCount);
}

CancellationToken

Quand l'utilisateur scrolle rapidement, Blazor Virtualize annule automatiquement les requêtes en cours et en lance de nouvelles. La SuperDataGrid propage ce CancellationToken jusqu'au provider, évitant ainsi des requêtes inutiles vers la base de données :

sequenceDiagram
    participant U as 👤 Utilisateur
    participant V as Virtualize
    participant P as ItemsProvider
    participant DB as 🗄️ Base de données

    U->>V: Scroll rapide (t=0ms)
    V->>P: Requête lignes 0-50
    U->>V: Scroll (t=50ms)
    V--xP: ❌ CancellationToken annulé
    V->>P: Requête lignes 200-250
    U->>V: Scroll (t=100ms)
    V--xP: ❌ CancellationToken annulé
    V->>P: Requête lignes 800-850
    U->>V: Scroll stabilisé (t=300ms)
    V--xP: ❌ CancellationToken annulé
    V->>P: Requête lignes 1200-1250
    P->>DB: SELECT ... OFFSET 1200 FETCH 50
    DB-->>P: 50 lignes
    P-->>V: ✅ GridItemsProviderResult
    V-->>U: Affichage lignes 1200-1250

Configuration

Paramètre Type Défaut Description
RowHeight float 40f Hauteur estimée d'une ligne en pixels
OverscanCount int 5 Nombre de lignes rendues hors de la zone visible
Height string 400px Hauteur du conteneur (accepte 100% pour flex)

2. Tri et Filtrage

Tri par colonne

Un clic sur un en-tête de colonne déclenche un tri. Un second clic inverse l'ordre. Un troisième clic réinitialise le tri. L'indicateur visuel (flèche ▲/▼) montre clairement l'état courant.

stateDiagram-v2
    [*] --> None : État initial
    None --> Ascending : 1er clic
    Ascending --> Descending : 2e clic
    Descending --> None : 3e clic

    None : Pas de tri
    Ascending : Tri ascendant ▲
    Descending : Tri descendant ▼
# Nom ▲ Prénom Date naiss.
1 Dupont Jean 15/03/1985
2 Martin Sophie 22/07/1990
3 Petit Marc 01/12/1978

Le tri est délégué au ItemsProvider : la grille ne fait que transmettre la colonne et la direction via SortColumn et SortDirection, ce qui permet un tri côté serveur (SQL ORDER BY) et non en mémoire.

public enum SortDirection
{
    None,
    Ascending,
    Descending
}

Filtrage intégré

La SuperDataGrid intègre un système de filtrage complet avec plusieurs types de filtres automatiquement détectés :

mindmap
    root((Filtres))
        Texte
            Contains
            StartsWith
            EndsWith
            Equals
            NotEquals
        Booléen
            Oui
            Non
            Tous
        Numérique
            "= Égal"
            "≠ Différent"
            "> Supérieur"
            "< Inférieur"
            "Entre X et Y"
        Période
            Date de début
            Date de fin
            Presets Mois/Année
        Enum
            Sélection multiple
            Checkboxes

Les filtres sont transportés dans un objet SuperDataGridFilterInfo qui couvre tous les cas :

public class SuperDataGridFilterInfo
{
    public string PropertyName { get; set; }
    public string? PropertyValue { get; set; }
    public IReadOnlyList<string> SelectedValues { get; set; } = [];
    public DateOnly? StartDate { get; set; }
    public DateOnly? EndDate { get; set; }
    public long? FromNumericValue { get; set; }
    public long? ToNumericValue { get; set; }
    public SuperDataGridFilterOperator Operator { get; set; }
}

Chaque filtre peut également être personnalisé via le FilterTemplate de la colonne, permettant d'intégrer n'importe quel composant de filtrage spécialisé.


3. Personnalisation des colonnes

Visibilité

Dans un logiciel métier, chaque utilisateur a ses propres besoins. Un comptable veut voir les montants, un commercial veut voir les coordonnées. La SuperDataGrid permet de masquer ou afficher les colonnes via un bouton dédié dans la toolbar.

flowchart LR
    subgraph Visibilité["👁️ Panneau de visibilité des colonnes"]
        direction TB
        C1["🔒 Nom — toujours visible"]
        C2["🔒 Prénom — toujours visible"]
        C3["✅ Email — visible"]
        C4["❌ Téléphone — masquée"]
        C5["✅ Date création — visible"]
        C6["❌ Commentaire — masquée"]
    end

    style C1 fill:#d1e7dd,stroke:#198754
    style C2 fill:#d1e7dd,stroke:#198754
    style C3 fill:#d1e7dd,stroke:#198754
    style C4 fill:#f8d7da,stroke:#dc3545
    style C5 fill:#d1e7dd,stroke:#198754
    style C6 fill:#f8d7da,stroke:#dc3545

Chaque colonne peut être marquée AlwaysVisible pour empêcher sa suppression accidentelle :

<DataGridColumn For="@(c => c.Nom)"
                Title="Nom"
                AlwaysVisible="true"
                Width="200px" />

<DataGridColumn For="@(c => c.Telephone)"
                Title="Téléphone"
                Visible="false" />

💡 Pas besoin de spécifier TItem sur chaque colonne ! La SuperDataGrid utilise l'attribut Blazor [CascadingTypeParameter], ce qui signifie que le paramètre de type générique TItem est automatiquement cascadé vers tous les composants enfants DataGridColumn. Cela simplifie considérablement l'écriture : on déclare TItem une seule fois sur la grille, et toutes les colonnes en héritent.

🎯 Adieu les magic strings ! La propriété For accepte une expression lambda typée (For="@(c => c.Nom)"). Contrairement à un simple Property="Nom" basé sur une chaîne de caractères, cette approche offre :

  • La vérification à la compilation — une faute de frappe provoque une erreur de build, pas un bug silencieux à l'exécution
  • Le refactoring sûr — renommer une propriété dans le modèle met automatiquement à jour toutes les colonnes via l'IDE
  • L'IntelliSense — l'auto-complétion guide le développeur vers les propriétés disponibles

Le nom de la propriété et le titre de la colonne sont automatiquement déduits de l'expression, ce qui évite toute duplication.

Redimensionnement

Le redimensionnement se fait par glisser-déposer sur le bord droit de l'en-tête de colonne. Un guide visuel bleu suit le curseur pendant l'opération. Le JavaScript interop gère la fluidité du redimensionnement :

flowchart LR
    subgraph Avant
        direction LR
        A1["Nom\n200px"] ~~~ A2["Prénom\n150px"] ~~~ A3["Email\n250px"]
    end
    subgraph Après["Après redimensionnement"]
        direction LR
        B1["Nom\n300px"] ~~~ B2["Prénom\n150px"] ~~~ B3["Email\n250px"]
    end
    Avant -- "↔ Drag resize" --> Après

    style B1 fill:#cfe2ff,stroke:#0d6efd,stroke-width:2px

Réorganisation (Drag & Drop)

Les colonnes peuvent être réordonnées par glisser-déposer. Un handle visuel (⠿) indique que la colonne est déplaçable :

<DataGridColumn For="@(c => c.Nom)"
                Reorderable="true"
                Resizable="true" />

Persistance

Toutes ces personnalisations (visibilité, largeur, ordre) sont automatiquement sauvegardées via l'interface ISuperDataGridSettingsStorage. Il suffit de fournir un GridId unique :

<SuperDataGrid TItem="Client"
               GridId="clients-list"
               ItemsProvider="LoadClientsAsync">
    ...
</SuperDataGrid>

L'interface de stockage est injectable et peut être implémentée pour sauvegarder en base de données, en localStorage ou en mémoire :

public interface ISuperDataGridSettingsStorage
{
    Task<IEnumerable<SuperDataGridColumnSettings>?> GetSettingsAsync(
        string gridId, CancellationToken cancellationToken = default);

    Task SaveSettingsAsync(
        string gridId,
        IEnumerable<SuperDataGridColumnSettings> settings,
        CancellationToken cancellationToken = default);

    Task ClearSettingsAsync(
        string gridId, CancellationToken cancellationToken = default);
}

Les réglages par défaut peuvent aussi être centralisés via DefaultSettingsName et la configuration SuperComponentsConfiguration, permettant d'appliquer un preset à toutes les grilles d'une même catégorie.


4. Numéros de ligne

La colonne de numéros de ligne (DisplayRowNumberColumn) permet à l'utilisateur de se repérer précisément dans un grand jeu de données. Chaque élément implémente l'interface IDataItem :

public interface IDataItem
{
    object KeyValue { get; }
    bool IsSelected { get; set; }
    int RowNumber { get; set; }
}

Le numéro de ligne est calculé dynamiquement en fonction de la position dans le flux virtualisé :

# Nom Prénom
1 Dupont Jean
2 Martin Sophie
3 Petit Marc
... ... ...
847 Legrand Pierre
848 Moreau Claire
849 Roux Thomas

💡 L'utilisateur scrolle et voit toujours le numéro de ligne exact, même à la ligne 847 sur 100 000.


5. Sélection des lignes

La sélection est un aspect critique pour les opérations en masse (suppression, export, modification de statut…). La SuperDataGrid propose trois modes de sélection :

public enum SuperDataGridSelectionMode
{
    None = 0,     // Pas de sélection
    Single = 1,   // Une seule ligne à la fois
    Multiple = 2  // Sélection multiple avec checkboxes
}

Sélection multiple

Le mode Multiple affiche une colonne de checkboxes. Le checkbox dans l'en-tête permet de tout sélectionner ou tout désélectionner.

# Nom Prénom État
1 Dupont Jean ✅ Sélectionné
2 Martin Sophie
3 Petit Marc ✅ Sélectionné
4 Legrand Pierre
5 Moreau Claire ✅ Sélectionné

Nombre de lignes: 12 458 — 3 sélectionnées

SelectionInfo

Avec la virtualisation, la sélection de 100 000 lignes pose un défi : on ne peut pas stocker 100 000 éléments en mémoire quand l'utilisateur clique sur "Tout sélectionner". La SuperDataGrid résout ce problème avec un algorithme d'exclusion inversée :

public sealed class SelectionInfo<TItem>
{
    public HashSet<TItem> SelectedItems { get; } = [];
    public bool AllSelected { get; set; }         // "Tout est sélectionné"
    public int ExcludedCount { get; set; }        // Sauf ceux-ci
    public int SelectedCountTotal => SelectedCount - ExcludedCount;
}

Quand AllSelected = true, au lieu de stocker les 100 000 clés, la grille stocke uniquement les exclusions (les lignes décochées par l'utilisateur). Ainsi, sélectionner 100 000 lignes puis en décocher 3 ne stocke que 3 clés.

Le sélecteur de lignes intègre un menu déroulant pour appliquer des actions en masse sur la sélection. Les éléments de menu peuvent être ajoutés dynamiquement :

await grid.AddSelectorMenuItemAsync(new SuperDataGridRowSelectorItem
{
    ActionName = "delete",
    Text = "Supprimer la sélection",
    Icon = "fa-trash"
});

await grid.AddSelectorMenuItemAsync(new SuperDataGridRowSelectorItem
{
    ActionName = "export",
    Text = "Exporter en CSV",
    Icon = "fa-file-csv"
});

6. Édition inline

La SuperDataGrid supporte l'édition directe des données, sans avoir à ouvrir un formulaire séparé. Deux modes d'édition sont disponibles :

public enum SuperDataGridEditionMode
{
    None = 0,   // Lecture seule
    Edit = 1    // Toutes les cellules en mode édition
}

Globale vs par ligne

En mode EditionMode.Edit, toutes les cellules qui possèdent un EditTemplate sont en mode édition simultanément. Pour une approche plus ciblée, le mode édition par ligne permet d'éditer une seule ligne à la fois via double-clic ou appel programmatique :

// Activer le mode édition pour une ligne
await grid.BeginEditAsync(selectedClient);

// Confirmer les modifications
await grid.EndEditAsync(selectedClient);

// Annuler les modifications
await grid.CancelEditAsync(selectedClient);

Templates

Chaque colonne peut définir deux templates distincts : un pour l'affichage et un pour l'édition. La bascule est automatique :

<DataGridColumn For="@(c => c.Nom)" Title="Nom">
    <Template Context="client">
        <span>@client.Nom</span>
    </Template>
    <EditTemplate Context="client">
        <input class="form-control form-control-sm"
               @bind="client.Nom"
               @bind:event="oninput" />
    </EditTemplate>
</DataGridColumn>

Actions

Le ActionsTemplate permet d'ajouter des boutons d'action (éditer, sauvegarder, annuler, supprimer) directement dans chaque ligne :

<SuperDataGrid TItem="Client"
               ItemsProvider="LoadClientsAsync"
               EditOnDoubleClick="true">
    <ActionsTemplate Context="client">
        @if (Grid.IsRowInEditMode(client))
        {
            <button @onclick="() => SaveAndEndEdit(client)">💾</button>
            <button @onclick="() => Grid.CancelEditAsync(client)">✕</button>
        }
        else
        {
            <button @onclick="() => Grid.BeginEditAsync(client)">✏️</button>
        }
    </ActionsTemplate>
    ...
</SuperDataGrid>

7. Responsive

Défi mobile

Une grille avec 15 colonnes ne peut pas s'afficher telle quelle sur un écran de 375px de large. Plutôt que de forcer un scroll horizontal pénible, la SuperDataGrid propose un mode vertical :

public enum SuperDataGridOrientation
{
    Horizontal,  // Mode classique (desktop)
    Vertical     // Mode fiche (mobile)
}

Mode Horizontal (Desktop)

# Nom Prénom Email Téléphone
1 Dupont Jean jean@mail.com 06 12 ...
2 Martin Sophie sophie@mail.com 06 34 ...

Mode Vertical (Mobile / Tablette)

En mode vertical, chaque enregistrement est affiché comme une fiche avec les propriétés empilées :

flowchart TB
    subgraph fiche1["📇 Fiche #1"]
        direction TB
        P1["Nom → Dupont"]
        P2["Prénom → Jean"]
        P3["Email → jean@mail.com"]
        P4["Téléphone → 06 12 34 56 78"]
    end
    subgraph fiche2["📇 Fiche #2"]
        direction TB
        P5["Nom → Martin"]
        P6["Prénom → Sophie"]
        P7["Email → sophie@mail.com"]
        P8["Téléphone → 06 34 56 78 90"]
    end
    fiche1 ~~~ fiche2

    style fiche1 fill:#f8f9fa,stroke:#0d6efd,stroke-width:2px
    style fiche2 fill:#f8f9fa,stroke:#0d6efd,stroke-width:2px

La virtualisation fonctionne de la même manière en mode vertical, en ajustant automatiquement la taille estimée d'un item (GetVerticalItemSize()) pour prendre en compte le nombre de colonnes visibles :

private float GetVerticalItemSize()
{
    var columnCount = GetVisibleColumns().Count;
    return RowHeight * Math.Max(1, columnCount);
}

8. Colonnes figées

Grilles larges

Dans un logiciel métier, il n'est pas rare d'afficher 15, 20, voire 30 colonnes sur une même grille : nom, prénom, email, téléphone, adresse, ville, code postal, statut, date de création, date de modification, commercial assigné, chiffre d'affaires, encours, dernière facture… La grille déborde alors largement de l'écran et l'utilisateur doit scroller horizontalement.

Et c'est là que le drame arrive : en scrollant vers la droite, on perd de vue les colonnes d'identification. On voit un montant de 12 450 €, mais… c'est le CA de quel client ? Il faut scroller à gauche pour vérifier, puis revenir à droite. Un aller-retour mental permanent, épuisant et source d'erreurs.

flowchart LR
    subgraph probleme["❌ Sans colonnes figées"]
        direction LR
        H1["Nom\n(invisible!)"] ~~~ H2["...\n(hors écran)"] ~~~ H3["CA\n12 450 €\n🤔 De qui ?"]
    end

    style H1 fill:#f8d7da,stroke:#dc3545
    style H3 fill:#fff3cd,stroke:#ffc107

FreezeLeft / FreezeRight

La SuperDataGrid permet de figer un nombre arbitraire de colonnes à gauche et/ou à droite. Ces colonnes restent immobiles pendant le scroll horizontal, garantissant que l'utilisateur ne perd jamais le contexte :

flowchart LR
    subgraph solution["✅ Avec colonnes figées"]
        direction LR
        F1["🔒 Nom\n(figé gauche)"] ~~~ F2["🔒 Prénom\n(figé gauche)"] ~~~ F3["... colonnes scrollables ..."] ~~~ F4["🔒 Actions\n(figé droite)"]
    end

    style F1 fill:#d1e7dd,stroke:#198754,stroke-width:2px
    style F2 fill:#d1e7dd,stroke:#198754,stroke-width:2px
    style F4 fill:#cfe2ff,stroke:#0d6efd,stroke-width:2px

L'utilisation est simple — deux propriétés suffisent :

<SuperDataGrid TItem="Client"
               ItemsProvider="LoadClientsAsync"
               FreezeLeftColumns="2"
               FreezeRightColumns="1">

    <DataGridColumn For="@(c => c.Nom)" Title="Nom" Width="200px" />
    <!-- ↑ Figée à gauche (colonne 1) -->

    <DataGridColumn For="@(c => c.Prenom)" Title="Prénom" Width="150px" />
    <!-- ↑ Figée à gauche (colonne 2) -->

    <DataGridColumn For="@(c => c.Email)" Title="Email" Width="250px" />
    <DataGridColumn For="@(c => c.Telephone)" Title="Téléphone" Width="180px" />
    <DataGridColumn For="@(c => c.Adresse)" Title="Adresse" Width="300px" />
    <DataGridColumn For="@(c => c.Ville)" Title="Ville" Width="150px" />
    <!-- ↑ Ces colonnes scrollent librement -->

    <DataGridColumn For="@(c => c.ChiffreAffaires)" Title="CA" Width="120px"
                    FormatString="{0:C}" TextAlign="SuperTextAlignment.Right" />
    <!-- ↑ Figée à droite (dernière colonne) -->
</SuperDataGrid>

Figer à gauche

Le cas d'usage principal du FreezeLeftColumns est de garder les colonnes d'identification toujours visibles. Peu importe combien l'utilisateur scrolle vers la droite, il voit toujours le nom du client, le numéro de commande ou la référence produit.

Les colonnes systèmes (numéro de ligne #, checkbox de sélection ☑, actions ✏️) sont déjà automatiquement figées par la grille. FreezeLeftColumns s'applique aux colonnes de données qui suivent :

flowchart LR
    subgraph sticky["Colonnes toujours visibles"]
        direction LR
        S1["#\n(auto)"]
        S2["☑\n(auto)"]
        S3["✏️\n(auto)"]
        S4["Nom\nFreeze 1"]
        S5["Prénom\nFreeze 2"]
    end
    subgraph scroll["↔ Zone scrollable"]
        direction LR
        D1["Email"]
        D2["Tél."]
        D3["Adresse"]
        D4["Ville"]
        D5["CP"]
    end
    sticky ~~~ scroll

    style S1 fill:#e9ecef,stroke:#6c757d
    style S2 fill:#e9ecef,stroke:#6c757d
    style S3 fill:#e9ecef,stroke:#6c757d
    style S4 fill:#d1e7dd,stroke:#198754,stroke-width:2px
    style S5 fill:#d1e7dd,stroke:#198754,stroke-width:2px

Figer à droite

Le FreezeRightColumns est particulièrement utile pour garder une colonne d'actions ou un montant clé toujours accessible en bout de ligne, sans avoir à scroller jusqu'au bout :

  • Colonne d'actions : boutons Éditer / Supprimer / Voir toujours accessibles
  • Montant principal : chiffre d'affaires, solde, total toujours visible
  • Statut : indicateur visuel (badge En cours / Validé / Refusé) immédiatement lisible

Implémentation CSS sticky

Derrière cette simplicité d'usage, l'implémentation est techniquement subtile. Chaque colonne figée utilise position: sticky avec un calcul CSS calc() dynamique de son décalage. La grille prend en compte la largeur de toutes les colonnes qui la précèdent :

// Colonne figée à gauche : calcul de inset-inline-start
// prend en compte les colonnes système (row number, selection, actions)
// puis les colonnes de données précédentes
styles.Add($"inset-inline-start: calc({calcExpression})");
styles.Add("z-index: 40 !important");
styles.Add("position: sticky");

// Colonne figée à droite : calcul de inset-inline-end
// prend en compte les colonnes de données qui suivent
styles.Add($"inset-inline-end: calc({calcExpression})");
styles.Add("z-index: 40 !important");
styles.Add("position: sticky");

Le jeu de z-index est soigneusement orchestré pour que les différentes couches ne se superposent pas de manière incohérente :

Couche z-index Éléments
En-tête sticky + figé 41 Header d'une colonne figée (intersection)
En-tête sticky 20-21 Headers normaux et figés
Cellules figées 40 Colonnes figées gauche et droite
Cellules normales 0 Contenu scrollable standard

Un séparateur visuel (outline) est automatiquement ajouté sur la dernière colonne figée à gauche (sdg-frozen-left-last) pour bien délimiter la zone fixe de la zone scrollable, sans affecter le fond de la cellule ni le striping du tableau.

Les colonnes figées fonctionnent en harmonie avec les en-têtes et pieds de page figés. L'intersection (un header de colonne qui est à la fois sticky en vertical ET figé en horizontal) reçoit un z-index encore plus élevé (41) pour rester au-dessus de tout :

flowchart TB
    subgraph header["Header sticky — z-index 20-21"]
        direction LR
        H1["# / Nom"]
        H2["Email / Tel / Adresse"]
        H3["CA"]
    end
    subgraph body["Corps scrollable"]
        direction LR
        B1["Colonnes figees\ngauche z:40"]
        B2["Zone de scroll\nlibre z:0"]
        B3["Colonnes figees\ndroite z:40"]
    end
    subgraph footer["Footer sticky — z-index 31"]
        direction LR
        F1["Totaux figes"]
        F2[" "]
        F3["Total CA"]
    end
    header ~~~ body ~~~ footer

    style header fill:#e9ecef,stroke:#6c757d,stroke-width:2px
    style B1 fill:#d1e7dd,stroke:#198754
    style B3 fill:#cfe2ff,stroke:#0d6efd
    style footer fill:#e9ecef,stroke:#6c757d,stroke-width:2px

Bonnes pratiques

💡 Ne figez pas trop de colonnes. Figer 1 à 3 colonnes à gauche est idéal. Au-delà, la zone scrollable devient trop étroite et l'intérêt du scroll horizontal disparaît.

🎯 Fiez à gauche les colonnes d'identification (nom, référence, numéro) et à droite les colonnes d'action (boutons, statut, montant clé).

⚠️ Pensez aux écrans mobiles. En mode SuperDataGridOrientation.Vertical, les colonnes figées n'ont plus de sens (chaque propriété est affichée comme une ligne de fiche). La grille gère cette transition automatiquement.


9. Configuration par presets

Répétition des paramètres

Dans un logiciel métier, la SuperDataGrid est utilisée sur des dizaines de pages différentes : liste de clients, factures, produits, commandes, utilisateurs… Sur chacune de ces pages, le développeur doit configurer les mêmes paramètres :

<!-- ❌ Page Clients -->
<SuperDataGrid TItem="Client"
               RowHeight="42"
               AllowColumnResize="true"
               AllowColumnReorder="true"
               AllowSorting="true"
               AllowFiltering="true"
               DisplaySelectionColumn="true"
               DisplayColumnVisibilityToggle="true"
               DisplayRowNumberColumn="true"
               DisplayRefreshButton="true"
               DisplayDefaultFooterTemplate="true"
               ItemsProvider="LoadClientsAsync">
    ...
</SuperDataGrid>

<!-- ❌ Page Factures — copier-coller des mêmes 10 paramètres -->
<SuperDataGrid TItem="Facture"
               RowHeight="42"
               AllowColumnResize="true"
               AllowColumnReorder="true"
               ...

Dix paramètres répétés sur trente pages, c'est 300 lignes de configuration dupliquées. Et quand le design évolue (on veut passer le RowHeight à 38, ou désactiver globalement le DisplayRefreshButton), il faut modifier trente fichiers.

⚠️ Radzen DataGrid ne propose aucun mécanisme de preset ou de configuration globale. Chaque instance de RadzenDataGrid doit être configurée individuellement, sans possibilité de centraliser les paramètres par défaut. C'est l'un des points de friction majeurs dans les applications avec de nombreuses grilles.

Presets nommés via DI

La SuperDataGrid résout ce problème avec un concept simple et puissant : les presets nommés. On définit des profils de configuration une seule fois dans Program.cs, et chaque grille référence un preset par son nom.

flowchart TD
    subgraph program["📦 Program.cs — Configuration centralisée"]
        P1["Preset 'SimpleGrid'\n• Pas de sélection\n• Pas de filtre\n• Pas de footer"]
        P2["Preset 'GridWithFooter'\n• Sélection multiple\n• Filtres activés\n• Footer avec totaux"]
        P3["Preset 'ReadOnlyGrid'\n• Lecture seule\n• Tri uniquement\n• Sans redimensionnement"]
    end

    P1 --> G1["Page Clients\nDefaultSettingsName='SimpleGrid'"]
    P1 --> G2["Page Produits\nDefaultSettingsName='SimpleGrid'"]
    P2 --> G3["Page Factures\nDefaultSettingsName='GridWithFooter'"]
    P2 --> G4["Page Commandes\nDefaultSettingsName='GridWithFooter'"]
    P3 --> G5["Page Rapports\nDefaultSettingsName='ReadOnlyGrid'"]

    style program fill:#e9ecef,stroke:#6c757d,stroke-width:2px
    style P1 fill:#d1e7dd,stroke:#198754
    style P2 fill:#cfe2ff,stroke:#0d6efd
    style P3 fill:#fff3cd,stroke:#ffc107

SuperDataGridSettings

Chaque preset est une instance de SuperDataGridSettings, un record qui regroupe toutes les propriétés configurables de la grille :

public sealed record SuperDataGridSettings
{
    public string Name { get; set; } = null!;

    // --- Dimensions ---
    public float RowHeight { get; set; } = 40f;
    public int OverscanCount { get; set; } = 5;

    // --- Freeze ---
    public bool FreezeHeader { get; set; } = true;
    public bool FreezeFooter { get; set; } = true;

    // --- Features ---
    public bool AllowColumnReorder { get; set; } = true;
    public bool AllowColumnResize { get; set; } = true;
    public bool AllowSorting { get; set; } = true;
    public bool AllowFiltering { get; set; } = true;

    // --- Edition ---
    public SuperDataGridEditionMode EditionMode { get; set; } = SuperDataGridEditionMode.None;
    public bool EditOnDoubleClick { get; set; } = true;

    // --- Display toggles ---
    public bool DisplayRowNumberColumn { get; set; } = true;
    public bool DisplayRefreshButton { get; set; } = false;
    public bool DisplayColumnVisibilityToggle { get; set; } = true;
    public bool DisplayDefaultFooterTemplate { get; set; } = true;

    // --- Appearance ---
    public string CurrentRowBackground { get; set; } = "#3b95c6";
    public string? ContainerCssClass { get; set; }
    public string TableCssClass { get; set; } = "table-striped table-hover table-bordered";
    public string HeaderCssClass { get; set; } = "";

    // --- Selection ---
    public SuperDataGridSelectionMode SelectionMode { get; set; } = SuperDataGridSelectionMode.Multiple;
    public bool DisplaySelectionColumn { get; set; } = true;
}

Le Name est l'identifiant unique du preset. Les valeurs par défaut du record correspondent au comportement standard de la grille : toutes les fonctionnalités activées.

Enregistrement

Les presets sont enregistrés dans le conteneur d'injection de dépendances via la méthode AddSuperComponents :

builder.Services.AddSuperComponents(options =>
{
    // Preset minimaliste — idéal pour les listes de sélection rapide
    options.SuperDataGridSettingsList.Add(new SuperDataGridSettings
    {
        Name = "SimpleGrid",
        RowHeight = 42f,
        AllowColumnResize = false,
        AllowColumnReorder = false,
        AllowSorting = true,
        AllowFiltering = false,
        DisplaySelectionColumn = false,
        DisplayColumnVisibilityToggle = false,
        DisplayRowNumberColumn = true,
        DisplayRefreshButton = false,
        DisplayDefaultFooterTemplate = false
    });

    // Preset complet — pour les grilles de gestion avec footer, filtres et sélection
    options.SuperDataGridSettingsList.Add(new SuperDataGridSettings
    {
        Name = "GridWithFooter",
        RowHeight = 42f,
        AllowColumnResize = true,
        AllowColumnReorder = true,
        AllowSorting = true,
        AllowFiltering = true,
        DisplaySelectionColumn = true,
        DisplayColumnVisibilityToggle = true,
        DisplayRowNumberColumn = true,
        DisplayRefreshButton = true,
        DisplayDefaultFooterTemplate = true
    });
});

Utilisation

Une fois les presets définis, chaque grille se configure en une seule propriété :

<!-- ✅ Avant : 10 paramètres dupliqués. Après : 1 seul. -->
<SuperDataGrid TItem="Client"
               ItemsProvider="LoadClientsAsync"
               DefaultSettingsName="SimpleGrid">
    <ChildContent>
        <DataGridColumn For="@(c => c.Nom)"    Title="Nom"    Width="200px" />
        <DataGridColumn For="@(c => c.Prenom)" Title="Prénom" Width="180px" />
        <DataGridColumn For="@(c => c.Email)"  Title="Email"  Width="250px" />
    </ChildContent>
</SuperDataGrid>

Comparez avec la version sans preset : la charge cognitive est divisée par dix, et un changement global de design ne nécessite qu'une seule modification dans Program.cs.

Surcharge locale

La puissance du système réside dans la méthode ApplyPresetDefaults() : le preset ne fait que fournir des valeurs par défaut. Si un paramètre est explicitement défini sur la grille, il prend le dessus sur le preset :

private void ApplyPresetDefaults(ParameterView parameters, SuperDataGridSettings preset)
{
    if (!parameters.TryGetValue<float>(nameof(RowHeight), out _))
        RowHeight = preset.RowHeight;

    if (!parameters.TryGetValue<bool>(nameof(AllowFiltering), out _))
        AllowFiltering = preset.AllowFiltering;

    // ... idem pour chaque propriété
}

Cela signifie qu'on peut utiliser un preset tout en le surchargeant localement :

<!-- Utilise le preset SimpleGrid, MAIS active le filtre pour cette grille spécifique -->
<SuperDataGrid TItem="Produit"
               ItemsProvider="LoadProduitsAsync"
               DefaultSettingsName="SimpleGrid"
               AllowFiltering="true">
    ...
</SuperDataGrid>
flowchart LR
    subgraph preset["Preset 'SimpleGrid'"]
        direction TB
        A1["AllowFiltering = false"]
        A2["RowHeight = 42"]
        A3["DisplaySelectionColumn = false"]
    end

    subgraph composant["Paramètres explicites"]
        direction TB
        B1["AllowFiltering = true ⬅ surcharge"]
    end

    subgraph resultat["Valeurs finales appliquées"]
        direction TB
        C1["AllowFiltering = true ✅ (explicite gagne)"]
        C2["RowHeight = 42 ✅ (preset)"]
        C3["DisplaySelectionColumn = false ✅ (preset)"]
    end

    preset --> resultat
    composant --> resultat

    style preset fill:#fff3cd,stroke:#ffc107,stroke-width:2px
    style composant fill:#cfe2ff,stroke:#0d6efd,stroke-width:2px
    style resultat fill:#d1e7dd,stroke:#198754,stroke-width:2px

Propriétés couvertes

Le tableau ci-dessous résume les 20 propriétés gérées par le système de presets :

Catégorie Propriété Type Valeur par défaut
Dimensions RowHeight float 40f
OverscanCount int 5
Freeze FreezeHeader bool true
FreezeFooter bool true
Features AllowColumnReorder bool true
AllowColumnResize bool true
AllowSorting bool true
AllowFiltering bool true
Édition EditionMode SuperDataGridEditionMode None
EditOnDoubleClick bool true
Affichage DisplayRowNumberColumn bool true
DisplayRefreshButton bool false
DisplayColumnVisibilityToggle bool true
DisplayDefaultFooterTemplate bool true
Apparence CurrentRowBackground string #3b95c6
ContainerCssClass string? null
TableCssClass string table-striped table-hover table-bordered
HeaderCssClass string ""
Sélection SelectionMode SuperDataGridSelectionMode Multiple
DisplaySelectionColumn bool true

Bonnes pratiques

💡 Nommez vos presets par usage, pas par page. Préférez "SimpleGrid", "GridWithFooter", "ReadOnlyGrid" à "ClientsGrid", "FacturesGrid". Un preset est un profil de comportement réutilisable sur plusieurs pages.

🎯 Limitez le nombre de presets. Deux à quatre profils suffisent dans la majorité des applications : un mode simple, un mode complet, un mode lecture seule, et éventuellement un mode édition.

Utilisez la surcharge locale avec parcimonie. Si vous surchargez systématiquement 5 propriétés d'un preset, c'est probablement le signe qu'il faut créer un nouveau preset.

🔄 Changement de design global en une ligne. Besoin de passer toutes les grilles de RowHeight="42" à RowHeight="38" ? Modifiez le preset dans Program.cs — les trente pages s'adaptent instantanément.


10. Headers et Footers figés

Header sticky

L'en-tête de la grille est figé par défaut (FreezeHeader = true). Quand l'utilisateur scrolle, les noms de colonnes restent toujours visibles. Cela semble évident, mais c'est techniquement délicat avec la virtualisation : le position: sticky doit cohabiter avec le conteneur scrollable.

Le pied de tableau peut également être figé (FreezeFooter = true), idéal pour afficher des totaux, des moyennes ou d'autres agrégats toujours visibles.

La SuperDataGrid affiche un pied de page par défaut indiquant le nombre total de lignes et le nombre de lignes sélectionnées :

  Nombre de lignes: 12 458 - 3 sélectionnées

11. Événements

La SuperDataGrid expose un riche ensemble d'événements pour réagir aux interactions utilisateur :

Événement Type Déclencheur
RowClicked EventCallback<TItem> Clic sur une ligne
RowDoubleClicked EventCallback<TItem> Double-clic sur une ligne
CellClicked EventCallback<CellClickedEventArgs> Clic sur une cellule spécifique
SelectionChanged EventCallback<IEnumerable<TItem>> Changement de sélection
SelectionStateChanged EventCallback<SelectionChangedEventArgs> Changement détaillé de sélection
DataLoaded EventCallback<SuperDataGridDataLoadedEventArgs> Données chargées
ColumnSettingsChanged EventCallback<IEnumerable<SuperDataGridColumnSettings>> Paramètres de colonne modifiés
RowEditStarted EventCallback<TItem> Début d'édition d'une ligne
RowEditEnded EventCallback<TItem> Fin d'édition d'une ligne

Le CellClickedEventArgs fournit un contexte complet :

public class CellClickedEventArgs<TItem>
{
    public TItem Item { get; }           // L'objet de la ligne
    public string PropertyName { get; }  // Le nom de la propriété
    public object? Value { get; }        // La valeur de la cellule
}

12. Exemple complet

Voici un exemple complet d'utilisation de la SuperDataGrid dans une page Blazor :

@page "/clients"

<SuperDataGrid TItem="Client"
               GridId="clients-main"
               ItemsProvider="LoadClientsAsync"
               Height="100%"
               RowHeight="38"
               OverscanCount="5"
               SelectionMode="SuperDataGridSelectionMode.Multiple"
               AllowColumnReorder="true"
               AllowColumnResize="true"
               AllowSorting="true"
               AllowFiltering="true"
               FreezeLeftColumns="1"
               EditOnDoubleClick="true"
               DisplayRefreshButton="true"
               RowClicked="OnClientClicked"
               SelectionChanged="OnSelectionChanged">

    <HeaderTemplate>
        <h6 class="mb-0">Liste des clients</h6>
    </HeaderTemplate>

    <ActionsTemplate Context="client">
        <SuperButton Icon="fa-pen" Click="() => EditClient(client)" />
    </ActionsTemplate>

    <DataGridColumn For="@(c => c.Nom)"
                    Title="Nom" Width="200px"
                    AlwaysVisible="true" />

    <DataGridColumn For="@(c => c.Prenom)"
                    Title="Prénom" Width="150px" />

    <DataGridColumn For="@(c => c.Email)"
                    Title="Email" Width="250px" />

    <DataGridColumn For="@(c => c.DateCreation)"
                    Title="Date création" Width="130px"
                    FormatString="{0:dd/MM/yyyy}" />

    <DataGridColumn For="@(c => c.ChiffreAffaires)"
                    Title="CA" Width="120px"
                    FormatString="{0:C}"
                    TextAlign="SuperTextAlignment.Right" />

    <FooterTemplate>
        <span class="text-muted">
            Total CA : @totalCA.ToString("C")
        </span>
    </FooterTemplate>
</SuperDataGrid>

13. SuperDataGrid vs Radzen

Constat initial

J'utilise la librairie Blazor Radzen depuis des années, leurs composants sur vraiment très pro, pour moi c'est la librairie la plus complète du marché, très facile à utiliser, mais concernant la DataGrid en mode scrolling continu, la c'est vraiment la galère, entre les pages blanches incompréensibles ou la très grosse lenteur, j'ai préféré creer ma propre grille de données. voici un tableau comparatif ce ce qui me manque dans la grille de Radzen et ce que j'ai ajouté dans la mienne.

Limites de Radzen

mindmap
    root((Radzen DataGrid\nPoints de friction))
        🔄 Virtualisation partielle
            Implémentation propre
            Contrôle CancellationToken limité
            Scroll rapide moins fluide
        🔗 Couplage fort
            Écosystème complet obligatoire
            Thèmes / Layouts / Dialogs
            Difficile d'isoler le DataGrid
        🔧 Personnalisation limitée
            Pas de persistance colonnes native
            Pas de presets par défaut
            Mode fiche/vertical absent
            Menu d'actions à construire
        ⚖️ Poids et dépendances
            CSS et JS conséquents
            Même pour un seul composant
        🗓️ Contrôle de la roadmap
            Dépendance éditeur tiers
            Corrections lentes
            Évolutions non alignées

Avantages SuperDataGrid

mindmap
    root((SuperDataGrid\nAvantages clés))
        ⚡ Virtualisation native Blazor
            Virtualize T de Microsoft
            Compatibilité .NET future
            Performances maximales
        🪶 Zéro dépendance tierce
            Bootstrap 5 uniquement
            JS isolé minimal
            Pas de CSS framework tiers
        📱 Orientation verticale
            Mode fiche pour mobile
            Basculement automatique
            Virtualisation conservée
        💾 Persistance intégrée
            ISuperDataGridSettingsStorage
            Sauvegarde automatique
            Presets par défaut via DI
        ☑️ Sélection optimisée
            Exclusion inversée
            100k lignes sans mémoire
            Menu d'actions dynamique
        ✏️ Édition inline granulaire
            Mode global ou par ligne
            BeginEdit / EndEdit
            Double-clic configurable
        🔧 Maîtrise totale
            Correction en heures
            Roadmap alignée métier
            Évolution sur mesure

Comparaison

Fonctionnalité Radzen DataGrid SuperDataGrid
Virtualisation Propre Virtualize<T> natif
Dépendances CSS/JS Écosystème complet Bootstrap 5 + JS isolé
Persistance colonnes ❌ À implémenter ✅ Intégrée
Mode vertical (mobile) ✅ Natif
Sélection "Tout" optimisée Partielle ✅ Exclusion inversée
Menu d'actions sur sélection ❌ À implémenter ✅ Intégré
Édition par ligne (double-clic)
Filtres typés (enum, date, num) ✅ Avec presets de période
Presets de configuration ✅ Via DI
Contrôle de la roadmap ❌ Dépendance externe ✅ Interne
Colonnes figées (freeze) ✅ Gauche + Droite
Drag & Drop colonnes ✅ Avec handle visuel
Communauté / Documentation ✅ Large Interne

Le choix Appliman

Le choix de développer la SuperDataGrid en interne n'est pas un rejet de Radzen — c'est un composant de qualité. C'est un choix stratégique motivé par :

  1. L'indépendance — Ne pas dépendre d'un éditeur tiers pour un composant aussi critique
  2. La performance — Exploiter au maximum Virtualize<T> natif sans couche d'abstraction supplémentaire
  3. L'adaptation métier — Intégrer nativement les patterns métier d'Appliman (persistance, presets, sélection en masse)
  4. La légèreté — Un seul fichier JS isolé de quelques Ko vs un framework UI complet

Conclusion

La SuperDataGrid n'est pas un simple tableau HTML. C'est un composant stratégique qui combine :

  • Virtualisation native pour des performances constantes, quelle que soit la volumétrie
  • Scroll continu sans pagination, pour une expérience utilisateur naturelle
  • Personnalisation complète des colonnes avec persistance automatique
  • Sélection intelligente capable de gérer des centaines de milliers de lignes
  • Édition inline pour un workflow rapide et sans rupture
  • Responsive natif avec basculement Horizontal / Vertical

C'est la pièce maîtresse de l'interface utilisateur Appliman, celle sur laquelle les utilisateurs passent 80% de leur temps. Et c'est pour cela qu'elle mérite d'être développée, maintenue et optimisée en interne.

"Une grille performante, c'est un utilisateur productif."

L'équipe Appliman