Compare commits

...

16 Commits

Author SHA1 Message Date
OlgunR
c73c7e63fe Move LayoutDto to its own file and update namespace
Separated LayoutDto from LayoutsController.cs into LayoutDto.cs and set its namespace to DbFirst.Application.Layouts for better code organization and maintainability.
2026-04-20 16:51:57 +02:00
OlgunR
3e78e2e2cf Move catalog title update validation to handler
Validation preventing CatTitle changes during updates is now enforced in UpdateCatalogHandler instead of the controller. The handler throws an exception if a title change is attempted. Also, streamlined UpdateProcedure handling and removed redundant CatTitle mapping logic.
2026-04-20 16:38:29 +02:00
OlgunR
05825b6815 Refactor DI: move repository registrations to Infrastructure
Centralize repository service registrations in Infrastructure's
DependencyInjection.cs for better maintainability and separation
of concerns. Remove direct registrations from Program.cs and
add necessary using statements.
2026-04-20 16:28:37 +02:00
OlgunR
4720c8f87b Use UTC for AddedWhen timestamps in layout repository
Changed AddedWhen to use DateTime.UtcNow instead of DateTime.Now to ensure timestamps are stored in UTC, avoiding issues with local time zones.
2026-04-20 16:22:18 +02:00
OlgunR
0c936d0bf9 Adopt C# 12 collection expressions for empty lists/dicts
Refactored code to use C# 12 collection expressions ([]) for initializing empty lists and dictionaries instead of the older constructors. This change modernizes and simplifies collection initialization across models, services, and API clients without altering any logic.
2026-04-20 14:33:08 +02:00
OlgunR
fcbc66f8f5 Add CancellationToken support to all API client methods
All API client interfaces and implementations now accept an optional CancellationToken parameter for each method. This enables consumers to cancel HTTP requests, improving responsiveness and resource management. No changes were made to core logic or return types; only method signatures and HttpClient calls were updated to support cancellation.
2026-04-20 13:50:53 +02:00
OlgunR
aab6478f9a Refactor API clients to use interface-based DI
Introduce interfaces for all API clients and update dependency injection to use these interfaces. Refactor services and components to depend on abstractions instead of concrete implementations, improving testability and maintainability.
2026-04-20 13:23:16 +02:00
OlgunR
177d418ac3 Use InvokeAsync for async state updates in BandGridBase
Replaced direct StateHasChanged() calls with InvokeAsync(StateHasChanged) to ensure asynchronous state updates. This helps prevent issues that can arise from synchronous state changes during component lifecycle events or event callbacks in Blazor.
2026-04-20 12:59:27 +02:00
OlgunR
7cc88c13f3 Cache layout user to reduce localStorage access
Add a private _cachedLayoutUser field to BandLayoutService and update EnsureLayoutUserAsync to cache the layout user value after first retrieval or generation. This avoids repeated localStorage calls and improves performance.
2026-04-20 11:48:50 +02:00
OlgunR
8bf172755b Update using directives in _Imports.razor for clarity
Added @using DbFirst.BlazorWebApp and reorganized the placement of @using Microsoft.Extensions.Options to improve code organization and maintain consistent dependency order.
2026-04-20 11:34:15 +02:00
OlgunR
27c8f92a3b Refactor API base URL config to use AppSettings class
Replaced direct IConfiguration usage with a strongly-typed AppSettings class for accessing the API base URL. Registered AppSettings with DI and updated Dashboard.razor to use IOptions<AppSettings>. Updated using statements and DI setup for improved type safety and centralized configuration management.
2026-04-20 11:32:45 +02:00
OlgunR
cd0a824064 Refactor grid logic into BandGridBase<TItem> base class
Move shared state and methods from CatalogsGrid and MassDataGrid into BandGridBase<TItem>. This centralizes edit context handling, validation, popup header logic, row editing/deleting, and layout feedback, reducing duplication and improving maintainability. Individual grid components now only override OnEditFieldChanged for custom validation.
2026-04-20 10:52:05 +02:00
OlgunR
0008fac1d2 Refactor HTTP client registration for API clients
Simplified registration of CatalogApiClient, DashboardApiClient, MassDataApiClient, and LayoutApiClient by introducing a ConfigureClient method. This method sets the BaseAddress if ApiBaseUrl is configured, removing the previous if-else logic and reducing code duplication.
2026-04-20 10:29:17 +02:00
OlgunR
4659913711 Move ApiResult<T> to ApiResult.cs with proper namespace
ApiResult<T> and its static methods were relocated from CatalogApiClient.cs to a new ApiResult.cs file. The new file now includes the DbFirst.BlazorWebApp.Models namespace for better code organization.
2026-04-20 10:24:47 +02:00
OlgunR
bb23cb6629 Refactor error handling to use ApiClientHelper
Replaced the private ReadErrorAsync method in LayoutApiClient with calls to ApiClientHelper.ReadErrorAsync, centralizing error handling logic for improved consistency and reuse.
2026-04-20 10:19:40 +02:00
OlgunR
b3f7df6801 Initialize SignalR hub in OnAfterRenderAsync after first render
Moved SignalR hub connection setup from OnInitializedAsync to OnAfterRenderAsync, ensuring initialization occurs only after the component's first render. This prevents premature connection attempts and aligns with best practices for component lifecycle management.
2026-04-20 10:08:24 +02:00
25 changed files with 215 additions and 228 deletions

