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.mdForAcSignalRDataSource: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
MgGridWithInfoPanelwrapper - 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
MgGridBase.BuildRenderTreewraps content inCascadingValue<IMgGridBase>- Child grids receive this via
[CascadingParameter] IMgGridBase? ParentGrid IsMasterGrid=ParentDataItem == nullGetRootGrid()walks theParentGridchain 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:
DxSplitterwith collapsible right pane- Fullscreen overlay (
mg-fullscreen-overlay) - Splitter size persistence (
Splitter_{key}in localStorage) RegisterGrid(grid)— called by MgGridBase inOnParametersSetRegisterInfoPanel(infoPanel)— called by MgGridInfoPanel inOnAfterRenderAsync
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.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<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
OnBackgroundRefreshCompletedevent 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):
- Outer
CascadingValue<IMgGridBase>— provides this grid asParentGridto children - If standalone fullscreen:
div.mg-fullscreen-overlay>div.mg-fullscreen-header+div.mg-fullscreen-body>base.BuildRenderTree - If normal:
div[style=display:contents]>base.BuildRenderTree _gridRenderKey(Guid) used as element key — changed byForceRenderAsync()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:
- Set
_isDisposed = true(guards all async callbacks) - Unsubscribe from
OnDataSourceLoaded,OnDataSourceItemChanged,OnSyncingStateChanged - Remove
OnCustomizeElementhandler 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 |