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);