Add custom date range filter UI for grid date columns

Introduced a custom date range filter menu for date columns in BandGridBase<TItem> 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.
This commit is contained in:
OlgunR
2026-05-11 09:49:17 +02:00
parent 9dc65ab92f
commit d9785baf5b
2 changed files with 104 additions and 0 deletions

View File

@@ -1,6 +1,7 @@
using DbFirst.BlazorWebApp.Models.Grid; using DbFirst.BlazorWebApp.Models.Grid;
using DbFirst.BlazorWebApp.Services; using DbFirst.BlazorWebApp.Services;
using DevExpress.Blazor; using DevExpress.Blazor;
using DevExpress.Data.Filtering;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Rendering;
@@ -24,6 +25,15 @@ public abstract class BandGridBase<TItem> : ComponentBase
protected bool gridLayoutApplied; protected bool gridLayoutApplied;
protected IGrid? gridRef; protected IGrid? gridRef;
// --- Datumsfilter-Zustand ---
private readonly Dictionary<string, DateTime?> _filterFrom = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, DateTime?> _filterTo = new(StringComparer.OrdinalIgnoreCase);
// Stabile Referenzen: werden einmal pro FieldName erstellt und wiederverwendet
private readonly Dictionary<string, EventCallback<DateTime?>> _fromCallbacks = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, EventCallback<DateTime?>> _toCallbacks = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, RenderFragment<GridDataColumnFilterMenuTemplateContext>> _dateFilterTemplates = new(StringComparer.OrdinalIgnoreCase);
// --- SizeMode --- // --- SizeMode ---
protected SizeMode _sizeMode = SizeMode.Medium; protected SizeMode _sizeMode = SizeMode.Medium;
protected static readonly List<SizeMode> _sizeModes = Enum.GetValues<SizeMode>().ToList(); protected static readonly List<SizeMode> _sizeModes = Enum.GetValues<SizeMode>().ToList();
@@ -224,9 +234,97 @@ public abstract class BandGridBase<TItem> : ComponentBase
builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat); builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat);
if (column.ReadOnly) if (column.ReadOnly)
builder.AddAttribute(seq++, "ReadOnly", true); builder.AddAttribute(seq++, "ReadOnly", true);
if (column.FilterType == ColumnFilterType.Date)
builder.AddAttribute(seq++, "FilterMenuTemplate", GetOrCreateDateFilterTemplate(column.FieldName));
builder.CloseComponent(); builder.CloseComponent();
} }
private RenderFragment<GridDataColumnFilterMenuTemplateContext> GetOrCreateDateFilterTemplate(string fieldName)
{
if (!_dateFilterTemplates.TryGetValue(fieldName, out var template))
{
// EventCallbacks einmalig erstellen stabile Referenzen über alle Renders
_fromCallbacks[fieldName] = EventCallback.Factory.Create<DateTime?>(this, (DateTime? v) => OnFilterFromChanged(fieldName, v));
_toCallbacks[fieldName] = EventCallback.Factory.Create<DateTime?>(this, (DateTime? v) => OnFilterToChanged(fieldName, v));
template = BuildDateFilterTemplate(fieldName);
_dateFilterTemplates[fieldName] = template;
}
return template;
}
private RenderFragment<GridDataColumnFilterMenuTemplateContext> 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<DxDateEdit<DateTime?>>(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<DxDateEdit<DateTime?>>(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<CriteriaOperator>();
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) protected void SetEditContext(EditContext context)
{ {
if (editContext == context) return; if (editContext == context) return;

View File

@@ -294,4 +294,10 @@ html.dx-dark .dxbl-grid > .dxbl-grid-top-panel {
.top-row .btn-gap { .top-row .btn-gap {
margin-left: 8px; 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;
} }