15 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/
Overview
MgGridBase is an abstract generic Blazor component that extends DevExpress DxGrid with:
- Automatic SignalR CRUD — data flows through
AcSignalRDataSourcewith 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
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, e.g. FruitBankGridBase<TDataItem>]
└── [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
MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClient>
| Parameter | Constraint | Purpose |
|---|---|---|
TSignalRDataSource |
: AcSignalRDataSource<TDataItem, TId, AcObservableCollection<TDataItem>> |
SignalR-backed data source with CRUD operations |
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 FruitBankGridBase<TDataItem>
: MgGridBase<SignalRDataSourceObservable<TDataItem>, TDataItem, int, LoggerClient>
where TDataItem : class, IId<int>
{
protected override int GetLayoutUserId() => LoggedInModel.CustomerDto?.Id ?? 0;
}
// Concrete grid — only TDataItem remains open
public class GridShippingBase : FruitBankGridBase<Shipping>
{
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<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 |
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
// 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
// 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:
// FruitBankGridBase overrides this:
protected override int GetLayoutUserId() => LoggedInModel.CustomerDto?.Id ?? 0;
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 = (Shipping)context.DataItem;
<GridShippingDocument ParentDataItem="@parent"
KeyFieldNameToParentId="ShippingId"
ContextIds="@(new object[] { parent.Id })" />
}
</DetailRowTemplate>
When ParentDataItem is set and KeyFieldNameToParentId is provided, new items automatically get their parent FK set via reflection.
InfoPanel Integration
MgGridWithInfoPanel Wrapper
<MgGridWithInfoPanel ShowInfoPanel="true" InfoPanelSize="400px">
<GridContent>
<GridShippingBase @ref="Grid" ... />
</GridContent>
@* Optional: custom InfoPanel via ChildContent *@
</MgGridWithInfoPanel>
The wrapper provides:
DxSplitterwith collapsible right pane- Fullscreen overlay (
mg-fullscreen-overlay) - Splitter size persistence (
Splitter_{key}in localStorage) - Default
MgGridInfoPanelif no customChildContent
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):
- 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 add more defaults (e.g., FruitBankGridBase sets EditMode=EditRow, FocusedRowEnabled=true, PageSize, ShowFilterRow based on IsMasterGrid).
Disposal
DisposeAsync() handles cleanup:
- Set
_isDisposed = true(guards all async callbacks) - Unsubscribe from
OnDataSourceLoaded,OnDataSourceItemChanged,OnSyncingStateChanged - Remove
OnCustomizeElementhandler
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 |