Refactor MgGrid documentation into modular files

Split MGGRID.md into focused docs (parameters, CRUD, layout, detail, rendering, InfoPanel, toolbar, columns, datasource) under AyCode.Blazor.Components/docs/. Updated all references, READMEs, and project/solution files to match new structure. Clarified doc layering conventions and core doc links. No code changes—documentation and project organization only.
This commit is contained in:
Loretta 2026-03-30 08:00:53 +02:00
parent 453e21a844
commit d1567323d8
20 changed files with 615 additions and 551 deletions

View File

@ -19,13 +19,13 @@
8. SignalR uses **AcBinaryHubProtocol** (custom binary) — not the default JSON protocol.
9. Grid filters use **AcExpressionNode** serialization — LINQ expressions are serialized and sent to server.
## Related Solutions
14. **AyCode.Core** solution (`../AyCode.Core/AyCode.Core.sln`) contains all core framework code (SignalR, serialization, data sources, base classes, interfaces). When a type is referenced in this Blazor solution but not defined here (e.g. `AcSignalRDataSource`, `AcBinaryHubProtocol`, `IId<TId>`, `AcObservableCollection`, `AcLoggerBase`), look it up in AyCode.Core source.
15. For AyCode.Core domain rules see: `../AyCode.Core/.github/copilot-instructions.md`
## Conventions
10. Do not suggest removal/rollback as a solution — find a fix for the problem.
11. All DLL references to AyCode.Core projects are via `DllReference` (not ProjectReference) — this is intentional.
12. **No redundant code** — before writing new logic, search for existing methods. Reuse or extract shared logic into smaller methods rather than duplicating.
13. **Keep all .md files in sync** — when you modify code, update any affected .md file in the same area. If you already read an .md file during your work and notice it contradicts the current code, fix the discrepancy — but do NOT proactively scan or open .md files just to check for issues.
16. **Documentation layering** — write `.md` documentation at the **defining layer** (where the code lives). Higher-layer `.md` files reference the base docs (e.g. `see AyCode.Core/docs/SIGNALR.md`) and document only project-specific overrides or extensions. Never duplicate base-layer descriptions in consumer-level docs.
14. **Documentation layering** — write `.md` documentation at the **defining layer** (where the code lives). Higher-layer `.md` files reference the base docs (e.g. `see AyCode.Core/docs/SIGNALR.md`) and document only project-specific overrides or extensions. Never duplicate base-layer descriptions in consumer-level docs.
## Related Solutions
15. **AyCode.Core** solution (`../AyCode.Core/AyCode.Core.sln`) contains all core framework code (SignalR, serialization, data sources, base classes, interfaces). When a type is referenced in this Blazor solution but not defined here (e.g. `AcSignalRDataSource`, `AcBinaryHubProtocol`, `IId<TId>`, `AcObservableCollection`, `AcLoggerBase`), look it up in AyCode.Core source.
16. For AyCode.Core domain rules see: `../AyCode.Core/.github/copilot-instructions.md`

View File

@ -55,6 +55,10 @@
<Folder Include="Services\Logins\" />
</ItemGroup>
<ItemGroup>
<None Include="docs\**\*.md" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AyCode.Blazor.Models.Server\AyCode.Blazor.Models.Server.csproj" />
<ProjectReference Include="..\AyCode.Blazor.Models\AyCode.Blazor.Models.csproj" />

View File

@ -1,6 +1,6 @@
# Grids
Core grid system built on DevExpress `DxGrid`. For the full technical reference see `AyCode.Blazor/docs/MGGRID.md`.
Core grid system built on DevExpress `DxGrid`. For the full technical reference see `AyCode.Blazor.Components/docs/MGGRID.md`.
## Key Files

View File

@ -2,6 +2,21 @@
Blazor Razor component library targeting .NET 10. Provides reusable DevExpress-based UI components, a SignalR-powered grid system, and LINQ expression serialization services.
## Documentation
| Document | Topic |
|---|---|
| `MGGRID.md` | MgGrid system — overview, hierarchy, generic params, IMgGridBase interface |
| `MGGRID_PARAMETERS.md` | Component parameters, event callbacks, default grid settings |
| `MGGRID_CRUD.md` | Lifecycle, CRUD operations, edit flow, disposal |
| `MGGRID_LAYOUT.md` | Layout persistence (storage keys, tiers, operations) |
| `MGGRID_DETAIL.md` | Master-detail hierarchy |
| `MGGRID_RENDERING.md` | Fullscreen mode, rendering |
| `MGGRID_INFOPANEL.md` | MgGridInfoPanel, MgGridWithInfoPanel wrapper |
| `MGGRID_TOOLBAR.md` | MgGridToolbarTemplate (buttons, parameters, state) |
| `MGGRID_COLUMNS.md` | MgGridDataColumn (InfoPanel params, UrlLink) |
| `MGGRID_DATASOURCE.md` | MgGridSignalRDataSource (server-side data, local cache) |
## Dependencies
- **DevExpress.Blazor** 25.1.3, **DevExpress.Data** 25.1.3