View File

@@ -50,17 +50,6 @@ public class CatalogsController : ControllerBase
[HttpPut("{id:int}")] [HttpPut("{id:int}")]
public async Task<ActionResult<CatalogReadDto>> Update(int id, CatalogWriteDto dto, CancellationToken cancellationToken) public async Task<ActionResult<CatalogReadDto>> Update(int id, CatalogWriteDto dto, CancellationToken cancellationToken)
{ {
var current = await _mediator.Send(new GetCatalogByIdQuery(id), cancellationToken);
if (current == null)
{
return NotFound();
}
if (dto.UpdateProcedure == CatalogUpdateProcedure.Update &&
!string.Equals(current.CatTitle, dto.CatTitle, StringComparison.OrdinalIgnoreCase))
{
return BadRequest("Titel kann nicht geändert werden.");
}
var updated = await _mediator.Send(new UpdateCatalogCommand(id, dto), cancellationToken); var updated = await _mediator.Send(new UpdateCatalogCommand(id, dto), cancellationToken);
if (updated == null) if (updated == null)
{ {

View File

@@ -2,6 +2,7 @@ using System.Text;
using DbFirst.Application.Repositories; using DbFirst.Application.Repositories;
using DbFirst.Domain.Entities; using DbFirst.Domain.Entities;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using DbFirst.Application.Layouts;
namespace DbFirst.API.Controllers; namespace DbFirst.API.Controllers;
@@ -83,12 +84,4 @@ public class LayoutsController : ControllerBase
LayoutData = layoutData LayoutData = layoutData
}; };
} }
public sealed class LayoutDto
{
public string LayoutType { get; set; } = string.Empty;
public string LayoutKey { get; set; } = string.Empty;
public string UserName { get; set; } = string.Empty;
public string LayoutData { get; set; } = string.Empty;
}
} }

View File

@@ -2,11 +2,9 @@ using DbFirst.API.Middleware;
using DbFirst.API.Dashboards; using DbFirst.API.Dashboards;
using DbFirst.API.Hubs; using DbFirst.API.Hubs;
using DbFirst.Application; using DbFirst.Application;
using DbFirst.Application.Repositories;
using DbFirst.Domain; using DbFirst.Domain;
using DbFirst.Domain.Entities; using DbFirst.Domain.Entities;
using DbFirst.Infrastructure; using DbFirst.Infrastructure;
using DbFirst.Infrastructure.Repositories;
using DevExpress.AspNetCore; using DevExpress.AspNetCore;
using DevExpress.DashboardAspNetCore; using DevExpress.DashboardAspNetCore;
using DevExpress.DashboardCommon; using DevExpress.DashboardCommon;
@@ -51,10 +49,6 @@ builder.Services.AddCors(options =>
builder.Services.AddInfrastructure(builder.Configuration); builder.Services.AddInfrastructure(builder.Configuration);
builder.Services.AddApplication(); builder.Services.AddApplication();
builder.Services.AddScoped<ICatalogRepository, CatalogRepository>();
builder.Services.AddScoped<IMassDataRepository, MassDataRepository>();
builder.Services.AddScoped<ILayoutRepository, LayoutRepository>();
builder.Services.AddDevExpressControls(); builder.Services.AddDevExpressControls();
builder.Services.AddSignalR(); builder.Services.AddSignalR();
builder.Services.AddSingleton<IDashboardChangeNotifier, DashboardChangeNotifier>(); builder.Services.AddSingleton<IDashboardChangeNotifier, DashboardChangeNotifier>();

View File

@@ -25,18 +25,20 @@ public class UpdateCatalogHandler : IRequestHandler<UpdateCatalogCommand, Catalo
return null; return null;
} }
if (request.Dto.UpdateProcedure == CatalogUpdateProcedure.Update &&
!string.Equals(existing.CatTitle, request.Dto.CatTitle, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("Titel kann nicht geändert werden.");
}
var entity = _mapper.Map<VwmyCatalog>(request.Dto); var entity = _mapper.Map<VwmyCatalog>(request.Dto);
entity.Guid = request.Id; entity.Guid = request.Id;
entity.CatTitle = request.Dto.UpdateProcedure == CatalogUpdateProcedure.Update
? existing.CatTitle
: request.Dto.CatTitle;
entity.AddedWho = existing.AddedWho; entity.AddedWho = existing.AddedWho;
entity.AddedWhen = existing.AddedWhen; entity.AddedWhen = existing.AddedWhen;
entity.ChangedWho = "system"; entity.ChangedWho = "system";
entity.ChangedWhen = DateTime.UtcNow; entity.ChangedWhen = DateTime.UtcNow;
var procedure = request.Dto.UpdateProcedure; var updated = await _repository.UpdateAsync(request.Id, entity, request.Dto.UpdateProcedure, cancellationToken);
var updated = await _repository.UpdateAsync(request.Id, entity, procedure, cancellationToken);
return updated == null ? null : _mapper.Map<CatalogReadDto>(updated); return updated == null ? null : _mapper.Map<CatalogReadDto>(updated);
} }
} }

