AyCode.Blazor/docs/MGGRID.md

526 lines
21 KiB
Markdown

# MgGrid System
> Comprehensive documentation for the **MgGrid** component family — the primary UI data grid pattern in the AyCode.Blazor framework.
> Source: `AyCode.Blazor.Components/Components/Grids/`
> For SignalR transport: `AyCode.Core/docs/SIGNALR.md`
> For `AcSignalRDataSource`: `AyCode.Core/docs/SIGNALR_DATASOURCE.md`
## Overview
**MgGridBase** is an abstract generic Blazor component that extends DevExpress `DxGrid` with:
- **Automatic SignalR CRUD** via `AcSignalRDataSource` (see AyCode.Core docs)
- **Layout persistence** — column order, widths, sorting, grouping auto-saved to `localStorage`
- **Master-detail hierarchy** — nested grids share context via `CascadingParameter`
- **InfoPanel integration** — side panel shows focused row details, supports edit mode
- **Fullscreen mode** — standalone overlay or via `MgGridWithInfoPanel` wrapper
- **Change tracking** — client-side tracking with server sync via `SaveChangesAsync`
## Component Hierarchy
```
DxGrid (DevExpress)
└── MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClient> (AyCode.Blazor — abstract)
└── [Project-specific adapter] ← consumer fixes TSignalRDataSource, TId, TLoggerClient
└── [Concrete entity grid] ← consumer sets CRUD tags in constructor
```
### Companion Components
| Component | Purpose |
|---|---|
| **MgGridWithInfoPanel** | `DxSplitter` wrapper: grid (left) + InfoPanel (right), fullscreen, splitter size persistence |
| **MgGridToolbarBase** | `DxToolbar` base with `Grid` reference, `RefreshClick` callback, `ShowOnlyIcon` toggle |
| **MgGridToolbarTemplate** | Full toolbar template: New/Edit/Delete/Save/Cancel, row navigation, layout menu, fullscreen, export, extensible via `ToolbarItemsExtended` |
| **MgGridDataColumn** | Extended `DxGridDataColumn` with InfoPanel parameters and `UrlLink` template (`{Property}` placeholders) |
| **MgGridInfoPanel** | Default InfoPanel: column-value pairs, edit mode with typed editors, responsive columns, sticky positioning |
| **MgGridSignalRDataSource** | `GridCustomDataSource` wrapper: server-side filter/sort/page, local cache for seen filter criteria, background refresh |
## Generic Type Parameters
```csharp
MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClient>
```
| Parameter | Constraint | Purpose |
|---|---|---|
| `TSignalRDataSource` | `: AcSignalRDataSource<…>` | SignalR-backed data source (see `AyCode.Core/docs/SIGNALR_DATASOURCE.md`) |
| `TDataItem` | `: class, IId<TId>` | Entity type displayed in the grid |
| `TId` | `: struct` | Primary key type (`int`, `Guid`) |
| `TLoggerClient` | `: AcLoggerBase` | Logger for diagnostics |
### Usage Example (Project-Specific Adapter)
```csharp
// Project adapter — fixes TSignalRDataSource, TId, TLoggerClient for the entire project
public class MyProjectGridBase<TDataItem>
: MgGridBase<MySignalRDataSource<TDataItem>, TDataItem, int, MyLoggerClient>
where TDataItem : class, IId<int>
{
[Inject] public required MyLoggedInModel LoggedInModel { get; set; }
protected override int GetLayoutUserId() => LoggedInModel.UserId;
}
// Concrete grid — only TDataItem remains open
public class GridOrderBase : MyProjectGridBase<Order>
{
public GridOrderBase()
{
GetAllMessageTag = MySignalRTags.GetOrders;
AddMessageTag = MySignalRTags.AddOrder;
UpdateMessageTag = MySignalRTags.UpdateOrder;
}
}
```
## Component Parameters
### Required Parameters
| Parameter | Type | Description |
|---|---|---|
| `SignalRClient` | `AcSignalRClientBase` | SignalR client for server communication |
| `Logger` | `TLoggerClient` | Logger instance |
| `GetAllMessageTag` | `int` | SignalR tag for loading all items |
### CRUD Message Tags
| Parameter | Type | Description |
|---|---|---|
| `GetAllMessageTag` | `int` | Tag for "get all items" request |
| `GetItemMessageTag` | `int` | Tag for "get single item" request |
| `AddMessageTag` | `int` | Tag for "add item" request |
| `UpdateMessageTag` | `int` | Tag for "update item" request |
| `RemoveMessageTag` | `int` | Tag for "remove item" request |
These are bundled into a `SignalRCrudTags` during `OnInitializedAsync`. See `AyCode.Core/docs/SIGNALR_DATASOURCE.md` for details.
### Data & Context Parameters
| Parameter | Type | Description |
|---|---|---|
| `DataSource` | `IList<TDataItem>` | Bind with `AcObservableCollection<TDataItem>` for external data. If not set, grid creates its own. |
| `ParentDataItem` | `IId<TId>?` | Parent entity for detail grids. `null` = master grid. |
| `KeyFieldNameToParentId` | `string?` | Property name on `TDataItem` that references the parent's ID |
| `ContextIds` | `object[]?` | Additional context passed to `AcSignalRDataSource` constructor |
| `FilterText` | `string?` | Text filter — propagated to data source, triggers reload |
### Display & Behavior Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| `Caption` | `string` | `typeof(TDataItem).Name` | Grid title (shown in fullscreen header) |
| `GridName` | `string` | `"{TDataItem.Name}Grid"` | Name used in log messages |
| `AutoSaveLayoutName` | `string?` | `"Grid{TDataItem.Name}"` | Base name for layout storage keys |
## Event Callbacks
All grid events are re-exposed with `OnGrid` prefix to avoid collisions with `DxGrid` base events:
| Event | DxGrid Equivalent | When Fired |
|---|---|---|
| `OnGridItemDeleting` | `DataItemDeleting` | Before item removal (can cancel via `e.Cancel`) |
| `OnGridEditModelSaving` | `EditModelSaving` | Before item save (can cancel via `e.Cancel`) |
| `OnGridEditStart` | `EditStart` | When edit mode begins |
| `OnGridCustomizeEditModel` | `CustomizeEditModel` | When edit model is being prepared |
| `OnGridFocusedRowChanged` | `FocusedRowChanged` | When focused row changes |
| `OnDataSourceChanged` | — | After data source is loaded/reloaded |
| `OnGridItemChanged` | — | After server confirms a CRUD operation |
| `OnGridItemChanging` | — | Before a CRUD operation is sent to server |
Internal event wiring (in `SetParametersAsyncCore`, first call):
| DxGrid Event | → Internal Handler |
|---|---|
| `DataItemDeleting` | `OnItemDeleting` |
| `EditModelSaving` | `OnItemSaving` |
| `CustomizeEditModel` | `OnCustomizeEditModel` |
| `FocusedRowChanged` | `OnFocusedRowChanged` |
| `EditStart` | `OnEditStart` |
| `EditCanceling` | `OnEditCanceling` |
## Lifecycle
```
1. OnInitializedAsync()
├── Validate Logger, SignalRClient (throw if null)
├── Create SignalRCrudTags from message tag parameters
├── Create TSignalRDataSource via Activator.CreateInstance(SignalRClient, crudTags, ContextIds)
├── Set DataSource.FilterText
├── Bind grid Data to data source inner list
└── Subscribe to: OnDataSourceLoaded, OnDataSourceItemChanged, OnSyncingStateChanged
2. SetParametersAsyncCore() [first time]
├── Set KeyFieldName = "Id"
├── Wire 6 DxGrid events → internal handlers (see table above)
├── Add OnCustomizeElement handler (edit row highlighting, detail cell styling)
└── Set defaults: TextWrapEnabled=false, AllowSelectRowByClick=true, etc.
3. OnParametersSet() [first time]
├── Set GridName default: "{TDataItem.Name}Grid"
├── Set AutoSaveLayoutName default: "Grid{TDataItem.Name}"
├── Wire layout auto-loading/saving handlers
└── Register with GridWrapper via GridWrapper.RegisterGrid(this)
4. OnAfterRenderAsync(firstRender: true)
├── If DataSource parameter was provided: LoadDataSource(dataSourceParam, sync, notify)
└── Else: LoadDataSourceAsync(notify) — fires SignalR GetAll request
```
## CRUD Operations
### Adding Items
```csharp
await grid.AddDataItem(item); // local add, sync later
await grid.AddDataItemAsync(item); // immediate server sync
await grid.InsertDataItem(0, item); // insert at index, sync later
await grid.InsertDataItemAsync(0, item); // insert at index, immediate sync
```
### Other CRUD Methods
| Method | Description |
|---|---|
| `UpdateDataItem(item)` | Local update, sync later |
| `UpdateDataItemAsync(item)` | Immediate server sync |
| `AddOrUpdateDataItem(item)` | Add if new, update if existing |
| `RemoveDataItem(item)` | Remove by entity reference |
| `RemoveDataItem(id)` | Remove by ID |
| `ReloadDataSourceAsync()` | Re-fetch all data from server |
| `ForceRenderAsync()` | Force grid re-initialization via new render key |
### ID Generation for New Items
New items get **temporary client-side IDs** until the server assigns real ones:
| TId Type | Strategy | Example |
|---|---|---|
| `Guid` | `Guid.NewGuid()` | `a1b2c3d4-...` |
| `int` | `-1 * AcDomain.NextUniqueInt32` | `-1`, `-2`, `-3`, ... |
**Convention:** Negative integer IDs indicate unsaved items. The server replaces them with real auto-increment IDs.
### Edit Flow (Inline)
```
User clicks Edit → OnEditStart → OnCustomizeEditModel
├── Set GridEditState = New/Edit
├── For new items: assign temp ID, set parent FK if detail grid
├── Notify InfoPanel: SetEditMode()
└── Fire OnGridCustomizeEditModel callback
User clicks Save → OnItemSaving
├── Fire OnGridEditModelSaving callback (can cancel)
├── If new: AddDataItemAsync / InsertDataItemAsync
├── If existing: UpdateDataItemAsync
├── Reset GridEditState = None
└── Clear InfoPanel edit mode
User clicks Cancel → OnEditCanceling
├── Reset GridEditState = None
└── Clear InfoPanel edit mode
```
### Edit Row Highlighting
When `GridEditState != None`, the focused row and its cells get `background-color: #fffbeb` (warm yellow) via `OnCustomizeElement`.
## Layout Persistence
### Storage Keys
Grid layouts are stored in **localStorage** with structured keys:
```
AutoSave: {AutoSaveLayoutName}_{MasterOrParentTypeName}_AutoSave_{UserId}
UserSave: {AutoSaveLayoutName}_{MasterOrParentTypeName}_UserSave_{UserId}
Splitter: Splitter_{grid.AutomaticLayoutStorageKey}
```
**Examples:**
```
GridOrder_Master_AutoSave_42 ← master grid, user #42
GridOrder_Order_AutoSave_42 ← detail grid under Order parent
GridOrder_Master_UserSave_42 ← manually saved layout
Splitter_GridOrder_Master_AutoSave_42 ← splitter pane size
```
### Three Layout Tiers
| Tier | Key Contains | When Saved | When Loaded |
|---|---|---|---|
| **Default** | (in-memory `_defaultLayoutJson`) | First `LayoutAutoLoading` — captures layout before any load | `ResetLayoutAsync()` — restores original |
| **AutoSave** | `_AutoSave_` | Every `LayoutAutoSaving` event (on any layout change) | Every `LayoutAutoLoading` event (on grid init, wrapped in `BeginUpdate`/`EndUpdate`) |
| **UserSave** | `_UserSave_` | `SaveUserLayoutAsync()` — explicit user action | `LoadUserLayoutAsync()` — explicit user action |
### Layout Operations
| Method | Behavior |
|---|---|
| `SaveUserLayoutAsync()` | Saves current layout to both UserSave AND AutoSave keys |
| `LoadUserLayoutAsync()` | Loads from UserSave key (if exists) |
| `ResetLayoutAsync()` | Removes AutoSave key, restores in-memory `_defaultLayoutJson` |
| `HasUserLayoutAsync()` | Checks if UserSave key exists in localStorage |
### Persisted State
The layout (`GridPersistentLayout`) includes: column order, column widths, sort descriptors, group descriptors, filter row values, page size — serialized as JSON via `System.Text.Json`.
### User Identification
`GetLayoutUserId()` is virtual — defaults to `0`. Override in project adapter to provide the logged-in user's ID.
## Master-Detail Hierarchy
### How It Works
1. `MgGridBase.BuildRenderTree` wraps content in `CascadingValue<IMgGridBase>`
2. Child grids receive this via `[CascadingParameter] IMgGridBase? ParentGrid`
3. `IsMasterGrid` = `ParentDataItem == null`
4. `GetRootGrid()` walks the `ParentGrid` chain to find the topmost grid
### Detail Grid Setup
```razor
<DetailRowTemplate>
@{
var parent = (ParentEntity)context.DataItem;
<GridChildEntity ParentDataItem="@parent"
KeyFieldNameToParentId="ParentEntityId"
ContextIds="@(new object[] { parent.Id })" />
}
</DetailRowTemplate>
```
When `ParentDataItem` is set and `KeyFieldNameToParentId` is provided, new items automatically get their parent FK set via reflection.
## MgGridWithInfoPanel Wrapper
```razor
<MgGridWithInfoPanel ShowInfoPanel="true" InfoPanelSize="400px">
<GridContent>
<GridMyEntityBase @ref="Grid" ... />
</GridContent>
<ChildContent>
@* Optional: custom InfoPanel — if omitted, default MgGridInfoPanel is used *@
</ChildContent>
</MgGridWithInfoPanel>
```
| Parameter | Type | Default | Description |
|---|---|---|---|
| `GridContent` | `RenderFragment` | — | The grid to display in the left pane |
| `ChildContent` | `RenderFragment?` | `null` | Custom InfoPanel. If `null`, renders `MgGridInfoPanel` |
| `ShowInfoPanel` | `bool` | `true` | Whether to show the right pane |
| `InfoPanelSize` | `string` | `"400px"` | Initial right pane size |
The wrapper provides:
- `DxSplitter` with collapsible right pane
- Fullscreen overlay (`mg-fullscreen-overlay`)
- Splitter size persistence (`Splitter_{key}` in localStorage)
- `RegisterGrid(grid)` — called by MgGridBase in `OnParametersSet`
- `RegisterInfoPanel(infoPanel)` — called by MgGridInfoPanel in `OnAfterRenderAsync`
## MgGridToolbarTemplate
The standard toolbar rendered inside grid's `ToolbarTemplate`. Provides all standard grid operations:
### Toolbar Buttons
| Group | Buttons | Visible |
|---|---|---|
| **CRUD** | New, Edit, Delete | When NOT editing |
| **Edit mode** | Save, Cancel | When editing |
| **Navigation** | Prev Row, Next Row | When NOT editing |
| **Layout** | Column Chooser, Layout (Load/Save/Reset) | When `OnlyGridEditTools=false` |
| **Actions** | Export (CSV/XLSX/XLS/PDF), Reload Data, Fullscreen | When `OnlyGridEditTools=false` |
### Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| `Grid` | `IMgGridBase` | required | The grid to control |
| `OnlyGridEditTools` | `bool` | `false` | Show only CRUD + navigation (used by InfoPanel) |
| `ShowOnlyIcon` | `bool` | `false` | Hide button text, show only icons |
| `EnableNew` | `bool` | `true` | Enable "New" button |
| `EnableEdit` | `bool` | `true` | Enable "Edit" button |
| `EnableDelete` | `bool` | `false` | Enable "Delete" button |
| `ToolbarItemsExtended` | `RenderFragment?` | `null` | Extra toolbar items after standard buttons |
| `OnReloadDataClick` | `EventCallback` | — | Callback for "Reload Data" button |
### State Properties (computed from Grid)
| Property | Source |
|---|---|
| `IsEditing` | `Grid.GridEditState != MgGridEditState.None` |
| `IsSyncing` | `Grid.IsSyncing` |
| `HasFocusedRow` | `Grid.GetFocusedRowIndex() >= 0` |
| `IsFullscreenMode` | `Grid.IsFullscreen` |
## MgGridInfoPanel
Default InfoPanel component implementing `IInfoPanelBase`. Displays focused-row details with edit support.
### IInfoPanelBase Interface
```csharp
public interface IInfoPanelBase
{
void RefreshData(IMgGridBase grid, object? dataItem, int visibleIndex = -1);
void SetEditMode(IMgGridBase grid, object editModel);
void ClearEditMode();
}
```
### InfoPanel Data Flow
```
FocusedRowChanged → InfoPanelInstance.RefreshData(grid, dataItem, visibleIndex)
Edit starts → InfoPanelInstance.SetEditMode(grid, editModel)
Edit ends/cancel → InfoPanelInstance.ClearEditMode()
```
`InfoPanelInstance` resolution: own `GridWrapper.InfoPanelInstance` → root grid's `GridWrapper.InfoPanelInstance``null`.
### Responsive Column Layout
| Breakpoint Parameter | Default | Columns |
|---|---|---|
| — | < 400px | 1 column |
| `TwoColumnBreakpoint` | 400px | 2 columns |
| `ThreeColumnBreakpoint` | 800px | 3 columns |
| `FourColumnBreakpoint` | 1300px | 4 columns |
`FixedColumnCount` (1-4) overrides responsive breakpoints if set.
### Template System
| Template | Context | Purpose |
|---|---|---|
| `HeaderTemplate` | `InfoPanelContext` | Custom header (default: grid Caption) |
| `BeforeColumnsTemplate` | `InfoPanelContext` | Content before column-value pairs |
| `ColumnsTemplate` | `InfoPanelContext` | Replace default column rendering entirely |
| `AfterColumnsTemplate` | `InfoPanelContext` | Content after column-value pairs |
| `FooterTemplate` | `InfoPanelContext` | Custom footer |
`InfoPanelContext` = `record(object? DataItem, bool IsEditMode)`.
### Edit Mode Editors (by property type)
| Type | Editor Component |
|---|---|
| `bool` | `DxCheckBox<bool>` |
| `DateTime` / `DateTime?` | `DxDateEdit<DateTime>` / `DxDateEdit<DateTime?>` |
| `DateOnly` / `DateOnly?` | `DxDateEdit<DateOnly>` / `DxDateEdit<DateOnly?>` |
| `int` | `DxSpinEdit<int>` |
| `decimal` | `DxSpinEdit<decimal>` |
| `double` | `DxSpinEdit<double>` |
| ComboBox (via `DxComboBoxSettings`) | `DxComboBox<TValue, TItem>` |
| Memo (via `EditSettingsType.Memo`) | `DxMemo` |
| Other | `DxTextBox` |
### Additional Features
- **Sticky positioning** via JS interop (`MgGridInfoPanel.initSticky`)
- **Built-in toolbar** with `MgGridToolbarTemplate` (`OnlyGridEditTools=true`)
- **OnDataItemChanged** callback when focused row changes
## MgGridDataColumn
Extended `DxGridDataColumn` with InfoPanel and URL link support.
| Parameter | Type | Default | Description |
|---|---|---|---|
| `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) |
| `UrlLink` | `string?` | `null` | URL template with `{Property}` placeholders |
**UrlLink example:** `https://admin.example.com/Entity/Edit/{Id}` renders cell as `<a href="..." target="_blank">`.
Uses compiled property accessors (`ConcurrentDictionary` cache) for performance.
## MgGridSignalRDataSource
`GridCustomDataSource` wrapper around `AcSignalRDataSource` for server-side data operations.
- Returns local data instantly for previously-seen filter criteria
- Refreshes from the server in the background
- Handles filter, sort, paging, unique values, and summary calculations locally
- `OnBackgroundRefreshCompleted` event fires when background refresh completes
See `AyCode.Core/docs/SIGNALR_DATASOURCE.md` for the underlying `AcSignalRDataSource`.
## Fullscreen Mode
Two modes depending on whether `MgGridWithInfoPanel` wraps the grid:
| Scenario | Behavior |
|---|---|
| **With wrapper** | `ToggleFullscreen()` delegates to wrapper fullscreen includes grid + InfoPanel |
| **Standalone** | Grid renders its own `mg-fullscreen-overlay` with header (Caption + close button) and body |
## Rendering
`BuildRenderTree` uses manual render tree building (not Razor markup):
1. Outer `CascadingValue<IMgGridBase>` provides this grid as `ParentGrid` to children
2. If standalone fullscreen: `div.mg-fullscreen-overlay` > `div.mg-fullscreen-header` + `div.mg-fullscreen-body` > `base.BuildRenderTree`
3. If normal: `div[style=display:contents]` > `base.BuildRenderTree`
4. `_gridRenderKey` (Guid) used as element key — changed by `ForceRenderAsync()` to force re-initialization
## Default Grid Settings
Set in `SetParametersAsyncCore` (first call only):
| Setting | Value |
|---|---|
| `KeyFieldName` | `"Id"` |
| `TextWrapEnabled` | `false` |
| `AllowSelectRowByClick` | `true` |
| `HighlightRowOnHover` | `true` |
| `AutoCollapseDetailRow` | `true` |
| `AutoExpandAllGroupRows` | `false` |
Project adapters typically add more defaults in `OnParametersSet` (e.g., `EditMode`, `FocusedRowEnabled`, `PageSize`, `ShowFilterRow`, `SizeMode` based on `IsMasterGrid`).
## Disposal
`DisposeAsync()` handles cleanup:
1. Set `_isDisposed = true` (guards all async callbacks)
2. Unsubscribe from `OnDataSourceLoaded`, `OnDataSourceItemChanged`, `OnSyncingStateChanged`
3. Remove `OnCustomizeElement` handler
4. `GC.SuppressFinalize(this)`
All async callbacks check `_isDisposed` before proceeding.
## Interface: IMgGridBase
The public contract exposed to companion components (toolbar, InfoPanel, wrapper):
| Member | Type | Description |
|---|---|---|
| `IsSyncing` | `bool` | Whether SignalR sync is in progress |
| `Caption` | `string` | Grid title |
| `GridEditState` | `MgGridEditState` | `None` / `New` / `Edit` |
| `ParentGrid` | `IMgGridBase?` | Parent in master-detail hierarchy |
| `GetRootGrid()` | `IMgGridBase` | Walks to topmost grid |
| `StepPrevRow()` | `void` | Navigate to previous visible row |
| `StepNextRow()` | `void` | Navigate to next visible row |
| `InfoPanelInstance` | `IInfoPanelBase?` | Resolved InfoPanel reference |
| `IsFullscreen` | `bool` | Current fullscreen state |
| `AutomaticLayoutStorageKey` | `string` | Current auto-save storage key |
| `ToggleFullscreen()` | `void` | Toggle fullscreen mode |
| `SaveUserLayoutAsync()` | `Task` | Save layout manually |
| `LoadUserLayoutAsync()` | `Task` | Load manually saved layout |
| `ResetLayoutAsync()` | `Task` | Reset to default layout |
| `HasUserLayoutAsync()` | `Task<bool>` | Check if manual save exists |
## Event Args Classes
| Class | Base | Extra Properties |
|---|---|---|
| `GridDataItemChangedEventArgs<T>` | — | `Grid`, `DataItem`, `TrackingState`, `CancelStateChangeInvoke` |
| `GridDataItemChangingEventArgs<T>` | `GridDataItemChangedEventArgs<T>` | `IsCanceled` |