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
This commit is contained in:
Loretta 2026-07-12 08:36:20 +02:00
parent 076162f6ee
commit a254dbe722
7 changed files with 146 additions and 17 deletions

View File

@ -509,11 +509,7 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
protected PropertyInfo? GetDataItemPropertyInfo(string propertyName)
=> typeof(TDataItem).GetProperty(propertyName);
protected virtual async Task OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
{
var editModel = (e.EditModel as TDataItem)!;
if (e.IsNew)
private void InitializeNewEditModel(TDataItem editModel)
{
if (!HasIdValue(editModel)) SetNewId(editModel);
@ -522,7 +518,15 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
KeyFieldPropertyInfoToParent ??= GetDataItemPropertyInfo(KeyFieldNameToParentId);
KeyFieldPropertyInfoToParent!.SetValue(editModel, ParentDataItem.Id);
}
}
protected virtual async Task OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
{
var editModel = e.EditModel as TDataItem;
if (e.IsNew && editModel != null)
{
InitializeNewEditModel(editModel);
e.EditModel = editModel;
}
@ -531,8 +535,24 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
await OnGridCustomizeEditModel.InvokeAsync(e);
// Re-read after the consumer callback — it may have created/replaced the edit model
// (the official DevExpress pattern for types without a parameterless constructor)
var preCallbackModel = editModel;
editModel = e.EditModel as TDataItem;
// Initialize any IsNew instance the pre-callback block has not seen (consumer-replaced or consumer-created)
if (e.IsNew && editModel != null && !ReferenceEquals(editModel, preCallbackModel))
InitializeNewEditModel(editModel);
// Update InfoPanel to edit mode
if (editModel != null)
{
InfoPanelInstance?.SetEditMode(this, editModel);
}
else
{
Logger.Warning($"[{GetType().Name}] CustomizeEditModel produced no edit model (EditModel is null or not {typeof(TDataItem).Name}); InfoPanel stays in view mode");
}
await InvokeAsync(StateHasChanged);
}

View File

@ -7,6 +7,12 @@ using System.Text.RegularExpressions;
namespace AyCode.Blazor.Components.Components.Grids;
/// <summary>
/// Context for <see cref="MgGridDataColumn.InfoPanelEditTemplate"/>: the edit model plus a Refresh action
/// that re-renders the InfoPanel — call it after mutations other fields depend on (e.g. cascading lookups).
/// </summary>
public sealed record MgGridInfoPanelEditContext(object EditModel, Action Refresh);
/// <summary>
/// Extended DxGridDataColumn with additional parameters for InfoPanel support.
/// </summary>
@ -36,6 +42,14 @@ public partial class MgGridDataColumn : DxGridDataColumn
[Parameter]
public int InfoPanelOrder { get; set; } = int.MaxValue;
/// <summary>
/// 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).
/// </summary>
[Parameter]
public RenderFragment<MgGridInfoPanelEditContext>? InfoPanelEditTemplate { get; set; }
/// <summary>
/// URL template with {property} placeholders that will be replaced with row values.
/// Example: https://shop.fruitbank.hu/Admin/Order/Edit/{Id}/{OrderId}

View File

@ -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)

View File

@ -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<MgGridInfoPanelEditContext>?` | `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 `<a href="..." target="_blank">`.
## 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
<MgGridDataColumn FieldName="CategoryId" Caption="Category">
<CellEditTemplate>@CategoryEditor((MyItem)context.EditModel)</CellEditTemplate>
<InfoPanelEditTemplate>@CategoryEditor((MyItem)context.EditModel, context.Refresh)</InfoPanelEditTemplate>
</MgGridDataColumn>
```
Define the editor once as a shared fragment (`private RenderFragment CategoryEditor(MyItem item, Action? refresh = null) => @<DxComboBox ...>;`)
so the grid cell and the InfoPanel render the same component — no duplicated markup.
Uses compiled property accessors (`ConcurrentDictionary` cache) for performance.

View File

@ -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<bool>` |
| `DateTime` / `DateTime?` | `DxDateEdit<DateTime>` / `DxDateEdit<DateTime?>` |
| `DateOnly` / `DateOnly?` | `DxDateEdit<DateOnly>` / `DxDateEdit<DateOnly?>` |
@ -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

View File

@ -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`: `<MgGridToolbarTemplate Grid="_currentGrid" OnlyGridEditTools="true" ShowOnlyIcon="true" />`),
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

View File

@ -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