Refactored several methods in BandGridBase<TItem> to async Task and updated their invocations to use await. EventCallbacks for date filter changes now use async lambdas. Awaited InvokeAsync(StateHasChanged) to ensure UI updates after async operations. These changes improve UI state consistency and reliability in Blazor.
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 async Task 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();
|
||
await SyncBandsFromAssignments();
|
||
}
|
||
|
||
protected void UpdateBandCaption(BandDefinition band, string value)
|
||
{
|
||
band.Caption = value;
|
||
UpdateBandOptions();
|
||
}
|
||
|
||
protected async Task UpdateColumnBand(string fieldName, string? bandId)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(bandId))
|
||
columnBandAssignments.Remove(fieldName);
|
||
else
|
||
columnBandAssignments[fieldName] = bandId;
|
||
await SyncBandsFromAssignments();
|
||
}
|
||
|
||
protected string GetColumnBand(string fieldName)
|
||
=> columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
|
||
|
||
protected async Task 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();
|
||
}
|
||
await 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, async (DateTime? v) => await OnFilterFromChanged(fieldName, v));
|
||
_toCallbacks[fieldName] = EventCallback.Factory.Create<DateTime?>(this, async (DateTime? v) => await 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 async Task OnFilterFromChanged(string fieldName, DateTime? value)
|
||
{
|
||
_filterFrom[fieldName] = value;
|
||
await ApplyDateFilter(fieldName);
|
||
}
|
||
|
||
private async Task OnFilterToChanged(string fieldName, DateTime? value)
|
||
{
|
||
_filterTo[fieldName] = value;
|
||
await ApplyDateFilter(fieldName);
|
||
}
|
||
|
||
private async Task 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;
|
||
await 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;
|
||
}
|
||
|
||
} |