View File

@@ -0,0 +1,9 @@
namespace DbFirst.Application.Layouts;
public class LayoutDto
{
public string LayoutType { get; set; } = string.Empty;
public string LayoutKey { get; set; } = string.Empty;
public string UserName { get; set; } = string.Empty;
public string LayoutData { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,6 @@
namespace DbFirst.BlazorWebApp;
public class AppSettings
{
public string ApiBaseUrl { get; set; } = string.Empty;
}

View File

@@ -2,6 +2,7 @@
using DbFirst.BlazorWebApp.Services; using DbFirst.BlazorWebApp.Services;
using DevExpress.Blazor; using DevExpress.Blazor;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Rendering;
namespace DbFirst.BlazorWebApp.Components; namespace DbFirst.BlazorWebApp.Components;
@@ -27,6 +28,15 @@ public abstract class BandGridBase<TItem> : ComponentBase
protected SizeMode _sizeMode = SizeMode.Medium; protected SizeMode _sizeMode = SizeMode.Medium;
protected static readonly List<SizeMode> _sizeModes = Enum.GetValues<SizeMode>().ToList(); 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"; private const string LayoutType = "GRID_BANDS";
// --- Lifecycle --- // --- Lifecycle ---
@@ -147,12 +157,12 @@ public abstract class BandGridBase<TItem> : ComponentBase
.Select(c => c.FieldName) .Select(c => c.FieldName)
.ToList(); .ToList();
} }
StateHasChanged(); _ = InvokeAsync(StateHasChanged);
} }
protected void UpdateBandOptions() protected void UpdateBandOptions()
{ {
bandOptions = new List<BandOption> { new() { Id = string.Empty, Caption = "Ohne Band" } }; bandOptions = [new() { Id = string.Empty, Caption = "Ohne Band" }];
bandOptions.AddRange(bandLayout.Bands.Select(b => new BandOption { Id = b.Id, Caption = b.Caption })); bandOptions.AddRange(bandLayout.Bands.Select(b => new BandOption { Id = b.Id, Caption = b.Caption }));
} }
@@ -216,4 +226,53 @@ public abstract class BandGridBase<TItem> : ComponentBase
builder.AddAttribute(seq++, "ReadOnly", true); builder.AddAttribute(seq++, "ReadOnly", true);
builder.CloseComponent(); builder.CloseComponent();
} }
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;
}
} }

View File

@@ -1,5 +1,5 @@
@inherits BandGridBase<CatalogReadDto> @inherits BandGridBase<CatalogReadDto>
@inject CatalogApiClient Api @inject ICatalogApiClient Api
@if (!string.IsNullOrWhiteSpace(errorMessage)) @if (!string.IsNullOrWhiteSpace(errorMessage))
{ {
@@ -149,14 +149,7 @@ else
@code { @code {
private List<CatalogReadDto> items = new(); private List<CatalogReadDto> items = new();
private bool isLoading;
private bool hasLoaded;
private string? errorMessage;
private string? infoMessage;
private EditContext? editContext;
private ValidationMessageStore? validationMessageStore;
private int? focusedRowKey; private int? focusedRowKey;
private string popupHeaderText = "Edit";
protected override string LayoutKey => "CatalogsGrid"; protected override string LayoutKey => "CatalogsGrid";
protected override bool ShowCommandColumn => false; protected override bool ShowCommandColumn => false;
@@ -209,17 +202,7 @@ else
} }
} }
private void SetEditContext(EditContext context) protected override void OnEditFieldChanged(object? sender, FieldChangedEventArgs e)
{
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 (validationMessageStore == null || editContext == null) return;
@@ -237,8 +220,6 @@ else
} }
} }
private void SetPopupHeaderText(bool isNew) => popupHeaderText = isNew ? "Neu" : "Edit";
private void OnCustomizeEditModel(GridCustomizeEditModelEventArgs e) private void OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
{ {
popupHeaderText = e.IsNew ? "Neu" : "Edit"; popupHeaderText = e.IsNew ? "Neu" : "Edit";
@@ -378,36 +359,4 @@ else
public int Value { get; set; } public int Value { get; set; }
public string Text { get; set; } = string.Empty; public string Text { get; set; } = string.Empty;
} }
private int _focusedVisibleIndex;
private async Task EditFocusedRow()
=> await gridRef!.StartEditRowAsync(_focusedVisibleIndex);
private Task DeleteFocusedRow()
{
gridRef!.ShowRowDeleteConfirmation(_focusedVisibleIndex);
return Task.CompletedTask;
}
private async Task SaveLayoutWithFeedbackAsync()
{
try
{
await SaveLayoutAsync();
infoMessage = "Layout gespeichert.";
errorMessage = null;
}
catch (Exception ex)
{
errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}";
}
}
private async Task ResetLayoutWithFeedbackAsync()
{
await ResetLayoutAsync();
infoMessage = "Layout zurückgesetzt.";
errorMessage = null;
}
} }

View File

