Refactor MassDataGrid editing and validation logic

- Switch to custom edit model (MassDataEditModel) for popup editing, enabling granular field validation and UI control
- Replace default editors with explicit DxTextBox/DxCheckBox bindings
- Add AmountText field for string input and validation; validate and convert in OnEditModelSaving
- Implement duplicate customer check via new GetByCustomerNameAsync API method
- Show ValidationSummary in popup; manage validation state with ValidationMessageStore and EditContext
- Make popup header and width dynamic; show procedure ComboBox only for existing records
- Restore "New" button in grid command column
- Refactor CatalogsGrid to handle validation clearing in OnEditFieldChanged instead of OnTitleChanged
- General improvements to real-time validation feedback
This commit is contained in:
OlgunR
2026-02-05 14:11:27 +01:00
parent a52d615750
commit 006ee78422
6 changed files with 366 additions and 48 deletions

View File

@@ -138,10 +138,7 @@ else
@{ SetEditContext(editFormContext.EditContext); var editModel = (CatalogEditModel)editFormContext.EditModel; SetPopupHeaderText(editModel.IsNew); }
<DxFormLayout ColCount="2">
<DxFormLayoutItem Caption="Titel">
<DxTextBox @bind-Text="editModel.CatTitle"
@bind-Text:after="OnTitleChanged"
BindValueMode="BindValueMode.OnInput"
Width="100%" />
<DxTextBox @bind-Text="editModel.CatTitle" Width="100%" />
</DxFormLayoutItem>
<DxFormLayoutItem Caption="Kennung">
<DxTextBox @bind-Text="editModel.CatString" Width="100%" />
@@ -215,19 +212,15 @@ else
{
validationMessageStore.Clear();
editContext.NotifyValidationStateChanged();
}
}
private void OnTitleChanged()
{
if (validationMessageStore == null || editContext == null)
{
return;
}
var field = new FieldIdentifier(editContext.Model, nameof(CatalogEditModel.CatTitle));
validationMessageStore.Clear(field);
editContext.NotifyValidationStateChanged();
if (e.FieldIdentifier.FieldName == nameof(CatalogEditModel.CatTitle))
{
var field = new FieldIdentifier(editContext.Model, nameof(CatalogEditModel.CatTitle));
validationMessageStore.Clear(field);
editContext.NotifyValidationStateChanged();
}
}
private void OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)

View File

