Add MassData feature with API, paging, and Blazor grid

Introduces MassData management to backend and Blazor frontend:
- Adds API endpoint for MassData count and paging
- Updates repository and controller for count support
- Implements MediatR query/handler for count
- Adds Blazor page and grid for viewing/editing MassData
- Registers MassDataApiClient and integrates with DI
- Supports paging, upsert, and UI feedback in grid
This commit is contained in:
OlgunR
2026-02-04 13:00:45 +01:00
parent 85b9b0b51a
commit 88c34ef94b
19 changed files with 572 additions and 0 deletions

View File

@@ -17,6 +17,13 @@ public class MassDataController : ControllerBase
_mediator = mediator;
}
[HttpGet("count")]
public async Task<ActionResult<int>> GetCount(CancellationToken cancellationToken)
{
var count = await _mediator.Send(new GetMassDataCountQuery(), cancellationToken);
return Ok(count);
}
[HttpGet]
public async Task<ActionResult<IEnumerable<MassDataReadDto>>> GetAll([FromQuery] int? skip, [FromQuery] int? take, CancellationToken cancellationToken)
{

View File

@@ -0,0 +1,19 @@
using DbFirst.Application.Repositories;
using MediatR;
namespace DbFirst.Application.MassData.Queries;
public class GetMassDataCountHandler : IRequestHandler<GetMassDataCountQuery, int>
{
private readonly IMassDataRepository _repository;
public GetMassDataCountHandler(IMassDataRepository repository)
{
_repository = repository;
}
public async Task<int> Handle(GetMassDataCountQuery request, CancellationToken cancellationToken)
{
return await _repository.GetCountAsync(cancellationToken);
}
}

View File

@@ -0,0 +1,5 @@
using MediatR;
namespace DbFirst.Application.MassData.Queries;
public record GetMassDataCountQuery : IRequest<int>;

View File

@@ -4,6 +4,7 @@ namespace DbFirst.Application.Repositories;
public interface IMassDataRepository
{
Task<int> GetCountAsync(CancellationToken cancellationToken = default);
Task<List<Massdata>> GetAllAsync(int? skip = null, int? take = null, CancellationToken cancellationToken = default);
Task<Massdata?> GetByCustomerNameAsync(string customerName, CancellationToken cancellationToken = default);
Task<Massdata> UpsertByCustomerNameAsync(string customerName, decimal amount, bool statusFlag, string category, CancellationToken cancellationToken = default);

View File

@@ -0,0 +1,196 @@
@inject MassDataApiClient Api
<style>
.action-panel { margin-bottom: 16px; }
.grid-section { margin-top: 12px; }
.pager-container {
display: flex;
justify-content: center;
margin-top: 12px;
margin-bottom: 16px;
}
</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>
}
<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>
}
else if (items.Count == 0)
{
<p>Keine Einträge vorhanden.</p>
}
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">
<Columns>
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.Id)" Caption="Id" Width="90px" />
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.CustomerName)" Caption="CustomerName" />
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.Amount)" Caption="Amount" DisplayFormat="c2" />
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.Category)" Caption="Category" />
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.StatusFlag)" Caption="Status" />
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.AddedWhen)" Caption="Added" />
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.ChangedWhen)" Caption="Changed" />
<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>
</DxGrid>
<div class="pager-container">
<DxPager PageCount="@pageCount" ActivePageIndex="@pageIndex" ActivePageIndexChanged="OnPageChanged" />
</div>
</div>
}
@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;
private int pageCount = 1;
protected override async Task OnInitializedAsync()
{
await LoadPage(0);
}
private async Task LoadPage(int page)
{
isLoading = true;
errorMessage = null;
try
{
var total = await Api.GetCountAsync();
pageCount = Math.Max(1, (int)Math.Ceiling(total / (double)PageSize));
pageIndex = Math.Clamp(page, 0, pageCount - 1);
var skip = pageIndex * PageSize;
items = await Api.GetAllAsync(skip, PageSize);
}
catch (Exception ex)
{
errorMessage = $"MassData konnten nicht geladen werden: {ex.Message}";
}
finally
{
isLoading = false;
StateHasChanged();
}
}
private async Task OnPageChanged(int index)
{
await LoadPage(index);
}
private void StartCreate()
{
infoMessage = "Anlegen ist aktuell noch nicht verfügbar.";
}
private void StartEdit(MassDataReadDto item)
{
formModel = new MassDataWriteDto
{
CustomerName = item.CustomerName,
Amount = item.Amount,
Category = item.Category,
StatusFlag = item.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);
infoMessage = "MassData aktualisiert.";
showForm = false;
await LoadPage(pageIndex);
}
catch (Exception ex)
{
errorMessage = $"Fehler beim Speichern: {ex.Message}";
}
}
private void CancelEdit()
{
showForm = false;
infoMessage = null;
errorMessage = null;
}
private void ShowDeleteNotReady()
{
infoMessage = "Löschen ist aktuell noch nicht verfügbar.";
}
}

