Added logic to synchronize the date filter UI with the current filter criteria by updating _filterFrom and _filterTo based on the CriteriaOperator. Introduced SyncDateFilterFromContext and ParseDateOperand helpers to extract and apply "from" and "to" date values, ensuring UI and filter state remain consistent.
414 lines
16 KiB
C#
414 lines
16 KiB
C#
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;
|
||
|
||
namespace DbFirst.BlazorWebApp.Components;
|
||
|
||
public abstract class BandGridBase<TItem> : ComponentBase
|
||
{
|
||
[Inject] protected BandLayoutService BandLayoutService { get; set; } = default!;
|
||
|
||
// --- Abstract: jedes Grid definiert diese selbst ---
|
||
protected abstract string LayoutKey { get; }
|
||
protected abstract List<ColumnDefinition> ColumnDefinitions { get; }
|
||
|
||
// --- Band-Layout Felder ---
|
||
protected BandLayout bandLayout = new();
|
||
protected Dictionary<string, string> columnBandAssignments = new();
|
||
protected List<BandOption> bandOptions = new();
|
||
protected Dictionary<string, ColumnDefinition> columnLookup = new();
|
||
protected string? layoutUser;
|
||
protected bool gridLayoutApplied;
|
||
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);
|
||
private readonly Dictionary<string, GridDataColumnFilterMenuTemplateContext> _filterContexts = new(StringComparer.OrdinalIgnoreCase);
|
||
|
||
// --- SizeMode ---
|
||
protected SizeMode _sizeMode = SizeMode.Medium;
|
||
protected static readonly List<SizeMode> _sizeModes = Enum.GetValues<SizeMode>().ToList();
|
||
|
||
protected string? errorMessage;
|
||
protected string? infoMessage;
|
||
protected bool isLoading;
|
||
protected bool hasLoaded;
|
||
protected EditContext? editContext;
|
||
protected ValidationMessageStore? validationMessageStore;
|
||
protected string popupHeaderText = "Edit";
|
||
protected int _focusedVisibleIndex;
|
||
|
||
private const string LayoutType = "GRID_BANDS";
|
||
|
||
// --- Lifecycle ---
|
||
protected async Task InitializeBandLayoutAsync()
|
||
{
|
||
columnLookup = ColumnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
||
layoutUser = await BandLayoutService.EnsureLayoutUserAsync();
|
||
bandLayout = await BandLayoutService.LoadBandLayoutAsync(LayoutType, LayoutKey, layoutUser, columnLookup);
|
||
columnBandAssignments = BandLayoutService.BuildAssignmentsFromLayout(bandLayout);
|
||
ApplyColumnLayoutFromStorage();
|
||
_sizeMode = bandLayout.SizeMode;
|
||
UpdateBandOptions();
|
||
}
|
||
|
||
protected async Task ApplyGridLayoutAfterRenderAsync()
|
||
{
|
||
if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
|
||
{
|
||
gridRef.LoadLayout(bandLayout.GridLayout);
|
||
gridLayoutApplied = true;
|
||
await InvokeAsync(StateHasChanged);
|
||
}
|
||
}
|
||
|
||
// --- Layout speichern / zurücksetzen ---
|
||
protected async Task SaveLayoutAsync()
|
||
{
|
||
if (string.IsNullOrWhiteSpace(layoutUser)) return;
|
||
CaptureColumnLayoutFromGrid();
|
||
await BandLayoutService.SaveBandLayoutAsync(LayoutType, LayoutKey, layoutUser, bandLayout);
|
||
}
|
||
|
||
protected async Task ResetLayoutAsync()
|
||
{
|
||
if (string.IsNullOrWhiteSpace(layoutUser)) return;
|
||
await BandLayoutService.ResetBandLayoutAsync(LayoutType, LayoutKey, layoutUser);
|
||
bandLayout = new BandLayout();
|
||
columnBandAssignments.Clear();
|
||
UpdateBandOptions();
|
||
foreach (var column in ColumnDefinitions)
|
||
column.Width = null;
|
||
columnLookup = ColumnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
||
_sizeMode = SizeMode.Medium;
|
||
gridRef?.LoadLayout(new GridPersistentLayout());
|
||
gridLayoutApplied = false;
|
||
}
|
||
|
||
private void CaptureColumnLayoutFromGrid()
|
||
{
|
||
if (gridRef == null) return;
|
||
var layout = gridRef.SaveLayout();
|
||
bandLayout.GridLayout = layout;
|
||
bandLayout.SizeMode = _sizeMode;
|
||
var orderedColumns = layout.Columns
|
||
.Where(c => !string.IsNullOrWhiteSpace(c.FieldName))
|
||
.OrderBy(c => c.VisibleIndex)
|
||
.ToList();
|
||
bandLayout.ColumnOrder = orderedColumns.Select(c => c.FieldName).ToList();
|
||
bandLayout.ColumnWidths = orderedColumns
|
||
.Where(c => !string.IsNullOrWhiteSpace(c.Width))
|
||
.ToDictionary(c => c.FieldName, c => c.Width, StringComparer.OrdinalIgnoreCase);
|
||
}
|
||
|
||
private void ApplyColumnLayoutFromStorage()
|
||
{
|
||
foreach (var column in ColumnDefinitions)
|
||
{
|
||
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
|
||
column.Width = width;
|
||
}
|
||
columnLookup = ColumnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
||
}
|
||
|
||
// --- Band-Methoden ---
|
||
protected bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
|
||
|
||
protected virtual bool ShowCommandColumn => true;
|
||
|
||
protected void AddBand()
|
||
{
|
||
bandLayout.Bands.Add(new BandDefinition { Id = Guid.NewGuid().ToString("N"), Caption = "Band" });
|
||
UpdateBandOptions();
|
||
}
|
||
|
||
protected void RemoveBand(BandDefinition band)
|
||
{
|
||
bandLayout.Bands.Remove(band);
|
||
foreach (var key in columnBandAssignments.Where(p => p.Value == band.Id).Select(p => p.Key).ToList())
|
||
columnBandAssignments.Remove(key);
|
||
UpdateBandOptions();
|
||
SyncBandsFromAssignments();
|
||
}
|
||
|
||
protected void UpdateBandCaption(BandDefinition band, string value)
|
||
{
|
||
band.Caption = value;
|
||
UpdateBandOptions();
|
||
}
|
||
|
||
protected void UpdateColumnBand(string fieldName, string? bandId)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(bandId))
|
||
columnBandAssignments.Remove(fieldName);
|
||
else
|
||
columnBandAssignments[fieldName] = bandId;
|
||
SyncBandsFromAssignments();
|
||
}
|
||
|
||
protected string GetColumnBand(string fieldName)
|
||
=> columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
|
||
|
||
protected void SyncBandsFromAssignments()
|
||
{
|
||
foreach (var band in bandLayout.Bands)
|
||
{
|
||
band.Columns = ColumnDefinitions
|
||
.Where(c => columnBandAssignments.TryGetValue(c.FieldName, out var id) && id == band.Id)
|
||
.Select(c => c.FieldName)
|
||
.ToList();
|
||
}
|
||
_ = InvokeAsync(StateHasChanged);
|
||
}
|
||
|
||
protected void UpdateBandOptions()
|
||
{
|
||
bandOptions = [new() { Id = string.Empty, Caption = "Ohne Band" }];
|
||
bandOptions.AddRange(bandLayout.Bands.Select(b => new BandOption { Id = b.Id, Caption = b.Caption }));
|
||
}
|
||
|
||
// --- SizeMode ---
|
||
protected string FormatSizeText(SizeMode size) => size switch
|
||
{
|
||
SizeMode.Small => "Klein",
|
||
SizeMode.Medium => "Mittel",
|
||
SizeMode.Large => "Groß",
|
||
_ => size.ToString()
|
||
};
|
||
|
||
protected void OnSizeChange(DropDownButtonItemClickEventArgs args)
|
||
{
|
||
_sizeMode = Enum.Parse<SizeMode>(args.ItemInfo.Id);
|
||
}
|
||
|
||
// --- RenderColumns / BuildDataColumn ---
|
||
protected RenderFragment RenderColumns() => builder =>
|
||
{
|
||
var seq = 0;
|
||
if (ShowCommandColumn)
|
||
{
|
||
builder.OpenComponent<DxGridCommandColumn>(seq++);
|
||
builder.AddAttribute(seq++, "Width", "120px");
|
||
builder.CloseComponent();
|
||
}
|
||
|
||
var grouped = bandLayout.Bands.SelectMany(b => b.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||
foreach (var column in ColumnDefinitions.Where(c => !grouped.Contains(c.FieldName)))
|
||
BuildDataColumn(builder, ref seq, column);
|
||
|
||
foreach (var band in bandLayout.Bands)
|
||
{
|
||
if (band.Columns.Count == 0) continue;
|
||
builder.OpenComponent<DxGridBandColumn>(seq++);
|
||
builder.AddAttribute(seq++, "Caption", band.Caption);
|
||
builder.AddAttribute(seq++, "Columns", (RenderFragment)(bandBuilder =>
|
||
{
|
||
var bandSeq = 0;
|
||
foreach (var columnName in band.Columns)
|
||
{
|
||
if (columnLookup.TryGetValue(columnName, out var column))
|
||
BuildDataColumn(bandBuilder, ref bandSeq, column);
|
||
}
|
||
}));
|
||
builder.CloseComponent();
|
||
}
|
||
};
|
||
|
||
protected void BuildDataColumn(RenderTreeBuilder builder, ref int seq, ColumnDefinition column)
|
||
{
|
||
builder.OpenComponent<DxGridDataColumn>(seq++);
|
||
builder.AddAttribute(seq++, "FieldName", column.FieldName);
|
||
builder.AddAttribute(seq++, "Caption", column.Caption);
|
||
if (!string.IsNullOrWhiteSpace(column.Width))
|
||
builder.AddAttribute(seq++, "Width", column.Width);
|
||
if (!string.IsNullOrWhiteSpace(column.DisplayFormat))
|
||
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<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) =>
|
||
ctx => b =>
|
||
{
|
||
_filterContexts[fieldName] = ctx;
|
||
SyncDateFilterFromContext(fieldName, ctx.FilterCriteria);
|
||
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 SyncDateFilterFromContext(string fieldName, CriteriaOperator? criteria)
|
||
{
|
||
if (criteria == null)
|
||
{
|
||
_filterFrom[fieldName] = null;
|
||
_filterTo[fieldName] = null;
|
||
return;
|
||
}
|
||
|
||
DateTime? from = null;
|
||
DateTime? to = null;
|
||
|
||
if (criteria is GroupOperator group)
|
||
{
|
||
foreach (var op in group.Operands.OfType<BinaryOperator>())
|
||
ParseDateOperand(op, ref from, ref to);
|
||
}
|
||
else if (criteria is BinaryOperator binary)
|
||
{
|
||
ParseDateOperand(binary, ref from, ref to);
|
||
}
|
||
|
||
_filterFrom[fieldName] = from;
|
||
_filterTo[fieldName] = to;
|
||
}
|
||
|
||
private static void ParseDateOperand(BinaryOperator op, ref DateTime? from, ref DateTime? to)
|
||
{
|
||
if (op.RightOperand is not OperandValue val || val.Value is not DateTime dt) return;
|
||
if (op.OperatorType == BinaryOperatorType.GreaterOrEqual) from = dt;
|
||
else if (op.OperatorType == BinaryOperatorType.Less) to = dt.AddDays(-1);
|
||
}
|
||
|
||
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);
|
||
if (_filterContexts.TryGetValue(fieldName, out var ctx))
|
||
ctx.FilterCriteria = criteria;
|
||
_ = InvokeAsync(StateHasChanged);
|
||
}
|
||
|
||
protected void SetEditContext(EditContext context)
|
||
{
|
||
if (editContext == context) return;
|
||
if (editContext != null)
|
||
editContext.OnFieldChanged -= OnEditFieldChanged;
|
||
editContext = context;
|
||
validationMessageStore = new ValidationMessageStore(editContext);
|
||
editContext.OnFieldChanged += OnEditFieldChanged;
|
||
}
|
||
|
||
protected virtual void OnEditFieldChanged(object? sender, FieldChangedEventArgs e)
|
||
{
|
||
validationMessageStore?.Clear();
|
||
editContext?.NotifyValidationStateChanged();
|
||
}
|
||
|
||
protected void SetPopupHeaderText(bool isNew) => popupHeaderText = isNew ? "Neu" : "Edit";
|
||
|
||
protected async Task EditFocusedRow()
|
||
=> await gridRef!.StartEditRowAsync(_focusedVisibleIndex);
|
||
|
||
protected Task DeleteFocusedRow()
|
||
{
|
||
gridRef!.ShowRowDeleteConfirmation(_focusedVisibleIndex);
|
||
return Task.CompletedTask;
|
||
}
|
||
|
||
protected async Task SaveLayoutWithFeedbackAsync()
|
||
{
|
||
try
|
||
{
|
||
await SaveLayoutAsync();
|
||
infoMessage = "Layout gespeichert.";
|
||
errorMessage = null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}";
|
||
}
|
||
}
|
||
|
||
protected async Task ResetLayoutWithFeedbackAsync()
|
||
{
|
||
await ResetLayoutAsync();
|
||
infoMessage = "Layout zurückgesetzt.";
|
||
errorMessage = null;
|
||
}
|
||
|
||
} |