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:
parent
076162f6ee
commit
a254dbe722
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue