From 2d62fad300b8cd568c474e4c3da727547c28db36 Mon Sep 17 00:00:00 2001 From: Loretta Date: Thu, 16 Jul 2026 06:28:55 +0200 Subject: [PATCH] Add quick-filter panel slot to MgGridBase Introduced a flexible quick-filter panel to the grid system via a new FilterPanel slot in MgGridBase. Added interface and implementation for filter panel visibility and toggle logic. Updated toolbar to show a Show/Hide Filters button when applicable. MgTagBox now applies per-field quick-filters using SetFieldQuickFilter. Removed redundant MgGridFilterPanelBase. Updated documentation and ADR 0004 to cover the new feature. Added CSS for consistent filter panel styling and demonstrated usage in GridPartner.razor. Legacy MgGridBase updated for compatibility. --- .../Components/Grids/MgGridBase.cs | 47 +++++++++++++++++-- .../Components/Grids/MgGridFilterPanelBase.cs | 32 ------------- .../Grids/MgGridToolbarTemplate.razor | 16 +++++++ .../Components/Grids/README.md | 4 +- .../Components/MgTagBox.cs | 34 +++++++------- AyCode.Blazor.Components/Components/README.md | 3 +- .../docs/MGGRID/MGGRID_PARAMETERS.md | 20 ++++++++ .../docs/MGGRID/MGGRID_TOOLBAR.md | 2 + .../docs/MGGRID/README.md | 7 ++- .../wwwroot/css/mg-grid-info-panel.css | 12 +++++ 10 files changed, 119 insertions(+), 58 deletions(-) delete mode 100644 AyCode.Blazor.Components/Components/Grids/MgGridFilterPanelBase.cs diff --git a/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs b/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs index 943e7d3..faf1036 100644 --- a/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs +++ b/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs @@ -101,6 +101,15 @@ public interface IMgGridBase : IGrid /// = null clears this field's filter. See ADR 0004. /// void SetFieldQuickFilter(string fieldName, CriteriaOperator? criteria); + + /// Whether this grid has a quick-filter panel (i.e. FilterPanel is set). + bool HasFilterPanel { get; } + + /// Whether the quick-filter panel is currently shown (toggled from the toolbar). + bool IsFilterPanelVisible { get; } + + /// Shows/hides the quick-filter panel. No-op when the grid has no filter panel. + void ToggleFilterPanel(); } public abstract class MgGridBase : DxGrid, IMgGridBase, IAsyncDisposable @@ -251,7 +260,13 @@ public abstract class MgGridBase - /// Optional quick-filter panel rendered above the grid (typically a GridFilterPanel hosting - /// controls). Rendered within this grid's + /// Optional quick-filter controls (typically ) rendered above the grid + /// 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. /// [Parameter] public RenderFragment? FilterPanel { get; set; } + /// Initial visibility of the ; the toolbar toggle flips it at runtime. Default: shown. + [Parameter] public bool ShowFilterPanel { get; set; } = true; + + /// + public bool HasFilterPanel => FilterPanel != null; + + /// + public bool IsFilterPanelVisible { get; private set; } = true; + + /// + public void ToggleFilterPanel() + { + IsFilterPanelVisible = !IsFilterPanelVisible; + InvokeAsync(StateHasChanged); + } + /// /// Name for auto-saving/loading grid layout. If not set, defaults to "Grid{TDataItem.Name}" /// @@ -406,6 +443,8 @@ public abstract class MgGridBase -/// Base for a grid quick-filter panel — a styled container placed in a grid's FilterPanel slot, -/// hosting (or other) per-field quick-filter controls above the grid. -/// -/// Framework base. Projects derive a (typically empty) GridFilterPanel per the Mg* seam policy -/// (cf. MgGridBase → project grid). The grid reference reaches the child controls via the grid's own -/// cascade (MgGridBase provides it) — this panel is layout only, no wiring. -/// -/// -public class MgGridFilterPanelBase : ComponentBase -{ - /// The quick-filter controls (e.g. ) to render in the panel. - [Parameter] public RenderFragment? ChildContent { get; set; } - - /// Extra CSS classes appended to the panel container. - [Parameter] public string? CssClass { get; set; } - - protected override void BuildRenderTree(RenderTreeBuilder builder) - { - builder.OpenElement(0, "div"); - builder.AddAttribute(1, "class", string.IsNullOrWhiteSpace(CssClass) - ? "mg-grid-filter-panel" - : $"mg-grid-filter-panel {CssClass}"); - builder.AddContent(2, ChildContent); - builder.CloseElement(); - } -} diff --git a/AyCode.Blazor.Components/Components/Grids/MgGridToolbarTemplate.razor b/AyCode.Blazor.Components/Components/Grids/MgGridToolbarTemplate.razor index 75885d3..0162ecb 100644 --- a/AyCode.Blazor.Components/Components/Grids/MgGridToolbarTemplate.razor +++ b/AyCode.Blazor.Components/Components/Grids/MgGridToolbarTemplate.razor @@ -30,6 +30,7 @@ + @ToolbarItemsExtended } @@ -82,6 +83,16 @@ /// private string FullscreenIconCssClass => IsFullscreenMode ? "grid-fullscreen-exit" : "grid-fullscreen"; + /// + /// Whether the quick-filter panel is currently shown + /// + private bool IsFilterPanelVisible => Grid?.IsFilterPanelVisible ?? false; + + /// + /// Button text for the filter-panel toggle + /// + private string FilterPanelButtonText => IsFilterPanelVisible ? "Hide Filters" : "Show Filters"; + protected override async Task OnInitializedAsync() { _hasUserLayout = await Grid.HasUserLayoutAsync(); @@ -145,6 +156,11 @@ Grid.ToggleFullscreen(); } + void FilterPanel_Click() + { + Grid.ToggleFilterPanel(); + } + async Task ExportXlsxItem_Click() { await Grid.ExportToXlsxAsync(ExportFileName); diff --git a/AyCode.Blazor.Components/Components/Grids/README.md b/AyCode.Blazor.Components/Components/Grids/README.md index eab2d31..4e3f772 100644 --- a/AyCode.Blazor.Components/Components/Grids/README.md +++ b/AyCode.Blazor.Components/Components/Grids/README.md @@ -4,10 +4,10 @@ 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. +- **`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). - **`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, fullscreen. Extensible via `ToolbarItemsExtended`. +- **`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`. - **`MgGridDataColumn.cs`** — Extends `DxGridDataColumn` with InfoPanel parameters (`ShowInInfoPanel`, `InfoPanelOrder`, `InfoPanelDisplayFormat`) and `UrlLink` template with `{Property}` placeholder substitution via compiled accessors. - **`MgGridInfoPanel.razor`** / **`.razor.cs`** — `MgGridInfoPanel` implementing `IInfoPanelBase`. Responsive column layout (1-4 columns with breakpoints), edit/view mode with typed editors, template system, sticky positioning via JS interop. - **`MgGridSignalRDataSource.cs`** — `GridCustomDataSource` wrapping `AcSignalRDataSource`. Local cache for seen filter criteria, background refresh. diff --git a/AyCode.Blazor.Components/Components/MgTagBox.cs b/AyCode.Blazor.Components/Components/MgTagBox.cs index aea3e5a..85d7692 100644 --- a/AyCode.Blazor.Components/Components/MgTagBox.cs +++ b/AyCode.Blazor.Components/Components/MgTagBox.cs @@ -34,32 +34,31 @@ public class MgTagBox : DxTagBox /// [CascadingParameter] public IMgGridBase? Grid { get; set; } - private bool _initialized; - private IEnumerable? _selected; - protected override void OnParametersSet() { base.OnParametersSet(); - // Own the selection internally so the consumer needs no @bind-Values. - // COMPILE-VERIFY (DevExpress version specific): Blazor re-applies base.Values / base.ValuesChanged from the - // consumer markup on every parameter set, so we re-assert ours here each time. First set adopts any initial Values. - if (!_initialized) - { - _selected = Values; - _initialized = true; - } - - base.Values = _selected; + // 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); } private void OnSelectionChanged(IEnumerable? values) { - _selected = values; - base.Values = values; // reflect the new selection in the editor + // DevExpress tracks its parameters; setting Values imperatively (outside markup, e.g. from this event + // callback) must be wrapped in BeginUpdate/EndUpdate — otherwise DevExpress throws + // "A parameter value is specified outside a component's markup." + BeginUpdate(); + try + { + Values = values; + } + finally + { + EndUpdate(); + } + ApplyQuickFilter(values); - StateHasChanged(); } private void ApplyQuickFilter(IEnumerable? values) @@ -68,8 +67,7 @@ public class MgTagBox : DxTagBox var selected = values?.Where(v => v is not null).Cast().ToArray() ?? []; - // COMPILE-VERIFY: confirm the InOperator(string, IEnumerable / params object[]) overload on your - // DevExpress.Data version. Empty selection => null => clears this field's filter. + // Empty selection => null => clears this field's filter. Otherwise IN(FieldName, selected...). CriteriaOperator? criteria = selected.Length > 0 ? new InOperator(FieldName, selected) : null; Grid.SetFieldQuickFilter(FieldName, criteria); diff --git a/AyCode.Blazor.Components/Components/README.md b/AyCode.Blazor.Components/Components/README.md index 008a925..97467a9 100644 --- a/AyCode.Blazor.Components/Components/README.md +++ b/AyCode.Blazor.Components/Components/README.md @@ -1,6 +1,6 @@ # Components -DevExpress component wrappers and grid infrastructure for the AyCode Blazor component library. Each `Ac*` class extends a DevExpress Blazor control to allow project-wide customization from a single point. +DevExpress component wrappers and grid infrastructure for the AyCode Blazor component library. Each wrapper class extends a DevExpress Blazor control to allow project-wide customization from a single point (`Mg*` = current prefix; `Ac*` = legacy prefix, kept as-is). ## Key Files @@ -13,6 +13,7 @@ DevExpress component wrappers and grid infrastructure for the AyCode Blazor comp - **`AcMaskedInput.cs`** -- Generic wrapper for `DxMaskedInput`. - **`AcMemo.cs`** -- Extends `DxMemo`. - **`AcSpinEdit.cs`** -- Generic wrapper for `DxSpinEdit`. +- **`MgTagBox.cs`** -- Generic wrapper for `DxTagBox` with grid quick-filter support: a `FieldName` parameter + the grid cascade let a selection apply a per-field `IN` filter via `IMgGridBase.SetFieldQuickFilter` (see `Grids/` and `docs/MGGRID/MGGRID_PARAMETERS.md` → Quick-Filter Panel). - **`MgComponentsHelper.cs`** -- Placeholder helper class (currently empty). ## Subfolders diff --git a/AyCode.Blazor.Components/docs/MGGRID/MGGRID_PARAMETERS.md b/AyCode.Blazor.Components/docs/MGGRID/MGGRID_PARAMETERS.md index 3dd5a05..0773e74 100644 --- a/AyCode.Blazor.Components/docs/MGGRID/MGGRID_PARAMETERS.md +++ b/AyCode.Blazor.Components/docs/MGGRID/MGGRID_PARAMETERS.md @@ -41,6 +41,26 @@ These are bundled into a `SignalRCrudTags` during `OnInitializedAsync`. See `AyC | `AutoSaveLayoutName` | `string?` | `"Grid{TDataItem.Name}"` | Base name for layout storage keys | | `EnableServerLayouts` | `bool` | `true` | Roams the UserSave layout tier via the SignalR server endpoint (see `MGGRID_LAYOUT.md` → Server Roaming). Server calls happen only on the Save/Load layout buttons. | +## Quick-Filter Panel + +| 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`). | +| `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. + +```razor + + + +``` + +Implementation note: imperative `Values` writes inside DevExpress editors 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`. + ## Event Callbacks All grid events are re-exposed with `OnGrid` prefix to avoid collisions with `DxGrid` base events: diff --git a/AyCode.Blazor.Components/docs/MGGRID/MGGRID_TOOLBAR.md b/AyCode.Blazor.Components/docs/MGGRID/MGGRID_TOOLBAR.md index 2885202..3a25848 100644 --- a/AyCode.Blazor.Components/docs/MGGRID/MGGRID_TOOLBAR.md +++ b/AyCode.Blazor.Components/docs/MGGRID/MGGRID_TOOLBAR.md @@ -15,6 +15,7 @@ The standard toolbar rendered inside grid's `ToolbarTemplate`. Provides all stan | **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` | +| **Quick-filter** | Show Filters / Hide Filters (toggles the `FilterPanel`) | When `OnlyGridEditTools=false` AND `Grid.HasFilterPanel` | ### Parameters @@ -37,3 +38,4 @@ The standard toolbar rendered inside grid's `ToolbarTemplate`. Provides all stan | `IsSyncing` | `Grid.IsSyncing` | | `HasFocusedRow` | `Grid.GetFocusedRowIndex() >= 0` | | `IsFullscreenMode` | `Grid.IsFullscreen` | +| `IsFilterPanelVisible` | `Grid.IsFilterPanelVisible` | diff --git a/AyCode.Blazor.Components/docs/MGGRID/README.md b/AyCode.Blazor.Components/docs/MGGRID/README.md index adafe30..ffbd8fb 100644 --- a/AyCode.Blazor.Components/docs/MGGRID/README.md +++ b/AyCode.Blazor.Components/docs/MGGRID/README.md @@ -14,12 +14,13 @@ - **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`) ## Detailed Documentation | File | Topics | |---|---| -| `MGGRID_PARAMETERS.md` | Component parameters (required, CRUD tags, data & context, display), event callbacks, default grid settings | +| `MGGRID_PARAMETERS.md` | Component parameters (required, CRUD tags, data & context, display), quick-filter panel (`FilterPanel` slot + `MgTagBox`), 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 | @@ -96,6 +97,10 @@ The public contract exposed to companion components (toolbar, InfoPanel, wrapper | `IsFullscreen` | `bool` | Current fullscreen state | | `AutomaticLayoutStorageKey` | `string` | Current auto-save storage key | | `ToggleFullscreen()` | `void` | Toggle fullscreen mode | +| `SetFieldQuickFilter(fieldName, criteria)` | `void` | Apply/clear a per-field quick-filter (delegates to DevExpress `SetFieldFilterCriteria`; `null` criteria clears the field) | +| `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 | | `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 9fde37d..9701eed 100644 --- a/AyCode.Blazor.Components/wwwroot/css/mg-grid-info-panel.css +++ b/AyCode.Blazor.Components/wwwroot/css/mg-grid-info-panel.css @@ -5,6 +5,18 @@ 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) */ +.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; +} + /* Main panel - uses DevExpress Design System variables */ .mg-grid-info-panel { background-color: var(--DS-color-surface-neutral-subdued-rest, #f8f9fa);