View File

@@ -26,6 +26,11 @@
<span class="oi oi-list-rich" aria-hidden="true"></span> Dashboards
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="massdata">
<span class="bi bi-table" aria-hidden="true"></span> MassData
</NavLink>
</div>
</nav>
</div>

View File

@@ -0,0 +1,12 @@
namespace DbFirst.BlazorWasm.Models;
public class MassDataReadDto
{
public int Id { get; set; }
public string CustomerName { get; set; } = string.Empty;
public decimal Amount { get; set; }
public string Category { get; set; } = string.Empty;
public bool StatusFlag { get; set; }
public DateTime AddedWhen { get; set; }
public DateTime? ChangedWhen { get; set; }
}

View File

@@ -0,0 +1,9 @@
namespace DbFirst.BlazorWasm.Models;
public class MassDataWriteDto
{
public string CustomerName { get; set; } = string.Empty;
public decimal Amount { get; set; }
public string Category { get; set; } = string.Empty;
public bool StatusFlag { get; set; }
}

View File

@@ -0,0 +1,7 @@
@page "/massdata"
<PageTitle>MassData</PageTitle>
<h1>MassData</h1>
<MassDataGrid />

View File

@@ -18,5 +18,6 @@ var apiBaseUrl = builder.Configuration["ApiBaseUrl"] ?? builder.HostEnvironment.
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(apiBaseUrl) });
builder.Services.AddScoped<CatalogApiClient>();
builder.Services.AddScoped<DashboardApiClient>();
builder.Services.AddScoped<MassDataApiClient>();
await builder.Build().RunAsync();

View File

@@ -0,0 +1,35 @@
using System.Net.Http.Json;
using DbFirst.BlazorWasm.Models;
namespace DbFirst.BlazorWasm.Services;
public class MassDataApiClient
{
private readonly HttpClient _httpClient;
private const string Endpoint = "api/massdata";
public MassDataApiClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<int> GetCountAsync()
{
var result = await _httpClient.GetFromJsonAsync<int?>("api/massdata/count");
return result ?? 0;
}
public async Task<List<MassDataReadDto>> GetAllAsync(int skip, int take)
{
var result = await _httpClient.GetFromJsonAsync<List<MassDataReadDto>>($"{Endpoint}?skip={skip}&take={take}");
return result ?? new List<MassDataReadDto>();
}
public async Task<MassDataReadDto> UpsertAsync(MassDataWriteDto dto)
{
var response = await _httpClient.PostAsJsonAsync($"{Endpoint}/upsert", dto);
response.EnsureSuccessStatusCode();
var payload = await response.Content.ReadFromJsonAsync<MassDataReadDto>();
return payload ?? new MassDataReadDto();
}
}

View File

@@ -36,6 +36,12 @@
<span class="oi oi-list-rich" aria-hidden="true"></span> Dashboards
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="massdata">
<span class="bi bi-table" aria-hidden="true"></span> MassData
</NavLink>
</div>
</nav>
</div>

View File

