Overview
Pattern: Framework component + Custom data
The high-level UI components of the framework (<wuic-list-grid>, <wuic-chart-list>, ...) accept a hand-built datasource (hardcodedDatasource) that does not go through the framework's data layer. Data comes from your custom backend (your own .NET Controller, external REST, static files, websocket, ...).
When to Use It
- You want the full UX of a list-grid/chart-list (filters, sort, client paging, export, edit dialog) but the data is produced by:
- An external REST endpoint (3rd-party API, microservice).
- A .NET Controller of yours not integrated with the framework data layer.
- Static files, aggregate calculations, live data (websocket, polling).
- You have a legacy domain you don't want to model in the framework.
- You are prototyping without yet having defined the data structure.
Architecture
- Developer: writes a small Angular component that fetches data from its backend and packages it in a local datasource.
- Framework: the list-grid behaves exactly as if the data came from the standard data layer (filters, sort, paging, export all work).
- Backend: total freedom. Classic REST endpoints, no metadata conventions.
What You Do (Frontend)
You create a standalone Angular component that:
1. Calls your custom endpoint with HttpClient.
2. Defines the columns (name, label, type) for the local datasource.
3. Publishes rows + columns on the datasource and passes it to the 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',What You Do (Backend, Optional)
If the data comes from your internal backend, a classic REST Controller is enough. No framework conventions, no metadata to write.
<!-- source: WuicTest/Controllers/SamplesController.cs -->
[ApiController]
[Route("api/samples")]
public class SamplesController : ControllerBase
{
[HttpGet("inventory")]
public IActionResult GetInventory()
{Trade-offs
| Pros | Cons |
|---|---|
wuic-list-grid UX with filters/sort/paging/export for free (client-side) | You maintain the consistency between rows and column definitions |
| Completely free backend | CRUD does not work out-of-the-box: edit/insert must be wired manually on your backend |
| Good for 3rd-party integrations | Server-side paging/sort/filter requires custom wiring (see below) |
| No scaffolding work | Type safety only via cast |
Filters / Sort / Paging: Client-Side vs Server-Side
The "filters/sort/paging/export for free" statement in the Trade-offs table applies only in client-side mode, and is subject to a metadata flag that must be explicitly set in the hardcoded datasource.
The Key Flag: md_server_side_operations
md_server_side_operations (property of MetadatiTabella) controls where paging/sort/filter are executed:
| Value | Meaning | When to use |
|---|---|---|
true (default) | The list-grid sends paging/sort/filter events to the backend via the framework's standard CRUD endpoint. The backend returns only the requested page and applies sort/filter SQL-side. | Patterns 1 and 2 (with real metadata route and WUIC backend behind). |
false | The list-grid executes paging/sort/filter in-memory on the already-loaded array. No server roundtrip. | Always in Pattern 3 hardcoded datasources (and any time you publish all rows at once via fetchInfo$.next). |
Typical Pattern 3 pitfall: if you forget to force md_server_side_operations: false, the list-grid shows the 50/100 received rows, but clicking page 2, sorting a column, or typing in the filter does nothing — the grid sends the event to the "framework backend" which does not exist, and the UX appears stuck even without console errors.
> Framework note: DataSourceComponent.fetchData() automatically detects the "hardcoded datasource" case (no [hardcodedRoute] set) and skips the backend call on every paging/sort/filter change, republishing the payload already in memory on fetchInfo$. This means that, once you populate fetchInfo$.next(...) the first time in your ngAfterViewInit, paging/sort/filter work client-side without any server roundtrip, even if the WUIC backend has not registered the route. See data-source.component.ts (short-circuit inside fetchData()).
Client-side Mode (Recommended Default for Pattern 3)
const meta = new MetaInfo();
// new MetadatiTabella() to inherit defaults (md_sortable, etc.).
const tableMeta = new MetadatiTabella();
tableMeta.md_server_side_operations = false; // <-- key: force in-memory
tableMeta.md_pageable = true; // enable UI pagination
tableMeta.md_pagesize = 10; // rows per page
meta.tableMetadata = tableMeta;- You load all rows with a single
fetchInfo$.next. - The list-grid applies filters/sort/paging/export on the already-present array.
- Zero extra code.
- Suited for small/medium datasets (a few thousand rows at most).
Server-side Mode (Manual Wiring)
For large datasets (tens/hundreds of thousands of rows) you do not want to load everything in memory. Leave md_server_side_operations: true (default), subscribe to the @Outputs of `<wuic-list-grid>` (onPaging, onSorting, onFiltering) and re-call your REST endpoint on every state change. The list-grid updates ds.currentPage / pageSize / sortInfo / filterInfo before emitting the event, so in the handler you just read the current state.
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; }
Complementary server endpoint (C# example, see SamplesController.GetInventory):
[HttpGet("inventory")]
public IActionResult GetInventory(
int offset = 0,
int limit = 10,
string? sortField = null,
string? sortDir = "asc",
string? filterField = null,Key points:
- The
@Outputs(onPaging) / (onSorting) / (onFiltering)of<wuic-list-grid>expose the UI events after the list-grid handler has already updated the datasource state. Nothing to reimplement: readds.currentPage,ds.pageSize,ds.sortInfo[0],ds.filterInfo.filters[0]. - The backend must return
{ rows, total }wheretotalis the POST-filter / PRE-page count. Without this, the UI pager doesn't know how many pages exist and does not work correctly. - The filter operator arrives in the
operatorefield of the filter entry ('eq','contains','startswith', etc., see the matchMode table in List Grid). Map it consistently on the server side.
Variant: Consuming the Framework's OData Endpoint
If the entity you want to display is already exposed by the framework as an OData entity set (/odata/<EntitySet>), you don't need to write ANY controller: just translate the list-grid UI state into a standard OData v4 query string.
> 100% framework-driven alternative (Pattern 1 with OData backend): if you accept registering a standard metadata route for the entity, you can configure md_props_bag.endpoint = {"type":"odata","uri":"/odata/Cities"} and the datasource does everything by itself (filter/sort/paging/export) via the internal OData provider. No custom Angular code. See OData for the complete setup. Pattern 3 (this page) applies instead when you want explicit frontend control or you don't have metadata registered for the entity.
The framework exposes DataProviderOdataService.filterInfoToOdata(filterInfo, entitySetName) which does all the WUIC operator -> $filter OData mapping (contains/startswith/endswith/eq/ne/gt/ge/lt/le) with automatic quoting for string/numeric, support for nested filter groups (recursive AND/OR) and isnull/isnotnull. The return is a relative URL like /odata/Cities?$filter=<encoded expression>. You just need to prefix the base URL and add $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,> Note on total/count: if the OData endpoint you use is configured to return the standard OData wrapper { value: [...], "@odata.count": N } (via $count=true), you can read total directly from the response without the second query. The current WUIC framework endpoint returns a plain array and requires the parallel query.
Live Examples in WuicTest
The three examples cover the three main strategies of Pattern 3:
- External REST grid (client-side) → loads ALL 50 posts from an external endpoint (
jsonplaceholder.typicode.com/posts) at once, paging/sort/filter applied in-memory by the list-grid (md_server_side_operations: false). Source folder:wwwroot/src/app/component/examples/pattern-3/3a-external-rest-grid/. Open demo. - Custom .NET grid (server-side, custom REST) → calls the
SamplesController.GetInventoryController with offset/limit/sort/filter as ad-hoc query params, reloads only the current page on every change (md_server_side_operations: true+ explicit wiring on(onPaging)/(onSorting)/(onFiltering)). Source folder:wwwroot/src/app/component/examples/pattern-3/3b-custom-dotnet-grid/+Controllers/SamplesController.cs. Open demo. - OData Cities grid (server-side, standard OData v4) → consumes the framework's generic OData endpoint (
GET /odata/Cities) with standard query string$top / $skip / $filter / $orderby, no custom controller to write. Translates the list-grid UI events into OData queries (contains(name,'v'),field eq value, etc.). Source folder:wwwroot/src/app/component/examples/pattern-3/3c-odata-cities-grid/. Open demo.
When to Choose Which Variant
| Example | Strategy | Backend | When to use it |
|---|---|---|---|
| 3a | Client-side | Classic REST endpoint returning an array | Small-medium dataset (< a few thousand rows), maximum simplicity, 3rd-party API with no server-side control |
| 3b | Server-side custom REST | Your REST Controller with paging/sort/filter query params | Large dataset, you want full control over the query; the end-dev already has an existing endpoint with offset/limit/etc. |
| 3c | Server-side OData | Framework OData endpoint (/odata/<EntitySet>) | Large dataset automatically exposed by the framework as an OData set; zero backend code; standard syntax compatible with other clients |
See Also
- Pattern 1 — Full autogeneration: if the standard UX is enough and data exists in the scaffolded model.
- Pattern 2 — Framework data + Custom component: the inverse (custom UI, framework data).
- Pattern 4 — Full custom: if you don't even need the list-grid.
- Pattern 5 — Framework component + Framework data (manual mount): the "framework" variant of this pattern: same manual widget composition, but metadata-driven data layer instead of custom backend.