Overview
Pattern: Framework component + Custom data
I componenti UI ad alto livello del framework (<wuic-list-grid>, <wuic-chart-list>, ...) accettano un datasource costruito a mano (hardcodedDatasource) che non passa dal data layer del framework. I dati arrivano dal tuo backend custom (Controller .NET tuo, REST esterna, file statici, websocket, ...).
Quando usarlo
- Vuoi l'UX completa di una list-grid/chart-list (filtri, sort, paging client, export, edit dialog) ma i dati sono prodotti da:
- Un endpoint REST esterno (3rd-party API, microservizio).
- Un Controller .NET tuo non integrato col data layer del framework.
- File statici, calcoli aggregati, dati live (websocket, polling).
- Hai un dominio legacy che non vuoi modellare nel framework.
- Stai prototipando senza ancora aver definito la struttura dati.
Architettura
- Sviluppatore: scrive un piccolo componente Angular che recupera i dati dal proprio backend e li impacchetta in un datasource locale.
- Framework: la list-grid si comporta esattamente come se i dati venissero dal data layer standard (filtri, sort, paging, export funzionano).
- Backend: liberta' totale. Endpoint REST classici, niente convenzioni di metadata.
Cosa fai tu (frontend)
Crei un componente Angular standalone che:
1. Chiama il tuo endpoint custom con HttpClient.
2. Definisce le colonne (nome, label, tipo) per il datasource locale.
3. Pubblica righe + colonne sul datasource e lo passa alla list-grid.
<!-- source: wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/3a-external-rest-grid.component.html -->
<wuic-data-source #ds></wuic-data-source>
<wuic-list-grid [hardcodedDatasource]="ds" [hideToolbar]="false"></wuic-list-grid><!-- source: wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/3a-external-rest-grid.component.ts -->
import { Component, ViewChild, AfterViewInit, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject } from 'rxjs';
import { DataSourceComponent, ListGridComponent, MetaInfo, MetadatiColonna, MetadatiTabella } from 'wuic-framework-lib';
@Component({
selector: 'app-external-rest-grid',
Cosa fai tu (backend, opzionale)
Se i dati vengono dal tuo backend interno, basta un Controller REST classico. Niente convenzioni del framework, niente metadata da scrivere.
<!-- source: WuicTest/Controllers/SamplesController.cs -->
[ApiController]
[Route("api/samples")]
public class SamplesController : ControllerBase
{
[HttpGet("inventory")]
public IActionResult GetInventory()
{
Se apri tu la connessione: su Oracle porta la sessione sullo schema
Quando il controller apre una connessione propria con la connection string DataSQLConnection
(ADO.NET, Dapper, EF Core con UseOracle(...)), su Oracle c'e' un passaggio in piu' che gli
altri motori non hanno: nella stringa non c'e' nessun Database / Initial Catalog, perche' su
Oracle lo schema non fa parte della connessione. DataSQLConnection autentica soltanto; allo
schema dei dati il framework porta la sessione da solo, eseguendo
ALTER SESSION SET CURRENT_SCHEMA = <schema> all'apertura.
Una connessione aperta a mano quel passaggio non ce l'ha: resta sullo schema dell'utente con cui
si e' autenticata, e la prima query sulle tue tabelle risponde `ORA-00942: table or view does not
exist` anche se la tabella esiste e i permessi ci sono. Due modi per farlo bene:
await using var cn = new OracleConnection(cs);
await cn.OpenAsync();
// 1) porta la sessione sullo schema dei dati (equivalente a cio' che fa il framework)
await using (var set = new OracleCommand("ALTER SESSION SET CURRENT_SCHEMA = MIO_SCHEMA", cn))
await set.ExecuteNonQueryAsync();
Sugli altri tre motori non serve: il database sta nella connection string e la sessione ci nasce
dentro.
Trade-off
| Pro | Contro |
|---|---|
UX wuic-list-grid con filtri/sort/paging/export gratis (client-side) | Mantieni tu la coerenza tra le righe e la definizione delle colonne |
| Backend completamente libero | CRUD non funziona out-of-the-box: edit/insert vanno wirati a mano sul tuo backend |
| Buono per integrazioni 3rd-party | Paging/sort/filter server-side richiede wiring custom (vedi sotto) |
| Nessun lavoro di scaffolding | Type safety solo via cast |
Filtri / sort / paging: client-side vs server-side
La frase "filtri/sort/paging/export gratis" della tabella Trade-off vale solo in modalita' client-side, ed e' subordinata a un flag metadata che va impostato esplicitamente nel datasource hardcoded.
Il flag chiave: md_server_side_operations
md_server_side_operations (proprieta' di MetadatiTabella) controlla dove vengono eseguite paging/sort/filter:
| Valore | Significato | Quando usarlo |
|---|---|---|
true (default) | La list-grid invia gli eventi paging/sort/filter al backend tramite l'endpoint CRUD standard del framework. Il backend ritorna solo la pagina richiesta e applica sort/filter SQL-side. | Pattern 1 e 2 (con route metadata reale e backend WUIC dietro). |
false | La list-grid esegue paging/sort/filter in-memory sull'array gia' caricato. Niente roundtrip server. | Sempre nei datasource hardcoded del Pattern 3 (e ogni volta che pubblichi tutte le righe in un colpo solo via fetchInfo$.next). |
Trappola tipica del Pattern 3: se dimentichi di forzare md_server_side_operations: false, la list-grid mostra le 50/100 righe ricevute, ma cliccare la pagina 2, ordinare una colonna o digitare nel filtro non fa niente — la grid invia l'evento al "backend del framework" che non esiste, e l'UX appare bloccata pur senza errori in console.
> Nota framework: DataSourceComponent.fetchData() rileva automaticamente il caso "hardcoded datasource" (nessun [hardcodedRoute] impostato) e salta la chiamata al backend ad ogni cambio paging/sort/filter, ripubblicando il payload gia' presente in memoria su fetchInfo$. Significa che, una volta popolato fetchInfo$.next(...) la prima volta nel tuo ngAfterViewInit, paging/sort/filter funzionano client-side senza nessun roundtrip server, anche se il backend WUIC non ha registrato la route. Vedi data-source.component.ts (short-circuit dentro fetchData()).
Modalita' client-side (default consigliato per Pattern 3)
const meta = new MetaInfo();
// new MetadatiTabella() per ereditare i default (md_sortable, ecc.).
const tableMeta = new MetadatiTabella();
tableMeta.md_server_side_operations = false; // <-- chiave: forza in-memory
tableMeta.md_pageable = true; // abilita paginazione UI
tableMeta.md_pagesize = 10; // righe per pagina
meta.tableMetadata = tableMeta;
- Carichi tutte le righe con un singolo
fetchInfo$.next. - La list-grid applica filtri/sort/paging/export sull'array gia' presente.
- Zero codice aggiuntivo.
- Indicato per dataset piccoli/medi (ordine di qualche migliaio di righe).
Modalita' server-side (wiring manuale)
Per dataset grandi (decine/centinaia di migliaia di righe) non vuoi caricare tutto in memoria. Lasci md_server_side_operations: true (default), ti sottoscrivi agli @Output di `<wuic-list-grid>` (onPaging, onSorting, onFiltering) e re-chiami il tuo endpoint REST a ogni cambio di stato. La list-grid aggiorna ds.currentPage / pageSize / sortInfo / filterInfo prima di emettere l'evento, quindi nel handler basta leggere lo stato corrente.
import { Component, ViewChild, AfterViewInit, inject } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { BehaviorSubject } from 'rxjs';
import { DataSourceComponent, ListGridComponent, MetaInfo, MetadatiColonna, MetadatiTabella, WtoolboxService } from 'wuic-framework-lib';
interface InventoryResponse { rows: any[]; total: number; }
Endpoint server complementare (esempio C#, vedi SamplesController.GetInventory):
[HttpGet("inventory")]
public IActionResult GetInventory(
int offset = 0,
int limit = 10,
string? sortField = null,
string? sortDir = "asc",
string? filterField = null,
Punti chiave:
- Gli
@Output(onPaging) / (onSorting) / (onFiltering)di<wuic-list-grid>espongono gli eventi UI dopo che il list-grid handler ha gia' aggiornato lo stato del datasource. Niente da reimplementare: leggids.currentPage,ds.pageSize,ds.sortInfo[0],ds.filterInfo.filters[0]. - Il backend deve ritornare
{ rows, total }dovetotale' il count POST-filter / PRE-page. Senza questo il pager UI non sa quante pagine esistono e non funziona correttamente. - L'operatore di filtro arriva nel campo
operatoredella filter entry ('eq','contains','startswith', ecc., vedi tabella matchMode in List Grid). Mappalo coerentemente lato server.
Variante: consumare l'endpoint OData del framework
Se l'entita' che vuoi visualizzare e' gia' esposta dal framework come entity set OData (/odata/<EntitySet>), non serve scrivere NESSUN controller: basta tradurre lo stato UI della list-grid in query string OData v4 standard.
> Alternativa 100% framework-driven (Pattern 1 con backend OData): se accetti di registrare una route metadata standard per l'entita', puoi configurare md_props_bag.endpoint = {"type":"odata","uri":"/odata/Cities"} e il datasource fa tutto da solo (filter/sort/paging/export) tramite il provider OData interno. Niente codice Angular custom. Vedi OData per il setup completo. Pattern 3 (questa pagina) si applica invece quando vuoi controllo esplicito lato frontend o non hai metadata registrati per l'entita'.
Il framework espone DataProviderOdataService.filterInfoToOdata(filterInfo, entitySetName) che fa tutto il mapping operator WUIC -> $filter OData (contains/startswith/endswith/eq/ne/gt/ge/lt/le) con quoting automatico per string/numeric, supporto nested filter groups (AND/OR ricorsivi) e isnull/isnotnull. Il return e' una URL relativa tipo /odata/Cities?$filter=<espressione encoded>. Ti basta prefissare la base URL e aggiungere $top / $skip / $orderby.
import { Component, ViewChild, AfterViewInit, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, forkJoin } from 'rxjs';
import {
DataProviderOdataService,
DataSourceComponent, ListGridComponent,
FilterInfo, MetaInfo, MetadatiColonna, MetadatiTabella,
> Nota su total/count: se l'endpoint OData che usi e' configurato per ritornare il wrapper OData standard { value: [...], "@odata.count": N } (via $count=true), puoi leggere il total direttamente dalla response senza la seconda query. L'endpoint del framework WUIC attualmente ritorna il plain array e richiede la query parallela.
Esempi vivi nel WuicTest
I tre esempi coprono le tre strategie principali del Pattern 3:
- External REST grid (client-side) → carica TUTTI i 50 post da un endpoint esterno (
jsonplaceholder.typicode.com/posts) in un colpo solo, paging/sort/filter applicati in-memory dalla list-grid (md_server_side_operations: false). Cartella sorgente:wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/. Apri demo. - Custom .NET grid (server-side, REST custom) → chiama il Controller
SamplesController.GetInventorycon offset/limit/sort/filter come query params ad hoc, ricarica solo la pagina corrente ad ogni cambio (md_server_side_operations: true+ wiring esplicito su(onPaging)/(onSorting)/(onFiltering)). Cartella sorgente:wwwroot/src/app/component/examples/pattern-3/3b-custom-dotnet-grid/+Controllers/SamplesController.cs. Apri demo. - OData Cities grid (server-side, standard OData v4) → consuma l'endpoint OData generico del framework (
GET /odata/Cities) con query string standard$top / $skip / $filter / $orderby, nessun controller custom da scrivere. Traduce gli eventi UI della list-grid in query OData (contains(name,'v'),field eq value, ecc.). Cartella sorgente:wwwroot/src/app/component/examples/pattern-3/3c-odata-cities-grid/. Apri demo.
Quando scegliere quale variante
| Esempio | Strategia | Backend | Quando usarla |
|---|---|---|---|
| 3a | Client-side | Endpoint REST classico che ritorna un array | Dataset piccolo-medio (< qualche migliaio di righe), semplicita' massima, API 3rd-party senza controllo server-side |
| 3b | Server-side REST custom | Controller REST tuo con query params di paging/sort/filter | Dataset grande, vuoi controllo totale sulla query; l'end-dev ha gia' un endpoint esistente con offset/limit/ecc. |
| 3c | Server-side OData | Endpoint OData del framework (/odata/<EntitySet>) | Dataset grande esposto automaticamente dal framework come OData set; zero codice backend; sintassi standard compatibile con altri client |
Vedi anche
- Pattern 1 — Full autogeneration: se l'UX standard basta e i dati esistono nel modello scaffoldato.
- Pattern 2 — Framework data + Custom component: inverso (UI custom, dati framework).
- Pattern 4 — Full custom: se non ti serve nemmeno la list-grid.
- Pattern 5 — Framework component + Framework data (manual mount): variante "framework" di questo pattern: stessa compozione manuale dei widget, ma data layer metadata-driven invece che backend custom.