@@ -1,3 +1,4 @@
@using Microsoft.AspNetCore.Components.Forms
@inject MassDataApiClient Api
<style>
@@ -50,6 +51,9 @@
background-position: right 0.5rem center;
background-size: 0.9rem;
}
.massdata-edit-popup {
min-width: 720px;
}
</style>
@if (!string.IsNullOrWhiteSpace(errorMessage))
@@ -82,11 +86,13 @@ else
PageSize="100"
CssClass="mb-3 massdata-grid"
EditMode="GridEditMode.PopupEditForm"
PopupEditFormHeaderText="Bearbeiten"
PopupEditFormCssClass="massdata-edit-popup"
PopupEditFormHeaderText="@popupHeaderText"
CustomizeEditModel="OnCustomizeEditModel"
EditModelSaving="OnEditModelSaving"
DataItemDeleting="OnDataItemDeleting">
<Columns>
<DxGridCommandColumn Width="120px" NewButtonVisible="false" />
<DxGridCommandColumn Width="120px" />
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.Id)" Caption="Id" Width="90px" ReadOnly="true">
<FilterRowCellTemplate Context="filter">
<DxTextBox Text="@(filter.FilterRowValue?.ToString())"
@@ -143,18 +149,34 @@ else
</DxGridDataColumn>
</Columns>
<EditFormTemplate Context="editFormContext">
<DxFormLayout>
@{ SetEditContext(editFormContext.EditContext); var editModel = (MassDataEditModel)editFormContext.EditModel; SetPopupHeaderText(editModel.IsNew); }
<DxFormLayout ColCount="2">
<DxFormLayoutItem Caption="CustomerName">
@editFormContext.GetEditor(nameof(MassDataReadDto.CustomerName))
<DxTextBox @bind-Text="editModel.CustomerName" Width="100%" />
</DxFormLayoutItem>
<DxFormLayoutItem Caption="Amount">
@editFormContext.GetEditor(nameof(MassDataReadDto.Amount))
<DxTextBox @bind-Text="editModel.AmountText" Width="100%" />
</DxFormLayoutItem>
<DxFormLayoutItem Caption="Category">
@editFormContext.GetEditor(nameof(MassDataReadDto.Category))
<DxTextBox @bind-Text="editModel.Category" Width="100%" ReadOnly="@(!editModel.IsNew)" />
</DxFormLayoutItem>
<DxFormLayoutItem Caption="Status">
@editFormContext.GetEditor(nameof(MassDataReadDto.StatusFlag))
<DxCheckBox @bind-Checked="editModel.StatusFlag" ReadOnly="@(!editModel.IsNew)" />
</DxFormLayoutItem>
@if (!editModel.IsNew)
{
<DxFormLayoutItem Caption="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>
@@ -174,6 +196,9 @@ else
private string? infoMessage;
private int pageIndex;
private int pageCount = 1;
private string popupHeaderText = "Edit";
private EditContext? editContext;
private ValidationMessageStore? validationMessageStore;
private readonly List<BoolFilterOption> statusFilterOptions = new()
{
@@ -182,6 +207,11 @@ else
new() { Value = false, Text = "False" }
};
private readonly List<ProcedureOption> procedureOptions = new()
{
new() { Value = 0, Text = "PRMassdata_UpsertByCustomerName" }
};
protected override async Task OnInitializedAsync()
{
await LoadPage(0);
@@ -216,16 +246,105 @@ else
await LoadPage(index);
}
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(MassDataEditModel.UpdateProcedure))
{
validationMessageStore.Clear();
editContext.NotifyValidationStateChanged();
return;
}
if (e.FieldIdentifier.FieldName == nameof(MassDataEditModel.CustomerName))
{
var field = new FieldIdentifier(editContext.Model, nameof(MassDataEditModel.CustomerName));
validationMessageStore.Clear(field);
editContext.NotifyValidationStateChanged();
}
}
private void SetPopupHeaderText(bool isNew)
{
popupHeaderText = isNew ? "Neu" : "Edit";
}
private void OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
{
if (e.IsNew)
{
e.EditModel = new MassDataEditModel { IsNew = true, UpdateProcedure = procedureOptions[0].Value };
SetPopupHeaderText(true);
return;
}
var item = (MassDataReadDto)e.DataItem;
e.EditModel = new MassDataEditModel
{
Id = item.Id,
CustomerName = item.CustomerName,
AmountText = item.Amount.ToString("0.00"),
Category = item.Category,
StatusFlag = item.StatusFlag,
UpdateProcedure = procedureOptions[0].Value,
IsNew = false,
OriginalCustomerName = item.CustomerName
};
SetPopupHeaderText(false);
}
private async Task OnEditModelSaving(GridEditModelSavingEventArgs e)
{
errorMessage = null;
infoMessage = null;
var editModel = (MassDataReadDto)e.EditModel;
validationMessageStore?.Clear();
editContext?.NotifyValidationStateChanged();
var editModel = (MassDataEditModel)e.EditModel;
if (!decimal.TryParse(editModel.AmountText, out var amount))
{
AddValidationError(editModel, nameof(MassDataEditModel.AmountText), "Amount ist ungültig.");
e.Cancel = true;
return;
}
if (editModel.IsNew)
{
var existing = await Api.GetByCustomerNameAsync(editModel.CustomerName);
if (existing != null)
{
AddValidationError(editModel, nameof(MassDataEditModel.CustomerName), "Kunde existiert bereits.");
e.Cancel = true;
return;
}
}
var dto = new MassDataWriteDto
{
CustomerName = editModel.CustomerName,
Amount = editModel.Amount,
Amount = amount,
Category = editModel.Category,
StatusFlag = editModel.StatusFlag
};
@@ -233,7 +352,7 @@ else
try
{
await Api.UpsertAsync(dto);
infoMessage = "MassData aktualisiert.";
infoMessage = editModel.IsNew ? "MassData angelegt." : "MassData aktualisiert.";
await LoadPage(pageIndex);
}
catch (Exception ex)
@@ -243,6 +362,18 @@ else
}
}
private void AddValidationError(MassDataEditModel editModel, string fieldName, string message)
{
if (editContext == null || validationMessageStore == null)
{
return;
}
var field = new FieldIdentifier(editModel, fieldName);
validationMessageStore.Add(field, message);
editContext.NotifyValidationStateChanged();
}
private Task OnDataItemDeleting(GridDataItemDeletingEventArgs e)
{
errorMessage = null;
@@ -251,6 +382,24 @@ else
return Task.CompletedTask;
}
private sealed class MassDataEditModel
{
public int Id { get; set; }
public string CustomerName { get; set; } = string.Empty;
public string AmountText { get; set; } = string.Empty;
public string Category { get; set; } = string.Empty;
public bool StatusFlag { get; set; }
public int UpdateProcedure { get; set; }
public bool IsNew { get; set; }
public string OriginalCustomerName { get; set; } = string.Empty;
}
private sealed class ProcedureOption
{
public int Value { get; set; }
public string Text { get; set; } = string.Empty;
}
private sealed class BoolFilterOption
{
public bool? Value { get; set; }

View File

@@ -32,4 +32,21 @@ public class MassDataApiClient
var payload = await response.Content.ReadFromJsonAsync<MassDataReadDto>();
return payload ?? new MassDataReadDto();
}
public async Task<MassDataReadDto?> GetByCustomerNameAsync(string customerName)
{
if (string.IsNullOrWhiteSpace(customerName))
{
return null;
}
var response = await _httpClient.GetAsync($"{Endpoint}/{Uri.EscapeDataString(customerName)}");
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return null;
}
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<MassDataReadDto>();
}
}

