Refactor grids to use DxGrid popup editing

Modernize CatalogsGrid.razor and MassDataGrid.razor to use DevExpress DxGrid's built-in popup editing with EditFormTemplate. Remove custom EditForm panels and manual editing state logic. Move CRUD operations and validation to grid event handlers. Add field-level validation and error display for catalogs. Update grid columns, add command columns, and set audit fields to read-only. Only editing is allowed in MassDataGrid; deletion is disabled. Streamline code and UI for improved maintainability and user experience.
This commit is contained in:
OlgunR
2026-02-05 10:43:40 +01:00
parent 4ef80ce875
commit 945c8aaf4a
4 changed files with 455 additions and 403 deletions

View File

@@ -1,8 +1,12 @@
@using Microsoft.AspNetCore.Components.Forms
@inject CatalogApiClient Api
<style>
.action-panel { margin-bottom: 16px; }
.grid-section { margin-top: 12px; }
.catalog-edit-popup {
min-width: 720px;
}
</style>
@if (!string.IsNullOrWhiteSpace(errorMessage))
@@ -14,41 +18,6 @@ else if (!string.IsNullOrWhiteSpace(infoMessage))
<div class="alert alert-success" role="alert">@infoMessage</div>
}
<div class="mb-3">
<DxButton RenderStyle="ButtonRenderStyle.Primary" Click="@StartCreate">Neuen Eintrag anlegen</DxButton>
</div>
@if (showForm)
{
<div class="action-panel">
<EditForm Model="formModel" OnValidSubmit="HandleSubmit" Context="editCtx">
<DxFormLayout ColCount="2">
<DxFormLayoutItem Caption="Titel" Context="itemCtx">
<DxTextBox @bind-Text="formModel.CatTitle" Enabled="@(isEditing ? formModel.UpdateProcedure != 0 : true)" />
</DxFormLayoutItem>
<DxFormLayoutItem Caption="Kennung" Context="itemCtx">
<DxTextBox @bind-Text="formModel.CatString" />
</DxFormLayoutItem>
@if (isEditing)
{
<DxFormLayoutItem Caption="Update-Prozedur" Context="itemCtx">
<DxComboBox Data="@procedureOptions"
TextFieldName="Text"
ValueFieldName="Value"
@bind-Value="formModel.UpdateProcedure" />
</DxFormLayoutItem>
}
<DxFormLayoutItem Caption=" " Context="itemCtx">
<DxStack Orientation="Orientation.Horizontal" Spacing="8">
<DxButton RenderStyle="ButtonRenderStyle.Success" ButtonType="ButtonType.Submit" SubmitFormOnClick="true" Context="btnCtx">@((isEditing ? "Speichern" : "Anlegen"))</DxButton>
<DxButton RenderStyle="ButtonRenderStyle.Secondary" Click="@CancelEdit" Context="btnCtx">Abbrechen</DxButton>
</DxStack>
</DxFormLayoutItem>
</DxFormLayout>
</EditForm>
</div>
}
@if (isLoading)
{
<p><em>Lade Daten...</em></p>
@@ -60,38 +29,61 @@ else if (items.Count == 0)
else
{
<div class="grid-section">
<DxGrid Data="@items" TItem="CatalogReadDto" KeyFieldName="@nameof(CatalogReadDto.Guid)" ShowFilterRow="true" PageSize="10" CssClass="mb-4 catalog-grid">
<DxGrid Data="@items"
TItem="CatalogReadDto"
KeyFieldName="@nameof(CatalogReadDto.Guid)"
ShowFilterRow="true"
PageSize="10"
CssClass="mb-4 catalog-grid"
EditMode="GridEditMode.PopupEditForm"
PopupEditFormCssClass="catalog-edit-popup"
CustomizeEditModel="OnCustomizeEditModel"
EditModelSaving="OnEditModelSaving"
DataItemDeleting="OnDataItemDeleting">
<Columns>
<DxGridCommandColumn Width="120px" />
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.Guid)" Caption="Id" Width="140px" SortIndex="0" SortOrder="GridColumnSortOrder.Ascending" />
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.CatTitle)" Caption="Titel" />
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.CatString)" Caption="String" />
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWho)" Caption="Angelegt von" />
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWhen)" Caption="Angelegt am" />
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWho)" Caption="Geändert von" />
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWhen)" Caption="Geändert am" />
<DxGridDataColumn Caption="" Width="220px" AllowSort="false">
<CellDisplayTemplate Context="cell">
@{ var item = (CatalogReadDto)cell.DataItem; }
<div style="white-space: nowrap;">
<DxButton RenderStyle="ButtonRenderStyle.Secondary" Size="ButtonSize.Small" Click="@(() => StartEdit(item))">Bearbeiten</DxButton>
<DxButton RenderStyle="ButtonRenderStyle.Danger" Size="ButtonSize.Small" Click="@(() => DeleteCatalog(item.Guid))">Löschen</DxButton>
</div>
</CellDisplayTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWho)" Caption="Angelegt von" ReadOnly="true" />
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWhen)" Caption="Angelegt am" ReadOnly="true" />
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWho)" Caption="Geändert von" ReadOnly="true" />
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWhen)" Caption="Geändert am" ReadOnly="true" />
</Columns>
<EditFormTemplate Context="editFormContext">
@{ SetEditContext(editFormContext.EditContext); var editModel = (CatalogEditModel)editFormContext.EditModel; }
<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>
<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 CatalogWriteDto formModel = new();
private int editingId;
private bool isLoading;
private bool isEditing;
private bool showForm;
private string? errorMessage;
private string? infoMessage;
private EditContext? editContext;
private ValidationMessageStore? validationMessageStore;
private readonly List<ProcedureOption> procedureOptions = new()
{
@@ -104,6 +96,57 @@ else
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) ||
e.FieldIdentifier.FieldName == nameof(CatalogEditModel.CatTitle))
{
validationMessageStore.Clear();
editContext.NotifyValidationStateChanged();
}
}
private void OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
{
if (e.IsNew)
{
e.EditModel = new CatalogEditModel();
return;
}
var item = (CatalogReadDto)e.DataItem;
e.EditModel = new CatalogEditModel
{
Guid = item.Guid,
CatTitle = item.CatTitle,
CatString = item.CatString,
UpdateProcedure = 0,
OriginalCatTitle = item.CatTitle
};
}
private async Task LoadCatalogs()
{
isLoading = true;
@@ -123,88 +166,115 @@ else
}
}
private void StartCreate()
private async Task OnEditModelSaving(GridEditModelSavingEventArgs e)
{
formModel = new CatalogWriteDto();
editingId = 0;
isEditing = false;
showForm = true;
infoMessage = null;
errorMessage = null;
}
infoMessage = null;
private void StartEdit(CatalogReadDto item)
{
formModel = new CatalogWriteDto
validationMessageStore?.Clear();
editContext?.NotifyValidationStateChanged();
var editModel = (CatalogEditModel)e.EditModel;
if (!ValidateEditModel(editModel, e.IsNew))
{
CatTitle = item.CatTitle,
CatString = item.CatString,
UpdateProcedure = 0
};
editingId = item.Guid;
isEditing = true;
showForm = true;
infoMessage = null;
errorMessage = null;
}
e.Cancel = true;
return;
}
private async Task HandleSubmit()
{
errorMessage = null;
infoMessage = null;
var dto = new CatalogWriteDto
{
CatTitle = editModel.CatTitle,
CatString = editModel.CatString,
UpdateProcedure = editModel.UpdateProcedure
};
try
{
if (isEditing)
if (e.IsNew)
{
var updated = await Api.UpdateAsync(editingId, formModel);
if (!updated.Success)
{
errorMessage = updated.Error ?? "Aktualisierung fehlgeschlagen.";
return;
}
infoMessage = "Katalog aktualisiert.";
}
else
{
var created = await Api.CreateAsync(formModel);
var created = await Api.CreateAsync(dto);
if (!created.Success || created.Value == null)
{
errorMessage = created.Error ?? "Anlegen fehlgeschlagen.";
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.";
}
showForm = false;
await LoadCatalogs();
}
catch (Exception ex)
{
errorMessage = $"Fehler beim Speichern: {ex.Message}";
e.Cancel = true;
}
}
private void CancelEdit()
private void AddValidationError(CatalogEditModel editModel, string fieldName, string message)
{
showForm = false;
infoMessage = null;
errorMessage = null;
if (editContext == null || validationMessageStore == null)
{
return;
}
var field = new FieldIdentifier(editModel, fieldName);
validationMessageStore.Add(field, message);
editContext.NotifyValidationStateChanged();
}
private async Task DeleteCatalog(int id)
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(id);
var deleted = await Api.DeleteAsync(item.Guid);
if (!deleted.Success)
{
errorMessage = deleted.Error ?? "Löschen fehlgeschlagen.";
e.Cancel = true;
return;
}
@@ -214,9 +284,19 @@ else
catch (Exception ex)
{
errorMessage = $"Fehler beim Löschen: {ex.Message}";
e.Cancel = true;
}
}
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;
}
private sealed class ProcedureOption
{
public int Value { get; set; }

View File

@@ -61,38 +61,6 @@ else if (!string.IsNullOrWhiteSpace(infoMessage))
<div class="alert alert-success" role="alert">@infoMessage</div>
}
<div class="mb-3">
<DxButton RenderStyle="ButtonRenderStyle.Primary" Click="@StartCreate">Neuen Eintrag anlegen</DxButton>
</div>
@if (showForm)
{
<div class="action-panel">
<EditForm Model="formModel" OnValidSubmit="HandleSubmit" Context="editCtx">
<DxFormLayout ColCount="2">
<DxFormLayoutItem Caption="CustomerName" Context="itemCtx">
<DxTextBox @bind-Text="formModel.CustomerName" />
</DxFormLayoutItem>
<DxFormLayoutItem Caption="Amount" Context="itemCtx">
<DxTextBox @bind-Text="amountText" />
</DxFormLayoutItem>
<DxFormLayoutItem Caption="Category" Context="itemCtx">
<DxTextBox @bind-Text="formModel.Category" ReadOnly="true" />
</DxFormLayoutItem>
<DxFormLayoutItem Caption="Status" Context="itemCtx">
<DxCheckBox @bind-Checked="formModel.StatusFlag" ReadOnly="true" />
</DxFormLayoutItem>
<DxFormLayoutItem Caption=" " Context="itemCtx">
<DxStack Orientation="Orientation.Horizontal" Spacing="8">
<DxButton RenderStyle="ButtonRenderStyle.Success" ButtonType="ButtonType.Submit" SubmitFormOnClick="true" Context="btnCtx">Speichern</DxButton>
<DxButton RenderStyle="ButtonRenderStyle.Secondary" Click="@CancelEdit" Context="btnCtx">Abbrechen</DxButton>
</DxStack>
</DxFormLayoutItem>
</DxFormLayout>
</EditForm>
</div>
}
@if (isLoading)
{
<p><em>Lade Daten...</em></p>
@@ -104,9 +72,21 @@ else if (items.Count == 0)
else
{
<div class="grid-section">
<DxGrid Data="@items" TItem="MassDataReadDto" KeyFieldName="@nameof(MassDataReadDto.Id)" ShowFilterRow="true" ShowGroupPanel="true" AllowColumnResize="true" PagerVisible="false" PageSize="100" CssClass="mb-3 massdata-grid">
<DxGrid Data="@items"
TItem="MassDataReadDto"
KeyFieldName="@nameof(MassDataReadDto.Id)"
ShowFilterRow="true"
ShowGroupPanel="true"
AllowColumnResize="true"
PagerVisible="false"
PageSize="100"
CssClass="mb-3 massdata-grid"
EditMode="GridEditMode.PopupEditForm"
EditModelSaving="OnEditModelSaving"
DataItemDeleting="OnDataItemDeleting">
<Columns>
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.Id)" Caption="Id" Width="90px">
<DxGridCommandColumn Width="120px" NewButtonVisible="false" />
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.Id)" Caption="Id" Width="90px" ReadOnly="true">
<FilterRowCellTemplate Context="filter">
<DxTextBox Text="@(filter.FilterRowValue?.ToString())"
TextChanged="@(value => filter.FilterRowValue = value)"
@@ -127,44 +107,51 @@ else
CssClass="filter-search-input" />
</FilterRowCellTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.Category)" Caption="Category">
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.Category)" Caption="Category" ReadOnly="true">
<FilterRowCellTemplate Context="filter">
<DxTextBox Text="@(filter.FilterRowValue as string)"
TextChanged="@(value => filter.FilterRowValue = value)"
CssClass="filter-search-input" />
</FilterRowCellTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.StatusFlag)" Caption="Status">
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.StatusFlag)" Caption="Status" ReadOnly="true">
<FilterRowCellTemplate Context="filter">
<DxTextBox Text="@(filter.FilterRowValue?.ToString())"
TextChanged="@(value => filter.FilterRowValue = value)"
CssClass="filter-search-input" />
</FilterRowCellTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.AddedWhen)" Caption="Added">
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.AddedWhen)" Caption="Added" ReadOnly="true">
<FilterRowCellTemplate Context="filter">
<DxTextBox Text="@(filter.FilterRowValue?.ToString())"
TextChanged="@(value => filter.FilterRowValue = value)"
CssClass="filter-search-input" />
</FilterRowCellTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.ChangedWhen)" Caption="Changed">
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.ChangedWhen)" Caption="Changed" ReadOnly="true">
<FilterRowCellTemplate Context="filter">
<DxTextBox Text="@(filter.FilterRowValue?.ToString())"
TextChanged="@(value => filter.FilterRowValue = value)"
CssClass="filter-search-input" />
</FilterRowCellTemplate>
</DxGridDataColumn>
<DxGridDataColumn Caption="" Width="220px" AllowSort="false">
<CellDisplayTemplate Context="cell">
@{ var item = (MassDataReadDto)cell.DataItem; }
<div style="white-space: nowrap;">
<DxButton RenderStyle="ButtonRenderStyle.Secondary" Size="ButtonSize.Small" Click="@(() => StartEdit(item))">Bearbeiten</DxButton>
<DxButton RenderStyle="ButtonRenderStyle.Danger" Size="ButtonSize.Small" Click="@ShowDeleteNotReady">Löschen</DxButton>
</div>
</CellDisplayTemplate>
</DxGridDataColumn>
</Columns>
<EditFormTemplate Context="editFormContext">
<DxFormLayout>
<DxFormLayoutItem Caption="CustomerName">
@editFormContext.GetEditor(nameof(MassDataReadDto.CustomerName))
</DxFormLayoutItem>
<DxFormLayoutItem Caption="Amount">
@editFormContext.GetEditor(nameof(MassDataReadDto.Amount))
</DxFormLayoutItem>
<DxFormLayoutItem Caption="Category">
@editFormContext.GetEditor(nameof(MassDataReadDto.Category))
</DxFormLayoutItem>
<DxFormLayoutItem Caption="Status">
@editFormContext.GetEditor(nameof(MassDataReadDto.StatusFlag))
</DxFormLayoutItem>
</DxFormLayout>
</EditFormTemplate>
</DxGrid>
<div class="pager-container">
@@ -176,10 +163,7 @@ else
@code {
private const int PageSize = 100;
private List<MassDataReadDto> items = new();
private MassDataWriteDto formModel = new();
private string amountText = string.Empty;
private bool isLoading;
private bool showForm;
private string? errorMessage;
private string? infoMessage;
private int pageIndex;
@@ -219,61 +203,38 @@ else
await LoadPage(index);
}
private void StartCreate()
private async Task OnEditModelSaving(GridEditModelSavingEventArgs e)
{
infoMessage = "Anlegen ist aktuell noch nicht verfügbar.";
}
errorMessage = null;
infoMessage = null;
private void StartEdit(MassDataReadDto item)
{
formModel = new MassDataWriteDto
var editModel = (MassDataReadDto)e.EditModel;
var dto = new MassDataWriteDto
{
CustomerName = item.CustomerName,
Amount = item.Amount,
Category = item.Category,
StatusFlag = item.StatusFlag
CustomerName = editModel.CustomerName,
Amount = editModel.Amount,
Category = editModel.Category,
StatusFlag = editModel.StatusFlag
};
amountText = item.Amount.ToString("0.00");
showForm = true;
infoMessage = null;
errorMessage = null;
}
private async Task HandleSubmit()
{
errorMessage = null;
infoMessage = null;
if (!decimal.TryParse(amountText, out var amount))
{
errorMessage = "Amount ist ungültig.";
return;
}
formModel.Amount = amount;
try
{
await Api.UpsertAsync(formModel);
await Api.UpsertAsync(dto);
infoMessage = "MassData aktualisiert.";
showForm = false;
await LoadPage(pageIndex);
}
catch (Exception ex)
{
errorMessage = $"Fehler beim Speichern: {ex.Message}";
e.Cancel = true;
}
}
private void CancelEdit()
private Task OnDataItemDeleting(GridDataItemDeletingEventArgs e)
{
showForm = false;
infoMessage = null;
errorMessage = null;
}
private void ShowDeleteNotReady()
{
infoMessage = "Löschen ist aktuell noch nicht verfügbar.";
e.Cancel = true;
return Task.CompletedTask;
}
}