Add MgGrid system documentation and architecture overview

Added a new section to ARCHITECTURE.md summarizing the MgGrid component system, its hierarchy, and key behaviors, with a link to the new MGGRID.md. Created MGGRID.md with comprehensive documentation on MgGrid design, usage, parameters, lifecycle, events, layout persistence, master-detail, InfoPanel, fullscreen, and public interface. Includes code examples, tables, and diagrams for developer reference.
This commit is contained in:
Loretta 2026-03-29 10:01:12 +02:00
parent b80b117a38
commit 6cce23a124
2 changed files with 424 additions and 0 deletions

View File

@ -35,6 +35,31 @@ User → DxGrid → AcSignalRDataSource → SignalR (AcBinary) → Server Hub
User ← DxGrid ← AcSignalRDataSource ← SignalR (AcBinary) ← Server Hub
```
## MgGrid Component System
The primary UI pattern for data screens. Full documentation: [`MGGRID.md`](MGGRID.md)
```
DxGrid (DevExpress)
└── MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClient>
└── [Project adapter, e.g. FruitBankGridBase<TDataItem>]
└── [Concrete grid, e.g. GridShippingBase]
```
| Component | Role |
|---|---|
| **MgGridBase** | Abstract base — SignalR CRUD, layout persistence, master-detail, edit state |
| **MgGridWithInfoPanel** | `DxSplitter` wrapper — grid + collapsible InfoPanel + fullscreen |
| **MgGridToolbarBase** | `DxToolbar` with grid reference and refresh action |
| **MgGridDataColumn** | `DxGridDataColumn` with URL template support |
| **MgGridInfoPanel** | Default InfoPanel — column-value display with edit mode |
Key behaviors:
- CRUD via **SignalR message tags** (`SignalRCrudTags`) — not REST
- **Layout auto-persistence** to `localStorage` (per-user, per-grid, per-master/detail)
- **Master-detail** via `CascadingParameter<IMgGridBase>`
- **New item IDs**: negative ints (client-side temp) or `Guid.NewGuid()`
## Key Design Decisions
- **DevExpress 25.1.3** exclusively — no mixing with other component libraries

399
docs/MGGRID.md Normal file
View File

@ -0,0 +1,399 @@
# 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/`
## Overview
**MgGridBase** is an abstract generic Blazor component that extends DevExpress `DxGrid` with:
- **Automatic SignalR CRUD** — data flows through `AcSignalRDataSource` with integer message tags
- **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, e.g. FruitBankGridBase<TDataItem>]
└── [Concrete grid, e.g. GridShippingBase]
```
### 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 |
| **MgGridDataColumn** | Extended `DxGridDataColumn` with URL template support (`{PropertyName}` placeholders) |
| **MgGridInfoPanel** | Default InfoPanel showing column-value pairs, supports edit mode with save/cancel |
## Generic Type Parameters
```csharp
MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClient>
```
| Parameter | Constraint | Purpose |
|---|---|---|
| `TSignalRDataSource` | `: AcSignalRDataSource<TDataItem, TId, AcObservableCollection<TDataItem>>` | SignalR-backed data source with CRUD operations |
| `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 FruitBankGridBase<TDataItem>
: MgGridBase<SignalRDataSourceObservable<TDataItem>, TDataItem, int, LoggerClient>
where TDataItem : class, IId<int>
{
protected override int GetLayoutUserId() => LoggedInModel.CustomerDto?.Id ?? 0;
}
// Concrete grid — only TDataItem remains open
public class GridShippingBase : FruitBankGridBase<Shipping>
{
public GridShippingBase()
{
GetAllMessageTag = SignalRTags.GetShippings;
AddMessageTag = SignalRTags.AddShipping;
UpdateMessageTag = SignalRTags.UpdateShipping;
}
}
```
## 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`.
### 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 |
## 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 DxGrid events → internal handlers (OnItemSaving, OnItemDeleting, etc.)
├── 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 for splitter persistence
4. OnAfterRenderAsync(firstRender: true)
├── If DataSource parameter was provided: LoadDataSource(dataSourceParam, sync, notify)
└── Else: LoadDataSourceAsync(notify) — fires SignalR GetAll request
```
### Data Load Flow
```
Grid.OnAfterRenderAsync
→ AcSignalRDataSource.LoadDataSourceAsync()
→ SignalRClient.SendMessage(GetAllMessageTag)
→ [AcBinary over SignalR]
→ Server Hub.OnReceiveMessage(tag, bytes, requestId)
→ DynamicMethodRegistry.TryFindMethod(tag)
→ [Tagged method executes, returns data]
→ [AcBinary response]
→ AcSignalRDataSource.BinaryToMerge()
→ OnDataSourceLoaded event
→ Grid.SetGridData() + StateHasChanged
```
## CRUD Operations
### Adding Items
```csharp
// Fire-and-forget (add to local list, sync later)
await grid.AddDataItem(item);
// Immediate server sync
await grid.AddDataItemAsync(item);
```
### 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).
### Server Sync
```csharp
// All CRUD methods eventually call:
AcSignalRDataSource.SaveChangesAsync()
// → Sends tracked changes via SignalR using appropriate CRUD tags
// → Server processes and responds
// → OnDataSourceItemChanged fires for each changed item
```
## 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:**
```
GridShipping_Master_AutoSave_42 ← master grid, user #42
GridShipping_Shipping_AutoSave_42 ← detail grid under Shipping parent
GridShipping_Master_UserSave_42 ← manually saved layout
Splitter_GridShipping_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) |
| **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 default layout |
| `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.
### User Identification
`GetLayoutUserId()` is virtual — defaults to `0`. Override in project adapter:
```csharp
// FruitBankGridBase overrides this:
protected override int GetLayoutUserId() => LoggedInModel.CustomerDto?.Id ?? 0;
```
## 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 = (Shipping)context.DataItem;
<GridShippingDocument ParentDataItem="@parent"
KeyFieldNameToParentId="ShippingId"
ContextIds="@(new object[] { parent.Id })" />
}
</DetailRowTemplate>
```
When `ParentDataItem` is set and `KeyFieldNameToParentId` is provided, new items automatically get their parent FK set via reflection.
## InfoPanel Integration
### MgGridWithInfoPanel Wrapper
```razor
<MgGridWithInfoPanel ShowInfoPanel="true" InfoPanelSize="400px">
<GridContent>
<GridShippingBase @ref="Grid" ... />
</GridContent>
@* Optional: custom InfoPanel via ChildContent *@
</MgGridWithInfoPanel>
```
The wrapper provides:
- `DxSplitter` with collapsible right pane
- Fullscreen overlay (`mg-fullscreen-overlay`)
- Splitter size persistence (`Splitter_{key}` in localStorage)
- Default `MgGridInfoPanel` if no custom `ChildContent`
### InfoPanel Data Flow
```
FocusedRowChanged → InfoPanelInstance.RefreshData(grid, dataItem, visibleIndex)
Edit starts → InfoPanelInstance.SetEditMode(grid, editModel)
Edit ends/cancel → InfoPanelInstance.ClearEditMode()
```
`InfoPanelInstance` resolution: own `GridWrapper` → root grid's `GridWrapper``null`.
## 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 add more defaults (e.g., `FruitBankGridBase` sets `EditMode=EditRow`, `FocusedRowEnabled=true`, `PageSize`, `ShowFilterRow` 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
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` |