diff --git a/docs/adr/0004-signalr-datasource-server-side-filtering.md b/docs/adr/0004-signalr-datasource-server-side-filtering.md index b5bfe17..098c5cf 100644 --- a/docs/adr/0004-signalr-datasource-server-side-filtering.md +++ b/docs/adr/0004-signalr-datasource-server-side-filtering.md @@ -68,6 +68,8 @@ Adopt a **criteria-string** server-side filter for the SignalR DataSource. The D **Second defect — hijacked `ValuesChanged` destroying the criteria (2026-07-17, console-proven):** the component owns `ValuesChanged`, but the callback cannot distinguish a genuine user edit from (a) the **echo** of the component's own programmatic `Values` write and (b) an **editor-internal revalidation drop** — console diagnostics caught DevExpress raising `ValuesChanged([])` unprompted right after a heal (the transient mid-`Reload`/in-place-`Reset` state rejects the not-yet-resolvable tag), which the handler misread as "user cleared the box" → wrote the criteria to `null` → the layout **AutoSave persisted the destruction** (the user's saved filter was permanently lost on every failing visit; the only upside: no invisible filter). The lookup content itself was proven resolvable (the same value is user-selectable from the same 20-item windowed list), killing the data-window explanation for the *current* repro. **Fix (2nd attempt, superseded):** a render-tick-windowed callback guard shipped and was **refuted the same day** by console evidence — the drop arrives through a JS-interop hop whose latency is unbounded in render ticks, so it landed *after* the window closed and destroyed the criteria again. **Fix (final, "safe order") — remove the drop's precondition instead of filtering callbacks:** (1) **no value is ever asserted into an empty editor** — while the lookup `Data` is empty the criteria stays untouched (rows filter, the Clear button signals it) and the editor holds no value the revalidation could drop; (2) the heal `Reload()`s the **empty** editor — nothing to drop ⇒ no spurious `ValuesChanged` is raised at all; (3) **one delayed assert** (~750 ms, generously covering the measured ~300 ms JS hop; re-reads the criteria at fire time) puts the selection into the settled editor — its echo rewrites the same criteria, which is harmless; (4) **ownership-guarded clearing**: an empty selection may only clear criteria the component OWNS — a filter-row value (`Contains("EUR")`) can never be wiped by an empty callback, genuine or echoed. Every ambiguous path still resolves toward **clearing** (never an invisible filter). Residual: the delay is measured-generous, not contractual — the v2 envelope/pull model removes the heal (and the delay) entirely. **Planned v2 (supersedes the heal):** fold the pull model into the already-planned layout envelope (`MgGridLayoutSave { Version, GridLayoutJson, FilterPanelLayoutJson }`): strip panel-owned criteria from the DX layout at **save** time (when registered components *can* answer `OwnsCriteria`) and store them as plain `field → values`; on restore **park** them and apply criteria **and** selection atomically only once the owning component is ready (registered **and** `Data` non-empty). This makes "can this criteria be displayed yet?" true **by construction** instead of detected: no `Reload()` dependency, no rows-filtered-while-chips-empty window, `ACBLAZOR-GRID-I-D4T7` fixed by construction, and the pending-reconcile flag, the snapshot heal and `RefreshFilterPanel()` all become deletable (net complexity **down**, consumer boilerplate → zero). Standalone pull (without the envelope) was **rejected**: at auto-load time no component is registered yet, forcing shape-sniffing of the DX criteria string, which collides with the shared filter-row slot. An `INotifyCollectionChanged` subscription was also **rejected**: strong collection→component references on page-shared collections outliving `OnDemand` tab components, in a design that holds components weakly precisely because the DX seams expose no disposal hook — a lifetime bug traded for a timing bug, fixing none of the structural issues. Design decision to record when v2 lands: a parked entry with no owner this session stays parked and is **re-persisted** on save, so a temporarily absent panel cannot silently destroy a saved filter. + + **Item ordering in the panel components (2026-07-17, planned — direction (B)):** `DxTagBox` (and its list-editor bases) exposes **no sort API** (verified against the v25.1 XML docs) — DevExpress expects pre-sorted `Data`. Inline markup sorting (`Data="@X.OrderBy(...)"`) is an **anti-pattern** here: LINQ returns a NEW lazy wrapper instance on every render, and both DevExpress's `Data`-change detection and the component's snapshot tracker key on **references** → constant reload churn, and it re-destabilizes the restore timing this ADR just fixed. The sorted sequence must be **materialized with a stable reference** that changes only when the data actually changes. Options weighed: **(A) consumer sort-at-fill** (`Replace((await Get...()).OrderBy(...))` — stable in-place order, benefits every consumer of the shared collection, but must be repeated at every fill site, and the grid-datasource pipeline fill of the same shared collection stays unsorted; root variant: server-side `ORDER BY` in the GetAll endpoints). **(B) framework `OrderItemsByText` on `MgGridFilterPanelTagBox`** (default on, plus `OrderItemsDescending`): the component builds an internal **materialized list sorted by the `TextFieldName` value** (what the user sees — Partner → name, Shipping → date), rebuilt ONLY when the tracked source changes, and hands it to DevExpress as the effective `Data` — the reference changes exactly when the content does, and the empty→filled rebuild funnels into the same delayed-assert safe order. **(B) is the chosen direction** (zero consumer changes, covers every fill path incl. the datasource pipeline); implementation pending — AyCode.Blazor `MGGRID_TODO.md` **ACBLAZOR-GRID-T-K9F4**. - Verify: `DevExpress.Data` version matching the client DevExpress Blazor major; license coverage on the server package feed; linq2db translation of `CriteriaToExpressionConverter` output (function whitelist). - **Quick-search (`SearchText`) — confirmed SEPARATE from `FilterCriteria`.** Per DevExpress docs (Filter API): *"Search settings do not affect the applied filter criteria"* — `SearchText` filters in parallel, NOT folded into `FilterCriteria`. So server-side search does **not** ride the criteria-string path for free; it needs its **own** handling: a thin `SearchText → OR-Contains criteria` adapter over searchable columns (then it rides the same channel), or a separate search field. Same whitelist / DoS caveats apply (search = OR-Contains across many columns). - AyCode.Blazor cross-ref: no `docs/adr/` folder there yet → note in MGGRID docs or a future AyCode.Blazor ADR.