@@ -1,5 +1,5 @@
@inherits BandGridBase<MassDataReadDto> @inherits BandGridBase<MassDataReadDto>
@inject MassDataApiClient Api @inject IMassDataApiClient Api
@if (!string.IsNullOrWhiteSpace(errorMessage)) @if (!string.IsNullOrWhiteSpace(errorMessage))
{ {
@@ -175,16 +175,9 @@ else
@code { @code {
private List<MassDataReadDto> items = new(); private List<MassDataReadDto> items = new();
private bool isLoading;
private bool hasLoaded;
private string? errorMessage;
private string? infoMessage;
private int pageIndex; private int pageIndex;
private int pageCount = 1; private int pageCount = 1;
private int? pageSize = 100; private int? pageSize = 100;
private string popupHeaderText = "Edit";
private EditContext? editContext;
private ValidationMessageStore? validationMessageStore;
private int? focusedRowKey; private int? focusedRowKey;
protected override string LayoutKey => "MassDataGrid"; protected override string LayoutKey => "MassDataGrid";
@@ -259,17 +252,7 @@ else
await LoadPage(0); await LoadPage(0);
} }
private void SetEditContext(EditContext context) protected override void OnEditFieldChanged(object? sender, FieldChangedEventArgs e)
{
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 (validationMessageStore == null || editContext == null) return;
if (e.FieldIdentifier.FieldName == nameof(MassDataEditModel.UpdateProcedure)) if (e.FieldIdentifier.FieldName == nameof(MassDataEditModel.UpdateProcedure))
@@ -285,8 +268,6 @@ else
} }
} }
private void SetPopupHeaderText(bool isNew) => popupHeaderText = isNew ? "Neu" : "Edit";
private void OnCustomizeEditModel(GridCustomizeEditModelEventArgs e) private void OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
{ {
if (e.IsNew) if (e.IsNew)
@@ -391,36 +372,4 @@ else
public int? Value { get; set; } public int? Value { get; set; }
public string Text { get; set; } = string.Empty; public string Text { get; set; } = string.Empty;
} }
private int _focusedVisibleIndex;
private async Task EditFocusedRow()
=> await gridRef!.StartEditRowAsync(_focusedVisibleIndex);
private Task DeleteFocusedRow()
{
gridRef!.ShowRowDeleteConfirmation(_focusedVisibleIndex);
return Task.CompletedTask;
}
private async Task SaveLayoutWithFeedbackAsync()
{
try
{
await SaveLayoutAsync();
infoMessage = "Layout gespeichert.";
errorMessage = null;
}
catch (Exception ex)
{
errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}";
}
}
private async Task ResetLayoutWithFeedbackAsync()
{
await ResetLayoutAsync();
infoMessage = "Layout zurückgesetzt.";
errorMessage = null;
}
} }

View File

@@ -1,9 +1,9 @@
@page "/dashboard" @page "/dashboard"
@page "/dashboards/{DashboardId?}" @page "/dashboards/{DashboardId?}"
@implements IAsyncDisposable @implements IAsyncDisposable
@inject Microsoft.Extensions.Configuration.IConfiguration Configuration @inject IOptions<AppSettings> AppSettingsOptions
@inject NavigationManager Navigation @inject NavigationManager Navigation
@inject DashboardApiClient DashboardApi @inject IDashboardApiClient DashboardApi
<PageTitle>Dashboards</PageTitle> <PageTitle>Dashboards</PageTitle>
@@ -45,12 +45,17 @@
private string SelectedDashboardId { get; set; } = string.Empty; private string SelectedDashboardId { get; set; } = string.Empty;
private string DashboardKey => $"{SelectedDashboardId}-{(IsDesigner ? "designer" : "viewer")}"; private string DashboardKey => $"{SelectedDashboardId}-{(IsDesigner ? "designer" : "viewer")}";
private string DashboardEndpoint => $"{Configuration["ApiBaseUrl"]?.TrimEnd('/')}/api/dashboard"; private string DashboardEndpoint => $"{AppSettingsOptions.Value.ApiBaseUrl.TrimEnd('/')}/api/dashboard";
private string HubEndpoint => $"{Configuration["ApiBaseUrl"]?.TrimEnd('/')}/hubs/dashboards"; private string HubEndpoint => $"{AppSettingsOptions.Value.ApiBaseUrl.TrimEnd('/')}/hubs/dashboards";
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
await RefreshDashboards(); await RefreshDashboards();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender) return;
_hubConnection = new HubConnectionBuilder() _hubConnection = new HubConnectionBuilder()
.WithUrl(HubEndpoint) .WithUrl(HubEndpoint)

View File

@@ -18,3 +18,4 @@
@using DevExpress.DashboardBlazor @using DevExpress.DashboardBlazor
@using DevExpress.DashboardWeb @using DevExpress.DashboardWeb
@using DevExpress.Data.Filtering @using DevExpress.Data.Filtering
@using Microsoft.Extensions.Options

View File

@@ -0,0 +1,7 @@
namespace DbFirst.BlazorWebApp.Models;
public record ApiResult<T>(bool Success, T? Value, string? Error)
{
public static ApiResult<T> Ok(T? value) => new(true, value, null);
public static ApiResult<T> Fail(string? error) => new(false, default, error);
}

