From d9785baf5be61b760cf50ed774b34a1686077d9d Mon Sep 17 00:00:00 2001 From: OlgunR Date: Mon, 11 May 2026 09:49:17 +0200 Subject: [PATCH] Add custom date range filter UI for grid date columns Introduced a custom date range filter menu for date columns in BandGridBase using DevExpress Blazor grids. The new UI provides "from" and "to" date pickers, applies filters immediately on selection, and hides the default filter dropdown footer for a smoother user experience. State management and filter criteria logic were added to support this feature. --- .../Components/BandGridBase.cs | 98 +++++++++++++++++++ DbFirst.BlazorWebApp/wwwroot/app.css | 6 ++ 2 files changed, 104 insertions(+) diff --git a/DbFirst.BlazorWebApp/Components/BandGridBase.cs b/DbFirst.BlazorWebApp/Components/BandGridBase.cs index 7f41118..5e201e6 100644 --- a/DbFirst.BlazorWebApp/Components/BandGridBase.cs +++ b/DbFirst.BlazorWebApp/Components/BandGridBase.cs @@ -1,6 +1,7 @@ using DbFirst.BlazorWebApp.Models.Grid; using DbFirst.BlazorWebApp.Services; using DevExpress.Blazor; +using DevExpress.Data.Filtering; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Components.Rendering; @@ -24,6 +25,15 @@ public abstract class BandGridBase : ComponentBase protected bool gridLayoutApplied; protected IGrid? gridRef; + // --- Datumsfilter-Zustand --- + private readonly Dictionary _filterFrom = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _filterTo = new(StringComparer.OrdinalIgnoreCase); + + // Stabile Referenzen: werden einmal pro FieldName erstellt und wiederverwendet + private readonly Dictionary> _fromCallbacks = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary> _toCallbacks = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary> _dateFilterTemplates = new(StringComparer.OrdinalIgnoreCase); + // --- SizeMode --- protected SizeMode _sizeMode = SizeMode.Medium; protected static readonly List _sizeModes = Enum.GetValues().ToList(); @@ -224,9 +234,97 @@ public abstract class BandGridBase : ComponentBase builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat); if (column.ReadOnly) builder.AddAttribute(seq++, "ReadOnly", true); + if (column.FilterType == ColumnFilterType.Date) + builder.AddAttribute(seq++, "FilterMenuTemplate", GetOrCreateDateFilterTemplate(column.FieldName)); builder.CloseComponent(); } + private RenderFragment GetOrCreateDateFilterTemplate(string fieldName) + { + if (!_dateFilterTemplates.TryGetValue(fieldName, out var template)) + { + // EventCallbacks einmalig erstellen – stabile Referenzen über alle Renders + _fromCallbacks[fieldName] = EventCallback.Factory.Create(this, (DateTime? v) => OnFilterFromChanged(fieldName, v)); + _toCallbacks[fieldName] = EventCallback.Factory.Create(this, (DateTime? v) => OnFilterToChanged(fieldName, v)); + template = BuildDateFilterTemplate(fieldName); + _dateFilterTemplates[fieldName] = template; + } + return template; + } + + private RenderFragment BuildDateFilterTemplate(string fieldName) => + _ => b => + { + int s = 0; + b.OpenElement(s++, "div"); + b.AddAttribute(s++, "class", "date-filter-menu p-2"); + + // Ab Datum + b.OpenElement(s++, "div"); + b.AddAttribute(s++, "class", "mb-2"); + b.OpenElement(s++, "label"); + b.AddAttribute(s++, "class", "form-label small fw-semibold"); + b.AddContent(s++, "Ab Datum"); + b.CloseElement(); + b.OpenComponent>(s++); + b.AddAttribute(s++, "Date", _filterFrom.GetValueOrDefault(fieldName)); + b.AddAttribute(s++, "DateChanged", _fromCallbacks[fieldName]); + b.AddAttribute(s++, "ClearButtonDisplayMode", DataEditorClearButtonDisplayMode.Auto); + b.AddAttribute(s++, "NullText", "Kein Startdatum"); + b.AddAttribute(s++, "Width", "100%"); + b.CloseComponent(); + b.CloseElement(); + + // Bis Datum + b.OpenElement(s++, "div"); + b.AddAttribute(s++, "class", "mb-0"); + b.OpenElement(s++, "label"); + b.AddAttribute(s++, "class", "form-label small fw-semibold"); + b.AddContent(s++, "Bis Datum"); + b.CloseElement(); + b.OpenComponent>(s++); + b.AddAttribute(s++, "Date", _filterTo.GetValueOrDefault(fieldName)); + b.AddAttribute(s++, "DateChanged", _toCallbacks[fieldName]); + b.AddAttribute(s++, "ClearButtonDisplayMode", DataEditorClearButtonDisplayMode.Auto); + b.AddAttribute(s++, "NullText", "Kein Enddatum"); + b.AddAttribute(s++, "Width", "100%"); + b.CloseComponent(); + b.CloseElement(); + + b.CloseElement(); + }; + + private void OnFilterFromChanged(string fieldName, DateTime? value) + { + _filterFrom[fieldName] = value; + ApplyDateFilter(fieldName); + } + + private void OnFilterToChanged(string fieldName, DateTime? value) + { + _filterTo[fieldName] = value; + ApplyDateFilter(fieldName); + } + + private void ApplyDateFilter(string fieldName) + { + var ops = new List(); + if (_filterFrom.TryGetValue(fieldName, out var from) && from.HasValue) + ops.Add(new BinaryOperator(fieldName, from.Value.Date, BinaryOperatorType.GreaterOrEqual)); + if (_filterTo.TryGetValue(fieldName, out var to) && to.HasValue) + ops.Add(new BinaryOperator(fieldName, to.Value.Date.AddDays(1), BinaryOperatorType.Less)); + + CriteriaOperator? criteria = ops.Count switch + { + 0 => null, + 1 => ops[0], + _ => new GroupOperator(GroupOperatorType.And, ops) + }; + + gridRef?.SetFieldFilterCriteria(fieldName, criteria); + _ = InvokeAsync(StateHasChanged); + } + protected void SetEditContext(EditContext context) { if (editContext == context) return; diff --git a/DbFirst.BlazorWebApp/wwwroot/app.css b/DbFirst.BlazorWebApp/wwwroot/app.css index 72224cc..6b6b0cc 100644 --- a/DbFirst.BlazorWebApp/wwwroot/app.css +++ b/DbFirst.BlazorWebApp/wwwroot/app.css @@ -294,4 +294,10 @@ html.dx-dark .dxbl-grid > .dxbl-grid-top-panel { .top-row .btn-gap { margin-left: 8px; +} + +/* Date-Filter: nativer Apply/Clear-Footer ausblenden, da Filter sofort bei Datumsauswahl angewendet wird */ +/* .dxbl-dropdown-footer ist der korrekte Selektor für das Filter-Dropdown (nicht .dxbl-popup-buttons-area, das ist für modale Popups) */ +.dxbl-filter-menu-dropdown-root:has(.date-filter-menu) .dxbl-dropdown-footer { + display: none !important; } \ No newline at end of file