diff --git a/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs b/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs index ca5a8cc..943e7d3 100644 --- a/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs +++ b/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs @@ -6,6 +6,7 @@ using AyCode.Core.Loggers; using AyCode.Services.SignalRs; using AyCode.Utils.Extensions; using DevExpress.Blazor; +using DevExpress.Data.Filtering; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.JSInterop; @@ -93,6 +94,13 @@ public interface IMgGridBase : IGrid /// returns true optimistically when server layouts are enabled (no server round-trip) /// Task HasUserLayoutAsync(); + + /// + /// Applies a per-field quick filter to the grid — delegates to DevExpress SetFieldFilterCriteria, + /// which AND-combines it with the user's filter row (and other fields' quick filters). Passing + /// = null clears this field's filter. See ADR 0004. + /// + void SetFieldQuickFilter(string fieldName, CriteriaOperator? criteria); } public abstract class MgGridBase : DxGrid, IMgGridBase, IAsyncDisposable @@ -243,6 +251,7 @@ public abstract class MgGridBase + /// Optional quick-filter panel rendered above the grid (typically a GridFilterPanel hosting + /// controls). Rendered 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; } + /// /// Name for auto-saving/loading grid layout. If not set, defaults to "Grid{TDataItem.Name}" /// @@ -1004,6 +1021,10 @@ public abstract class MgGridBase + public void SetFieldQuickFilter(string fieldName, CriteriaOperator? criteria) + => SetFieldFilterCriteria(fieldName, criteria); + /// /// Navigates to the previous row in the grid /// diff --git a/AyCode.Blazor.Components/Components/Grids/MgGridFilterPanelBase.cs b/AyCode.Blazor.Components/Components/Grids/MgGridFilterPanelBase.cs new file mode 100644 index 0000000..5a87c80 --- /dev/null +++ b/AyCode.Blazor.Components/Components/Grids/MgGridFilterPanelBase.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Rendering; + +namespace AyCode.Blazor.Components.Components.Grids; + +/// +/// 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/MgTagBox.cs b/AyCode.Blazor.Components/Components/MgTagBox.cs new file mode 100644 index 0000000..aea3e5a --- /dev/null +++ b/AyCode.Blazor.Components/Components/MgTagBox.cs @@ -0,0 +1,77 @@ +using AyCode.Blazor.Components.Components.Grids; +using DevExpress.Blazor; +using DevExpress.Data.Filtering; +using Microsoft.AspNetCore.Components; + +namespace AyCode.Blazor.Components.Components; + +/// +/// DxTagBox seam (per the Mg* "derive every DevExpress component" policy) with an added grid +/// quick-filter capability. +/// +/// When placed inside a grid's FilterPanel, changing the selection applies a per-field +/// IN(FieldName, selected...) filter to the grid via . +/// DevExpress AND-combines it with the user's filter row (it uses SetFieldFilterCriteria, NOT +/// SetFilterCriteria) — see ADR 0004. +/// +/// The selection is managed internally, so the consumer writes just +/// <MgTagBox FieldName="..." Data="..." /> — no @bind-Values / backing field needed. +/// Outside a grid filter panel (no cascade) it behaves as a plain tag box. +/// +/// Data item type (as for DxTagBox). +/// Selected value type (as for DxTagBox). +public class MgTagBox : DxTagBox +{ + /// + /// Grid column field this quick-filter targets. Consistent with DxGridDataColumn.FieldName and the + /// SetFieldFilterCriteria(fieldName, ...) parameter. When null/empty, no grid filter is applied. + /// + [Parameter] public string? FieldName { get; set; } + + /// + /// The grid to filter — supplied by the grid's FilterPanel cascade (MgGridBase provides the + /// cascade). Null when the tag box is not inside a grid filter panel. + /// + [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; + base.ValuesChanged = EventCallback.Factory.Create>(this, OnSelectionChanged); + } + + private void OnSelectionChanged(IEnumerable? values) + { + _selected = values; + base.Values = values; // reflect the new selection in the editor + ApplyQuickFilter(values); + StateHasChanged(); + } + + private void ApplyQuickFilter(IEnumerable? values) + { + if (Grid is null || string.IsNullOrWhiteSpace(FieldName)) return; + + 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. + CriteriaOperator? criteria = selected.Length > 0 ? new InOperator(FieldName, selected) : null; + + Grid.SetFieldQuickFilter(FieldName, criteria); + } +}