diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 37d51e0..995f4b0 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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 + └── [Project adapter, e.g. FruitBankGridBase] + └── [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` +- **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 diff --git a/docs/MGGRID.md b/docs/MGGRID.md new file mode 100644 index 0000000..b08c048 --- /dev/null +++ b/docs/MGGRID.md @@ -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 (AyCode.Blazor — abstract) + └── [Project-specific adapter, e.g. FruitBankGridBase] + └── [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 +``` + +| Parameter | Constraint | Purpose | +|---|---|---| +| `TSignalRDataSource` | `: AcSignalRDataSource>` | SignalR-backed data source with CRUD operations | +| `TDataItem` | `: class, IId` | 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 + : MgGridBase, TDataItem, int, LoggerClient> + where TDataItem : class, IId +{ + protected override int GetLayoutUserId() => LoggedInModel.CustomerDto?.Id ?? 0; +} + +// Concrete grid — only TDataItem remains open +public class GridShippingBase : FruitBankGridBase +{ + 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` | Bind with `AcObservableCollection` for external data. If not set, grid creates its own. | +| `ParentDataItem` | `IId?` | 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` +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 + + @{ + var parent = (Shipping)context.DataItem; + + } + +``` + +When `ParentDataItem` is set and `KeyFieldNameToParentId` is provided, new items automatically get their parent FK set via reflection. + +## InfoPanel Integration + +### MgGridWithInfoPanel Wrapper + +```razor + + + + + @* Optional: custom InfoPanel via ChildContent *@ + +``` + +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` — 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` | Check if manual save exists | + +## Event Args Classes + +| Class | Base | Extra Properties | +|---|---|---| +| `GridDataItemChangedEventArgs` | — | `Grid`, `DataItem`, `TrackingState`, `CancelStateChangeInvoke` | +| `GridDataItemChangingEventArgs` | `GridDataItemChangedEventArgs` | `IsCanceled` |