# 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 (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 ``` | Parameter | Constraint | Purpose | |---|---|---| | `TSignalRDataSource` | `: AcSignalRDataSource<…>` | SignalR-backed data source (see `AyCode.Core/docs/SIGNALR_DATASOURCE.md`) | | `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 MyProjectGridBase : MgGridBase, TDataItem, int, MyLoggerClient> where TDataItem : class, IId { [Inject] public required MyLoggedInModel LoggedInModel { get; set; } protected override int GetLayoutUserId() => LoggedInModel.UserId; } // Concrete grid — only TDataItem remains open public class GridOrderBase : MyProjectGridBase { 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` | 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 | 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` 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 = (ParentEntity)context.DataItem; } ``` When `ParentDataItem` is set and `KeyFieldNameToParentId` is provided, new items automatically get their parent FK set via reflection. ## MgGridWithInfoPanel Wrapper ```razor @* Optional: custom InfoPanel — if omitted, default MgGridInfoPanel is used *@ ``` | 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` | | `DateTime` / `DateTime?` | `DxDateEdit` / `DxDateEdit` | | `DateOnly` / `DateOnly?` | `DxDateEdit` / `DxDateEdit` | | `int` | `DxSpinEdit` | | `decimal` | `DxSpinEdit` | | `double` | `DxSpinEdit` | | ComboBox (via `DxComboBoxSettings`) | `DxComboBox` | | 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 ``. 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` — 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` | Check if manual save exists | ## Event Args Classes | Class | Base | Extra Properties | |---|---|---| | `GridDataItemChangedEventArgs` | — | `Grid`, `DataItem`, `TrackingState`, `CancelStateChangeInvoke` | | `GridDataItemChangingEventArgs` | `GridDataItemChangedEventArgs` | `IsCanceled` |