From a254dbe722e013bb96645b70da0f52ad4d614afd Mon Sep 17 00:00:00 2001 From: Loretta Date: Sun, 12 Jul 2026 08:36:20 +0200 Subject: [PATCH] InfoPanel: custom edit templates & null edit model fix - Add InfoPanelEditTemplate to MgGridDataColumn for custom InfoPanel editors with refresh support - Introduce MgGridInfoPanelEditContext for edit context/refresh - Harden OnCustomizeEditModel to handle null edit models, preventing InfoPanel crashes and logging warnings - Update docs: usage, editor resolution order, and new issue entry (ACBLAZOR-GRID-I-N7K2) - Refactor grid templates to use new features and ensure consistent editor behavior - Minor doc and formatting improvements --- .../Components/Grids/MgGridBase.cs | 42 +++++++++---- .../Components/Grids/MgGridDataColumn.cs | 14 +++++ .../Components/Grids/MgGridInfoPanel.razor | 8 +++ .../docs/MGGRID/MGGRID_COLUMNS.md | 18 ++++++ .../docs/MGGRID/MGGRID_INFOPANEL.md | 18 +++++- .../docs/MGGRID/MGGRID_ISSUES.md | 61 ++++++++++++++++++- .../docs/MGGRID/README.md | 2 +- 7 files changed, 146 insertions(+), 17 deletions(-) diff --git a/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs b/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs index 4f95af2..2b01aae 100644 --- a/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs +++ b/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs @@ -509,20 +509,24 @@ public abstract class MgGridBase typeof(TDataItem).GetProperty(propertyName); + private void InitializeNewEditModel(TDataItem editModel) + { + if (!HasIdValue(editModel)) SetNewId(editModel); + + if (ParentDataItem != null && !KeyFieldNameToParentId.IsNullOrWhiteSpace()) + { + KeyFieldPropertyInfoToParent ??= GetDataItemPropertyInfo(KeyFieldNameToParentId); + KeyFieldPropertyInfoToParent!.SetValue(editModel, ParentDataItem.Id); + } + } + protected virtual async Task OnCustomizeEditModel(GridCustomizeEditModelEventArgs e) { - var editModel = (e.EditModel as TDataItem)!; + var editModel = e.EditModel as TDataItem; - if (e.IsNew) + if (e.IsNew && editModel != null) { - if (!HasIdValue(editModel)) SetNewId(editModel); - - if (ParentDataItem != null && !KeyFieldNameToParentId.IsNullOrWhiteSpace()) - { - KeyFieldPropertyInfoToParent ??= GetDataItemPropertyInfo(KeyFieldNameToParentId); - KeyFieldPropertyInfoToParent!.SetValue(editModel, ParentDataItem.Id); - } - + InitializeNewEditModel(editModel); e.EditModel = editModel; } @@ -531,8 +535,24 @@ public abstract class MgGridBase +/// Context for : the edit model plus a Refresh action +/// that re-renders the InfoPanel — call it after mutations other fields depend on (e.g. cascading lookups). +/// +public sealed record MgGridInfoPanelEditContext(object EditModel, Action Refresh); + /// /// Extended DxGridDataColumn with additional parameters for InfoPanel support. /// @@ -36,6 +42,14 @@ public partial class MgGridDataColumn : DxGridDataColumn [Parameter] public int InfoPanelOrder { get; set; } = int.MaxValue; + /// + /// Custom InfoPanel editor for this column (edit mode). Takes precedence over the EditSettings / + /// property-type based default editors. Use when the grid-side editor is a CellEditTemplate the + /// InfoPanel cannot reuse (e.g. cascading lookups). + /// + [Parameter] + public RenderFragment? InfoPanelEditTemplate { get; set; } + /// /// URL template with {property} placeholders that will be replaced with row values. /// Example: https://shop.fruitbank.hu/Admin/Order/Edit/{Id}/{OrderId} diff --git a/AyCode.Blazor.Components/Components/Grids/MgGridInfoPanel.razor b/AyCode.Blazor.Components/Components/Grids/MgGridInfoPanel.razor index 5ed8495..11a6abf 100644 --- a/AyCode.Blazor.Components/Components/Grids/MgGridInfoPanel.razor +++ b/AyCode.Blazor.Components/Components/Grids/MgGridInfoPanel.razor @@ -133,6 +133,14 @@ return builder => { var seq = 0; + + // Column-provided editor template wins (e.g. cascading lookups the settings pipeline cannot express) + if (column is MgGridDataColumn { InfoPanelEditTemplate: not null } mgColumn) + { + builder.AddContent(seq++, mgColumn.InfoPanelEditTemplate(new MgGridInfoPanelEditContext(dataItem, () => InvokeAsync(StateHasChanged)))); + return; + } + var propertyInfo = dataItemType.GetProperty(column.FieldName); if (propertyInfo == null) diff --git a/AyCode.Blazor.Components/docs/MGGRID/MGGRID_COLUMNS.md b/AyCode.Blazor.Components/docs/MGGRID/MGGRID_COLUMNS.md index 2795096..2af1a75 100644 --- a/AyCode.Blazor.Components/docs/MGGRID/MGGRID_COLUMNS.md +++ b/AyCode.Blazor.Components/docs/MGGRID/MGGRID_COLUMNS.md @@ -11,8 +11,26 @@ Extended `DxGridDataColumn` with InfoPanel and URL link support. | `ShowInInfoPanel` | `bool` | `true` | Whether this column is visible in InfoPanel | | `InfoPanelDisplayFormat` | `string?` | `null` | Custom display format for InfoPanel | | `InfoPanelOrder` | `int` | `int.MaxValue` | Column order in InfoPanel (lower = earlier) | +| `InfoPanelEditTemplate` | `RenderFragment?` | `null` | Custom InfoPanel editor (edit mode) — takes precedence over the `EditSettings` / property-type default editors | | `UrlLink` | `string?` | `null` | URL template with `{Property}` placeholders | **UrlLink example:** `https://admin.example.com/Entity/Edit/{Id}` — renders cell as ``. +## InfoPanelEditTemplate + +Use when the grid-side editor is a `CellEditTemplate` the InfoPanel cannot reuse (it is bound to the grid-cell +context) — without it, the panel falls back to the property-type default editor (e.g. `int` → number spinner). +Context: `MgGridInfoPanelEditContext(object EditModel, Action Refresh)` — call `Refresh()` after mutations +other fields depend on (cascading lookups), it re-renders the InfoPanel. + +```razor + + @CategoryEditor((MyItem)context.EditModel) + @CategoryEditor((MyItem)context.EditModel, context.Refresh) + +``` + +Define the editor once as a shared fragment (`private RenderFragment CategoryEditor(MyItem item, Action? refresh = null) => @;`) +so the grid cell and the InfoPanel render the same component — no duplicated markup. + Uses compiled property accessors (`ConcurrentDictionary` cache) for performance. diff --git a/AyCode.Blazor.Components/docs/MGGRID/MGGRID_INFOPANEL.md b/AyCode.Blazor.Components/docs/MGGRID/MGGRID_INFOPANEL.md index fdfbe98..573d8cc 100644 --- a/AyCode.Blazor.Components/docs/MGGRID/MGGRID_INFOPANEL.md +++ b/AyCode.Blazor.Components/docs/MGGRID/MGGRID_INFOPANEL.md @@ -52,6 +52,10 @@ Edit starts → InfoPanelInstance.SetEditMode(grid, editModel) Edit ends/cancel → InfoPanelInstance.ClearEditMode() ``` +When the grid produces no edit model (`CustomizeEditModel` fires with a null `EditModel` — e.g. stale +focused row after a datasource reload), `SetEditMode` is skipped with a `Logger.Warning` and the panel +stays in view mode (see `MGGRID_ISSUES.md#acblazor-grid-i-n7k2`). + `InfoPanelInstance` resolution: own `GridWrapper.InfoPanelInstance` → root grid's `GridWrapper.InfoPanelInstance` → `null`. ### Responsive Column Layout @@ -77,10 +81,20 @@ Edit ends/cancel → InfoPanelInstance.ClearEditMode() `InfoPanelContext` = `record(object? DataItem, bool IsEditMode)`. -### Edit Mode Editors (by property type) +### Edit Mode Editors + +Edit mode reads/writes properties via reflection and **assumes non-throwing accessors** — computed +properties (e.g. GenericAttribute-backed getters that fail on edit-model copies, or deliberately +throwing setters) MUST be declared `ReadOnly` on the column, otherwise rendering the editor crashes. +This is by design (fail-fast): the missing `ReadOnly` declaration is a consumer bug that should surface +in development, not be masked by silent fallbacks. + +Editor resolution order: **column `InfoPanelEditTemplate` first** (`MgGridDataColumn` — see `MGGRID_COLUMNS.md`), +then `EditSettings` (`DxComboBoxSettings`, `Memo`), then property type: | Type | Editor Component | |---|---| +| Column `InfoPanelEditTemplate` (`MgGridDataColumn`) | the provided template — wins over everything below | | `bool` | `DxCheckBox` | | `DateTime` / `DateTime?` | `DxDateEdit` / `DxDateEdit` | | `DateOnly` / `DateOnly?` | `DxDateEdit` / `DxDateEdit` | @@ -94,5 +108,5 @@ Edit ends/cancel → InfoPanelInstance.ClearEditMode() ### Additional Features - **Sticky positioning** via JS interop (`MgGridInfoPanel.initSticky`) -- **Built-in toolbar** with `MgGridToolbarTemplate` (`OnlyGridEditTools=true`) +- **Built-in toolbar** with `MgGridToolbarTemplate` (`OnlyGridEditTools=true`) — its edit tools are currently not configurable per grid (hardcoded `EnableNew`/`EnableEdit` defaults; see `MGGRID_ISSUES.md#acblazor-grid-i-k5r3`) - **OnDataItemChanged** callback when focused row changes diff --git a/AyCode.Blazor.Components/docs/MGGRID/MGGRID_ISSUES.md b/AyCode.Blazor.Components/docs/MGGRID/MGGRID_ISSUES.md index 7da20c0..555c794 100644 --- a/AyCode.Blazor.Components/docs/MGGRID/MGGRID_ISSUES.md +++ b/AyCode.Blazor.Components/docs/MGGRID/MGGRID_ISSUES.md @@ -1,10 +1,65 @@ # MGGRID — Known Issues -For planned/actionable work see `MGGRID_TODO.md`. +For planned/actionable work see `MGGRID_TODO.md`. In-code TODOs are recorded as TODO entries (`ACBLAZOR-GRID-T-*`) there, since they describe unfinished work rather than confirmed broken behaviour. -No formally-tracked issues yet. In-code TODOs are recorded as TODO entries (`ACBLAZOR-GRID-T-*`) in `MGGRID_TODO.md`, since they describe unfinished work rather than confirmed broken behaviour. +## ACBLAZOR-GRID-I-N7K2: InfoPanel.SetEditMode crashes the WASM renderer when DxGrid produces no edit model -Add the first `## ACBLAZOR-GRID-I-XXXX: ...` entry below when a concrete issue is observed. +**Severity:** Major (unhandled exception killed the renderer) · **Status:** Closed (2026-07-12) · **Area:** `Components/Grids/MgGridBase.cs` → `OnCustomizeEditModel` + +### Description +Clicking the toolbar Edit button could crash the entire WebAssembly renderer with +`System.ArgumentNullException: editModel` thrown by `MgGridInfoPanel.SetEditMode`. Observed on a +`ProductDto` grid right after a datasource reload (`OnDataSourceLoaded` → immediate Edit click): +DevExpress `StartEditRowAsync` raised `CustomizeEditModel` with `e.EditModel == null` (stale +`FocusedRowVisibleIndex` → null data item), and `OnCustomizeEditModel` passed the null straight to +`SetEditMode`, whose `ArgumentNullException.ThrowIfNull` escalated to an unhandled rendering exception. + +### Root cause +`MgGridBase.OnCustomizeEditModel` captured `(e.EditModel as TDataItem)!` (null-forgiving cast) with no +null check, and on the Edit path (`IsNew == false`) nothing validated it before +`InfoPanelInstance.SetEditMode`. The local was also captured BEFORE the `OnGridCustomizeEditModel` +consumer callback — so a consumer-created edit model (the official DevExpress pattern for types without +a parameterless constructor) never reached the InfoPanel either. + +### Resolution (2026-07-12) +`OnCustomizeEditModel` hardened: null-tolerant cast, guarded `IsNew` branch, the edit model is re-read +AFTER the consumer callback, and when it is still null the InfoPanel stays in view mode with a +`Logger.Warning` (`[GridName] CustomizeEditModel produced no edit model...`) instead of crashing. + +### Known workaround +None needed after the fix. + +### Related TODO +None. + +## ACBLAZOR-GRID-I-K5R3: InfoPanel built-in toolbar edit tools are not configurable + +**Severity:** Minor (UX / consistency gap; can re-open a crash path on listing-only grids) · **Status:** Open · **Area:** `Components/Grids/MgGridInfoPanel.razor` (built-in toolbar) + `MgGridToolbarTemplate` + +### Description +`MgGridInfoPanel` renders its built-in toolbar with hardcoded defaults +(`MgGridInfoPanel.razor:20`: ``), +so `EnableNew` / `EnableEdit` are always `true` there. When a consumer disables New/Edit on its own +master toolbar (e.g. a listing-only grid such as a product grid whose data is managed in an external +admin and has no Add/Update SignalR tags), the InfoPanel toolbar still offers **+** and **edit** — +the edit flow can be started through a side door the consumer cannot close. + +### Root cause +The panel's toolbar instance is not parameterizable from the outside, and there is no grid-level +edit-enable flag the toolbar could consult — the `Enable*` flags live only on individual +`MgGridToolbarTemplate` instances. + +### Known workaround +None from the consumer side (the panel toolbar cannot be reached). Master-toolbar `EnableNew="false" +EnableEdit="false"` covers the primary path only. + +### Proposed fix +Grid-level `EditItemsEnabled` parameter on `MgGridBase` / `IMgGridBase` (default `true`); +`MgGridToolbarTemplate` computes effective enablement as `own Enable* && Grid.EditItemsEnabled`, +so both the master and the built-in InfoPanel toolbar respect a single consumer declaration. + +### Related TODO +None yet — becomes a TODO when scheduled. ## Issue entry template diff --git a/AyCode.Blazor.Components/docs/MGGRID/README.md b/AyCode.Blazor.Components/docs/MGGRID/README.md index e423f53..69a95ef 100644 --- a/AyCode.Blazor.Components/docs/MGGRID/README.md +++ b/AyCode.Blazor.Components/docs/MGGRID/README.md @@ -28,7 +28,7 @@ | `MGGRID_TOOLBAR.md` | MgGridToolbarTemplate (buttons, parameters, state) | | `MGGRID_COLUMNS.md` | MgGridDataColumn (InfoPanel params, UrlLink) | | `MGGRID_DATASOURCE.md` | MgGridSignalRDataSource (server-side data, local cache, background refresh) | -| `MGGRID_ISSUES.md` | Known issues (`ACBLAZOR-GRID-I-*`, `ACBLAZOR-GRID-B-*`) — currently none formally tracked | +| `MGGRID_ISSUES.md` | Known issues (`ACBLAZOR-GRID-I-*`, `ACBLAZOR-GRID-B-*`) | | `MGGRID_TODO.md` | Planned work (`ACBLAZOR-GRID-T-*`) — refactors, missing features, optimizations | ## Component Hierarchy