View File

@ -0,0 +1,116 @@
# MgGrid System
> Comprehensive documentation for the **MgGrid** component family — the primary UI data grid pattern in the AyCode.Blazor framework.
> Source: `Components/Grids/`
> For SignalR transport: `AyCode.Services/docs/SIGNALR.md` (in AyCode.Core repo)
> For `AcSignalRDataSource`: `AyCode.Services.Server/docs/SIGNALR_DATASOURCE.md` (in AyCode.Core repo)
## 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`
## Detailed Documentation
| File | Topics |
|---|---|
| `MGGRID_PARAMETERS.md` | Component parameters (required, CRUD tags, data & context, display), event callbacks, default grid settings |
| `MGGRID_CRUD.md` | Lifecycle, CRUD operations, ID generation, edit flow, disposal |
| `MGGRID_LAYOUT.md` | Layout persistence (storage keys, three tiers, operations, persisted state) |
| `MGGRID_DETAIL.md` | Master-detail hierarchy |
| `MGGRID_RENDERING.md` | Fullscreen mode, rendering |
| `MGGRID_INFOPANEL.md` | MgGridInfoPanel, MgGridWithInfoPanel wrapper, responsive layout, templates, editors |
| `MGGRID_TOOLBAR.md` | MgGridToolbarTemplate (buttons, parameters, state) |
| `MGGRID_COLUMNS.md` | MgGridDataColumn (InfoPanel params, UrlLink) |
| `MGGRID_DATASOURCE.md` | MgGridSignalRDataSource (server-side data, local cache, background refresh) |
## 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 | Docs |
|---|---|---|
| **MgGridWithInfoPanel** | `DxSplitter` wrapper: grid + InfoPanel, fullscreen, splitter persistence | `MGGRID_INFOPANEL.md` |
| **MgGridToolbarBase** | `DxToolbar` base with `Grid` reference, `RefreshClick` callback | `MGGRID_TOOLBAR.md` |
| **MgGridToolbarTemplate** | Full toolbar: CRUD, navigation, layout menu, fullscreen, export | `MGGRID_TOOLBAR.md` |
| **MgGridDataColumn** | Extended `DxGridDataColumn` with InfoPanel params and `UrlLink` | `MGGRID_COLUMNS.md` |
| **MgGridInfoPanel** | Default InfoPanel: column-value pairs, edit mode, typed editors | `MGGRID_INFOPANEL.md` |
| **MgGridSignalRDataSource** | `GridCustomDataSource` wrapper: server-side filter/sort/page, local cache | `MGGRID_DATASOURCE.md` |
## Generic Type Parameters
```csharp
MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClient>
```
| Parameter | Constraint | Purpose |
|---|---|---|
| `TSignalRDataSource` | `: AcSignalRDataSource<…>` | SignalR-backed data source (see `AyCode.Services.Server/docs/SIGNALR_DATASOURCE.md` in AyCode.Core repo) |
| `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)
```csharp
// 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;
}
}
```
## 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` |

View File

@ -0,0 +1,18 @@
# MgGrid — Columns
> Part of the MgGrid system. See `MGGRID.md` for overview and component hierarchy.
## 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.

View File

@ -0,0 +1,101 @@
# MgGrid — Lifecycle & CRUD
> Part of the MgGrid system. See `MGGRID.md` for overview and component hierarchy.
## 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 MGGRID_PARAMETERS.md)
├── 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`.
## 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.

View File

@ -0,0 +1,13 @@
# MgGrid — DataSource
> Part of the MgGrid system. See `MGGRID.md` for overview and component hierarchy.
> For the underlying `AcSignalRDataSource`: `AyCode.Services.Server/docs/SIGNALR_DATASOURCE.md` (in AyCode.Core repo)
## 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

View File

@ -0,0 +1,25 @@
# MgGrid — Master-Detail
> Part of the MgGrid system. See `MGGRID.md` for overview and component 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
```razor
<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.

