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.
This commit is contained in:
Loretta 2026-07-16 06:28:55 +02:00
parent 4eb0d1566c
commit 2d62fad300
10 changed files with 119 additions and 58 deletions

View File

@ -101,6 +101,15 @@ public interface IMgGridBase : IGrid
/// <paramref name="criteria"/> = null clears this field's filter. See ADR 0004.
/// </summary>
void SetFieldQuickFilter(string fieldName, CriteriaOperator? criteria);
/// <summary>Whether this grid has a quick-filter panel (i.e. <c>FilterPanel</c> is set).</summary>
bool HasFilterPanel { get; }
/// <summary>Whether the quick-filter panel is currently shown (toggled from the toolbar).</summary>
bool IsFilterPanelVisible { get; }
/// <summary>Shows/hides the quick-filter panel. No-op when the grid has no filter panel.</summary>
void ToggleFilterPanel();
}
public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClient> : DxGrid, IMgGridBase, IAsyncDisposable
@ -251,7 +260,13 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
// Body
contentBuilder.OpenElement(12, "div");
contentBuilder.AddAttribute(13, "class", "mg-fullscreen-body");
if (FilterPanel != null) contentBuilder.AddContent(14, FilterPanel);
if (FilterPanel != null && IsFilterPanelVisible)
{
contentBuilder.OpenElement(14, "div");
contentBuilder.AddAttribute(15, "class", "mg-grid-filter-panel");
contentBuilder.AddContent(16, FilterPanel);
contentBuilder.CloseElement();
}
base.BuildRenderTree(contentBuilder);
contentBuilder.CloseElement(); // body div
@ -263,7 +278,13 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
contentBuilder.OpenElement(0, "div");
contentBuilder.SetKey(_gridRenderKey);
contentBuilder.AddAttribute(1, "style", "display: contents;");
if (FilterPanel != null) contentBuilder.AddContent(2, FilterPanel);
if (FilterPanel != null && IsFilterPanelVisible)
{
contentBuilder.OpenElement(2, "div");
contentBuilder.AddAttribute(3, "class", "mg-grid-filter-panel");
contentBuilder.AddContent(4, FilterPanel);
contentBuilder.CloseElement();
}
base.BuildRenderTree(contentBuilder);
contentBuilder.CloseElement();
}
@ -294,12 +315,28 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
[Parameter] public string Caption { get; set; } = typeof(TDataItem).Name;
/// <summary>
/// Optional quick-filter panel rendered above the grid (typically a <c>GridFilterPanel</c> hosting
/// <see cref="MgTagBox{TData,TValue}"/> controls). Rendered within this grid's <see cref="IMgGridBase"/>
/// Optional quick-filter controls (typically <see cref="MgTagBox{TData,TValue}"/>) rendered above the grid
/// inside a styled <c>.mg-grid-filter-panel</c> container (the slot wraps the content). Within this grid's <see cref="IMgGridBase"/>
/// cascade, so the controls can reach this grid and apply per-field filters. See ADR 0004.
/// </summary>
[Parameter] public RenderFragment? FilterPanel { get; set; }
/// <summary>Initial visibility of the <see cref="FilterPanel"/>; the toolbar toggle flips it at runtime. Default: shown.</summary>
[Parameter] public bool ShowFilterPanel { get; set; } = true;
/// <inheritdoc />
public bool HasFilterPanel => FilterPanel != null;
/// <inheritdoc />
public bool IsFilterPanelVisible { get; private set; } = true;
/// <inheritdoc />
public void ToggleFilterPanel()
{
IsFilterPanelVisible = !IsFilterPanelVisible;
InvokeAsync(StateHasChanged);
}
/// <summary>
/// Name for auto-saving/loading grid layout. If not set, defaults to "Grid{TDataItem.Name}"
/// </summary>
@ -406,6 +443,8 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
if (Logger == null)
throw new NullReferenceException($"[{GetType().Name}] Logger == null");
IsFilterPanelVisible = ShowFilterPanel;
if (SignalRClient == null)
{
Logger.Error($"[{GetType().Name}] SignalRClient == null");

View File

@ -1,32 +0,0 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace AyCode.Blazor.Components.Components.Grids;
/// <summary>
/// Base for a grid quick-filter panel — a styled container placed in a grid's <c>FilterPanel</c> slot,
/// hosting <see cref="MgTagBox{TData,TValue}"/> (or other) per-field quick-filter controls above the grid.
/// <para>
/// Framework base. Projects derive a (typically empty) <c>GridFilterPanel</c> per the Mg* seam policy
/// (cf. <c>MgGridBase</c> → project grid). The grid reference reaches the child controls via the grid's own
/// <see cref="IMgGridBase"/> cascade (<c>MgGridBase</c> provides it) — this panel is layout only, no wiring.
/// </para>
/// </summary>
public class MgGridFilterPanelBase : ComponentBase
{
/// <summary>The quick-filter controls (e.g. <see cref="MgTagBox{TData,TValue}"/>) to render in the panel.</summary>
[Parameter] public RenderFragment? ChildContent { get; set; }
/// <summary>Extra CSS classes appended to the panel container.</summary>
[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();
}
}

View File

@ -30,6 +30,7 @@
</Items>
</DxToolbarItem>
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "Reload data")" BeginGroup="true" Click="ReloadData_Click" IconCssClass="grid-refresh" Enabled="@(!IsSyncing && !_isReloadInProgress && !IsEditing)" />
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : FilterPanelButtonText)" Click="FilterPanel_Click" IconCssClass="grid-filter" Visible="@Grid.HasFilterPanel" Enabled="@(!IsEditing)" />
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : FullscreenButtonText)" Click="Fullscreen_Click" IconCssClass="@FullscreenIconCssClass" Enabled="@(!IsEditing)" />
@ToolbarItemsExtended
}
@ -82,6 +83,16 @@
/// </summary>
private string FullscreenIconCssClass => IsFullscreenMode ? "grid-fullscreen-exit" : "grid-fullscreen";
/// <summary>
/// Whether the quick-filter panel is currently shown
/// </summary>
private bool IsFilterPanelVisible => Grid?.IsFilterPanelVisible ?? false;
/// <summary>
/// Button text for the filter-panel toggle
/// </summary>
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);

