Files
DbFirst/DbFirst.BlazorWebApp/Components/CatalogsGrid.razor
OlgunR 8824492057 Enable grouping features in DxGrid components
Added ShowGroupPanel, ShowGroupedColumns, and AllowGroup to DxGrid in CatalogsGrid.razor and MassDataGrid.razor for enhanced data grouping. Also set PagerVisible="false" in MassDataGrid.razor to hide pager controls.
2026-02-09 14:03:47 +01:00

735 lines
23 KiB
Plaintext

@using System.Text.Json
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Rendering
@using Microsoft.AspNetCore.Components.Forms
@using DevExpress.Blazor
@inject CatalogApiClient Api
@inject LayoutApiClient LayoutApi
@inject IJSRuntime JsRuntime
<style>
.action-panel { margin-bottom: 16px; }
.grid-section { margin-top: 12px; }
.catalog-edit-popup {
min-width: 720px;
}
.band-editor {
display: grid;
gap: 12px;
margin-bottom: 16px;
}
.band-controls {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.band-row {
display: flex;
gap: 8px;
align-items: center;
}
.band-columns {
max-width: 720px;
}
</style>
@if (!string.IsNullOrWhiteSpace(errorMessage))
{
<div class="alert alert-danger" role="alert">@errorMessage</div>
}
else if (!string.IsNullOrWhiteSpace(infoMessage))
{
<div class="alert alert-success" role="alert">@infoMessage</div>
}
@if (isLoading)
{
<p><em>Lade Daten...</em></p>
}
else if (items.Count == 0)
{
<p>Keine Einträge vorhanden.</p>
}
else
{
<div class="band-editor">
<div class="band-controls">
<DxButton Text="Band hinzufügen" Click="AddBand" />
<DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" />
<DxButton Text="Band-Layout zurücksetzen" Click="ResetBandLayoutAsync" />
</div>
@foreach (var band in bandLayout.Bands)
{
<div class="band-row">
<DxTextBox Text="@band.Caption" TextChanged="@(value => UpdateBandCaption(band, value))" />
<DxButton Text="Entfernen" Click="@(() => RemoveBand(band))" />
</div>
}
<DxFormLayout CssClass="band-columns" ColCount="2">
@foreach (var column in columnDefinitions)
{
<DxFormLayoutItem Caption="@column.Caption">
<DxComboBox Data="@bandOptions"
TData="BandOption"
TValue="string"
TextFieldName="Caption"
ValueFieldName="Id"
Value="@GetColumnBand(column.FieldName)"
ValueChanged="@(value => UpdateColumnBand(column.FieldName, value))"
Width="100%" />
</DxFormLayoutItem>
}
</DxFormLayout>
</div>
<div class="grid-section">
<DxGrid Data="@items"
TItem="CatalogReadDto"
KeyFieldName="@nameof(CatalogReadDto.Guid)"
ShowGroupPanel="true"
ShowGroupedColumns="true"
AllowGroup="true"
ShowFilterRow="true"
AllowColumnResize="true"
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
AllowColumnReorder="true"
PageSize="10"
CssClass="mb-4 catalog-grid"
EditMode="GridEditMode.PopupEditForm"
PopupEditFormCssClass="catalog-edit-popup"
PopupEditFormHeaderText="@popupHeaderText"
CustomizeEditModel="OnCustomizeEditModel"
EditModelSaving="OnEditModelSaving"
DataItemDeleting="OnDataItemDeleting"
@ref="gridRef">
<Columns>
@RenderColumns()
</Columns>
<EditFormTemplate Context="editFormContext">
@{ SetEditContext(editFormContext.EditContext); var editModel = (CatalogEditModel)editFormContext.EditModel; SetPopupHeaderText(editModel.IsNew); }
<DxFormLayout ColCount="2">
<DxFormLayoutItem Caption="Titel">
<DxTextBox @bind-Text="editModel.CatTitle" Width="100%" />
</DxFormLayoutItem>
<DxFormLayoutItem Caption="Kennung">
<DxTextBox @bind-Text="editModel.CatString" Width="100%" />
</DxFormLayoutItem>
@if (!editModel.IsNew)
{
<DxFormLayoutItem Caption="Update-Prozedur">
<DxComboBox Data="@procedureOptions"
TData="ProcedureOption"
TValue="int"
TextFieldName="Text"
ValueFieldName="Value"
@bind-Value="editModel.UpdateProcedure"
Width="100%" />
</DxFormLayoutItem>
}
<DxFormLayoutItem ColSpanMd="12">
<ValidationSummary />
</DxFormLayoutItem>
</DxFormLayout>
</EditFormTemplate>
</DxGrid>
</div>
}
@code {
private List<CatalogReadDto> items = new();
private bool isLoading;
private string? errorMessage;
private string? infoMessage;
private EditContext? editContext;
private ValidationMessageStore? validationMessageStore;
private IGrid? gridRef;
private string popupHeaderText = "Edit";
private const string LayoutType = "GRID_BANDS";
private const string LayoutKey = "CatalogsGrid";
private const string LayoutUserStorageKey = "layoutUser";
private string? layoutUser;
private BandLayout bandLayout = new();
private Dictionary<string, string> columnBandAssignments = new();
private List<BandOption> bandOptions = new();
private Dictionary<string, ColumnDefinition> columnLookup = new();
private readonly JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web);
private List<ColumnDefinition> columnDefinitions = new()
{
new() { FieldName = nameof(CatalogReadDto.Guid), Caption = "Id", Width = "140px", FilterType = ColumnFilterType.Text },
new() { FieldName = nameof(CatalogReadDto.CatTitle), Caption = "Titel", FilterType = ColumnFilterType.Text },
new() { FieldName = nameof(CatalogReadDto.CatString), Caption = "String", FilterType = ColumnFilterType.Text },
new() { FieldName = nameof(CatalogReadDto.AddedWho), Caption = "Angelegt von", ReadOnly = true, FilterType = ColumnFilterType.Text },
new() { FieldName = nameof(CatalogReadDto.AddedWhen), Caption = "Angelegt am", ReadOnly = true, FilterType = ColumnFilterType.Date },
new() { FieldName = nameof(CatalogReadDto.ChangedWho), Caption = "Geändert von", ReadOnly = true, FilterType = ColumnFilterType.Text },
new() { FieldName = nameof(CatalogReadDto.ChangedWhen), Caption = "Geändert am", ReadOnly = true, FilterType = ColumnFilterType.Date }
};
private readonly List<ProcedureOption> procedureOptions = new()
{
new() { Value = 0, Text = "PRTBMY_CATALOG_UPDATE" },
new() { Value = 1, Text = "PRTBMY_CATALOG_SAVE" }
};
private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
protected override async Task OnInitializedAsync()
{
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
await EnsureLayoutUserAsync();
await LoadBandLayoutAsync();
await LoadCatalogs();
}
private void SetEditContext(EditContext context)
{
if (editContext == context)
{
return;
}
if (editContext != null)
{
editContext.OnFieldChanged -= OnEditFieldChanged;
}
editContext = context;
validationMessageStore = new ValidationMessageStore(editContext);
editContext.OnFieldChanged += OnEditFieldChanged;
}
private void OnEditFieldChanged(object? sender, FieldChangedEventArgs e)
{
if (validationMessageStore == null || editContext == null)
{
return;
}
if (e.FieldIdentifier.FieldName == nameof(CatalogEditModel.UpdateProcedure))
{
validationMessageStore.Clear();
editContext.NotifyValidationStateChanged();
return;
}
if (e.FieldIdentifier.FieldName == nameof(CatalogEditModel.CatTitle))
{
var field = new FieldIdentifier(editContext.Model, nameof(CatalogEditModel.CatTitle));
validationMessageStore.Clear(field);
editContext.NotifyValidationStateChanged();
}
}
private void SetPopupHeaderText(bool isNew)
{
popupHeaderText = isNew ? "Neu" : "Edit";
}
private void OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
{
popupHeaderText = e.IsNew ? "Neu" : "Edit";
if (e.IsNew)
{
e.EditModel = new CatalogEditModel { IsNew = true };
return;
}
var item = (CatalogReadDto)e.DataItem;
e.EditModel = new CatalogEditModel
{
Guid = item.Guid,
CatTitle = item.CatTitle,
CatString = item.CatString,
UpdateProcedure = 0,
OriginalCatTitle = item.CatTitle,
IsNew = false
};
}
private async Task LoadCatalogs()
{
isLoading = true;
errorMessage = null;
try
{
items = await Api.GetAllAsync();
}
catch (Exception ex)
{
errorMessage = $"Kataloge konnten nicht geladen werden: {ex.Message}";
}
finally
{
isLoading = false;
StateHasChanged();
}
}
private async Task OnEditModelSaving(GridEditModelSavingEventArgs e)
{
errorMessage = null;
infoMessage = null;
validationMessageStore?.Clear();
editContext?.NotifyValidationStateChanged();
var editModel = (CatalogEditModel)e.EditModel;
if (!ValidateEditModel(editModel, e.IsNew))
{
e.Cancel = true;
return;
}
var dto = new CatalogWriteDto
{
CatTitle = editModel.CatTitle,
CatString = editModel.CatString,
UpdateProcedure = editModel.UpdateProcedure
};
try
{
if (e.IsNew)
{
var created = await Api.CreateAsync(dto);
if (!created.Success || created.Value == null)
{
if (!string.IsNullOrWhiteSpace(created.Error))
{
AddValidationError(editModel, nameof(CatalogEditModel.CatTitle), created.Error);
}
else
{
errorMessage = "Anlegen fehlgeschlagen.";
}
e.Cancel = true;
return;
}
infoMessage = "Katalog angelegt.";
}
else
{
var updated = await Api.UpdateAsync(editModel.Guid, dto);
if (!updated.Success)
{
errorMessage = updated.Error ?? "Aktualisierung fehlgeschlagen.";
e.Cancel = true;
return;
}
infoMessage = "Katalog aktualisiert.";
}
await LoadCatalogs();
}
catch (Exception ex)
{
errorMessage = $"Fehler beim Speichern: {ex.Message}";
e.Cancel = true;
}
}
private void AddValidationError(CatalogEditModel editModel, string fieldName, string message)
{
if (editContext == null || validationMessageStore == null)
{
return;
}
var field = new FieldIdentifier(editModel, fieldName);
validationMessageStore.Add(field, message);
editContext.NotifyValidationStateChanged();
}
private bool ValidateEditModel(CatalogEditModel editModel, bool isNew)
{
if (isNew)
{
return true;
}
if (editModel.UpdateProcedure == 0 &&
!string.Equals(editModel.CatTitle, editModel.OriginalCatTitle, StringComparison.OrdinalIgnoreCase))
{
AddValidationError(editModel, nameof(CatalogEditModel.CatTitle), "Titel kann nicht geändert werden.");
return false;
}
return true;
}
private async Task OnDataItemDeleting(GridDataItemDeletingEventArgs e)
{
errorMessage = null;
infoMessage = null;
var item = (CatalogReadDto)e.DataItem;
try
{
var deleted = await Api.DeleteAsync(item.Guid);
if (!deleted.Success)
{
errorMessage = deleted.Error ?? "Löschen fehlgeschlagen.";
e.Cancel = true;
return;
}
infoMessage = "Katalog gelöscht.";
await LoadCatalogs();
}
catch (Exception ex)
{
errorMessage = $"Fehler beim Löschen: {ex.Message}";
e.Cancel = true;
}
}
private async Task EnsureLayoutUserAsync()
{
layoutUser = await JsRuntime.InvokeAsync<string?>("localStorage.getItem", LayoutUserStorageKey);
if (string.IsNullOrWhiteSpace(layoutUser))
{
layoutUser = Guid.NewGuid().ToString("N");
await JsRuntime.InvokeVoidAsync("localStorage.setItem", LayoutUserStorageKey, layoutUser);
}
}
private async Task SaveLayoutAsync()
{
if (string.IsNullOrWhiteSpace(layoutUser))
{
return;
}
try
{
CaptureColumnLayoutFromGrid();
var layoutData = JsonSerializer.Serialize(bandLayout, jsonOptions);
await LayoutApi.UpsertAsync(new LayoutDto
{
LayoutType = LayoutType,
LayoutKey = LayoutKey,
UserName = layoutUser,
LayoutData = layoutData
});
infoMessage = "Layout gespeichert.";
errorMessage = null;
}
catch (Exception ex)
{
errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}";
}
}
private void CaptureColumnLayoutFromGrid()
{
if (gridRef == null)
{
return;
}
var layout = gridRef.SaveLayout();
bandLayout.GridLayout = layout;
var orderedColumns = layout.Columns
.Where(column => !string.IsNullOrWhiteSpace(column.FieldName))
.OrderBy(column => column.VisibleIndex)
.ToList();
bandLayout.ColumnOrder = orderedColumns
.Select(column => column.FieldName)
.ToList();
bandLayout.ColumnWidths = orderedColumns
.Where(column => !string.IsNullOrWhiteSpace(column.Width))
.ToDictionary(column => column.FieldName, column => column.Width, StringComparer.OrdinalIgnoreCase);
}
private async Task LoadBandLayoutAsync()
{
if (string.IsNullOrWhiteSpace(layoutUser))
{
bandLayout = new BandLayout();
UpdateBandOptions();
return;
}
var stored = await LayoutApi.GetAsync(LayoutType, LayoutKey, layoutUser);
if (stored != null && !string.IsNullOrWhiteSpace(stored.LayoutData))
{
var parsed = JsonSerializer.Deserialize<BandLayout>(stored.LayoutData, jsonOptions);
bandLayout = NormalizeBandLayout(parsed);
}
else
{
bandLayout = new BandLayout();
}
columnBandAssignments = BuildAssignmentsFromLayout(bandLayout);
ApplyColumnLayoutFromStorage();
UpdateBandOptions();
}
private void AddBand()
{
bandLayout.Bands.Add(new BandDefinition
{
Id = Guid.NewGuid().ToString("N"),
Caption = "Band"
});
UpdateBandOptions();
}
private void RemoveBand(BandDefinition band)
{
bandLayout.Bands.Remove(band);
var removedColumns = columnBandAssignments.Where(pair => pair.Value == band.Id)
.Select(pair => pair.Key)
.ToList();
foreach (var column in removedColumns)
{
columnBandAssignments.Remove(column);
}
UpdateBandOptions();
SyncBandsFromAssignments();
}
private void UpdateBandCaption(BandDefinition band, string value)
{
band.Caption = value;
UpdateBandOptions();
}
private void UpdateColumnBand(string fieldName, string? bandId)
{
if (string.IsNullOrWhiteSpace(bandId))
{
columnBandAssignments.Remove(fieldName);
}
else
{
columnBandAssignments[fieldName] = bandId;
}
SyncBandsFromAssignments();
}
private string GetColumnBand(string fieldName)
{
return columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
}
private void SyncBandsFromAssignments()
{
foreach (var band in bandLayout.Bands)
{
band.Columns = columnDefinitions
.Where(column => columnBandAssignments.TryGetValue(column.FieldName, out var bandId) && bandId == band.Id)
.Select(column => column.FieldName)
.ToList();
}
StateHasChanged();
}
private void UpdateBandOptions()
{
bandOptions = new List<BandOption> { new() { Id = string.Empty, Caption = "Ohne Band" } };
bandOptions.AddRange(bandLayout.Bands.Select(band => new BandOption { Id = band.Id, Caption = band.Caption }));
}
private BandLayout NormalizeBandLayout(BandLayout? layout)
{
layout ??= new BandLayout();
layout.Bands ??= new List<BandDefinition>();
layout.ColumnOrder ??= new List<string>();
layout.ColumnWidths ??= new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
foreach (var band in layout.Bands)
{
if (string.IsNullOrWhiteSpace(band.Id))
{
band.Id = Guid.NewGuid().ToString("N");
}
if (string.IsNullOrWhiteSpace(band.Caption))
{
band.Caption = "Band";
}
band.Columns = band.Columns?.Where(columnLookup.ContainsKey).ToList() ?? new List<string>();
}
return layout;
}
private Dictionary<string, string> BuildAssignmentsFromLayout(BandLayout layout)
{
var assignments = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var band in layout.Bands)
{
foreach (var column in band.Columns)
{
assignments[column] = band.Id;
}
}
return assignments;
}
private RenderFragment RenderColumns() => builder =>
{
var seq = 0;
builder.OpenComponent<DxGridCommandColumn>(seq++);
builder.AddAttribute(seq++, "Width", "120px");
builder.CloseComponent();
var grouped = bandLayout.Bands.SelectMany(band => band.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase);
foreach (var column in columnDefinitions.Where(column => !grouped.Contains(column.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();
}
};
private 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);
}
builder.CloseComponent();
}
private sealed class BandLayout
{
public List<BandDefinition> Bands { get; set; } = new();
public List<string> ColumnOrder { get; set; } = new();
public Dictionary<string, string?> ColumnWidths { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public GridPersistentLayout? GridLayout { get; set; }
}
private sealed class BandDefinition
{
public string Id { get; set; } = string.Empty;
public string Caption { get; set; } = string.Empty;
public List<string> Columns { get; set; } = new();
}
private sealed class BandOption
{
public string Id { get; set; } = string.Empty;
public string Caption { get; set; } = string.Empty;
}
private sealed class ColumnDefinition
{
public string FieldName { get; init; } = string.Empty;
public string Caption { get; init; } = string.Empty;
public string? Width { get; set; }
public string? DisplayFormat { get; init; }
public bool ReadOnly { get; init; }
public ColumnFilterType FilterType { get; init; }
}
private enum ColumnFilterType
{
Text,
Date
}
private sealed class CatalogEditModel
{
public int Guid { get; set; }
public string CatTitle { get; set; } = string.Empty;
public string CatString { get; set; } = string.Empty;
public int UpdateProcedure { get; set; }
public string OriginalCatTitle { get; set; } = string.Empty;
public bool IsNew { get; set; }
}
private sealed class ProcedureOption
{
public int Value { get; set; }
public string Text { get; set; } = string.Empty;
}
private async Task ResetBandLayoutAsync()
{
if (string.IsNullOrWhiteSpace(layoutUser))
{
return;
}
await LayoutApi.DeleteAsync(LayoutType, LayoutKey, layoutUser);
bandLayout = new BandLayout();
columnBandAssignments.Clear();
UpdateBandOptions();
infoMessage = "Band-Layout zurückgesetzt.";
}
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(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
}
private bool gridLayoutApplied;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
{
gridRef.LoadLayout(bandLayout.GridLayout);
gridLayoutApplied = true;
await InvokeAsync(StateHasChanged);
}
}
}