Files
DbFirst/DbFirst.BlazorWebApp/Components/BandGridBase.cs
OlgunR 1112fa215c Sync date filter UI with filter criteria in BandGridBase
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.
2026-05-11 10:27:13 +02:00

414 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}