AyCode.Blazor/docs/MGGRID.md

21 KiB

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

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)

// 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

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

<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

<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

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.InfoPanelInstancenull.

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