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:
@@ -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;
|
||||||
|
|||||||
@@ -295,3 +295,9 @@ 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user