@@ -0,0 +1,196 @@
@inject MassDataApiClient Api
<style>
.action-panel { margin-bottom: 16px; }
.grid-section { margin-top: 12px; }
.pager-container {
display: flex;
justify-content: center;
margin-top: 12px;
margin-bottom: 16px;
}
</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>
}
<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>
}
else if (items.Count == 0)
{
<p>Keine Einträge vorhanden.</p>
}
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">
<Columns>
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.Id)" Caption="Id" Width="90px" />
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.CustomerName)" Caption="CustomerName" />
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.Amount)" Caption="Amount" DisplayFormat="c2" />
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.Category)" Caption="Category" />
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.StatusFlag)" Caption="Status" />
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.AddedWhen)" Caption="Added" />
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.ChangedWhen)" Caption="Changed" />
<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>
</DxGrid>
<div class="pager-container">
<DxPager PageCount="@pageCount" ActivePageIndex="@pageIndex" ActivePageIndexChanged="OnPageChanged" />
</div>
</div>
}
@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;
private int pageCount = 1;
protected override async Task OnInitializedAsync()
{
await LoadPage(0);
}
private async Task LoadPage(int page)
{
isLoading = true;
errorMessage = null;
try
{
var total = await Api.GetCountAsync();
pageCount = Math.Max(1, (int)Math.Ceiling(total / (double)PageSize));
pageIndex = Math.Clamp(page, 0, pageCount - 1);
var skip = pageIndex * PageSize;
items = await Api.GetAllAsync(skip, PageSize);
}
catch (Exception ex)
{
errorMessage = $"MassData konnten nicht geladen werden: {ex.Message}";
}
finally
{
isLoading = false;
StateHasChanged();
}
}
private async Task OnPageChanged(int index)
{
await LoadPage(index);
}
private void StartCreate()
{
infoMessage = "Anlegen ist aktuell noch nicht verfügbar.";
}
private void StartEdit(MassDataReadDto item)
{
formModel = new MassDataWriteDto
{
CustomerName = item.CustomerName,
Amount = item.Amount,
Category = item.Category,
StatusFlag = item.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);
infoMessage = "MassData aktualisiert.";
showForm = false;
await LoadPage(pageIndex);
}
catch (Exception ex)
{
errorMessage = $"Fehler beim Speichern: {ex.Message}";
}
}
private void CancelEdit()
{
showForm = false;
infoMessage = null;
errorMessage = null;
}
private void ShowDeleteNotReady()
{
infoMessage = "Löschen ist aktuell noch nicht verfügbar.";
}
}

View File

@@ -0,0 +1,7 @@
@page "/massdata"
<PageTitle>MassData</PageTitle>
<h1>MassData</h1>
<MassDataGrid />

View File

@@ -0,0 +1,12 @@
namespace DbFirst.BlazorWebApp.Models;
public class MassDataReadDto
{
public int Id { get; set; }
public string CustomerName { get; set; } = string.Empty;
public decimal Amount { get; set; }
public string Category { get; set; } = string.Empty;
public bool StatusFlag { get; set; }
public DateTime AddedWhen { get; set; }
public DateTime? ChangedWhen { get; set; }
}

View File

@@ -0,0 +1,9 @@
namespace DbFirst.BlazorWebApp.Models;
public class MassDataWriteDto
{
public string CustomerName { get; set; } = string.Empty;
public decimal Amount { get; set; }
public string Category { get; set; } = string.Empty;
public bool StatusFlag { get; set; }
}

View File

@@ -21,11 +21,16 @@ if (!string.IsNullOrWhiteSpace(apiBaseUrl))
{
client.BaseAddress = new Uri(apiBaseUrl);
});
builder.Services.AddHttpClient<MassDataApiClient>(client =>
{
client.BaseAddress = new Uri(apiBaseUrl);
});
}
else
{
builder.Services.AddHttpClient<CatalogApiClient>();
builder.Services.AddHttpClient<DashboardApiClient>();
builder.Services.AddHttpClient<MassDataApiClient>();
}
var app = builder.Build();

View File

@@ -0,0 +1,35 @@
using System.Net.Http.Json;
using DbFirst.BlazorWebApp.Models;
namespace DbFirst.BlazorWebApp.Services;
public class MassDataApiClient
{
private readonly HttpClient _httpClient;
private const string Endpoint = "api/massdata";
public MassDataApiClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<int> GetCountAsync()
{
var result = await _httpClient.GetFromJsonAsync<int?>("api/massdata/count");
return result ?? 0;
}
public async Task<List<MassDataReadDto>> GetAllAsync(int skip, int take)
{
var result = await _httpClient.GetFromJsonAsync<List<MassDataReadDto>>($"{Endpoint}?skip={skip}&take={take}");
return result ?? new List<MassDataReadDto>();
}
public async Task<MassDataReadDto> UpsertAsync(MassDataWriteDto dto)
{
var response = await _httpClient.PostAsJsonAsync($"{Endpoint}/upsert", dto);
response.EnsureSuccessStatusCode();
var payload = await response.Content.ReadFromJsonAsync<MassDataReadDto>();
return payload ?? new MassDataReadDto();
}
}

View File

@@ -15,6 +15,11 @@ public class MassDataRepository : IMassDataRepository
_db = db;
}
public async Task<int> GetCountAsync(CancellationToken cancellationToken = default)
{
return await _db.Massdata.AsNoTracking().CountAsync(cancellationToken);
}
public async Task<List<Massdata>> GetAllAsync(CancellationToken cancellationToken = default)
{
return await _db.Massdata.AsNoTracking().ToListAsync(cancellationToken);