diff --git a/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs b/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs
index faf1036..48f51df 100644
--- a/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs
+++ b/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs
@@ -110,6 +110,13 @@ public interface IMgGridBase : IGrid
/// Shows/hides the quick-filter panel. No-op when the grid has no filter panel.
void ToggleFilterPanel();
+
+ ///
+ /// The grid's current DevExpress SizeMode (from DxGrid). Quick-filter controls in the
+ /// FilterPanel (e.g. ) inherit it as their default when the
+ /// consumer didn't set one, so the panel matches the grid's density.
+ ///
+ SizeMode? GridSizeMode { get; }
}
public abstract class MgGridBase : DxGrid, IMgGridBase, IAsyncDisposable
@@ -260,13 +267,6 @@ public abstract class MgGridBase
- /// Optional quick-filter controls (typically ) rendered above the grid
+ /// Optional quick-filter controls (typically ) rendered below the toolbar (composed into the grid's ToolbarTemplate)
/// inside a styled .mg-grid-filter-panel container (the slot wraps the content). Within this grid's
/// cascade, so the controls can reach this grid and apply per-field filters. See ADR 0004.
///
@@ -337,6 +330,56 @@ public abstract class MgGridBase
+ public SizeMode? GridSizeMode => this.SizeMode;
+
+ private RenderFragment? _consumerToolbarTemplate;
+ private RenderFragment? _wrappedToolbarTemplate;
+
+ ///
+ /// Renders the BELOW the toolbar by composing it into the grid's ToolbarTemplate:
+ /// the consumer's toolbar first, then the styled .mg-grid-filter-panel container. Re-wraps whenever the
+ /// consumer supplies a new template instance (Blazor re-applies markup parameters on every SetParametersAsync);
+ /// no-op when the incoming template is already our wrapper. is read at render
+ /// time inside the fragment, so the toolbar toggle works without re-wrapping.
+ /// COMPILE-VERIFY: ToolbarTemplate is RenderFragment<GridToolbarTemplateContext> on this DevExpress version.
+ ///
+ private void WrapToolbarTemplateWithFilterPanel()
+ {
+ if (FilterPanel == null) return;
+ if (ReferenceEquals(ToolbarTemplate, _wrappedToolbarTemplate)) return;
+
+ _consumerToolbarTemplate = ToolbarTemplate;
+
+ _wrappedToolbarTemplate = context => builder =>
+ {
+ // Wrap toolbar + filter panel in a vertical (column) container so the panel renders BELOW the toolbar,
+ // not beside it — the grid's toolbar region lays its children out horizontally otherwise.
+ builder.OpenElement(0, "div");
+ builder.AddAttribute(1, "class", "mg-grid-toolbar-with-filter");
+
+ // Toolbar row carries the toolbar-container's native padding tokens (the container's own padding is
+ // zeroed by CSS when it hosts this wrapper) so the toolbar keeps its exact native look while the
+ // filter band below can span the grid's full width. See .mg-grid-toolbar-row in mg-grid-info-panel.css.
+ builder.OpenElement(2, "div");
+ builder.AddAttribute(3, "class", "mg-grid-toolbar-row");
+ if (_consumerToolbarTemplate != null) builder.AddContent(4, _consumerToolbarTemplate(context));
+ builder.CloseElement();
+
+ if (IsFilterPanelVisible)
+ {
+ builder.OpenElement(5, "div");
+ builder.AddAttribute(6, "class", "mg-grid-filter-panel");
+ builder.AddContent(7, FilterPanel);
+ builder.CloseElement();
+ }
+
+ builder.CloseElement();
+ };
+
+ ToolbarTemplate = _wrappedToolbarTemplate;
+ }
+
///
/// Name for auto-saving/loading grid layout. If not set, defaults to "Grid{TDataItem.Name}"
///
@@ -345,19 +388,25 @@ public abstract class MgGridBase ParentDataItem == null;
protected PropertyInfo? KeyFieldPropertyInfoToParent;
- private string? _filterText = null;
+ private string? _customFilterCriteria = null;
+ ///
+ /// Consumer-supplied base filter criteria (a DevExpress CriteriaOperator string) — propagated to the data source
+ /// (appended as the LAST positional GetAll parameter) and triggers a reload; the server parses + whitelists it.
+ /// Renamed FilterText → CustomFilterText → CustomFilterCriteria (2026-07-16) as the semantics settled to a
+ /// criteria string. Under the ADR-0004 unified model it is AND-merged with the grid's automatic FilterCriteria.
+ ///
[Parameter]
- public string? FilterText
+ public string? CustomFilterCriteria
{
- get => _filterText;
+ get => _customFilterCriteria;
set
{
- _filterText = value;
+ _customFilterCriteria = value;
- if (_dataSource != null && _dataSource.FilterText != value)
+ if (_dataSource != null && _dataSource.CustomFilterCriteria != value)
{
- _dataSource.FilterText = value;
+ _dataSource.CustomFilterCriteria = value;
ReloadDataSourceAsync().Forget(Logger);
}
}
@@ -454,7 +503,7 @@ public abstract class MgGridBase || typeof(TDataItem) is IId)
diff --git a/AyCode.Blazor.Components/Components/Grids/README.md b/AyCode.Blazor.Components/Components/Grids/README.md
index 4e3f772..6f87916 100644
--- a/AyCode.Blazor.Components/Components/Grids/README.md
+++ b/AyCode.Blazor.Components/Components/Grids/README.md
@@ -4,7 +4,7 @@ Core grid system built on DevExpress `DxGrid`. For the full technical reference
## Key Files
-- **`MgGridBase.cs`** — `MgGridBase`, the main abstract grid component. Extends `DxGrid` with SignalR CRUD, layout persistence, master-detail hierarchy, edit state tracking, fullscreen toggle, and a quick-filter `FilterPanel` slot (renders `MgTagBox` controls above the grid in a styled `.mg-grid-filter-panel` container; toolbar Show/Hide toggle).
+- **`MgGridBase.cs`** — `MgGridBase`, the main abstract grid component. Extends `DxGrid` with SignalR CRUD, layout persistence, master-detail hierarchy, edit state tracking, fullscreen toggle, and a quick-filter `FilterPanel` slot (renders `MgTagBox` controls below the toolbar in a styled `.mg-grid-filter-panel` container; toolbar Show/Hide toggle).
- **`MgGridWithInfoPanel.razor`** — `DxSplitter` wrapper: grid (left) + InfoPanel (right), fullscreen overlay, splitter size persistence.
- **`MgGridToolbarBase.cs`** — Extends `DxToolbar` with `Grid`, `RefreshClick`, and `ShowOnlyIcon` parameters.
- **`MgGridToolbarTemplate.razor`** — Full toolbar template: New/Edit/Delete/Save/Cancel, row navigation, layout menu (Load/Save/Reset), column chooser, export, reload, filter-panel toggle (visible only when the grid has a `FilterPanel`), fullscreen. Extensible via `ToolbarItemsExtended`.
diff --git a/AyCode.Blazor.Components/Components/MgTagBox.cs b/AyCode.Blazor.Components/Components/MgTagBox.cs
index 85d7692..3a8c703 100644
--- a/AyCode.Blazor.Components/Components/MgTagBox.cs
+++ b/AyCode.Blazor.Components/Components/MgTagBox.cs
@@ -41,6 +41,31 @@ public class MgTagBox : DxTagBox
// Route the tag box's selection through our handler so a change applies the grid filter.
// (The consumer does not bind Values; the selection is owned internally — see OnSelectionChanged.)
base.ValuesChanged = EventCallback.Factory.Create>(this, OnSelectionChanged);
+
+ InheritSizeModeFromGrid();
+ }
+
+ ///
+ /// Defaults SizeMode to the grid's (via the cascade) so the filter panel matches the
+ /// grid's density — but only when the consumer left it unset (null); an explicit consumer value wins.
+ /// Once inherited, SizeMode is non-null, so this defaults once rather than overriding every render.
+ /// Imperative parameter writes on a DevExpress editor must be wrapped in BeginUpdate/EndUpdate
+ /// (same reason as Values in ).
+ ///
+ private void InheritSizeModeFromGrid()
+ {
+ // SizeMode != null => the consumer set it (or we already inherited it) => leave it alone.
+ if (this.SizeMode is not null || Grid?.GridSizeMode is not { } gridSizeMode) return;
+
+ BeginUpdate();
+ try
+ {
+ this.SizeMode = gridSizeMode;
+ }
+ finally
+ {
+ EndUpdate();
+ }
}
private void OnSelectionChanged(IEnumerable? values)
diff --git a/AyCode.Blazor.Components/docs/MGGRID/MGGRID_CRUD.md b/AyCode.Blazor.Components/docs/MGGRID/MGGRID_CRUD.md
index 3079098..abd9d5f 100644
--- a/AyCode.Blazor.Components/docs/MGGRID/MGGRID_CRUD.md
+++ b/AyCode.Blazor.Components/docs/MGGRID/MGGRID_CRUD.md
@@ -9,7 +9,7 @@
├── Validate Logger, SignalRClient (throw if null)
├── Create SignalRCrudTags from message tag parameters
├── Create TSignalRDataSource via Activator.CreateInstance(SignalRClient, crudTags, ContextIds)
- ├── Set DataSource.FilterText
+ ├── Set DataSource.CustomFilterCriteria
├── Bind grid Data to data source inner list
└── Subscribe to: OnDataSourceLoaded, OnDataSourceItemChanged, OnSyncingStateChanged
diff --git a/AyCode.Blazor.Components/docs/MGGRID/MGGRID_PARAMETERS.md b/AyCode.Blazor.Components/docs/MGGRID/MGGRID_PARAMETERS.md
index 0773e74..f04e0e3 100644
--- a/AyCode.Blazor.Components/docs/MGGRID/MGGRID_PARAMETERS.md
+++ b/AyCode.Blazor.Components/docs/MGGRID/MGGRID_PARAMETERS.md
@@ -30,7 +30,7 @@ These are bundled into a `SignalRCrudTags` during `OnInitializedAsync`. See `AyC
| `ParentDataItem` | `IId?` | Parent entity for detail grids. `null` = master grid. |
| `KeyFieldNameToParentId` | `string?` | Property name on `TDataItem` that references the parent's ID |
| `ContextIds` | `object[]?` | Additional context passed to `AcSignalRDataSource` constructor |
-| `FilterText` | `string?` | Text filter — propagated to data source, triggers reload |
+| `CustomFilterCriteria` | `string?` | Consumer-supplied **base** filter criteria (a DevExpress criteria string) — propagated to the data source, triggers reload; server parses + whitelists it. AND-merged with the grid's automatic `FilterCriteria` (ADR-0004 unified model). Renamed `FilterText` → `CustomFilterText` → `CustomFilterCriteria` (2026-07-16). |
## Display & Behavior Parameters
@@ -45,10 +45,10 @@ These are bundled into a `SignalRCrudTags` during `OnInitializedAsync`. See `AyC
| Parameter | Type | Default | Description |
|---|---|---|---|
-| `FilterPanel` | `RenderFragment?` | `null` | Quick-filter controls rendered above the grid; the slot wraps them in a styled `.mg-grid-filter-panel` container. When set, the toolbar shows a Show/Hide Filters toggle (`MGGRID_TOOLBAR.md`). |
+| `FilterPanel` | `RenderFragment?` | `null` | Quick-filter controls rendered below the toolbar (composed into the grid's `ToolbarTemplate`); the slot wraps them in a styled `.mg-grid-filter-panel` container. When set, the toolbar shows a Show/Hide Filters toggle (`MGGRID_TOOLBAR.md`). |
| `ShowFilterPanel` | `bool` | `true` | Initial visibility of the filter panel; the toolbar toggle flips it at runtime (`IsFilterPanelVisible` / `ToggleFilterPanel`). |
-Place `MgTagBox` controls (from `Components/`) in the slot. Each control applies a per-field `IN(FieldName, selected...)` criteria via `IMgGridBase.SetFieldQuickFilter` → DevExpress `SetFieldFilterCriteria`; DevExpress AND-combines the per-field criteria with each other and with the user's filter row automatically (no clobber). Empty selection clears that field's filter.
+Place `MgTagBox` controls (from `Components/`) in the slot. Each control applies a per-field `IN(FieldName, selected...)` criteria via `IMgGridBase.SetFieldQuickFilter` → DevExpress `SetFieldFilterCriteria`; DevExpress AND-combines the per-field criteria with each other and with the user's filter row automatically (no clobber). Empty selection clears that field's filter. Several controls in one panel therefore narrow the grid cumulatively (field A **and** field B).
```razor
@@ -57,7 +57,23 @@ Place `MgTagBox` controls (from `Components/`) in the slot. Each control applies
```
-Implementation note: imperative `Values` writes inside DevExpress editors must be wrapped in `BeginUpdate()`/`EndUpdate()` (the parameter tracker throws otherwise) — `MgTagBox` handles this internally.
+**Lookup (foreign-key) columns.** When the target column holds an ID (e.g. `PartnerId`) but users pick by name, bind the tag box to the lookup objects and let `ValueFieldName` supply the IDs. `FieldName` still names the *grid* column, so the emitted `IN(PartnerId, 3, 7)` matches the underlying data field — not the cell's display template:
+
+```razor
+
+
+
+```
+
+A nullable FK column (`int?`) behaves the same — rows whose value is `null` match no selection.
+
+**Sizing.** `MgTagBox` defaults its `SizeMode` to the grid's (`IMgGridBase.GridSizeMode`) when the consumer leaves it unset, so the panel matches the grid's density with no per-control declaration. An explicit `SizeMode` on the control always wins.
+
+**Layout.** The panel is a flex row: controls sit side by side sharing the width and wrap to the next line only when they no longer fit (`.mg-grid-filter-panel > *` — `flex` / `min-width` / `max-width` in `mg-grid-info-panel.css`; tune there for a different density). It is styled to read as its **own band** between the toolbar and the group panel, aligned with the native group panel via the grid's own SizeMode-adaptive tokens (`--dxbl-grid-group-panel-container-padding-*`). Since it is composed into the `ToolbarTemplate` it physically lives inside `.dxbl-grid-toolbar-container`; that container's padding is zeroed (`:has(.mg-grid-toolbar-with-filter)`) and each row supplies its own: the toolbar row (`.mg-grid-toolbar-row`) reuses the container's native padding tokens, the band reuses the group panel's. Separator above = the band's `border-top`; below = the container's native `border-bottom`. A consumer `.hide-toolbar` class (detail grids) therefore hides the panel together with the toolbar.
+
+Implementation note: imperative parameter writes inside DevExpress editors (`Values`, `SizeMode`) must be wrapped in `BeginUpdate()`/`EndUpdate()` (the parameter tracker throws otherwise) — `MgTagBox` handles this internally.
Design + the server-side forward path (`FilterMode.Server`): AyCode.Core repo `docs/adr/0004-signalr-datasource-server-side-filtering.md`.
diff --git a/AyCode.Blazor.Components/docs/MGGRID/README.md b/AyCode.Blazor.Components/docs/MGGRID/README.md
index ffbd8fb..c09fc67 100644
--- a/AyCode.Blazor.Components/docs/MGGRID/README.md
+++ b/AyCode.Blazor.Components/docs/MGGRID/README.md
@@ -14,7 +14,7 @@
- **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`
-- **Quick-filter panel** — `FilterPanel` slot above the grid hosting `MgTagBox` controls; per-field criteria AND-combined by DevExpress with the user's filter row; toolbar Show/Hide toggle (see `MGGRID_PARAMETERS.md` → Quick-Filter Panel; design: AyCode.Core repo `docs/adr/0004-signalr-datasource-server-side-filtering.md`)
+- **Quick-filter panel** — `FilterPanel` slot below the toolbar hosting `MgTagBox` controls; per-field criteria AND-combined by DevExpress with the user's filter row; toolbar Show/Hide toggle (see `MGGRID_PARAMETERS.md` → Quick-Filter Panel; design: AyCode.Core repo `docs/adr/0004-signalr-datasource-server-side-filtering.md`)
## Detailed Documentation
@@ -101,6 +101,7 @@ The public contract exposed to companion components (toolbar, InfoPanel, wrapper
| `HasFilterPanel` | `bool` | Whether the grid has a `FilterPanel` (drives toolbar-toggle visibility) |
| `IsFilterPanelVisible` | `bool` | Current quick-filter panel visibility |
| `ToggleFilterPanel()` | `void` | Show/hide the quick-filter panel |
+| `GridSizeMode` | `SizeMode?` | The grid's DevExpress `SizeMode`; `MgTagBox` in the `FilterPanel` inherits it as its default when the consumer sets none |
| `SaveUserLayoutAsync()` | `Task` | Save layout manually |
| `LoadUserLayoutAsync()` | `Task` | Load manually saved layout |
| `ResetLayoutAsync()` | `Task` | Reset to default layout |
diff --git a/AyCode.Blazor.Components/wwwroot/css/mg-grid-info-panel.css b/AyCode.Blazor.Components/wwwroot/css/mg-grid-info-panel.css
index 9701eed..17417cf 100644
--- a/AyCode.Blazor.Components/wwwroot/css/mg-grid-info-panel.css
+++ b/AyCode.Blazor.Components/wwwroot/css/mg-grid-info-panel.css
@@ -5,16 +5,51 @@
These are defined on .dxbl-theme-fluent class and inherited by child elements.
*/
-/* Grid quick-filter panel (rendered above the grid; hosts MgTagBox quick-filter controls) */
+/* Grid toolbar + quick-filter column: stacks the filter panel BELOW the toolbar
+ (the grid's toolbar region lays its children out horizontally otherwise) */
+.mg-grid-toolbar-with-filter {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+}
+
+/* Grid quick-filter panel — composed into the grid's ToolbarTemplate, so it physically lives INSIDE
+ .dxbl-grid-toolbar-container. Styled to read as its OWN band below the toolbar, aligned with the native
+ group panel ("Drag a column header...") band: it reuses the grid's OWN group-panel padding tokens, which
+ the grid root redefines per SizeMode — so the band's left edge and height track the group panel exactly
+ at every grid size. Separator above = our border-top (grid border tokens); separator below = the toolbar
+ container's own native border-bottom (its padding is zeroed below, so that border sits directly under
+ this band — one line, no double border, no extra gap). */
.mg-grid-filter-panel {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--DS-sizing-80, 0.5rem);
- padding: var(--DS-sizing-80, 0.5rem) var(--DS-sizing-160, 1rem);
- background-color: var(--DS-color-surface-neutral-subdued-rest, #f8f9fa);
- border: 1px solid var(--DS-color-border-neutral-default-rest, #dee2e6);
- border-bottom: none;
+ padding: var(--dxbl-grid-group-panel-container-padding-y, 0.5rem) var(--dxbl-grid-group-panel-container-padding-x, 0.75rem);
+ border-top: var(--dxbl-grid-border-width, 1px) var(--dxbl-grid-border-style, solid) var(--dxbl-grid-border-color, #dee2e6);
+}
+
+/* The toolbar container's own padding wrapped BOTH rows (toolbar + filter band), which broke the band
+ illusion: the band's separators stopped short of the grid's edges, and below the band the container's
+ leftover bottom padding + its native border produced a second line and an asymmetric gap. Zero the
+ container's padding when it hosts our stacked wrapper; each row then supplies its own padding. */
+.dxbl-grid-toolbar-container:has(.mg-grid-toolbar-with-filter) {
+ padding: 0;
+}
+
+/* Toolbar row inside the wrapper: carries the toolbar-container's native padding tokens (SizeMode-adaptive),
+ so the toolbar itself looks exactly as it does without a filter panel. */
+.mg-grid-toolbar-row {
+ padding: var(--dxbl-grid-toolbar-container-padding-y, 0.5rem) var(--dxbl-grid-toolbar-container-padding-x, 0.75rem);
+}
+
+/* Quick-filter controls: sit side by side and share the row width; wrap to the next line only when
+ they can't fit. Overrides the editors' default (near) full-width so they don't stack one-per-row.
+ flex-basis / min / max are tunable if a different density is wanted. */
+.mg-grid-filter-panel > * {
+ flex: 1 1 240px;
+ min-width: 200px;
+ max-width: 480px;
}
/* Main panel - uses DevExpress Design System variables */
diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md
index ec5c430..19a3317 100644
--- a/docs/GLOSSARY.md
+++ b/docs/GLOSSARY.md
@@ -17,14 +17,16 @@ For full technical reference see `AyCode.Blazor.Components/docs/MGGRID/README.md
| Term | Definition |
|---|---|
-| **MgGridBase** | Abstract generic grid component extending `DxGrid` with SignalR CRUD, layout persistence, master-detail, InfoPanel, fullscreen. |
+| **MgGridBase** | Abstract generic grid component extending `DxGrid` with SignalR CRUD, layout persistence, master-detail, InfoPanel, fullscreen, and a quick-filter `FilterPanel` slot. |
+| **FilterPanel** | `MgGridBase` `RenderFragment` slot for quick-filter controls (`MgTagBox`); rendered below the toolbar as its own full-bleed band (styled like the native group panel) in a `.mg-grid-filter-panel` container, with a toolbar Show/Hide toggle. |
+| **MgTagBox** | `DxTagBox` seam with a `FieldName` — a selection applies a per-field `IN` criteria to the grid via `IMgGridBase.SetFieldQuickFilter` (DevExpress `SetFieldFilterCriteria`); used inside `FilterPanel`. Defaults its `SizeMode` to the grid's when unset; bind to lookup objects (`ValueFieldName`/`TextFieldName`) to filter an FK column by name. |
| **MgGridWithInfoPanel** | `DxSplitter` wrapper: grid (left pane) + InfoPanel (right pane), fullscreen overlay, splitter size persistence. |
| **MgGridToolbarBase** | `DxToolbar` base with `Grid` (`IMgGridBase`) reference and `RefreshClick` callback. |
-| **MgGridToolbarTemplate** | Full toolbar: New/Edit/Delete/Save/Cancel, row navigation, layout menu, export, fullscreen. Extensible via `ToolbarItemsExtended`. |
+| **MgGridToolbarTemplate** | Full toolbar: New/Edit/Delete/Save/Cancel, row navigation, layout menu, export, filter-panel toggle (when the grid has a `FilterPanel`), fullscreen. Extensible via `ToolbarItemsExtended`. |
| **MgGridDataColumn** | Extended `DxGridDataColumn` with InfoPanel parameters and `UrlLink` template (`{Property}` placeholders). |
| **MgGridInfoPanel** | Default InfoPanel: column-value pairs for focused row, responsive columns, edit mode with typed editors, template system. |
| **MgGridSignalRDataSource** | `GridCustomDataSource` wrapping `AcSignalRDataSource`. Local cache, background refresh. |
-| **IMgGridBase** | Public interface: `IsSyncing`, `GridEditState`, `ParentGrid`, `StepPrevRow/NextRow`, layout persistence methods. |
+| **IMgGridBase** | Public interface: `IsSyncing`, `GridEditState`, `ParentGrid`, `StepPrevRow/NextRow`, layout persistence, quick-filter (`SetFieldQuickFilter`, `HasFilterPanel`, `IsFilterPanelVisible`, `ToggleFilterPanel`), `GridSizeMode`. |
| **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.Services/docs/SIGNALR_DATASOURCE/README.md` in AyCode.Core repo. |
| **IsMasterGrid** | `true` when `ParentDataItem == null` — top-level grid (not detail). |