View File

@@ -97,10 +97,7 @@ else
@{ SetEditContext(editFormContext.EditContext); var editModel = (CatalogEditModel)editFormContext.EditModel; SetPopupHeaderText(editModel.IsNew); }
<DxFormLayout ColCount="2">
<DxFormLayoutItem Caption="Titel">
<DxTextBox @bind-Text="editModel.CatTitle"
@bind-Text:after="OnTitleChanged"
BindValueMode="BindValueMode.OnInput"
Width="100%" />
<DxTextBox @bind-Text="editModel.CatTitle" Width="100%" />
</DxFormLayoutItem>
<DxFormLayoutItem Caption="Kennung">
<DxTextBox @bind-Text="editModel.CatString" Width="100%" />
@@ -174,19 +171,15 @@ else
{
validationMessageStore.Clear();
editContext.NotifyValidationStateChanged();
}
}
private void OnTitleChanged()
{
if (validationMessageStore == null || editContext == null)
{
return;
}
var field = new FieldIdentifier(editContext.Model, nameof(CatalogEditModel.CatTitle));
validationMessageStore.Clear(field);
editContext.NotifyValidationStateChanged();
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)

View File

@@ -1,3 +1,4 @@
@using Microsoft.AspNetCore.Components.Forms
@inject MassDataApiClient Api
<style>
@@ -50,6 +51,9 @@
background-position: right 0.5rem center;
background-size: 0.9rem;
}
.massdata-edit-popup {
min-width: 720px;
}
</style>
@if (!string.IsNullOrWhiteSpace(errorMessage))
@@ -82,11 +86,13 @@ else
PageSize="100"
CssClass="mb-3 massdata-grid"
EditMode="GridEditMode.PopupEditForm"
PopupEditFormHeaderText="Bearbeiten"
PopupEditFormCssClass="massdata-edit-popup"
PopupEditFormHeaderText="@popupHeaderText"
CustomizeEditModel="OnCustomizeEditModel"
EditModelSaving="OnEditModelSaving"
DataItemDeleting="OnDataItemDeleting">
<Columns>
<DxGridCommandColumn Width="120px" NewButtonVisible="false" />
<DxGridCommandColumn Width="120px" />
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.Id)" Caption="Id" Width="90px" ReadOnly="true">
<FilterRowCellTemplate Context="filter">
<DxTextBox Text="@(filter.FilterRowValue?.ToString())"
@@ -143,18 +149,34 @@ else
</DxGridDataColumn>
</Columns>
<EditFormTemplate Context="editFormContext">
<DxFormLayout>
@{ SetEditContext(editFormContext.EditContext); var editModel = (MassDataEditModel)editFormContext.EditModel; SetPopupHeaderText(editModel.IsNew); }
<DxFormLayout ColCount="2">
<DxFormLayoutItem Caption="CustomerName">
@editFormContext.GetEditor(nameof(MassDataReadDto.CustomerName))
<DxTextBox @bind-Text="editModel.CustomerName" Width="100%" />
</DxFormLayoutItem>
<DxFormLayoutItem Caption="Amount">
@editFormContext.GetEditor(nameof(MassDataReadDto.Amount))
<DxTextBox @bind-Text="editModel.AmountText" Width="100%" />
</DxFormLayoutItem>
<DxFormLayoutItem Caption="Category">
@editFormContext.GetEditor(nameof(MassDataReadDto.Category))
<DxTextBox @bind-Text="editModel.Category" Width="100%" ReadOnly="@(!editModel.IsNew)" />
</DxFormLayoutItem>
<DxFormLayoutItem Caption="Status">
@editFormContext.GetEditor(nameof(MassDataReadDto.StatusFlag))
<DxCheckBox @bind-Checked="editModel.StatusFlag" ReadOnly="@(!editModel.IsNew)" />
</DxFormLayoutItem>
@if (!editModel.IsNew)
{
<DxFormLayoutItem Caption="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>
@@ -174,6 +196,9 @@ else
private string? infoMessage;
private int pageIndex;
private int pageCount = 1;
private string popupHeaderText = "Edit";
private EditContext? editContext;
private ValidationMessageStore? validationMessageStore;
private readonly List<BoolFilterOption> statusFilterOptions = new()
{
@@ -182,6 +207,11 @@ else
new() { Value = false, Text = "False" }
};
private readonly List<ProcedureOption> procedureOptions = new()
{
new() { Value = 0, Text = "PRMassdata_UpsertByCustomerName" }
};
protected override async Task OnInitializedAsync()
{
await LoadPage(0);
@@ -216,16 +246,105 @@ else
await LoadPage(index);
}
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(MassDataEditModel.UpdateProcedure))
{
validationMessageStore.Clear();
editContext.NotifyValidationStateChanged();
return;
}
if (e.FieldIdentifier.FieldName == nameof(MassDataEditModel.CustomerName))
{
var field = new FieldIdentifier(editContext.Model, nameof(MassDataEditModel.CustomerName));
validationMessageStore.Clear(field);
editContext.NotifyValidationStateChanged();
}
}
private void SetPopupHeaderText(bool isNew)
{
popupHeaderText = isNew ? "Neu" : "Edit";
}
private void OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
{
if (e.IsNew)
{
e.EditModel = new MassDataEditModel { IsNew = true, UpdateProcedure = procedureOptions[0].Value };
SetPopupHeaderText(true);
return;
}
var item = (MassDataReadDto)e.DataItem;
e.EditModel = new MassDataEditModel
{
Id = item.Id,
CustomerName = item.CustomerName,
AmountText = item.Amount.ToString("0.00"),
Category = item.Category,
StatusFlag = item.StatusFlag,
UpdateProcedure = procedureOptions[0].Value,
IsNew = false,
OriginalCustomerName = item.CustomerName
};
SetPopupHeaderText(false);
}
private async Task OnEditModelSaving(GridEditModelSavingEventArgs e)
{
errorMessage = null;
infoMessage = null;
var editModel = (MassDataReadDto)e.EditModel;
validationMessageStore?.Clear();
editContext?.NotifyValidationStateChanged();
var editModel = (MassDataEditModel)e.EditModel;
if (!decimal.TryParse(editModel.AmountText, out var amount))
{
AddValidationError(editModel, nameof(MassDataEditModel.AmountText), "Amount ist ungültig.");
e.Cancel = true;
return;
}
if (editModel.IsNew)
{
var existing = await Api.GetByCustomerNameAsync(editModel.CustomerName);
if (existing != null)
{
AddValidationError(editModel, nameof(MassDataEditModel.CustomerName), "Kunde existiert bereits.");
e.Cancel = true;
return;
}
}
var dto = new MassDataWriteDto
{
CustomerName = editModel.CustomerName,
Amount = editModel.Amount,
Amount = amount,
Category = editModel.Category,
StatusFlag = editModel.StatusFlag
};
@@ -233,7 +352,7 @@ else
try
{
await Api.UpsertAsync(dto);
infoMessage = "MassData aktualisiert.";
infoMessage = editModel.IsNew ? "MassData angelegt." : "MassData aktualisiert.";
await LoadPage(pageIndex);
}
catch (Exception ex)
@@ -243,6 +362,18 @@ else
}
}
private void AddValidationError(MassDataEditModel editModel, string fieldName, string message)
{
if (editContext == null || validationMessageStore == null)
{
return;
}
var field = new FieldIdentifier(editModel, fieldName);
validationMessageStore.Add(field, message);
editContext.NotifyValidationStateChanged();
}
private Task OnDataItemDeleting(GridDataItemDeletingEventArgs e)
{
errorMessage = null;
@@ -251,6 +382,24 @@ else
return Task.CompletedTask;
}
private sealed class MassDataEditModel
{
public int Id { get; set; }
public string CustomerName { get; set; } = string.Empty;
public string AmountText { get; set; } = string.Empty;
public string Category { get; set; } = string.Empty;
public bool StatusFlag { get; set; }
public int UpdateProcedure { get; set; }
public bool IsNew { get; set; }
public string OriginalCustomerName { get; set; } = string.Empty;
}
private sealed class ProcedureOption
{
public int Value { get; set; }
public string Text { get; set; } = string.Empty;
}
private sealed class BoolFilterOption
{
public bool? Value { get; set; }

View File

@@ -32,4 +32,21 @@ public class MassDataApiClient
var payload = await response.Content.ReadFromJsonAsync<MassDataReadDto>();
return payload ?? new MassDataReadDto();
}
public async Task<MassDataReadDto?> GetByCustomerNameAsync(string customerName)
{
if (string.IsNullOrWhiteSpace(customerName))
{
return null;
}
var response = await _httpClient.GetAsync($"{Endpoint}/{Uri.EscapeDataString(customerName)}");
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return null;
}
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<MassDataReadDto>();
}
}