View File

@ -0,0 +1,98 @@
# MgGrid — InfoPanel
> Part of the MgGrid system. See `MGGRID.md` for overview and component hierarchy.
## MgGridWithInfoPanel Wrapper
```razor
<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`
## 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<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

View File

@ -0,0 +1,46 @@
# MgGrid — Layout Persistence
> Part of the MgGrid system. See `MGGRID.md` for overview and component hierarchy.
## 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.

View File

@ -0,0 +1,82 @@
# MgGrid — Parameters & Events
> Part of the MgGrid system. See `MGGRID.md` for overview and component hierarchy.
## 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.Services.Server/docs/SIGNALR_DATASOURCE.md` (in AyCode.Core repo) 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` |
## 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`).

View File

@ -0,0 +1,21 @@
# MgGrid — Fullscreen & Rendering
> Part of the MgGrid system. See `MGGRID.md` for overview and component hierarchy.
## 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

View File

@ -0,0 +1,39 @@
# MgGrid — Toolbar
> Part of the MgGrid system. See `MGGRID.md` for overview and component hierarchy.
## 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` |

View File

@ -28,7 +28,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{C3A1D7E2-B
docs\ARCHITECTURE.md = docs\ARCHITECTURE.md
docs\CONVENTIONS.md = docs\CONVENTIONS.md
docs\GLOSSARY.md = docs\GLOSSARY.md
docs\MGGRID.md = docs\MGGRID.md
EndProjectSection
EndProject
Global

View File

@ -4,7 +4,7 @@ Blazor Server + MAUI Hybrid UI framework built on DevExpress 25.1.3 components.
## LLM Context
Domain rules and UI pitfalls live in a single file: [`.github/copilot-instructions.md`](.github/copilot-instructions.md)
Domain rules and UI pitfalls live in a single file: `.github/copilot-instructions.md`
| Tool | Auto-loaded | Action needed |
|------|------------|---------------|
@ -12,22 +12,34 @@ Domain rules and UI pitfalls live in a single file: [`.github/copilot-instructio
| Claude Code | ✅ `CLAUDE.md` → references above | None |
| Cursor / Windsurf | ✅ `README.md` | Read `copilot-instructions.md` via @file |
Detailed docs: [`docs/`](docs/) — GLOSSARY.md, ARCHITECTURE.md
Solution-level docs in `docs/`:
Core framework rules: [`../AyCode.Core/.github/copilot-instructions.md`](../AyCode.Core/.github/copilot-instructions.md)
| Document | Topic |
|---|---|
| `GLOSSARY.md` | Blazor/MAUI terminology |
| `ARCHITECTURE.md` | Solution layers, dependency rules |
| `CONVENTIONS.md` | Coding conventions |
Project-level docs:
| Project | Documents |
|---|---|
| `AyCode.Blazor.Components/docs/` | `MGGRID.md` — MgGrid system (grid base, toolbar, InfoPanel, layout, CRUD) |
Core framework rules: `../AyCode.Core/.github/copilot-instructions.md`
## Solution Structure
| Project | Purpose | README |
|---|---|---|
| [`AyCode.Blazor.Components`](AyCode.Blazor.Components/README.md) | DevExpress UI components, grids, SignalR data sources, expression helpers | [README](AyCode.Blazor.Components/README.md) |
| [`AyCode.Blazor.Models`](AyCode.Blazor.Models/README.md) | Shared view models for Blazor components | [README](AyCode.Blazor.Models/README.md) |
| [`AyCode.Blazor.Models.Server`](AyCode.Blazor.Models.Server/README.md) | Server-side model scaffolding | [README](AyCode.Blazor.Models.Server/README.md) |
| [`AyCode.Blazor.Controllers`](AyCode.Blazor.Controllers/README.md) | Controller scaffolding (minimal) | [README](AyCode.Blazor.Controllers/README.md) |
| [`AyCode.Maui.Core`](AyCode.Maui.Core/README.md) | MAUI cross-platform: Android, iOS, Windows | [README](AyCode.Maui.Core/README.md) |
| Project | Purpose |
|---|---|
| `AyCode.Blazor.Components` | DevExpress UI components, grids, SignalR data sources, expression helpers |
| `AyCode.Blazor.Models` | Shared view models for Blazor components |
| `AyCode.Blazor.Models.Server` | Server-side model scaffolding |
| `AyCode.Blazor.Controllers` | Controller scaffolding (minimal) |
| `AyCode.Maui.Core` | MAUI cross-platform: Android, iOS, Windows |
### Test Projects
| Project | Purpose | README |
|---|---|---|
| [`AyCode.Blazor.Components.Tests`](AyCode.Blazor.Components.Tests/README.md) | Grid and component tests | [README](AyCode.Blazor.Components.Tests/README.md) |
| Project | Purpose |
|---|---|
| `AyCode.Blazor.Components.Tests` | Grid and component tests |

View File

@ -38,7 +38,7 @@ User ← DxGrid ← AcSignalRDataSource ← SignalR (AcBinary) ← Server Hub
## MgGrid Component System
The primary UI pattern for data screens. Full documentation: [`MGGRID.md`](MGGRID.md)
The primary UI pattern for data screens. Overview and index: `AyCode.Blazor.Components/docs/MGGRID.md`
```
DxGrid (DevExpress)

View File

@ -1,6 +1,6 @@
# Conventions
For core framework conventions (Ac prefix, Session/Transaction pattern, etc.) see [`../../AyCode.Core/docs/CONVENTIONS.md`](../../AyCode.Core/docs/CONVENTIONS.md).
For core framework conventions (Ac prefix, Session/Transaction pattern, etc.) see `AyCode.Core/docs/CONVENTIONS.md`.
## Naming

View File

@ -1,6 +1,6 @@
# Glossary
Blazor/MAUI UI terminology. For core framework terms see [`../../AyCode.Core/docs/GLOSSARY.md`](../../AyCode.Core/docs/GLOSSARY.md).
Blazor/MAUI UI terminology. For core framework terms see `AyCode.Core/docs/GLOSSARY.md`.
## UI Components
@ -8,12 +8,12 @@ Blazor/MAUI UI terminology. For core framework terms see [`../../AyCode.Core/doc
|---|---|
| **DxGrid** | DevExpress Blazor data grid. Used with `AcSignalRDataSource` for real-time SignalR data. |
| **CardView** | Card-style layout wrapping DxGrid. Mobile-friendly alternative to table grids. |
| **AcSignalRDataSource** | Grid data source backed by SignalR. Handles load, CRUD, filtering, and change tracking. See `AyCode.Core/docs/SIGNALR_DATASOURCE.md`. |
| **AcSignalRDataSource** | Grid data source backed by SignalR. Handles load, CRUD, filtering, and change tracking. See `AyCode.Services.Server/docs/SIGNALR_DATASOURCE.md` in AyCode.Core repo. |
| **AcExpressionNode** | Serializable LINQ expression tree. Grid filters are serialized as expression nodes and sent to server. |
## MgGrid System
For full technical reference see [`MGGRID.md`](MGGRID.md).
For full technical reference see `AyCode.Blazor.Components/docs/MGGRID.md`.
| Term | Definition |
|---|---|
@ -26,7 +26,7 @@ For full technical reference see [`MGGRID.md`](MGGRID.md).
| **MgGridSignalRDataSource** | `GridCustomDataSource` wrapping `AcSignalRDataSource`. Local cache, background refresh. |
| **IMgGridBase** | Public interface: `IsSyncing`, `GridEditState`, `ParentGrid`, `StepPrevRow/NextRow`, layout persistence methods. |
| **MgGridEditState** | Enum: `None` (no edit), `New` (adding item), `Edit` (modifying item). |
| **SignalRCrudTags** | Bundle of 5 integer message tags (GetAll, GetItem, Add, Update, Remove) for one entity type. See `AyCode.Core/docs/SIGNALR_DATASOURCE.md`. |
| **SignalRCrudTags** | Bundle of 5 integer message tags (GetAll, GetItem, Add, Update, Remove) for one entity type. See `AyCode.Services.Server/docs/SIGNALR_DATASOURCE.md` in AyCode.Core repo. |
| **IsMasterGrid** | `true` when `ParentDataItem == null` — top-level grid (not detail). |
| **AutoSaveLayoutName** | Base name for localStorage layout keys. Default: `"Grid{TDataItem.Name}"`. |

View File

@ -1,525 +0,0 @@
# 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
```csharp
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)
```csharp
// 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
```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<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
```razor
<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
```razor
<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
```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<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` |