View File

@@ -4,8 +4,8 @@ namespace DbFirst.BlazorWebApp.Models.Grid
{ {
public class BandLayout public class BandLayout
{ {
public List<BandDefinition> Bands { get; set; } = new(); public List<BandDefinition> Bands { get; set; } = [];
public List<string> ColumnOrder { get; set; } = new(); public List<string> ColumnOrder { get; set; } = [];
public Dictionary<string, string?> ColumnWidths { get; set; } = new(StringComparer.OrdinalIgnoreCase); public Dictionary<string, string?> ColumnWidths { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public GridPersistentLayout? GridLayout { get; set; } public GridPersistentLayout? GridLayout { get; set; }
public SizeMode SizeMode { get; set; } = SizeMode.Medium; public SizeMode SizeMode { get; set; } = SizeMode.Medium;
@@ -15,7 +15,7 @@ namespace DbFirst.BlazorWebApp.Models.Grid
{ {
public string Id { get; set; } = string.Empty; public string Id { get; set; } = string.Empty;
public string Caption { get; set; } = string.Empty; public string Caption { get; set; } = string.Empty;
public List<string> Columns { get; set; } = new(); public List<string> Columns { get; set; } = [];
} }
public class BandOption public class BandOption

View File

@@ -1,3 +1,4 @@
using DbFirst.BlazorWebApp;
using DbFirst.BlazorWebApp.Components; using DbFirst.BlazorWebApp.Components;
using DbFirst.BlazorWebApp.Services; using DbFirst.BlazorWebApp.Services;
using DevExpress.Blazor; using DevExpress.Blazor;
@@ -13,33 +14,18 @@ builder.Services.AddScoped<ThemeState>();
builder.Services.AddScoped<BandLayoutService>(); builder.Services.AddScoped<BandLayoutService>();
var apiBaseUrl = builder.Configuration["ApiBaseUrl"]; var apiBaseUrl = builder.Configuration["ApiBaseUrl"];
if (!string.IsNullOrWhiteSpace(apiBaseUrl)) builder.Services.Configure<AppSettings>(builder.Configuration);
void ConfigureClient(HttpClient client)
{ {
builder.Services.AddHttpClient<CatalogApiClient>(client => if (!string.IsNullOrWhiteSpace(apiBaseUrl))
{
client.BaseAddress = new Uri(apiBaseUrl); client.BaseAddress = new Uri(apiBaseUrl);
});
builder.Services.AddHttpClient<DashboardApiClient>(client =>
{
client.BaseAddress = new Uri(apiBaseUrl);
});
builder.Services.AddHttpClient<MassDataApiClient>(client =>
{
client.BaseAddress = new Uri(apiBaseUrl);
});
builder.Services.AddHttpClient<LayoutApiClient>(client =>
{
client.BaseAddress = new Uri(apiBaseUrl);
});
}
else
{
builder.Services.AddHttpClient<CatalogApiClient>();
builder.Services.AddHttpClient<DashboardApiClient>();
builder.Services.AddHttpClient<MassDataApiClient>();
builder.Services.AddHttpClient<LayoutApiClient>();
} }
builder.Services.AddHttpClient<ICatalogApiClient, CatalogApiClient>(ConfigureClient);
builder.Services.AddHttpClient<IDashboardApiClient, DashboardApiClient>(ConfigureClient);
builder.Services.AddHttpClient<IMassDataApiClient, MassDataApiClient>(ConfigureClient);
builder.Services.AddHttpClient<ILayoutApiClient, LayoutApiClient>(ConfigureClient);
var app = builder.Build(); var app = builder.Build();
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.

View File

@@ -5,20 +5,26 @@ using System.Text.Json;
namespace DbFirst.BlazorWebApp.Services namespace DbFirst.BlazorWebApp.Services
{ {
public class BandLayoutService(LayoutApiClient layoutApi, IJSRuntime jsRuntime) public class BandLayoutService(ILayoutApiClient layoutApi, IJSRuntime jsRuntime)
{ {
private const string LayoutUserStorageKey = "layoutUser"; private const string LayoutUserStorageKey = "layoutUser";
private readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web); private readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
private string? _cachedLayoutUser;
public async Task<string> EnsureLayoutUserAsync() public async Task<string> EnsureLayoutUserAsync()
{ {
if (!string.IsNullOrWhiteSpace(_cachedLayoutUser))
return _cachedLayoutUser;
var layoutUser = await jsRuntime.InvokeAsync<string?>("localStorage.getItem", LayoutUserStorageKey); var layoutUser = await jsRuntime.InvokeAsync<string?>("localStorage.getItem", LayoutUserStorageKey);
if (string.IsNullOrWhiteSpace(layoutUser)) if (string.IsNullOrWhiteSpace(layoutUser))
{ {
layoutUser = Guid.NewGuid().ToString("N"); layoutUser = Guid.NewGuid().ToString("N");
await jsRuntime.InvokeVoidAsync("localStorage.setItem", LayoutUserStorageKey, layoutUser); await jsRuntime.InvokeVoidAsync("localStorage.setItem", LayoutUserStorageKey, layoutUser);
} }
return layoutUser;
_cachedLayoutUser = layoutUser;
return _cachedLayoutUser;
} }
public async Task<BandLayout> LoadBandLayoutAsync( public async Task<BandLayout> LoadBandLayoutAsync(
@@ -82,9 +88,9 @@ namespace DbFirst.BlazorWebApp.Services
Dictionary<string, ColumnDefinition> columnLookup) Dictionary<string, ColumnDefinition> columnLookup)
{ {
layout ??= new BandLayout(); layout ??= new BandLayout();
layout.Bands ??= new List<BandDefinition>(); layout.Bands ??= [];
layout.ColumnOrder ??= new List<string>(); layout.ColumnOrder ??= [];
layout.ColumnWidths ??= new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase); layout.ColumnWidths ??= [];
foreach (var band in layout.Bands) foreach (var band in layout.Bands)
{ {

View File

@@ -4,7 +4,7 @@ using DbFirst.BlazorWebApp.Models;
namespace DbFirst.BlazorWebApp.Services; namespace DbFirst.BlazorWebApp.Services;
public class CatalogApiClient public class CatalogApiClient : ICatalogApiClient
{ {
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
private const string Endpoint = "api/catalogs"; private const string Endpoint = "api/catalogs";
@@ -14,20 +14,20 @@ public class CatalogApiClient
_httpClient = httpClient; _httpClient = httpClient;
} }
public async Task<List<CatalogReadDto>> GetAllAsync() public async Task<List<CatalogReadDto>> GetAllAsync(CancellationToken ct = default)
{ {
var result = await _httpClient.GetFromJsonAsync<List<CatalogReadDto>>(Endpoint); var result = await _httpClient.GetFromJsonAsync<List<CatalogReadDto>>(Endpoint, ct);
return result ?? new List<CatalogReadDto>(); return result ?? [];
} }
public async Task<CatalogReadDto?> GetByIdAsync(int id) public async Task<CatalogReadDto?> GetByIdAsync(int id, CancellationToken ct = default)
{ {
return await _httpClient.GetFromJsonAsync<CatalogReadDto>($"{Endpoint}/{id}"); return await _httpClient.GetFromJsonAsync<CatalogReadDto>($"{Endpoint}/{id}", ct);
} }
public async Task<ApiResult<CatalogReadDto?>> CreateAsync(CatalogWriteDto dto) public async Task<ApiResult<CatalogReadDto?>> CreateAsync(CatalogWriteDto dto, CancellationToken ct = default)
{ {
var response = await _httpClient.PostAsJsonAsync(Endpoint, dto); var response = await _httpClient.PostAsJsonAsync(Endpoint, dto, ct);
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
{ {
var payload = await response.Content.ReadFromJsonAsync<CatalogReadDto>(); var payload = await response.Content.ReadFromJsonAsync<CatalogReadDto>();
@@ -38,9 +38,9 @@ public class CatalogApiClient
return ApiResult<CatalogReadDto?>.Fail(error); return ApiResult<CatalogReadDto?>.Fail(error);
} }
public async Task<ApiResult<bool>> UpdateAsync(int id, CatalogWriteDto dto) public async Task<ApiResult<bool>> UpdateAsync(int id, CatalogWriteDto dto, CancellationToken ct = default)
{ {
var response = await _httpClient.PutAsJsonAsync($"{Endpoint}/{id}", dto); var response = await _httpClient.PutAsJsonAsync($"{Endpoint}/{id}", dto, ct);
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
{ {
return ApiResult<bool>.Ok(true); return ApiResult<bool>.Ok(true);
@@ -50,9 +50,9 @@ public class CatalogApiClient
return ApiResult<bool>.Fail(error); return ApiResult<bool>.Fail(error);
} }
public async Task<ApiResult<bool>> DeleteAsync(int id) public async Task<ApiResult<bool>> DeleteAsync(int id, CancellationToken ct = default)
{ {
var response = await _httpClient.DeleteAsync($"{Endpoint}/{id}"); var response = await _httpClient.DeleteAsync($"{Endpoint}/{id}", ct);
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
{ {
return ApiResult<bool>.Ok(true); return ApiResult<bool>.Ok(true);
@@ -62,10 +62,3 @@ public class CatalogApiClient
return ApiResult<bool>.Fail(error); return ApiResult<bool>.Fail(error);
} }
} }
public record ApiResult<T>(bool Success, T? Value, string? Error)
{
public static ApiResult<T> Ok(T? value) => new(true, value, null);
public static ApiResult<T> Fail(string? error) => new(false, default, error);
}

View File

@@ -3,7 +3,7 @@ using DbFirst.BlazorWebApp.Models;
namespace DbFirst.BlazorWebApp.Services; namespace DbFirst.BlazorWebApp.Services;
public class DashboardApiClient public class DashboardApiClient : IDashboardApiClient
{ {
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
private const string Endpoint = "api/dashboard/dashboards"; private const string Endpoint = "api/dashboard/dashboards";
@@ -13,9 +13,9 @@ public class DashboardApiClient
_httpClient = httpClient; _httpClient = httpClient;
} }
public async Task<List<DashboardInfoDto>> GetAllAsync() public async Task<List<DashboardInfoDto>> GetAllAsync(CancellationToken ct = default)
{ {
var result = await _httpClient.GetFromJsonAsync<List<DashboardInfoDto>>(Endpoint); var result = await _httpClient.GetFromJsonAsync<List<DashboardInfoDto>>(Endpoint, ct);
return result ?? new List<DashboardInfoDto>(); return result ?? [];
} }
} }

View File

@@ -0,0 +1,12 @@
using DbFirst.BlazorWebApp.Models;
namespace DbFirst.BlazorWebApp.Services;
public interface ICatalogApiClient
{
Task<List<CatalogReadDto>> GetAllAsync(CancellationToken ct = default);
Task<CatalogReadDto?> GetByIdAsync(int id, CancellationToken ct = default);
Task<ApiResult<CatalogReadDto?>> CreateAsync(CatalogWriteDto dto, CancellationToken ct = default);
Task<ApiResult<bool>> UpdateAsync(int id, CatalogWriteDto dto, CancellationToken ct = default);
Task<ApiResult<bool>> DeleteAsync(int id, CancellationToken ct = default);
}

View File

@@ -0,0 +1,9 @@
using DbFirst.BlazorWebApp.Models;
namespace DbFirst.BlazorWebApp.Services
{
public interface IDashboardApiClient
{
Task<List<DashboardInfoDto>> GetAllAsync(CancellationToken ct = default);
}
}

View File

@@ -0,0 +1,11 @@
using DbFirst.BlazorWebApp.Models;
namespace DbFirst.BlazorWebApp.Services
{
public interface ILayoutApiClient
{
Task<LayoutDto?> GetAsync(string layoutType, string layoutKey, string userName, CancellationToken ct = default);
Task<LayoutDto> UpsertAsync(LayoutDto dto, CancellationToken ct = default);
Task DeleteAsync(string layoutType, string layoutKey, string userName, CancellationToken ct = default);
}
}

View File

@@ -0,0 +1,12 @@
using DbFirst.BlazorWebApp.Models;
namespace DbFirst.BlazorWebApp.Services
{
public interface IMassDataApiClient
{
Task<int> GetCountAsync(CancellationToken ct = default);
Task<List<MassDataReadDto>> GetAllAsync(int? skip, int? take, CancellationToken ct = default);
Task<ApiResult<MassDataReadDto?>> UpsertAsync(MassDataWriteDto dto, CancellationToken ct = default);
Task<MassDataReadDto?> GetByCustomerNameAsync(string customerName, CancellationToken ct = default);
}
}

View File

@@ -3,7 +3,7 @@ using DbFirst.BlazorWebApp.Models;
namespace DbFirst.BlazorWebApp.Services; namespace DbFirst.BlazorWebApp.Services;
public class LayoutApiClient public class LayoutApiClient : ILayoutApiClient
{ {
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
private const string Endpoint = "api/layouts"; private const string Endpoint = "api/layouts";
@@ -13,10 +13,10 @@ public class LayoutApiClient
_httpClient = httpClient; _httpClient = httpClient;
} }
public async Task<LayoutDto?> GetAsync(string layoutType, string layoutKey, string userName) public async Task<LayoutDto?> GetAsync(string layoutType, string layoutKey, string userName, CancellationToken ct = default)
{ {
var url = $"{Endpoint}?layoutType={Uri.EscapeDataString(layoutType)}&layoutKey={Uri.EscapeDataString(layoutKey)}&userName={Uri.EscapeDataString(userName)}"; var url = $"{Endpoint}?layoutType={Uri.EscapeDataString(layoutType)}&layoutKey={Uri.EscapeDataString(layoutKey)}&userName={Uri.EscapeDataString(userName)}";
var response = await _httpClient.GetAsync(url); var response = await _httpClient.GetAsync(url, ct);
if (response.StatusCode == System.Net.HttpStatusCode.NotFound) if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{ {
return null; return null;
@@ -26,12 +26,12 @@ public class LayoutApiClient
return await response.Content.ReadFromJsonAsync<LayoutDto>(); return await response.Content.ReadFromJsonAsync<LayoutDto>();
} }
public async Task<LayoutDto> UpsertAsync(LayoutDto dto) public async Task<LayoutDto> UpsertAsync(LayoutDto dto, CancellationToken ct = default)
{ {
var response = await _httpClient.PostAsJsonAsync(Endpoint, dto); var response = await _httpClient.PostAsJsonAsync(Endpoint, dto, ct);
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
{ {
var detail = await ReadErrorAsync(response); var detail = await ApiClientHelper.ReadErrorAsync(response);
throw new InvalidOperationException(detail); throw new InvalidOperationException(detail);
} }
@@ -39,21 +39,10 @@ public class LayoutApiClient
return payload ?? dto; return payload ?? dto;
} }
private static async Task<string> ReadErrorAsync(HttpResponseMessage response) public async Task DeleteAsync(string layoutType, string layoutKey, string userName, CancellationToken ct = default)
{
var body = await response.Content.ReadAsStringAsync();
if (!string.IsNullOrWhiteSpace(body))
{
return body;
}
return $"{(int)response.StatusCode} {response.ReasonPhrase}".Trim();
}
public async Task DeleteAsync(string layoutType, string layoutKey, string userName)
{ {
var url = $"{Endpoint}?layoutType={Uri.EscapeDataString(layoutType)}&layoutKey={Uri.EscapeDataString(layoutKey)}&userName={Uri.EscapeDataString(userName)}"; var url = $"{Endpoint}?layoutType={Uri.EscapeDataString(layoutType)}&layoutKey={Uri.EscapeDataString(layoutKey)}&userName={Uri.EscapeDataString(userName)}";
var response = await _httpClient.DeleteAsync(url); var response = await _httpClient.DeleteAsync(url, ct);
if (response.StatusCode == System.Net.HttpStatusCode.NotFound) if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{ {
return; return;

View File

@@ -3,7 +3,7 @@ using DbFirst.BlazorWebApp.Models;
namespace DbFirst.BlazorWebApp.Services; namespace DbFirst.BlazorWebApp.Services;
public class MassDataApiClient public class MassDataApiClient : IMassDataApiClient
{ {
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
private const string Endpoint = "api/massdata"; private const string Endpoint = "api/massdata";
@@ -13,13 +13,13 @@ public class MassDataApiClient
_httpClient = httpClient; _httpClient = httpClient;
} }
public async Task<int> GetCountAsync() public async Task<int> GetCountAsync(CancellationToken ct = default)
{ {
var result = await _httpClient.GetFromJsonAsync<int?>("api/massdata/count"); var result = await _httpClient.GetFromJsonAsync<int?>("api/massdata/count", ct);
return result ?? 0; return result ?? 0;
} }
public async Task<List<MassDataReadDto>> GetAllAsync(int? skip, int? take) public async Task<List<MassDataReadDto>> GetAllAsync(int? skip, int? take, CancellationToken ct = default)
{ {
var query = new List<string>(); var query = new List<string>();
if (skip.HasValue) if (skip.HasValue)
@@ -32,13 +32,13 @@ public class MassDataApiClient
} }
var url = query.Count == 0 ? Endpoint : $"{Endpoint}?{string.Join("&", query)}"; var url = query.Count == 0 ? Endpoint : $"{Endpoint}?{string.Join("&", query)}";
var result = await _httpClient.GetFromJsonAsync<List<MassDataReadDto>>(url); var result = await _httpClient.GetFromJsonAsync<List<MassDataReadDto>>(url, ct);
return result ?? new List<MassDataReadDto>(); return result ?? [];
} }
public async Task<ApiResult<MassDataReadDto?>> UpsertAsync(MassDataWriteDto dto) public async Task<ApiResult<MassDataReadDto?>> UpsertAsync(MassDataWriteDto dto, CancellationToken ct = default)
{ {
var response = await _httpClient.PostAsJsonAsync($"{Endpoint}/upsert", dto); var response = await _httpClient.PostAsJsonAsync($"{Endpoint}/upsert", dto, ct);
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
{ {
var payload = await response.Content.ReadFromJsonAsync<MassDataReadDto>(); var payload = await response.Content.ReadFromJsonAsync<MassDataReadDto>();
@@ -49,14 +49,14 @@ public class MassDataApiClient
return ApiResult<MassDataReadDto?>.Fail(error); return ApiResult<MassDataReadDto?>.Fail(error);
} }
public async Task<MassDataReadDto?> GetByCustomerNameAsync(string customerName) public async Task<MassDataReadDto?> GetByCustomerNameAsync(string customerName, CancellationToken ct = default)
{ {
if (string.IsNullOrWhiteSpace(customerName)) if (string.IsNullOrWhiteSpace(customerName))
{ {
return null; return null;
} }
var response = await _httpClient.GetAsync($"{Endpoint}/{Uri.EscapeDataString(customerName)}"); var response = await _httpClient.GetAsync($"{Endpoint}/{Uri.EscapeDataString(customerName)}", ct);
if (response.StatusCode == System.Net.HttpStatusCode.NotFound) if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{ {
return null; return null;

View File

@@ -1,3 +1,5 @@
using DbFirst.Application.Repositories;
using DbFirst.Infrastructure.Repositories;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@@ -15,6 +17,10 @@ public static class DependencyInjection
services.AddDbContext<MassDataDbContext>(options => services.AddDbContext<MassDataDbContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("MassDataConnection"))); options.UseSqlServer(configuration.GetConnectionString("MassDataConnection")));
services.AddScoped<ICatalogRepository, CatalogRepository>();
services.AddScoped<IMassDataRepository, MassDataRepository>();
services.AddScoped<ILayoutRepository, LayoutRepository>();
return services; return services;
} }
} }

View File

@@ -34,7 +34,7 @@ public class LayoutRepository : ILayoutRepository
UserName = userName, UserName = userName,
LayoutData = layoutData, LayoutData = layoutData,
AddedWho = userName, AddedWho = userName,
AddedWhen = DateTime.Now AddedWhen = DateTime.UtcNow
}; };
_db.SmfLayouts.Add(entity); _db.SmfLayouts.Add(entity);
} }