View File

@ -4,10 +4,10 @@ Core grid system built on DevExpress `DxGrid`. For the full technical reference
## Key Files
- **`MgGridBase.cs`** — `MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClient>`, the main abstract grid component. Extends `DxGrid` with SignalR CRUD, layout persistence, master-detail hierarchy, edit state tracking, fullscreen toggle.
- **`MgGridBase.cs`** — `MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClient>`, 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.

View File

@ -34,32 +34,31 @@ public class MgTagBox<TData, TValue> : DxTagBox<TData, TValue>
/// </summary>
[CascadingParameter] public IMgGridBase? Grid { get; set; }
private bool _initialized;
private IEnumerable<TValue>? _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<IEnumerable<TValue>>(this, OnSelectionChanged);
}
private void OnSelectionChanged(IEnumerable<TValue>? 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<TValue>? values)
@ -68,8 +67,7 @@ public class MgTagBox<TData, TValue> : DxTagBox<TData, TValue>
var selected = values?.Where(v => v is not null).Cast<object>().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);

View File

@ -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<T>`.
- **`AcMemo.cs`** -- Extends `DxMemo`.
- **`AcSpinEdit.cs`** -- Generic wrapper for `DxSpinEdit<T>`.
- **`MgTagBox.cs`** -- Generic wrapper for `DxTagBox<TData, TValue>` 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

View File

@ -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
<FilterPanel>
<MgTagBox TData="string" TValue="string" FieldName="Status"
Data="@_statusOptions" NullText="All statuses" />
</FilterPanel>
```
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:

View File

@ -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` |

View File

@ -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 |

View File

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