# 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` |